tifffile.py 814 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006900790089009901090119012901390149015901690179018901990209021902290239024902590269027902890299030903190329033903490359036903790389039904090419042904390449045904690479048904990509051905290539054905590569057905890599060906190629063906490659066906790689069907090719072907390749075907690779078907990809081908290839084908590869087908890899090909190929093909490959096909790989099910091019102910391049105910691079108910991109111911291139114911591169117911891199120912191229123912491259126912791289129913091319132913391349135913691379138913991409141914291439144914591469147914891499150915191529153915491559156915791589159916091619162916391649165916691679168916991709171917291739174917591769177917891799180918191829183918491859186918791889189919091919192919391949195919691979198919992009201920292039204920592069207920892099210921192129213921492159216921792189219922092219222922392249225922692279228922992309231923292339234923592369237923892399240924192429243924492459246924792489249925092519252925392549255925692579258925992609261926292639264926592669267926892699270927192729273927492759276927792789279928092819282928392849285928692879288928992909291929292939294929592969297929892999300930193029303930493059306930793089309931093119312931393149315931693179318931993209321932293239324932593269327932893299330933193329333933493359336933793389339934093419342934393449345934693479348934993509351935293539354935593569357935893599360936193629363936493659366936793689369937093719372937393749375937693779378937993809381938293839384938593869387938893899390939193929393939493959396939793989399940094019402940394049405940694079408940994109411941294139414941594169417941894199420942194229423942494259426942794289429943094319432943394349435943694379438943994409441944294439444944594469447944894499450945194529453945494559456945794589459946094619462946394649465946694679468946994709471947294739474947594769477947894799480948194829483948494859486948794889489949094919492949394949495949694979498949995009501950295039504950595069507950895099510951195129513951495159516951795189519952095219522952395249525952695279528952995309531953295339534953595369537953895399540954195429543954495459546954795489549955095519552955395549555955695579558955995609561956295639564956595669567956895699570957195729573957495759576957795789579958095819582958395849585958695879588958995909591959295939594959595969597959895999600960196029603960496059606960796089609961096119612961396149615961696179618961996209621962296239624962596269627962896299630963196329633963496359636963796389639964096419642964396449645964696479648964996509651965296539654965596569657965896599660966196629663966496659666966796689669967096719672967396749675967696779678967996809681968296839684968596869687968896899690969196929693969496959696969796989699970097019702970397049705970697079708970997109711971297139714971597169717971897199720972197229723972497259726972797289729973097319732973397349735973697379738973997409741974297439744974597469747974897499750975197529753975497559756975797589759976097619762976397649765976697679768976997709771977297739774977597769777977897799780978197829783978497859786978797889789979097919792979397949795979697979798979998009801980298039804980598069807980898099810981198129813981498159816981798189819982098219822982398249825982698279828982998309831983298339834983598369837983898399840984198429843984498459846984798489849985098519852985398549855985698579858985998609861986298639864986598669867986898699870987198729873987498759876987798789879988098819882988398849885988698879888988998909891989298939894989598969897989898999900990199029903990499059906990799089909991099119912991399149915991699179918991999209921992299239924992599269927992899299930993199329933993499359936993799389939994099419942994399449945994699479948994999509951995299539954995599569957995899599960996199629963996499659966996799689969997099719972997399749975997699779978997999809981998299839984998599869987998899899990999199929993999499959996999799989999100001000110002100031000410005100061000710008100091001010011100121001310014100151001610017100181001910020100211002210023100241002510026100271002810029100301003110032100331003410035100361003710038100391004010041100421004310044100451004610047100481004910050100511005210053100541005510056100571005810059100601006110062100631006410065100661006710068100691007010071100721007310074100751007610077100781007910080100811008210083100841008510086100871008810089100901009110092100931009410095100961009710098100991010010101101021010310104101051010610107101081010910110101111011210113101141011510116101171011810119101201012110122101231012410125101261012710128101291013010131101321013310134101351013610137101381013910140101411014210143101441014510146101471014810149101501015110152101531015410155101561015710158101591016010161101621016310164101651016610167101681016910170101711017210173101741017510176101771017810179101801018110182101831018410185101861018710188101891019010191101921019310194101951019610197101981019910200102011020210203102041020510206102071020810209102101021110212102131021410215102161021710218102191022010221102221022310224102251022610227102281022910230102311023210233102341023510236102371023810239102401024110242102431024410245102461024710248102491025010251102521025310254102551025610257102581025910260102611026210263102641026510266102671026810269102701027110272102731027410275102761027710278102791028010281102821028310284102851028610287102881028910290102911029210293102941029510296102971029810299103001030110302103031030410305103061030710308103091031010311103121031310314103151031610317103181031910320103211032210323103241032510326103271032810329103301033110332103331033410335103361033710338103391034010341103421034310344103451034610347103481034910350103511035210353103541035510356103571035810359103601036110362103631036410365103661036710368103691037010371103721037310374103751037610377103781037910380103811038210383103841038510386103871038810389103901039110392103931039410395103961039710398103991040010401104021040310404104051040610407104081040910410104111041210413104141041510416104171041810419104201042110422104231042410425104261042710428104291043010431104321043310434104351043610437104381043910440104411044210443104441044510446104471044810449104501045110452104531045410455104561045710458104591046010461104621046310464104651046610467104681046910470104711047210473104741047510476104771047810479104801048110482104831048410485104861048710488104891049010491104921049310494104951049610497104981049910500105011050210503105041050510506105071050810509105101051110512105131051410515105161051710518105191052010521105221052310524105251052610527105281052910530105311053210533105341053510536105371053810539105401054110542105431054410545105461054710548105491055010551105521055310554105551055610557105581055910560105611056210563105641056510566105671056810569105701057110572105731057410575105761057710578105791058010581105821058310584105851058610587105881058910590105911059210593105941059510596105971059810599106001060110602106031060410605106061060710608106091061010611106121061310614106151061610617106181061910620106211062210623106241062510626106271062810629106301063110632106331063410635106361063710638106391064010641106421064310644106451064610647106481064910650106511065210653106541065510656106571065810659106601066110662106631066410665106661066710668106691067010671106721067310674106751067610677106781067910680106811068210683106841068510686106871068810689106901069110692106931069410695106961069710698106991070010701107021070310704107051070610707107081070910710107111071210713107141071510716107171071810719107201072110722107231072410725107261072710728107291073010731107321073310734107351073610737107381073910740107411074210743107441074510746107471074810749107501075110752107531075410755107561075710758107591076010761107621076310764107651076610767107681076910770107711077210773107741077510776107771077810779107801078110782107831078410785107861078710788107891079010791107921079310794107951079610797107981079910800108011080210803108041080510806108071080810809108101081110812108131081410815108161081710818108191082010821108221082310824108251082610827108281082910830108311083210833108341083510836108371083810839108401084110842108431084410845108461084710848108491085010851108521085310854108551085610857108581085910860108611086210863108641086510866108671086810869108701087110872108731087410875108761087710878108791088010881108821088310884108851088610887108881088910890108911089210893108941089510896108971089810899109001090110902109031090410905109061090710908109091091010911109121091310914109151091610917109181091910920109211092210923109241092510926109271092810929109301093110932109331093410935109361093710938109391094010941109421094310944109451094610947109481094910950109511095210953109541095510956109571095810959109601096110962109631096410965109661096710968109691097010971109721097310974109751097610977109781097910980109811098210983109841098510986109871098810989109901099110992109931099410995109961099710998109991100011001110021100311004110051100611007110081100911010110111101211013110141101511016110171101811019110201102111022110231102411025110261102711028110291103011031110321103311034110351103611037110381103911040110411104211043110441104511046110471104811049110501105111052110531105411055110561105711058110591106011061110621106311064110651106611067110681106911070110711107211073110741107511076110771107811079110801108111082110831108411085110861108711088110891109011091110921109311094110951109611097110981109911100111011110211103111041110511106111071110811109111101111111112111131111411115111161111711118111191112011121111221112311124111251112611127111281112911130111311113211133111341113511136111371113811139111401114111142111431114411145111461114711148111491115011151111521115311154111551115611157111581115911160111611116211163111641116511166111671116811169111701117111172111731117411175111761117711178111791118011181111821118311184111851118611187111881118911190111911119211193111941119511196111971119811199112001120111202112031120411205112061120711208112091121011211112121121311214112151121611217112181121911220112211122211223112241122511226112271122811229112301123111232112331123411235112361123711238112391124011241112421124311244112451124611247112481124911250112511125211253112541125511256112571125811259112601126111262112631126411265112661126711268112691127011271112721127311274112751127611277112781127911280112811128211283112841128511286112871128811289112901129111292112931129411295112961129711298112991130011301113021130311304113051130611307113081130911310113111131211313113141131511316113171131811319113201132111322113231132411325113261132711328113291133011331113321133311334113351133611337113381133911340113411134211343113441134511346113471134811349113501135111352113531135411355113561135711358113591136011361113621136311364113651136611367113681136911370113711137211373113741137511376113771137811379113801138111382113831138411385113861138711388113891139011391113921139311394113951139611397113981139911400114011140211403114041140511406114071140811409114101141111412114131141411415114161141711418114191142011421114221142311424114251142611427114281142911430114311143211433114341143511436114371143811439114401144111442114431144411445114461144711448114491145011451114521145311454114551145611457114581145911460114611146211463114641146511466114671146811469114701147111472114731147411475114761147711478114791148011481114821148311484114851148611487114881148911490114911149211493114941149511496114971149811499115001150111502115031150411505115061150711508115091151011511115121151311514115151151611517115181151911520115211152211523115241152511526115271152811529115301153111532115331153411535115361153711538115391154011541115421154311544115451154611547115481154911550115511155211553115541155511556115571155811559115601156111562115631156411565115661156711568115691157011571115721157311574115751157611577115781157911580115811158211583115841158511586115871158811589115901159111592115931159411595115961159711598115991160011601116021160311604116051160611607116081160911610116111161211613116141161511616116171161811619116201162111622116231162411625116261162711628116291163011631116321163311634116351163611637116381163911640116411164211643116441164511646116471164811649116501165111652116531165411655116561165711658116591166011661116621166311664116651166611667116681166911670116711167211673116741167511676116771167811679116801168111682116831168411685116861168711688116891169011691116921169311694116951169611697116981169911700117011170211703117041170511706117071170811709117101171111712117131171411715117161171711718117191172011721117221172311724117251172611727117281172911730117311173211733117341173511736117371173811739117401174111742117431174411745117461174711748117491175011751117521175311754117551175611757117581175911760117611176211763117641176511766117671176811769117701177111772117731177411775117761177711778117791178011781117821178311784117851178611787117881178911790117911179211793117941179511796117971179811799118001180111802118031180411805118061180711808118091181011811118121181311814118151181611817118181181911820118211182211823118241182511826118271182811829118301183111832118331183411835118361183711838118391184011841118421184311844118451184611847118481184911850118511185211853118541185511856118571185811859118601186111862118631186411865118661186711868118691187011871118721187311874118751187611877118781187911880118811188211883118841188511886118871188811889118901189111892118931189411895118961189711898118991190011901119021190311904119051190611907119081190911910119111191211913119141191511916119171191811919119201192111922119231192411925119261192711928119291193011931119321193311934119351193611937119381193911940119411194211943119441194511946119471194811949119501195111952119531195411955119561195711958119591196011961119621196311964119651196611967119681196911970119711197211973119741197511976119771197811979119801198111982119831198411985119861198711988119891199011991119921199311994119951199611997119981199912000120011200212003120041200512006120071200812009120101201112012120131201412015120161201712018120191202012021120221202312024120251202612027120281202912030120311203212033120341203512036120371203812039120401204112042120431204412045120461204712048120491205012051120521205312054120551205612057120581205912060120611206212063120641206512066120671206812069120701207112072120731207412075120761207712078120791208012081120821208312084120851208612087120881208912090120911209212093120941209512096120971209812099121001210112102121031210412105121061210712108121091211012111121121211312114121151211612117121181211912120121211212212123121241212512126121271212812129121301213112132121331213412135121361213712138121391214012141121421214312144121451214612147121481214912150121511215212153121541215512156121571215812159121601216112162121631216412165121661216712168121691217012171121721217312174121751217612177121781217912180121811218212183121841218512186121871218812189121901219112192121931219412195121961219712198121991220012201122021220312204122051220612207122081220912210122111221212213122141221512216122171221812219122201222112222122231222412225122261222712228122291223012231122321223312234122351223612237122381223912240122411224212243122441224512246122471224812249122501225112252122531225412255122561225712258122591226012261122621226312264122651226612267122681226912270122711227212273122741227512276122771227812279122801228112282122831228412285122861228712288122891229012291122921229312294122951229612297122981229912300123011230212303123041230512306123071230812309123101231112312123131231412315123161231712318123191232012321123221232312324123251232612327123281232912330123311233212333123341233512336123371233812339123401234112342123431234412345123461234712348123491235012351123521235312354123551235612357123581235912360123611236212363123641236512366123671236812369123701237112372123731237412375123761237712378123791238012381123821238312384123851238612387123881238912390123911239212393123941239512396123971239812399124001240112402124031240412405124061240712408124091241012411124121241312414124151241612417124181241912420124211242212423124241242512426124271242812429124301243112432124331243412435124361243712438124391244012441124421244312444124451244612447124481244912450124511245212453124541245512456124571245812459124601246112462124631246412465124661246712468124691247012471124721247312474124751247612477124781247912480124811248212483124841248512486124871248812489124901249112492124931249412495124961249712498124991250012501125021250312504125051250612507125081250912510125111251212513125141251512516125171251812519125201252112522125231252412525125261252712528125291253012531125321253312534125351253612537125381253912540125411254212543125441254512546125471254812549125501255112552125531255412555125561255712558125591256012561125621256312564125651256612567125681256912570125711257212573125741257512576125771257812579125801258112582125831258412585125861258712588125891259012591125921259312594125951259612597125981259912600126011260212603126041260512606126071260812609126101261112612126131261412615126161261712618126191262012621126221262312624126251262612627126281262912630126311263212633126341263512636126371263812639126401264112642126431264412645126461264712648126491265012651126521265312654126551265612657126581265912660126611266212663126641266512666126671266812669126701267112672126731267412675126761267712678126791268012681126821268312684126851268612687126881268912690126911269212693126941269512696126971269812699127001270112702127031270412705127061270712708127091271012711127121271312714127151271612717127181271912720127211272212723127241272512726127271272812729127301273112732127331273412735127361273712738127391274012741127421274312744127451274612747127481274912750127511275212753127541275512756127571275812759127601276112762127631276412765127661276712768127691277012771127721277312774127751277612777127781277912780127811278212783127841278512786127871278812789127901279112792127931279412795127961279712798127991280012801128021280312804128051280612807128081280912810128111281212813128141281512816128171281812819128201282112822128231282412825128261282712828128291283012831128321283312834128351283612837128381283912840128411284212843128441284512846128471284812849128501285112852128531285412855128561285712858128591286012861128621286312864128651286612867128681286912870128711287212873128741287512876128771287812879128801288112882128831288412885128861288712888128891289012891128921289312894128951289612897128981289912900129011290212903129041290512906129071290812909129101291112912129131291412915129161291712918129191292012921129221292312924129251292612927129281292912930129311293212933129341293512936129371293812939129401294112942129431294412945129461294712948129491295012951129521295312954129551295612957129581295912960129611296212963129641296512966129671296812969129701297112972129731297412975129761297712978129791298012981129821298312984129851298612987129881298912990129911299212993129941299512996129971299812999130001300113002130031300413005130061300713008130091301013011130121301313014130151301613017130181301913020130211302213023130241302513026130271302813029130301303113032130331303413035130361303713038130391304013041130421304313044130451304613047130481304913050130511305213053130541305513056130571305813059130601306113062130631306413065130661306713068130691307013071130721307313074130751307613077130781307913080130811308213083130841308513086130871308813089130901309113092130931309413095130961309713098130991310013101131021310313104131051310613107131081310913110131111311213113131141311513116131171311813119131201312113122131231312413125131261312713128131291313013131131321313313134131351313613137131381313913140131411314213143131441314513146131471314813149131501315113152131531315413155131561315713158131591316013161131621316313164131651316613167131681316913170131711317213173131741317513176131771317813179131801318113182131831318413185131861318713188131891319013191131921319313194131951319613197131981319913200132011320213203132041320513206132071320813209132101321113212132131321413215132161321713218132191322013221132221322313224132251322613227132281322913230132311323213233132341323513236132371323813239132401324113242132431324413245132461324713248132491325013251132521325313254132551325613257132581325913260132611326213263132641326513266132671326813269132701327113272132731327413275132761327713278132791328013281132821328313284132851328613287132881328913290132911329213293132941329513296132971329813299133001330113302133031330413305133061330713308133091331013311133121331313314133151331613317133181331913320133211332213323133241332513326133271332813329133301333113332133331333413335133361333713338133391334013341133421334313344133451334613347133481334913350133511335213353133541335513356133571335813359133601336113362133631336413365133661336713368133691337013371133721337313374133751337613377133781337913380133811338213383133841338513386133871338813389133901339113392133931339413395133961339713398133991340013401134021340313404134051340613407134081340913410134111341213413134141341513416134171341813419134201342113422134231342413425134261342713428134291343013431134321343313434134351343613437134381343913440134411344213443134441344513446134471344813449134501345113452134531345413455134561345713458134591346013461134621346313464134651346613467134681346913470134711347213473134741347513476134771347813479134801348113482134831348413485134861348713488134891349013491134921349313494134951349613497134981349913500135011350213503135041350513506135071350813509135101351113512135131351413515135161351713518135191352013521135221352313524135251352613527135281352913530135311353213533135341353513536135371353813539135401354113542135431354413545135461354713548135491355013551135521355313554135551355613557135581355913560135611356213563135641356513566135671356813569135701357113572135731357413575135761357713578135791358013581135821358313584135851358613587135881358913590135911359213593135941359513596135971359813599136001360113602136031360413605136061360713608136091361013611136121361313614136151361613617136181361913620136211362213623136241362513626136271362813629136301363113632136331363413635136361363713638136391364013641136421364313644136451364613647136481364913650136511365213653136541365513656136571365813659136601366113662136631366413665136661366713668136691367013671136721367313674136751367613677136781367913680136811368213683136841368513686136871368813689136901369113692136931369413695136961369713698136991370013701137021370313704137051370613707137081370913710137111371213713137141371513716137171371813719137201372113722137231372413725137261372713728137291373013731137321373313734137351373613737137381373913740137411374213743137441374513746137471374813749137501375113752137531375413755137561375713758137591376013761137621376313764137651376613767137681376913770137711377213773137741377513776137771377813779137801378113782137831378413785137861378713788137891379013791137921379313794137951379613797137981379913800138011380213803138041380513806138071380813809138101381113812138131381413815138161381713818138191382013821138221382313824138251382613827138281382913830138311383213833138341383513836138371383813839138401384113842138431384413845138461384713848138491385013851138521385313854138551385613857138581385913860138611386213863138641386513866138671386813869138701387113872138731387413875138761387713878138791388013881138821388313884138851388613887138881388913890138911389213893138941389513896138971389813899139001390113902139031390413905139061390713908139091391013911139121391313914139151391613917139181391913920139211392213923139241392513926139271392813929139301393113932139331393413935139361393713938139391394013941139421394313944139451394613947139481394913950139511395213953139541395513956139571395813959139601396113962139631396413965139661396713968139691397013971139721397313974139751397613977139781397913980139811398213983139841398513986139871398813989139901399113992139931399413995139961399713998139991400014001140021400314004140051400614007140081400914010140111401214013140141401514016140171401814019140201402114022140231402414025140261402714028140291403014031140321403314034140351403614037140381403914040140411404214043140441404514046140471404814049140501405114052140531405414055140561405714058140591406014061140621406314064140651406614067140681406914070140711407214073140741407514076140771407814079140801408114082140831408414085140861408714088140891409014091140921409314094140951409614097140981409914100141011410214103141041410514106141071410814109141101411114112141131411414115141161411714118141191412014121141221412314124141251412614127141281412914130141311413214133141341413514136141371413814139141401414114142141431414414145141461414714148141491415014151141521415314154141551415614157141581415914160141611416214163141641416514166141671416814169141701417114172141731417414175141761417714178141791418014181141821418314184141851418614187141881418914190141911419214193141941419514196141971419814199142001420114202142031420414205142061420714208142091421014211142121421314214142151421614217142181421914220142211422214223142241422514226142271422814229142301423114232142331423414235142361423714238142391424014241142421424314244142451424614247142481424914250142511425214253142541425514256142571425814259142601426114262142631426414265142661426714268142691427014271142721427314274142751427614277142781427914280142811428214283142841428514286142871428814289142901429114292142931429414295142961429714298142991430014301143021430314304143051430614307143081430914310143111431214313143141431514316143171431814319143201432114322143231432414325143261432714328143291433014331143321433314334143351433614337143381433914340143411434214343143441434514346143471434814349143501435114352143531435414355143561435714358143591436014361143621436314364143651436614367143681436914370143711437214373143741437514376143771437814379143801438114382143831438414385143861438714388143891439014391143921439314394143951439614397143981439914400144011440214403144041440514406144071440814409144101441114412144131441414415144161441714418144191442014421144221442314424144251442614427144281442914430144311443214433144341443514436144371443814439144401444114442144431444414445144461444714448144491445014451144521445314454144551445614457144581445914460144611446214463144641446514466144671446814469144701447114472144731447414475144761447714478144791448014481144821448314484144851448614487144881448914490144911449214493144941449514496144971449814499145001450114502145031450414505145061450714508145091451014511145121451314514145151451614517145181451914520145211452214523145241452514526145271452814529145301453114532145331453414535145361453714538145391454014541145421454314544145451454614547145481454914550145511455214553145541455514556145571455814559145601456114562145631456414565145661456714568145691457014571145721457314574145751457614577145781457914580145811458214583145841458514586145871458814589145901459114592145931459414595145961459714598145991460014601146021460314604146051460614607146081460914610146111461214613146141461514616146171461814619146201462114622146231462414625146261462714628146291463014631146321463314634146351463614637146381463914640146411464214643146441464514646146471464814649146501465114652146531465414655146561465714658146591466014661146621466314664146651466614667146681466914670146711467214673146741467514676146771467814679146801468114682146831468414685146861468714688146891469014691146921469314694146951469614697146981469914700147011470214703147041470514706147071470814709147101471114712147131471414715147161471714718147191472014721147221472314724147251472614727147281472914730147311473214733147341473514736147371473814739147401474114742147431474414745147461474714748147491475014751147521475314754147551475614757147581475914760147611476214763147641476514766147671476814769147701477114772147731477414775147761477714778147791478014781147821478314784147851478614787147881478914790147911479214793147941479514796147971479814799148001480114802148031480414805148061480714808148091481014811148121481314814148151481614817148181481914820148211482214823148241482514826148271482814829148301483114832148331483414835148361483714838148391484014841148421484314844148451484614847148481484914850148511485214853148541485514856148571485814859148601486114862148631486414865148661486714868148691487014871148721487314874148751487614877148781487914880148811488214883148841488514886148871488814889148901489114892148931489414895148961489714898148991490014901149021490314904149051490614907149081490914910149111491214913149141491514916149171491814919149201492114922149231492414925149261492714928149291493014931149321493314934149351493614937149381493914940149411494214943149441494514946149471494814949149501495114952149531495414955149561495714958149591496014961149621496314964149651496614967149681496914970149711497214973149741497514976149771497814979149801498114982149831498414985149861498714988149891499014991149921499314994149951499614997149981499915000150011500215003150041500515006150071500815009150101501115012150131501415015150161501715018150191502015021150221502315024150251502615027150281502915030150311503215033150341503515036150371503815039150401504115042150431504415045150461504715048150491505015051150521505315054150551505615057150581505915060150611506215063150641506515066150671506815069150701507115072150731507415075150761507715078150791508015081150821508315084150851508615087150881508915090150911509215093150941509515096150971509815099151001510115102151031510415105151061510715108151091511015111151121511315114151151511615117151181511915120151211512215123151241512515126151271512815129151301513115132151331513415135151361513715138151391514015141151421514315144151451514615147151481514915150151511515215153151541515515156151571515815159151601516115162151631516415165151661516715168151691517015171151721517315174151751517615177151781517915180151811518215183151841518515186151871518815189151901519115192151931519415195151961519715198151991520015201152021520315204152051520615207152081520915210152111521215213152141521515216152171521815219152201522115222152231522415225152261522715228152291523015231152321523315234152351523615237152381523915240152411524215243152441524515246152471524815249152501525115252152531525415255152561525715258152591526015261152621526315264152651526615267152681526915270152711527215273152741527515276152771527815279152801528115282152831528415285152861528715288152891529015291152921529315294152951529615297152981529915300153011530215303153041530515306153071530815309153101531115312153131531415315153161531715318153191532015321153221532315324153251532615327153281532915330153311533215333153341533515336153371533815339153401534115342153431534415345153461534715348153491535015351153521535315354153551535615357153581535915360153611536215363153641536515366153671536815369153701537115372153731537415375153761537715378153791538015381153821538315384153851538615387153881538915390153911539215393153941539515396153971539815399154001540115402154031540415405154061540715408154091541015411154121541315414154151541615417154181541915420154211542215423154241542515426154271542815429154301543115432154331543415435154361543715438154391544015441154421544315444154451544615447154481544915450154511545215453154541545515456154571545815459154601546115462154631546415465154661546715468154691547015471154721547315474154751547615477154781547915480154811548215483154841548515486154871548815489154901549115492154931549415495154961549715498154991550015501155021550315504155051550615507155081550915510155111551215513155141551515516155171551815519155201552115522155231552415525155261552715528155291553015531155321553315534155351553615537155381553915540155411554215543155441554515546155471554815549155501555115552155531555415555155561555715558155591556015561155621556315564155651556615567155681556915570155711557215573155741557515576155771557815579155801558115582155831558415585155861558715588155891559015591155921559315594155951559615597155981559915600156011560215603156041560515606156071560815609156101561115612156131561415615156161561715618156191562015621156221562315624156251562615627156281562915630156311563215633156341563515636156371563815639156401564115642156431564415645156461564715648156491565015651156521565315654156551565615657156581565915660156611566215663156641566515666156671566815669156701567115672156731567415675156761567715678156791568015681156821568315684156851568615687156881568915690156911569215693156941569515696156971569815699157001570115702157031570415705157061570715708157091571015711157121571315714157151571615717157181571915720157211572215723157241572515726157271572815729157301573115732157331573415735157361573715738157391574015741157421574315744157451574615747157481574915750157511575215753157541575515756157571575815759157601576115762157631576415765157661576715768157691577015771157721577315774157751577615777157781577915780157811578215783157841578515786157871578815789157901579115792157931579415795157961579715798157991580015801158021580315804158051580615807158081580915810158111581215813158141581515816158171581815819158201582115822158231582415825158261582715828158291583015831158321583315834158351583615837158381583915840158411584215843158441584515846158471584815849158501585115852158531585415855158561585715858158591586015861158621586315864158651586615867158681586915870158711587215873158741587515876158771587815879158801588115882158831588415885158861588715888158891589015891158921589315894158951589615897158981589915900159011590215903159041590515906159071590815909159101591115912159131591415915159161591715918159191592015921159221592315924159251592615927159281592915930159311593215933159341593515936159371593815939159401594115942159431594415945159461594715948159491595015951159521595315954159551595615957159581595915960159611596215963159641596515966159671596815969159701597115972159731597415975159761597715978159791598015981159821598315984159851598615987159881598915990159911599215993159941599515996159971599815999160001600116002160031600416005160061600716008160091601016011160121601316014160151601616017160181601916020160211602216023160241602516026160271602816029160301603116032160331603416035160361603716038160391604016041160421604316044160451604616047160481604916050160511605216053160541605516056160571605816059160601606116062160631606416065160661606716068160691607016071160721607316074160751607616077160781607916080160811608216083160841608516086160871608816089160901609116092160931609416095160961609716098160991610016101161021610316104161051610616107161081610916110161111611216113161141611516116161171611816119161201612116122161231612416125161261612716128161291613016131161321613316134161351613616137161381613916140161411614216143161441614516146161471614816149161501615116152161531615416155161561615716158161591616016161161621616316164161651616616167161681616916170161711617216173161741617516176161771617816179161801618116182161831618416185161861618716188161891619016191161921619316194161951619616197161981619916200162011620216203162041620516206162071620816209162101621116212162131621416215162161621716218162191622016221162221622316224162251622616227162281622916230162311623216233162341623516236162371623816239162401624116242162431624416245162461624716248162491625016251162521625316254162551625616257162581625916260162611626216263162641626516266162671626816269162701627116272162731627416275162761627716278162791628016281162821628316284162851628616287162881628916290162911629216293162941629516296162971629816299163001630116302163031630416305163061630716308163091631016311163121631316314163151631616317163181631916320163211632216323163241632516326163271632816329163301633116332163331633416335163361633716338163391634016341163421634316344163451634616347163481634916350163511635216353163541635516356163571635816359163601636116362163631636416365163661636716368163691637016371163721637316374163751637616377163781637916380163811638216383163841638516386163871638816389163901639116392163931639416395163961639716398163991640016401164021640316404164051640616407164081640916410164111641216413164141641516416164171641816419164201642116422164231642416425164261642716428164291643016431164321643316434164351643616437164381643916440164411644216443164441644516446164471644816449164501645116452164531645416455164561645716458164591646016461164621646316464164651646616467164681646916470164711647216473164741647516476164771647816479164801648116482164831648416485164861648716488164891649016491164921649316494164951649616497164981649916500165011650216503165041650516506165071650816509165101651116512165131651416515165161651716518165191652016521165221652316524165251652616527165281652916530165311653216533165341653516536165371653816539165401654116542165431654416545165461654716548165491655016551165521655316554165551655616557165581655916560165611656216563165641656516566165671656816569165701657116572165731657416575165761657716578165791658016581165821658316584165851658616587165881658916590165911659216593165941659516596165971659816599166001660116602166031660416605166061660716608166091661016611166121661316614166151661616617166181661916620166211662216623166241662516626166271662816629166301663116632166331663416635166361663716638166391664016641166421664316644166451664616647166481664916650166511665216653166541665516656166571665816659166601666116662166631666416665166661666716668166691667016671166721667316674166751667616677166781667916680166811668216683166841668516686166871668816689166901669116692166931669416695166961669716698166991670016701167021670316704167051670616707167081670916710167111671216713167141671516716167171671816719167201672116722167231672416725167261672716728167291673016731167321673316734167351673616737167381673916740167411674216743167441674516746167471674816749167501675116752167531675416755167561675716758167591676016761167621676316764167651676616767167681676916770167711677216773167741677516776167771677816779167801678116782167831678416785167861678716788167891679016791167921679316794167951679616797167981679916800168011680216803168041680516806168071680816809168101681116812168131681416815168161681716818168191682016821168221682316824168251682616827168281682916830168311683216833168341683516836168371683816839168401684116842168431684416845168461684716848168491685016851168521685316854168551685616857168581685916860168611686216863168641686516866168671686816869168701687116872168731687416875168761687716878168791688016881168821688316884168851688616887168881688916890168911689216893168941689516896168971689816899169001690116902169031690416905169061690716908169091691016911169121691316914169151691616917169181691916920169211692216923169241692516926169271692816929169301693116932169331693416935169361693716938169391694016941169421694316944169451694616947169481694916950169511695216953169541695516956169571695816959169601696116962169631696416965169661696716968169691697016971169721697316974169751697616977169781697916980169811698216983169841698516986169871698816989169901699116992169931699416995169961699716998169991700017001170021700317004170051700617007170081700917010170111701217013170141701517016170171701817019170201702117022170231702417025170261702717028170291703017031170321703317034170351703617037170381703917040170411704217043170441704517046170471704817049170501705117052170531705417055170561705717058170591706017061170621706317064170651706617067170681706917070170711707217073170741707517076170771707817079170801708117082170831708417085170861708717088170891709017091170921709317094170951709617097170981709917100171011710217103171041710517106171071710817109171101711117112171131711417115171161711717118171191712017121171221712317124171251712617127171281712917130171311713217133171341713517136171371713817139171401714117142171431714417145171461714717148171491715017151171521715317154171551715617157171581715917160171611716217163171641716517166171671716817169171701717117172171731717417175171761717717178171791718017181171821718317184171851718617187171881718917190171911719217193171941719517196171971719817199172001720117202172031720417205172061720717208172091721017211172121721317214172151721617217172181721917220172211722217223172241722517226172271722817229172301723117232172331723417235172361723717238172391724017241172421724317244172451724617247172481724917250172511725217253172541725517256172571725817259172601726117262172631726417265172661726717268172691727017271172721727317274172751727617277172781727917280172811728217283172841728517286172871728817289172901729117292172931729417295172961729717298172991730017301173021730317304173051730617307173081730917310173111731217313173141731517316173171731817319173201732117322173231732417325173261732717328173291733017331173321733317334173351733617337173381733917340173411734217343173441734517346173471734817349173501735117352173531735417355173561735717358173591736017361173621736317364173651736617367173681736917370173711737217373173741737517376173771737817379173801738117382173831738417385173861738717388173891739017391173921739317394173951739617397173981739917400174011740217403174041740517406174071740817409174101741117412174131741417415174161741717418174191742017421174221742317424174251742617427174281742917430174311743217433174341743517436174371743817439174401744117442174431744417445174461744717448174491745017451174521745317454174551745617457174581745917460174611746217463174641746517466174671746817469174701747117472174731747417475174761747717478174791748017481174821748317484174851748617487174881748917490174911749217493174941749517496174971749817499175001750117502175031750417505175061750717508175091751017511175121751317514175151751617517175181751917520175211752217523175241752517526175271752817529175301753117532175331753417535175361753717538175391754017541175421754317544175451754617547175481754917550175511755217553175541755517556175571755817559175601756117562175631756417565175661756717568175691757017571175721757317574175751757617577175781757917580175811758217583175841758517586175871758817589175901759117592175931759417595175961759717598175991760017601176021760317604176051760617607176081760917610176111761217613176141761517616176171761817619176201762117622176231762417625176261762717628176291763017631176321763317634176351763617637176381763917640176411764217643176441764517646176471764817649176501765117652176531765417655176561765717658176591766017661176621766317664176651766617667176681766917670176711767217673176741767517676176771767817679176801768117682176831768417685176861768717688176891769017691176921769317694176951769617697176981769917700177011770217703177041770517706177071770817709177101771117712177131771417715177161771717718177191772017721177221772317724177251772617727177281772917730177311773217733177341773517736177371773817739177401774117742177431774417745177461774717748177491775017751177521775317754177551775617757177581775917760177611776217763177641776517766177671776817769177701777117772177731777417775177761777717778177791778017781177821778317784177851778617787177881778917790177911779217793177941779517796177971779817799178001780117802178031780417805178061780717808178091781017811178121781317814178151781617817178181781917820178211782217823178241782517826178271782817829178301783117832178331783417835178361783717838178391784017841178421784317844178451784617847178481784917850178511785217853178541785517856178571785817859178601786117862178631786417865178661786717868178691787017871178721787317874178751787617877178781787917880178811788217883178841788517886178871788817889178901789117892178931789417895178961789717898178991790017901179021790317904179051790617907179081790917910179111791217913179141791517916179171791817919179201792117922179231792417925179261792717928179291793017931179321793317934179351793617937179381793917940179411794217943179441794517946179471794817949179501795117952179531795417955179561795717958179591796017961179621796317964179651796617967179681796917970179711797217973179741797517976179771797817979179801798117982179831798417985179861798717988179891799017991179921799317994179951799617997179981799918000180011800218003180041800518006180071800818009180101801118012180131801418015180161801718018180191802018021180221802318024180251802618027180281802918030180311803218033180341803518036180371803818039180401804118042180431804418045180461804718048180491805018051180521805318054180551805618057180581805918060180611806218063180641806518066180671806818069180701807118072180731807418075180761807718078180791808018081180821808318084180851808618087180881808918090180911809218093180941809518096180971809818099181001810118102181031810418105181061810718108181091811018111181121811318114181151811618117181181811918120181211812218123181241812518126181271812818129181301813118132181331813418135181361813718138181391814018141181421814318144181451814618147181481814918150181511815218153181541815518156181571815818159181601816118162181631816418165181661816718168181691817018171181721817318174181751817618177181781817918180181811818218183181841818518186181871818818189181901819118192181931819418195181961819718198181991820018201182021820318204182051820618207182081820918210182111821218213182141821518216182171821818219182201822118222182231822418225182261822718228182291823018231182321823318234182351823618237182381823918240182411824218243182441824518246182471824818249182501825118252182531825418255182561825718258182591826018261182621826318264182651826618267182681826918270182711827218273182741827518276182771827818279182801828118282182831828418285182861828718288182891829018291182921829318294182951829618297182981829918300183011830218303183041830518306183071830818309183101831118312183131831418315183161831718318183191832018321183221832318324183251832618327183281832918330183311833218333183341833518336183371833818339183401834118342183431834418345183461834718348183491835018351183521835318354183551835618357183581835918360183611836218363183641836518366183671836818369183701837118372183731837418375183761837718378183791838018381183821838318384183851838618387183881838918390183911839218393183941839518396183971839818399184001840118402184031840418405184061840718408184091841018411184121841318414184151841618417184181841918420184211842218423184241842518426184271842818429184301843118432184331843418435184361843718438184391844018441184421844318444184451844618447184481844918450184511845218453184541845518456184571845818459184601846118462184631846418465184661846718468184691847018471184721847318474184751847618477184781847918480184811848218483184841848518486184871848818489184901849118492184931849418495184961849718498184991850018501185021850318504185051850618507185081850918510185111851218513185141851518516185171851818519185201852118522185231852418525185261852718528185291853018531185321853318534185351853618537185381853918540185411854218543185441854518546185471854818549185501855118552185531855418555185561855718558185591856018561185621856318564185651856618567185681856918570185711857218573185741857518576185771857818579185801858118582185831858418585185861858718588185891859018591185921859318594185951859618597185981859918600186011860218603186041860518606186071860818609186101861118612186131861418615186161861718618186191862018621186221862318624186251862618627186281862918630186311863218633186341863518636186371863818639186401864118642186431864418645186461864718648186491865018651186521865318654186551865618657186581865918660186611866218663186641866518666186671866818669186701867118672186731867418675186761867718678186791868018681186821868318684186851868618687186881868918690186911869218693186941869518696186971869818699187001870118702187031870418705187061870718708187091871018711187121871318714187151871618717187181871918720187211872218723187241872518726187271872818729187301873118732187331873418735187361873718738187391874018741187421874318744187451874618747187481874918750187511875218753187541875518756187571875818759187601876118762187631876418765187661876718768187691877018771187721877318774187751877618777187781877918780187811878218783187841878518786187871878818789187901879118792187931879418795187961879718798187991880018801188021880318804188051880618807188081880918810188111881218813188141881518816188171881818819188201882118822188231882418825188261882718828188291883018831188321883318834188351883618837188381883918840188411884218843188441884518846188471884818849188501885118852188531885418855188561885718858188591886018861188621886318864188651886618867188681886918870188711887218873188741887518876188771887818879188801888118882188831888418885188861888718888188891889018891188921889318894188951889618897188981889918900189011890218903189041890518906189071890818909189101891118912189131891418915189161891718918189191892018921189221892318924189251892618927189281892918930189311893218933189341893518936189371893818939189401894118942189431894418945189461894718948189491895018951189521895318954189551895618957189581895918960189611896218963189641896518966189671896818969189701897118972189731897418975189761897718978189791898018981189821898318984189851898618987189881898918990189911899218993189941899518996189971899818999190001900119002190031900419005190061900719008190091901019011190121901319014190151901619017190181901919020190211902219023190241902519026190271902819029190301903119032190331903419035190361903719038190391904019041190421904319044190451904619047190481904919050190511905219053190541905519056190571905819059190601906119062190631906419065190661906719068190691907019071190721907319074190751907619077190781907919080190811908219083190841908519086190871908819089190901909119092190931909419095190961909719098190991910019101191021910319104191051910619107191081910919110191111911219113191141911519116191171911819119191201912119122191231912419125191261912719128191291913019131191321913319134191351913619137191381913919140191411914219143191441914519146191471914819149191501915119152191531915419155191561915719158191591916019161191621916319164191651916619167191681916919170191711917219173191741917519176191771917819179191801918119182191831918419185191861918719188191891919019191191921919319194191951919619197191981919919200192011920219203192041920519206192071920819209192101921119212192131921419215192161921719218192191922019221192221922319224192251922619227192281922919230192311923219233192341923519236192371923819239192401924119242192431924419245192461924719248192491925019251192521925319254192551925619257192581925919260192611926219263192641926519266192671926819269192701927119272192731927419275192761927719278192791928019281192821928319284192851928619287192881928919290192911929219293192941929519296192971929819299193001930119302193031930419305193061930719308193091931019311193121931319314193151931619317193181931919320193211932219323193241932519326193271932819329193301933119332193331933419335193361933719338193391934019341193421934319344193451934619347193481934919350193511935219353193541935519356193571935819359193601936119362193631936419365193661936719368193691937019371193721937319374193751937619377193781937919380193811938219383193841938519386193871938819389193901939119392193931939419395193961939719398193991940019401194021940319404194051940619407194081940919410194111941219413194141941519416194171941819419194201942119422194231942419425194261942719428194291943019431194321943319434194351943619437194381943919440194411944219443194441944519446194471944819449194501945119452194531945419455194561945719458194591946019461194621946319464194651946619467194681946919470194711947219473194741947519476194771947819479194801948119482194831948419485194861948719488194891949019491194921949319494194951949619497194981949919500195011950219503195041950519506195071950819509195101951119512195131951419515195161951719518195191952019521195221952319524195251952619527195281952919530195311953219533195341953519536195371953819539195401954119542195431954419545195461954719548195491955019551195521955319554195551955619557195581955919560195611956219563195641956519566195671956819569195701957119572195731957419575195761957719578195791958019581195821958319584195851958619587195881958919590195911959219593195941959519596195971959819599196001960119602196031960419605196061960719608196091961019611196121961319614196151961619617196181961919620196211962219623196241962519626196271962819629196301963119632196331963419635196361963719638196391964019641196421964319644196451964619647196481964919650196511965219653196541965519656196571965819659196601966119662196631966419665196661966719668196691967019671196721967319674196751967619677196781967919680196811968219683196841968519686196871968819689196901969119692196931969419695196961969719698196991970019701197021970319704197051970619707197081970919710197111971219713197141971519716197171971819719197201972119722197231972419725197261972719728197291973019731197321973319734197351973619737197381973919740197411974219743197441974519746197471974819749197501975119752197531975419755197561975719758197591976019761197621976319764197651976619767197681976919770197711977219773197741977519776197771977819779197801978119782197831978419785197861978719788197891979019791197921979319794197951979619797197981979919800198011980219803198041980519806198071980819809198101981119812198131981419815198161981719818198191982019821198221982319824198251982619827198281982919830198311983219833198341983519836198371983819839198401984119842198431984419845198461984719848198491985019851198521985319854198551985619857198581985919860198611986219863198641986519866198671986819869198701987119872198731987419875198761987719878198791988019881198821988319884198851988619887198881988919890198911989219893198941989519896198971989819899199001990119902199031990419905199061990719908199091991019911199121991319914199151991619917199181991919920199211992219923199241992519926199271992819929199301993119932199331993419935199361993719938199391994019941199421994319944199451994619947199481994919950199511995219953199541995519956199571995819959199601996119962199631996419965199661996719968199691997019971199721997319974199751997619977199781997919980199811998219983199841998519986199871998819989199901999119992199931999419995199961999719998199992000020001200022000320004200052000620007200082000920010200112001220013200142001520016200172001820019200202002120022200232002420025200262002720028200292003020031200322003320034200352003620037200382003920040200412004220043200442004520046200472004820049200502005120052200532005420055200562005720058200592006020061200622006320064200652006620067200682006920070200712007220073200742007520076200772007820079200802008120082200832008420085200862008720088200892009020091200922009320094200952009620097200982009920100201012010220103201042010520106201072010820109201102011120112201132011420115201162011720118201192012020121201222012320124201252012620127201282012920130201312013220133201342013520136201372013820139201402014120142201432014420145201462014720148201492015020151201522015320154201552015620157201582015920160201612016220163201642016520166201672016820169201702017120172201732017420175201762017720178201792018020181201822018320184201852018620187201882018920190201912019220193201942019520196201972019820199202002020120202202032020420205202062020720208202092021020211202122021320214202152021620217202182021920220202212022220223202242022520226202272022820229202302023120232202332023420235202362023720238202392024020241202422024320244202452024620247202482024920250202512025220253202542025520256202572025820259202602026120262202632026420265202662026720268202692027020271202722027320274202752027620277202782027920280202812028220283202842028520286202872028820289202902029120292202932029420295202962029720298202992030020301203022030320304203052030620307203082030920310203112031220313203142031520316203172031820319203202032120322203232032420325203262032720328203292033020331203322033320334203352033620337203382033920340203412034220343203442034520346203472034820349203502035120352203532035420355203562035720358203592036020361203622036320364203652036620367203682036920370203712037220373203742037520376203772037820379203802038120382203832038420385203862038720388203892039020391203922039320394203952039620397203982039920400204012040220403204042040520406204072040820409204102041120412204132041420415204162041720418204192042020421204222042320424204252042620427204282042920430204312043220433204342043520436204372043820439204402044120442204432044420445204462044720448204492045020451204522045320454204552045620457204582045920460204612046220463204642046520466204672046820469204702047120472204732047420475204762047720478204792048020481204822048320484204852048620487204882048920490204912049220493204942049520496204972049820499205002050120502205032050420505205062050720508205092051020511205122051320514205152051620517205182051920520205212052220523205242052520526205272052820529205302053120532205332053420535205362053720538205392054020541205422054320544205452054620547205482054920550205512055220553205542055520556205572055820559205602056120562205632056420565205662056720568205692057020571205722057320574205752057620577205782057920580205812058220583205842058520586205872058820589205902059120592205932059420595205962059720598205992060020601206022060320604206052060620607206082060920610206112061220613206142061520616206172061820619206202062120622206232062420625206262062720628206292063020631206322063320634206352063620637206382063920640206412064220643206442064520646206472064820649206502065120652206532065420655206562065720658206592066020661206622066320664206652066620667206682066920670206712067220673206742067520676206772067820679206802068120682206832068420685206862068720688206892069020691206922069320694206952069620697206982069920700207012070220703207042070520706207072070820709207102071120712207132071420715207162071720718207192072020721207222072320724207252072620727207282072920730207312073220733207342073520736207372073820739207402074120742207432074420745207462074720748207492075020751207522075320754207552075620757207582075920760207612076220763207642076520766207672076820769207702077120772207732077420775207762077720778207792078020781207822078320784207852078620787207882078920790207912079220793207942079520796207972079820799208002080120802208032080420805208062080720808208092081020811208122081320814208152081620817208182081920820208212082220823208242082520826208272082820829208302083120832208332083420835208362083720838208392084020841208422084320844208452084620847208482084920850208512085220853208542085520856208572085820859208602086120862208632086420865208662086720868208692087020871208722087320874208752087620877208782087920880208812088220883208842088520886208872088820889208902089120892208932089420895208962089720898208992090020901209022090320904209052090620907209082090920910209112091220913209142091520916209172091820919209202092120922209232092420925209262092720928209292093020931209322093320934209352093620937209382093920940209412094220943209442094520946209472094820949209502095120952209532095420955209562095720958209592096020961209622096320964209652096620967209682096920970209712097220973209742097520976209772097820979209802098120982209832098420985209862098720988209892099020991209922099320994209952099620997209982099921000210012100221003210042100521006210072100821009210102101121012210132101421015210162101721018210192102021021210222102321024210252102621027210282102921030210312103221033210342103521036210372103821039210402104121042210432104421045210462104721048210492105021051210522105321054210552105621057210582105921060210612106221063210642106521066210672106821069210702107121072210732107421075210762107721078210792108021081210822108321084210852108621087210882108921090210912109221093210942109521096210972109821099211002110121102211032110421105211062110721108211092111021111211122111321114211152111621117211182111921120211212112221123211242112521126211272112821129211302113121132211332113421135211362113721138211392114021141211422114321144211452114621147211482114921150211512115221153211542115521156211572115821159211602116121162211632116421165211662116721168211692117021171211722117321174211752117621177211782117921180211812118221183211842118521186211872118821189211902119121192211932119421195211962119721198211992120021201212022120321204212052120621207212082120921210212112121221213212142121521216212172121821219212202122121222212232122421225212262122721228212292123021231212322123321234212352123621237212382123921240212412124221243212442124521246212472124821249212502125121252212532125421255212562125721258212592126021261212622126321264212652126621267212682126921270212712127221273212742127521276212772127821279212802128121282212832128421285212862128721288212892129021291212922129321294212952129621297212982129921300213012130221303213042130521306213072130821309213102131121312213132131421315213162131721318213192132021321213222132321324213252132621327213282132921330213312133221333213342133521336213372133821339213402134121342213432134421345213462134721348213492135021351213522135321354213552135621357213582135921360213612136221363213642136521366213672136821369213702137121372213732137421375213762137721378213792138021381213822138321384213852138621387213882138921390213912139221393213942139521396213972139821399214002140121402214032140421405214062140721408214092141021411214122141321414214152141621417214182141921420214212142221423214242142521426214272142821429214302143121432214332143421435214362143721438214392144021441214422144321444214452144621447214482144921450214512145221453214542145521456214572145821459214602146121462214632146421465214662146721468214692147021471214722147321474214752147621477214782147921480214812148221483214842148521486214872148821489214902149121492214932149421495214962149721498214992150021501215022150321504215052150621507215082150921510215112151221513215142151521516215172151821519215202152121522215232152421525215262152721528215292153021531215322153321534215352153621537215382153921540215412154221543215442154521546215472154821549215502155121552215532155421555215562155721558215592156021561215622156321564215652156621567215682156921570215712157221573215742157521576215772157821579215802158121582215832158421585215862158721588215892159021591215922159321594215952159621597215982159921600216012160221603216042160521606216072160821609216102161121612216132161421615216162161721618216192162021621216222162321624216252162621627216282162921630216312163221633216342163521636216372163821639216402164121642216432164421645216462164721648216492165021651216522165321654216552165621657216582165921660216612166221663216642166521666216672166821669216702167121672216732167421675216762167721678216792168021681216822168321684216852168621687216882168921690216912169221693216942169521696216972169821699217002170121702217032170421705217062170721708217092171021711217122171321714217152171621717217182171921720217212172221723217242172521726217272172821729217302173121732217332173421735217362173721738217392174021741217422174321744217452174621747217482174921750217512175221753217542175521756217572175821759217602176121762217632176421765217662176721768217692177021771217722177321774217752177621777217782177921780217812178221783217842178521786217872178821789217902179121792217932179421795217962179721798217992180021801218022180321804218052180621807218082180921810218112181221813218142181521816218172181821819218202182121822218232182421825218262182721828218292183021831218322183321834218352183621837218382183921840218412184221843218442184521846218472184821849218502185121852218532185421855218562185721858218592186021861218622186321864218652186621867218682186921870218712187221873218742187521876218772187821879218802188121882218832188421885218862188721888218892189021891218922189321894218952189621897218982189921900219012190221903219042190521906219072190821909219102191121912219132191421915219162191721918219192192021921219222192321924219252192621927219282192921930219312193221933219342193521936219372193821939219402194121942219432194421945219462194721948219492195021951219522195321954219552195621957219582195921960219612196221963219642196521966219672196821969219702197121972219732197421975219762197721978219792198021981219822198321984219852198621987219882198921990219912199221993219942199521996219972199821999220002200122002220032200422005220062200722008220092201022011220122201322014220152201622017220182201922020220212202222023220242202522026220272202822029220302203122032220332203422035220362203722038220392204022041220422204322044220452204622047220482204922050220512205222053220542205522056220572205822059220602206122062220632206422065220662206722068220692207022071220722207322074220752207622077220782207922080220812208222083220842208522086220872208822089220902209122092220932209422095220962209722098220992210022101221022210322104221052210622107221082210922110221112211222113221142211522116221172211822119221202212122122221232212422125221262212722128221292213022131221322213322134221352213622137221382213922140221412214222143221442214522146221472214822149221502215122152221532215422155221562215722158221592216022161221622216322164221652216622167221682216922170221712217222173221742217522176221772217822179221802218122182221832218422185221862218722188221892219022191221922219322194221952219622197221982219922200222012220222203222042220522206222072220822209222102221122212222132221422215222162221722218222192222022221222222222322224222252222622227222282222922230222312223222233222342223522236222372223822239222402224122242222432224422245222462224722248222492225022251222522225322254222552225622257222582225922260222612226222263222642226522266222672226822269222702227122272222732227422275222762227722278222792228022281222822228322284222852228622287222882228922290222912229222293222942229522296222972229822299223002230122302223032230422305223062230722308223092231022311223122231322314223152231622317223182231922320223212232222323223242232522326223272232822329223302233122332223332233422335223362233722338223392234022341223422234322344223452234622347223482234922350223512235222353223542235522356223572235822359223602236122362223632236422365223662236722368223692237022371223722237322374223752237622377223782237922380223812238222383223842238522386223872238822389223902239122392223932239422395223962239722398223992240022401224022240322404224052240622407224082240922410224112241222413224142241522416224172241822419224202242122422224232242422425224262242722428224292243022431224322243322434224352243622437224382243922440224412244222443224442244522446224472244822449224502245122452224532245422455224562245722458224592246022461224622246322464224652246622467224682246922470224712247222473224742247522476224772247822479224802248122482224832248422485224862248722488224892249022491224922249322494224952249622497224982249922500225012250222503225042250522506225072250822509225102251122512225132251422515225162251722518225192252022521225222252322524225252252622527225282252922530225312253222533225342253522536225372253822539225402254122542225432254422545225462254722548225492255022551225522255322554225552255622557225582255922560225612256222563225642256522566225672256822569225702257122572225732257422575225762257722578225792258022581225822258322584225852258622587225882258922590225912259222593225942259522596225972259822599226002260122602226032260422605226062260722608226092261022611226122261322614226152261622617226182261922620226212262222623226242262522626226272262822629226302263122632226332263422635226362263722638226392264022641226422264322644226452264622647226482264922650226512265222653226542265522656226572265822659226602266122662226632266422665226662266722668226692267022671226722267322674226752267622677226782267922680226812268222683226842268522686226872268822689226902269122692226932269422695226962269722698226992270022701227022270322704227052270622707227082270922710227112271222713227142271522716227172271822719227202272122722227232272422725227262272722728227292273022731227322273322734227352273622737227382273922740227412274222743227442274522746227472274822749227502275122752227532275422755227562275722758227592276022761227622276322764227652276622767227682276922770227712277222773227742277522776227772277822779227802278122782227832278422785227862278722788227892279022791227922279322794227952279622797227982279922800228012280222803228042280522806228072280822809228102281122812228132281422815228162281722818228192282022821228222282322824228252282622827228282282922830228312283222833228342283522836228372283822839228402284122842228432284422845228462284722848228492285022851228522285322854228552285622857228582285922860228612286222863228642286522866228672286822869228702287122872228732287422875228762287722878228792288022881228822288322884228852288622887228882288922890228912289222893228942289522896228972289822899229002290122902229032290422905229062290722908229092291022911229122291322914229152291622917229182291922920229212292222923229242292522926229272292822929229302293122932229332293422935229362293722938229392294022941229422294322944229452294622947229482294922950229512295222953229542295522956229572295822959229602296122962229632296422965229662296722968229692297022971229722297322974229752297622977229782297922980229812298222983229842298522986229872298822989229902299122992229932299422995229962299722998229992300023001230022300323004230052300623007230082300923010230112301223013230142301523016230172301823019230202302123022230232302423025230262302723028230292303023031230322303323034230352303623037230382303923040230412304223043230442304523046230472304823049230502305123052230532305423055230562305723058230592306023061230622306323064230652306623067230682306923070230712307223073230742307523076230772307823079230802308123082230832308423085230862308723088230892309023091230922309323094230952309623097230982309923100231012310223103231042310523106231072310823109231102311123112231132311423115231162311723118231192312023121231222312323124231252312623127231282312923130231312313223133231342313523136231372313823139231402314123142231432314423145231462314723148231492315023151231522315323154231552315623157231582315923160231612316223163231642316523166231672316823169231702317123172231732317423175231762317723178231792318023181231822318323184231852318623187231882318923190231912319223193231942319523196231972319823199232002320123202232032320423205232062320723208232092321023211232122321323214232152321623217232182321923220232212322223223232242322523226232272322823229232302323123232232332323423235232362323723238232392324023241232422324323244232452324623247232482324923250232512325223253232542325523256232572325823259232602326123262232632326423265232662326723268232692327023271232722327323274232752327623277232782327923280232812328223283232842328523286232872328823289232902329123292232932329423295232962329723298232992330023301233022330323304233052330623307233082330923310233112331223313233142331523316233172331823319233202332123322233232332423325233262332723328233292333023331233322333323334233352333623337233382333923340233412334223343233442334523346233472334823349233502335123352233532335423355233562335723358233592336023361233622336323364233652336623367233682336923370233712337223373233742337523376233772337823379233802338123382233832338423385233862338723388233892339023391233922339323394233952339623397233982339923400234012340223403234042340523406234072340823409234102341123412234132341423415234162341723418234192342023421234222342323424234252342623427234282342923430234312343223433234342343523436234372343823439234402344123442234432344423445234462344723448234492345023451234522345323454234552345623457234582345923460234612346223463234642346523466234672346823469234702347123472234732347423475234762347723478234792348023481234822348323484234852348623487234882348923490234912349223493234942349523496234972349823499235002350123502235032350423505235062350723508235092351023511235122351323514235152351623517235182351923520235212352223523235242352523526235272352823529235302353123532235332353423535235362353723538235392354023541235422354323544235452354623547235482354923550235512355223553235542355523556235572355823559235602356123562235632356423565235662356723568235692357023571235722357323574235752357623577235782357923580235812358223583235842358523586235872358823589235902359123592235932359423595235962359723598235992360023601236022360323604236052360623607236082360923610236112361223613236142361523616236172361823619236202362123622236232362423625236262362723628236292363023631236322363323634236352363623637236382363923640236412364223643236442364523646236472364823649236502365123652236532365423655236562365723658236592366023661236622366323664236652366623667236682366923670236712367223673236742367523676236772367823679236802368123682236832368423685236862368723688236892369023691236922369323694236952369623697236982369923700237012370223703237042370523706237072370823709237102371123712237132371423715237162371723718237192372023721237222372323724237252372623727237282372923730237312373223733237342373523736237372373823739237402374123742237432374423745237462374723748237492375023751237522375323754237552375623757237582375923760237612376223763237642376523766237672376823769237702377123772237732377423775237762377723778237792378023781237822378323784237852378623787237882378923790237912379223793237942379523796237972379823799238002380123802238032380423805238062380723808238092381023811238122381323814238152381623817238182381923820238212382223823238242382523826238272382823829238302383123832238332383423835238362383723838238392384023841238422384323844238452384623847238482384923850238512385223853238542385523856238572385823859238602386123862238632386423865238662386723868238692387023871238722387323874238752387623877238782387923880238812388223883238842388523886238872388823889238902389123892238932389423895238962389723898238992390023901239022390323904239052390623907239082390923910239112391223913239142391523916239172391823919239202392123922239232392423925239262392723928239292393023931239322393323934239352393623937239382393923940239412394223943239442394523946239472394823949239502395123952239532395423955239562395723958239592396023961239622396323964239652396623967239682396923970239712397223973239742397523976239772397823979239802398123982239832398423985239862398723988239892399023991239922399323994239952399623997239982399924000240012400224003240042400524006240072400824009240102401124012240132401424015240162401724018240192402024021240222402324024240252402624027240282402924030240312403224033240342403524036240372403824039240402404124042240432404424045240462404724048240492405024051240522405324054240552405624057240582405924060240612406224063240642406524066240672406824069240702407124072240732407424075240762407724078240792408024081240822408324084240852408624087240882408924090240912409224093240942409524096240972409824099241002410124102241032410424105241062410724108241092411024111241122411324114241152411624117241182411924120241212412224123241242412524126241272412824129241302413124132241332413424135241362413724138241392414024141241422414324144241452414624147241482414924150241512415224153241542415524156241572415824159241602416124162241632416424165241662416724168241692417024171241722417324174241752417624177241782417924180241812418224183241842418524186241872418824189241902419124192241932419424195241962419724198241992420024201242022420324204242052420624207242082420924210242112421224213242142421524216242172421824219242202422124222242232422424225242262422724228242292423024231242322423324234242352423624237242382423924240242412424224243242442424524246242472424824249242502425124252242532425424255242562425724258242592426024261242622426324264
  1. # tifffile.py
  2. # Copyright (c) 2008-2025, Christoph Gohlke
  3. # All rights reserved.
  4. #
  5. # Redistribution and use in source and binary forms, with or without
  6. # modification, are permitted provided that the following conditions are met:
  7. #
  8. # 1. Redistributions of source code must retain the above copyright notice,
  9. # this list of conditions and the following disclaimer.
  10. #
  11. # 2. Redistributions in binary form must reproduce the above copyright notice,
  12. # this list of conditions and the following disclaimer in the documentation
  13. # and/or other materials provided with the distribution.
  14. #
  15. # 3. Neither the name of the copyright holder nor the names of its
  16. # contributors may be used to endorse or promote products derived from
  17. # this software without specific prior written permission.
  18. #
  19. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  20. # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  21. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  22. # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  23. # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  24. # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  25. # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  26. # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  27. # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  28. # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  29. # POSSIBILITY OF SUCH DAMAGE.
  30. r"""Read and write TIFF files.
  31. Tifffile is a Python library to
  32. (1) store NumPy arrays in TIFF (Tagged Image File Format) files, and
  33. (2) read image and metadata from TIFF-like files used in bioimaging.
  34. Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, GeoTIFF,
  35. Adobe DNG, ZIF (Zoomable Image File Format), MetaMorph STK, Zeiss LSM,
  36. ImageJ hyperstack, Micro-Manager MMStack and NDTiff, SGI, NIHImage,
  37. Olympus FluoView and SIS, ScanImage, Molecular Dynamics GEL,
  38. Aperio SVS, Leica SCN, Roche BIF, PerkinElmer QPTIFF (QPI, PKI),
  39. Hamamatsu NDPI, Argos AVS, Philips DP, and ThermoFisher EER formatted files.
  40. Image data can be read as NumPy arrays or Zarr arrays/groups from strips,
  41. tiles, pages (IFDs), SubIFDs, higher-order series, and pyramidal levels.
  42. Image data can be written to TIFF, BigTIFF, OME-TIFF, and ImageJ hyperstack
  43. compatible files in multi-page, volumetric, pyramidal, memory-mappable,
  44. tiled, predicted, or compressed form.
  45. Many compression and predictor schemes are supported via the imagecodecs
  46. library, including LZW, PackBits, Deflate, PIXTIFF, LZMA, LERC, Zstd,
  47. JPEG (8 and 12-bit, lossless), JPEG 2000, JPEG XR, JPEG XL, WebP, PNG, EER,
  48. Jetraw, 24-bit floating-point, and horizontal differencing.
  49. Tifffile can also be used to inspect TIFF structures, read image data from
  50. multi-dimensional file sequences, write fsspec ReferenceFileSystem for
  51. TIFF files and image file sequences, patch TIFF tag values, and parse
  52. many proprietary metadata formats.
  53. :Author: `Christoph Gohlke <https://www.cgohlke.com>`_
  54. :License: BSD-3-Clause
  55. :Version: 2025.12.20
  56. :DOI: `10.5281/zenodo.6795860 <https://doi.org/10.5281/zenodo.6795860>`_
  57. Quickstart
  58. ----------
  59. Install the tifffile package and all dependencies from the
  60. `Python Package Index <https://pypi.org/project/tifffile/>`_::
  61. python -m pip install -U tifffile[all]
  62. Tifffile is also available in other package repositories such as Anaconda,
  63. Debian, and MSYS2.
  64. The tifffile library is type annotated and documented via docstrings::
  65. python -c "import tifffile; help(tifffile)"
  66. Tifffile can be used as a console script to inspect and preview TIFF files::
  67. python -m tifffile --help
  68. See `Examples`_ for using the programming interface.
  69. Source code and support are available on
  70. `GitHub <https://github.com/cgohlke/tifffile>`_.
  71. Support is also provided on the
  72. `image.sc <https://forum.image.sc/tag/tifffile>`_ forum.
  73. Requirements
  74. ------------
  75. This revision was tested with the following requirements and dependencies
  76. (other versions may work):
  77. - `CPython <https://www.python.org>`_ 3.11.9, 3.12.10, 3.13.11, 3.14.2 64-bit
  78. - `NumPy <https://pypi.org/project/numpy>`_ 2.4.0
  79. - `Imagecodecs <https://pypi.org/project/imagecodecs/>`_ 2025.11.11
  80. (required for encoding or decoding LZW, JPEG, etc. compressed segments)
  81. - `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.10.8
  82. (required for plotting)
  83. - `Lxml <https://pypi.org/project/lxml/>`_ 6.0.2
  84. (required only for validating and printing XML)
  85. - `Zarr <https://pypi.org/project/zarr/>`_ 3.1.5
  86. (required only for using Zarr stores; Zarr 2 is not compatible)
  87. - `Kerchunk <https://pypi.org/project/kerchunk/>`_ 0.2.9
  88. (required only for opening ReferenceFileSystem files)
  89. Revisions
  90. ---------
  91. 2025.12.20
  92. - Pass 5128 tests.
  93. - Do not initialize output arrays.
  94. 2025.12.12
  95. - Improve code quality.
  96. 2025.10.16
  97. - Add option to decode EER super-resolution sub-pixels (breaking, #313).
  98. - Parse EER metadata to dict (breaking).
  99. 2025.10.4
  100. - Fix parsing SVS description ending with "|".
  101. 2025.9.30
  102. - Fix reading NDTiff series with unordered axes in index (#311).
  103. 2025.9.20
  104. - Derive TiffFileError from ValueError.
  105. - Natural-sort files in glob pattern passed to imread by default (breaking).
  106. - Fix optional sorting of list of files passed to FileSequence and imread.
  107. 2025.9.9
  108. - Consolidate Nuvu camera metadata.
  109. 2025.8.28
  110. - Support DNG DCP files (#306).
  111. 2025.6.11
  112. - Fix reading images with dimension length 1 through Zarr (#303).
  113. 2025.6.1
  114. - Add experimental option to write iterator of bytes and bytecounts (#301).
  115. 2025.5.26
  116. - Use threads in Zarr stores.
  117. 2025.5.24
  118. - Fix incorrect tags created by Philips DP v1.1 (#299).
  119. - Make Zarr stores partially listable.
  120. 2025.5.21
  121. - Move Zarr stores to tifffile.zarr namespace (breaking).
  122. - Require Zarr 3 for Zarr stores and remove support for Zarr 2 (breaking).
  123. - Drop support for Python 3.10.
  124. 2025.5.10
  125. - Raise ValueError when using Zarr 3 (#296).
  126. - Fall back to compression.zstd on Python >= 3.14 if no imagecodecs.
  127. - Remove doctest command line option.
  128. - Support Python 3.14.
  129. 2025.3.30
  130. - …
  131. Refer to the CHANGES file for older revisions.
  132. Notes
  133. -----
  134. TIFF, the Tagged Image File Format, was created by the Aldus Corporation and
  135. Adobe Systems Incorporated.
  136. Tifffile supports a subset of the TIFF6 specification, mainly 8, 16, 32, and
  137. 64-bit integer, 16, 32, and 64-bit float, grayscale and multi-sample images.
  138. Specifically, CCITT and OJPEG compression, chroma subsampling without JPEG
  139. compression, color space transformations, samples with differing types, or
  140. IPTC, ICC, and XMP metadata are not implemented.
  141. Besides classic TIFF, tifffile supports several TIFF-like formats that do not
  142. strictly adhere to the TIFF6 specification. Some formats allow file and data
  143. sizes to exceed the 4 GB limit of the classic TIFF:
  144. - **BigTIFF** is identified by version number 43 and uses different file
  145. header, IFD, and tag structures with 64-bit offsets. The format also adds
  146. 64-bit data types. Tifffile can read and write BigTIFF files.
  147. - **ImageJ hyperstacks** store all image data, which may exceed 4 GB,
  148. contiguously after the first IFD. Files > 4 GB contain one IFD only.
  149. The size and shape of the up to 6-dimensional image data can be determined
  150. from the ImageDescription tag of the first IFD, which is Latin-1 encoded.
  151. Tifffile can read and write ImageJ hyperstacks.
  152. - **OME-TIFF** files store up to 8-dimensional image data in one or multiple
  153. TIFF or BigTIFF files. The UTF-8 encoded OME-XML metadata found in the
  154. ImageDescription tag of the first IFD defines the position of TIFF IFDs in
  155. the high-dimensional image data. Tifffile can read OME-TIFF files (except
  156. multi-file pyramidal) and write NumPy arrays to single-file OME-TIFF.
  157. - **Micro-Manager NDTiff** stores multi-dimensional image data in one
  158. or more classic TIFF files. Metadata contained in a separate NDTiff.index
  159. binary file defines the position of the TIFF IFDs in the image array.
  160. Each TIFF file also contains metadata in a non-TIFF binary structure at
  161. offset 8. Downsampled image data of pyramidal datasets are stored in
  162. separate folders. Tifffile can read NDTiff files. Version 0 and 1 series,
  163. tiling, stitching, and multi-resolution pyramids are not supported.
  164. - **Micro-Manager MMStack** stores 6-dimensional image data in one or more
  165. classic TIFF files. Metadata contained in non-TIFF binary structures and
  166. JSON strings define the image stack dimensions and the position of the image
  167. frame data in the file and the image stack. The TIFF structures and metadata
  168. are often corrupted or wrong. Tifffile can read MMStack files.
  169. - **Carl Zeiss LSM** files store all IFDs below 4 GB and wrap around 32-bit
  170. StripOffsets pointing to image data above 4 GB. The StripOffsets of each
  171. series and position require separate unwrapping. The StripByteCounts tag
  172. contains the number of bytes for the uncompressed data. Tifffile can read
  173. LSM files of any size.
  174. - **MetaMorph STK** files contain additional image planes stored
  175. contiguously after the image data of the first page. The total number of
  176. planes is equal to the count of the UIC2tag. Tifffile can read STK files.
  177. - **ZIF**, the Zoomable Image File format, is a subspecification of BigTIFF
  178. with SGI's ImageDepth extension and additional compression schemes.
  179. Only little-endian, tiled, interleaved, 8-bit per sample images with
  180. JPEG, PNG, JPEG XR, and JPEG 2000 compression are allowed. Tifffile can
  181. read and write ZIF files.
  182. - **Hamamatsu NDPI** files use some 64-bit offsets in the file header, IFD,
  183. and tag structures. Single, LONG typed tag values can exceed 32-bit.
  184. The high bytes of 64-bit tag values and offsets are stored after IFD
  185. structures. Tifffile can read NDPI files > 4 GB.
  186. JPEG compressed segments with dimensions >65530 or missing restart markers
  187. cannot be decoded with common JPEG libraries. Tifffile works around this
  188. limitation by separately decoding the MCUs between restart markers, which
  189. performs poorly. BitsPerSample, SamplesPerPixel, and
  190. PhotometricInterpretation tags may contain wrong values, which can be
  191. corrected using the value of tag 65441.
  192. - **Philips TIFF** slides store padded ImageWidth and ImageLength tag values
  193. for tiled pages. The values can be corrected using the DICOM_PIXEL_SPACING
  194. attributes of the XML formatted description of the first page. Tile offsets
  195. and byte counts may be 0. Tifffile can read Philips slides.
  196. - **Ventana/Roche BIF** slides store tiles and metadata in a BigTIFF container.
  197. Tiles may overlap and require stitching based on the TileJointInfo elements
  198. in the XMP tag. Volumetric scans are stored using the ImageDepth extension.
  199. Tifffile can read BIF and decode individual tiles but does not perform
  200. stitching.
  201. - **ScanImage** optionally allows corrupted non-BigTIFF files > 2 GB.
  202. The values of StripOffsets and StripByteCounts can be recovered using the
  203. constant differences of the offsets of IFD and tag values throughout the
  204. file. Tifffile can read such files if the image data are stored contiguously
  205. in each page.
  206. - **GeoTIFF sparse** files allow strip or tile offsets and byte counts to be 0.
  207. Such segments are implicitly set to 0 or the NODATA value on reading.
  208. Tifffile can read GeoTIFF sparse files.
  209. - **Tifffile shaped** files store the array shape and user-provided metadata
  210. of multi-dimensional image series in JSON format in the ImageDescription tag
  211. of the first page of the series. The format allows multiple series,
  212. SubIFDs, sparse segments with zero offset and byte count, and truncated
  213. series, where only the first page of a series is present, and the image data
  214. are stored contiguously. No other software besides Tifffile supports the
  215. truncated format.
  216. Other libraries for reading, writing, inspecting, or manipulating scientific
  217. TIFF files from Python are
  218. `bioio <https://github.com/bioio-devs/bioio>`_,
  219. `aicsimageio <https://github.com/AllenCellModeling/aicsimageio>`_,
  220. `apeer-ometiff-library
  221. <https://github.com/apeer-micro/apeer-ometiff-library>`_,
  222. `bigtiff <https://pypi.org/project/bigtiff>`_,
  223. `fabio.TiffIO <https://github.com/silx-kit/fabio>`_,
  224. `GDAL <https://github.com/OSGeo/gdal/>`_,
  225. `imread <https://github.com/luispedro/imread>`_,
  226. `large_image <https://github.com/girder/large_image>`_,
  227. `openslide-python <https://github.com/openslide/openslide-python>`_,
  228. `opentile <https://github.com/imi-bigpicture/opentile>`_,
  229. `pylibtiff <https://github.com/pearu/pylibtiff>`_,
  230. `pylsm <https://launchpad.net/pylsm>`_,
  231. `pymimage <https://github.com/ardoi/pymimage>`_,
  232. `python-bioformats <https://github.com/CellProfiler/python-bioformats>`_,
  233. `pytiff <https://github.com/FZJ-INM1-BDA/pytiff>`_,
  234. `scanimagetiffreader-python
  235. <https://gitlab.com/vidriotech/scanimagetiffreader-python>`_,
  236. `SimpleITK <https://github.com/SimpleITK/SimpleITK>`_,
  237. `slideio <https://gitlab.com/bioslide/slideio>`_,
  238. `tiffslide <https://github.com/bayer-science-for-a-better-life/tiffslide>`_,
  239. `tifftools <https://github.com/DigitalSlideArchive/tifftools>`_,
  240. `tyf <https://github.com/Moustikitos/tyf>`_,
  241. `xtiff <https://github.com/BodenmillerGroup/xtiff>`_, and
  242. `ndtiff <https://github.com/micro-manager/NDTiffStorage>`_.
  243. References
  244. ----------
  245. - TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated.
  246. https://www.adobe.io/open/standards/TIFF.html
  247. https://download.osgeo.org/libtiff/doc/
  248. - TIFF File Format FAQ. https://www.awaresystems.be/imaging/tiff/faq.html
  249. - The BigTIFF File Format.
  250. https://www.awaresystems.be/imaging/tiff/bigtiff.html
  251. - MetaMorph Stack (STK) Image File Format.
  252. http://mdc.custhelp.com/app/answers/detail/a_id/18862
  253. - Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010).
  254. Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011
  255. - The OME-TIFF format.
  256. https://docs.openmicroscopy.org/ome-model/latest/
  257. - UltraQuant(r) Version 6.0 for Windows Start-Up Guide.
  258. http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf
  259. - Micro-Manager File Formats.
  260. https://micro-manager.org/wiki/Micro-Manager_File_Formats
  261. - ScanImage BigTiff Specification.
  262. https://docs.scanimage.org/Appendix/ScanImage+BigTiff+Specification.html
  263. - ZIF, the Zoomable Image File format. https://zif.photo/
  264. - GeoTIFF File Format. https://gdal.org/drivers/raster/gtiff.html
  265. - Cloud optimized GeoTIFF.
  266. https://github.com/cogeotiff/cog-spec/blob/master/spec.md
  267. - Tags for TIFF and Related Specifications. Digital Preservation.
  268. https://www.loc.gov/preservation/digital/formats/content/tiff_tags.shtml
  269. - CIPA DC-008-2016: Exchangeable image file format for digital still cameras:
  270. Exif Version 2.31.
  271. http://www.cipa.jp/std/documents/e/DC-008-Translation-2016-E.pdf
  272. - The EER (Electron Event Representation) file format.
  273. https://github.com/fei-company/EerReaderLib
  274. - Digital Negative (DNG) Specification. Version 1.7.1.0, September 2023.
  275. https://helpx.adobe.com/content/dam/help/en/photoshop/pdf/DNG_Spec_1_7_1_0.pdf
  276. - Roche Digital Pathology. BIF image file format for digital pathology.
  277. https://diagnostics.roche.com/content/dam/diagnostics/Blueprint/en/pdf/rmd/Roche-Digital-Pathology-BIF-Whitepaper.pdf
  278. - Astro-TIFF specification. https://astro-tiff.sourceforge.io/
  279. - Aperio Technologies, Inc. Digital Slides and Third-Party Data Interchange.
  280. Aperio_Digital_Slides_and_Third-party_data_interchange.pdf
  281. - PerkinElmer image format.
  282. https://downloads.openmicroscopy.org/images/Vectra-QPTIFF/perkinelmer/PKI_Image%20Format.docx
  283. - NDTiffStorage. https://github.com/micro-manager/NDTiffStorage
  284. - Argos AVS File Format.
  285. https://github.com/user-attachments/files/15580286/ARGOS.AVS.File.Format.pdf
  286. Examples
  287. --------
  288. Write a NumPy array to a single-page RGB TIFF file:
  289. >>> import numpy
  290. >>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
  291. >>> imwrite('temp.tif', data, photometric='rgb')
  292. Read the image from the TIFF file as NumPy array:
  293. >>> image = imread('temp.tif')
  294. >>> image.shape
  295. (256, 256, 3)
  296. Use the `photometric` and `planarconfig` arguments to write a 3x3x3 NumPy
  297. array to an interleaved RGB, a planar RGB, or a 3-page grayscale TIFF:
  298. >>> data = numpy.random.randint(0, 255, (3, 3, 3), 'uint8')
  299. >>> imwrite('temp.tif', data, photometric='rgb')
  300. >>> imwrite('temp.tif', data, photometric='rgb', planarconfig='separate')
  301. >>> imwrite('temp.tif', data, photometric='minisblack')
  302. Use the `extrasamples` argument to specify how extra components are
  303. interpreted, for example, for an RGBA image with unassociated alpha channel:
  304. >>> data = numpy.random.randint(0, 255, (256, 256, 4), 'uint8')
  305. >>> imwrite('temp.tif', data, photometric='rgb', extrasamples=['unassalpha'])
  306. Write a 3-dimensional NumPy array to a multi-page, 16-bit grayscale TIFF file:
  307. >>> data = numpy.random.randint(0, 2**12, (64, 301, 219), 'uint16')
  308. >>> imwrite('temp.tif', data, photometric='minisblack')
  309. Read the whole image stack from the multi-page TIFF file as NumPy array:
  310. >>> image_stack = imread('temp.tif')
  311. >>> image_stack.shape
  312. (64, 301, 219)
  313. >>> image_stack.dtype
  314. dtype('uint16')
  315. Read the image from the first page in the TIFF file as NumPy array:
  316. >>> image = imread('temp.tif', key=0)
  317. >>> image.shape
  318. (301, 219)
  319. Read images from a selected range of pages:
  320. >>> images = imread('temp.tif', key=range(4, 40, 2))
  321. >>> images.shape
  322. (18, 301, 219)
  323. Iterate over all pages in the TIFF file and successively read images:
  324. >>> with TiffFile('temp.tif') as tif:
  325. ... for page in tif.pages:
  326. ... image = page.asarray()
  327. ...
  328. Get information about the image stack in the TIFF file without reading
  329. any image data:
  330. >>> tif = TiffFile('temp.tif')
  331. >>> len(tif.pages) # number of pages in the file
  332. 64
  333. >>> page = tif.pages[0] # get shape and dtype of image in first page
  334. >>> page.shape
  335. (301, 219)
  336. >>> page.dtype
  337. dtype('uint16')
  338. >>> page.axes
  339. 'YX'
  340. >>> series = tif.series[0] # get shape and dtype of first image series
  341. >>> series.shape
  342. (64, 301, 219)
  343. >>> series.dtype
  344. dtype('uint16')
  345. >>> series.axes
  346. 'QYX'
  347. >>> tif.close()
  348. Inspect the "XResolution" tag from the first page in the TIFF file:
  349. >>> with TiffFile('temp.tif') as tif:
  350. ... tag = tif.pages[0].tags['XResolution']
  351. ...
  352. >>> tag.value
  353. (1, 1)
  354. >>> tag.name
  355. 'XResolution'
  356. >>> tag.code
  357. 282
  358. >>> tag.count
  359. 1
  360. >>> tag.dtype
  361. <DATATYPE.RATIONAL: 5>
  362. Iterate over all tags in the TIFF file:
  363. >>> with TiffFile('temp.tif') as tif:
  364. ... for page in tif.pages:
  365. ... for tag in page.tags:
  366. ... tag_name, tag_value = tag.name, tag.value
  367. ...
  368. Overwrite the value of an existing tag, for example, XResolution:
  369. >>> with TiffFile('temp.tif', mode='r+') as tif:
  370. ... _ = tif.pages[0].tags['XResolution'].overwrite((96000, 1000))
  371. ...
  372. Write a 5-dimensional floating-point array using BigTIFF format, separate
  373. color components, tiling, Zlib compression level 8, horizontal differencing
  374. predictor, and additional metadata:
  375. >>> data = numpy.random.rand(2, 5, 3, 301, 219).astype('float32')
  376. >>> imwrite(
  377. ... 'temp.tif',
  378. ... data,
  379. ... bigtiff=True,
  380. ... photometric='rgb',
  381. ... planarconfig='separate',
  382. ... tile=(32, 32),
  383. ... compression='zlib',
  384. ... compressionargs={'level': 8},
  385. ... predictor=True,
  386. ... metadata={'axes': 'TZCYX'},
  387. ... )
  388. Write a 10 fps time series of volumes with xyz voxel size 2.6755x2.6755x3.9474
  389. micron^3 to an ImageJ hyperstack formatted TIFF file:
  390. >>> volume = numpy.random.randn(6, 57, 256, 256).astype('float32')
  391. >>> image_labels = [f'{i}' for i in range(volume.shape[0] * volume.shape[1])]
  392. >>> imwrite(
  393. ... 'temp.tif',
  394. ... volume,
  395. ... imagej=True,
  396. ... resolution=(1.0 / 2.6755, 1.0 / 2.6755),
  397. ... metadata={
  398. ... 'spacing': 3.947368,
  399. ... 'unit': 'um',
  400. ... 'finterval': 1 / 10,
  401. ... 'fps': 10.0,
  402. ... 'axes': 'TZYX',
  403. ... 'Labels': image_labels,
  404. ... },
  405. ... )
  406. Read the volume and metadata from the ImageJ hyperstack file:
  407. >>> with TiffFile('temp.tif') as tif:
  408. ... volume = tif.asarray()
  409. ... axes = tif.series[0].axes
  410. ... imagej_metadata = tif.imagej_metadata
  411. ...
  412. >>> volume.shape
  413. (6, 57, 256, 256)
  414. >>> axes
  415. 'TZYX'
  416. >>> imagej_metadata['slices']
  417. 57
  418. >>> imagej_metadata['frames']
  419. 6
  420. Memory-map the contiguous image data in the ImageJ hyperstack file:
  421. >>> memmap_volume = memmap('temp.tif')
  422. >>> memmap_volume.shape
  423. (6, 57, 256, 256)
  424. >>> del memmap_volume
  425. Create a TIFF file containing an empty image and write to the memory-mapped
  426. NumPy array (note: this does not work with compression or tiling):
  427. >>> memmap_image = memmap(
  428. ... 'temp.tif', shape=(256, 256, 3), dtype='float32', photometric='rgb'
  429. ... )
  430. >>> type(memmap_image)
  431. <class 'numpy.memmap'>
  432. >>> memmap_image[255, 255, 1] = 1.0
  433. >>> memmap_image.flush()
  434. >>> del memmap_image
  435. Write two NumPy arrays to a multi-series TIFF file (note: other TIFF readers
  436. will not recognize the two series; use the OME-TIFF format for better
  437. interoperability):
  438. >>> series0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
  439. >>> series1 = numpy.random.randint(0, 255, (4, 256, 256), 'uint16')
  440. >>> with TiffWriter('temp.tif') as tif:
  441. ... tif.write(series0, photometric='rgb')
  442. ... tif.write(series1, photometric='minisblack')
  443. ...
  444. Read the second image series from the TIFF file:
  445. >>> series1 = imread('temp.tif', series=1)
  446. >>> series1.shape
  447. (4, 256, 256)
  448. Successively write the frames of one contiguous series to a TIFF file:
  449. >>> data = numpy.random.randint(0, 255, (30, 301, 219), 'uint8')
  450. >>> with TiffWriter('temp.tif') as tif:
  451. ... for frame in data:
  452. ... tif.write(frame, contiguous=True)
  453. ...
  454. Append an image series to the existing TIFF file (note: this does not work
  455. with ImageJ hyperstack or OME-TIFF files):
  456. >>> data = numpy.random.randint(0, 255, (301, 219, 3), 'uint8')
  457. >>> imwrite('temp.tif', data, photometric='rgb', append=True)
  458. Create a TIFF file from a generator of tiles:
  459. >>> data = numpy.random.randint(0, 2**12, (31, 33, 3), 'uint16')
  460. >>> def tiles(data, tileshape):
  461. ... for y in range(0, data.shape[0], tileshape[0]):
  462. ... for x in range(0, data.shape[1], tileshape[1]):
  463. ... yield data[y : y + tileshape[0], x : x + tileshape[1]]
  464. ...
  465. >>> imwrite(
  466. ... 'temp.tif',
  467. ... tiles(data, (16, 16)),
  468. ... tile=(16, 16),
  469. ... shape=data.shape,
  470. ... dtype=data.dtype,
  471. ... photometric='rgb',
  472. ... )
  473. Write a multi-dimensional, multi-resolution (pyramidal), multi-series OME-TIFF
  474. file with optional metadata. Sub-resolution images are written to SubIFDs.
  475. Limit parallel encoding to 2 threads. Write a thumbnail image as a separate
  476. image series:
  477. >>> data = numpy.random.randint(0, 255, (8, 2, 512, 512, 3), 'uint8')
  478. >>> subresolutions = 2
  479. >>> pixelsize = 0.29 # micrometer
  480. >>> with TiffWriter('temp.ome.tif', bigtiff=True) as tif:
  481. ... metadata = {
  482. ... 'axes': 'TCYXS',
  483. ... 'SignificantBits': 8,
  484. ... 'TimeIncrement': 0.1,
  485. ... 'TimeIncrementUnit': 's',
  486. ... 'PhysicalSizeX': pixelsize,
  487. ... 'PhysicalSizeXUnit': 'µm',
  488. ... 'PhysicalSizeY': pixelsize,
  489. ... 'PhysicalSizeYUnit': 'µm',
  490. ... 'Channel': {'Name': ['Channel 1', 'Channel 2']},
  491. ... 'Plane': {'PositionX': [0.0] * 16, 'PositionXUnit': ['µm'] * 16},
  492. ... 'Description': 'A multi-dimensional, multi-resolution image',
  493. ... 'MapAnnotation': { # for OMERO
  494. ... 'Namespace': 'openmicroscopy.org/PyramidResolution',
  495. ... '1': '256 256',
  496. ... '2': '128 128',
  497. ... },
  498. ... }
  499. ... options = dict(
  500. ... photometric='rgb',
  501. ... tile=(128, 128),
  502. ... compression='jpeg',
  503. ... resolutionunit='CENTIMETER',
  504. ... maxworkers=2,
  505. ... )
  506. ... tif.write(
  507. ... data,
  508. ... subifds=subresolutions,
  509. ... resolution=(1e4 / pixelsize, 1e4 / pixelsize),
  510. ... metadata=metadata,
  511. ... **options,
  512. ... )
  513. ... # write pyramid levels to the two subifds
  514. ... # in production use resampling to generate sub-resolution images
  515. ... for level in range(subresolutions):
  516. ... mag = 2 ** (level + 1)
  517. ... tif.write(
  518. ... data[..., ::mag, ::mag, :],
  519. ... subfiletype=1, # FILETYPE.REDUCEDIMAGE
  520. ... resolution=(1e4 / mag / pixelsize, 1e4 / mag / pixelsize),
  521. ... **options,
  522. ... )
  523. ... # add a thumbnail image as a separate series
  524. ... # it is recognized by QuPath as an associated image
  525. ... thumbnail = (data[0, 0, ::8, ::8] >> 2).astype('uint8')
  526. ... tif.write(thumbnail, metadata={'Name': 'thumbnail'})
  527. ...
  528. Access the image levels in the pyramidal OME-TIFF file:
  529. >>> baseimage = imread('temp.ome.tif')
  530. >>> second_level = imread('temp.ome.tif', series=0, level=1)
  531. >>> with TiffFile('temp.ome.tif') as tif:
  532. ... baseimage = tif.series[0].asarray()
  533. ... second_level = tif.series[0].levels[1].asarray()
  534. ... number_levels = len(tif.series[0].levels) # includes base level
  535. ...
  536. Iterate over and decode single JPEG compressed tiles in the TIFF file:
  537. >>> with TiffFile('temp.ome.tif') as tif:
  538. ... fh = tif.filehandle
  539. ... for page in tif.pages:
  540. ... for index, (offset, bytecount) in enumerate(
  541. ... zip(page.dataoffsets, page.databytecounts)
  542. ... ):
  543. ... _ = fh.seek(offset)
  544. ... data = fh.read(bytecount)
  545. ... tile, indices, shape = page.decode(
  546. ... data, index, jpegtables=page.jpegtables
  547. ... )
  548. ...
  549. Use Zarr to read parts of the tiled, pyramidal images in the TIFF file:
  550. >>> import zarr
  551. >>> store = imread('temp.ome.tif', aszarr=True)
  552. >>> z = zarr.open(store, mode='r')
  553. >>> z
  554. <Group ZarrTiffStore>
  555. >>> z['0'] # base layer
  556. <Array ZarrTiffStore/0 shape=(8, 2, 512, 512, 3) dtype=uint8>
  557. >>> z['0'][2, 0, 128:384, 256:].shape # read a tile from the base layer
  558. (256, 256, 3)
  559. >>> store.close()
  560. Load the base layer from the Zarr store as a dask array:
  561. >>> import dask.array
  562. >>> store = imread('temp.ome.tif', aszarr=True)
  563. >>> dask.array.from_zarr(store, '0', zarr_format=2)
  564. dask.array<...shape=(8, 2, 512, 512, 3)...chunksize=(1, 1, 128, 128, 3)...
  565. >>> store.close()
  566. Write the Zarr store to a fsspec ReferenceFileSystem in JSON format:
  567. >>> store = imread('temp.ome.tif', aszarr=True)
  568. >>> store.write_fsspec('temp.ome.tif.json', url='file://')
  569. >>> store.close()
  570. Open the fsspec ReferenceFileSystem as a Zarr group:
  571. >>> from kerchunk.utils import refs_as_store
  572. >>> import imagecodecs.numcodecs
  573. >>> imagecodecs.numcodecs.register_codecs(verbose=False)
  574. >>> z = zarr.open(refs_as_store('temp.ome.tif.json'), mode='r')
  575. >>> z
  576. <Group <FsspecStore(ReferenceFileSystem, /)>>
  577. Create an OME-TIFF file containing an empty, tiled image series and write
  578. to it via the Zarr interface (note: this does not work with compression):
  579. >>> imwrite(
  580. ... 'temp2.ome.tif',
  581. ... shape=(8, 800, 600),
  582. ... dtype='uint16',
  583. ... photometric='minisblack',
  584. ... tile=(128, 128),
  585. ... metadata={'axes': 'CYX'},
  586. ... )
  587. >>> store = imread('temp2.ome.tif', mode='r+', aszarr=True)
  588. >>> z = zarr.open(store, mode='r+')
  589. >>> z
  590. <Array ZarrTiffStore shape=(8, 800, 600) dtype=uint16>
  591. >>> z[3, 100:200, 200:300:2] = 1024
  592. >>> store.close()
  593. Read images from a sequence of TIFF files as NumPy array using two I/O worker
  594. threads:
  595. >>> imwrite('temp_C001T001.tif', numpy.random.rand(64, 64))
  596. >>> imwrite('temp_C001T002.tif', numpy.random.rand(64, 64))
  597. >>> image_sequence = imread(
  598. ... ['temp_C001T001.tif', 'temp_C001T002.tif'], ioworkers=2, maxworkers=1
  599. ... )
  600. >>> image_sequence.shape
  601. (2, 64, 64)
  602. >>> image_sequence.dtype
  603. dtype('float64')
  604. Read an image stack from a series of TIFF files with a file name pattern
  605. as NumPy or Zarr arrays:
  606. >>> image_sequence = TiffSequence('temp_C0*.tif', pattern=r'_(C)(\d+)(T)(\d+)')
  607. >>> image_sequence.shape
  608. (1, 2)
  609. >>> image_sequence.axes
  610. 'CT'
  611. >>> data = image_sequence.asarray()
  612. >>> data.shape
  613. (1, 2, 64, 64)
  614. >>> store = image_sequence.aszarr()
  615. >>> zarr.open(store, mode='r', ioworkers=2, maxworkers=1)
  616. <Array ZarrFileSequenceStore shape=(1, 2, 64, 64) dtype=float64>
  617. >>> image_sequence.close()
  618. Write the Zarr store to a fsspec ReferenceFileSystem in JSON format:
  619. >>> store = image_sequence.aszarr()
  620. >>> store.write_fsspec('temp.json', url='file://')
  621. Open the fsspec ReferenceFileSystem as a Zarr array:
  622. >>> from kerchunk.utils import refs_as_store
  623. >>> import tifffile.numcodecs
  624. >>> tifffile.numcodecs.register_codec()
  625. >>> zarr.open(refs_as_store('temp.json'), mode='r')
  626. <Array <FsspecStore(ReferenceFileSystem, /)> shape=(1, 2, 64, 64) ...>
  627. Inspect the TIFF file from the command line::
  628. $ python -m tifffile temp.ome.tif
  629. """
  630. from __future__ import annotations
  631. __version__ = '2025.12.20'
  632. __all__ = [
  633. 'CHUNKMODE',
  634. 'COMPRESSION',
  635. 'DATATYPE',
  636. 'EXTRASAMPLE',
  637. 'FILETYPE',
  638. 'FILLORDER',
  639. 'OFILETYPE',
  640. 'ORIENTATION',
  641. 'PHOTOMETRIC',
  642. 'PLANARCONFIG',
  643. 'PREDICTOR',
  644. 'RESUNIT',
  645. 'SAMPLEFORMAT',
  646. 'TIFF',
  647. '_TIFF', # private
  648. 'FileCache',
  649. 'FileHandle',
  650. 'FileSequence',
  651. 'NullContext',
  652. 'OmeXml',
  653. 'OmeXmlError',
  654. 'StoredShape',
  655. 'TiffFile',
  656. 'TiffFileError',
  657. 'TiffFormat',
  658. 'TiffFrame',
  659. 'TiffPage',
  660. 'TiffPageSeries',
  661. 'TiffPages',
  662. 'TiffReader',
  663. 'TiffSequence',
  664. 'TiffTag',
  665. 'TiffTagRegistry',
  666. 'TiffTags',
  667. 'TiffWriter',
  668. 'TiledSequence',
  669. 'Timer',
  670. '__version__',
  671. 'askopenfilename',
  672. 'astype',
  673. 'create_output',
  674. 'enumarg',
  675. 'enumstr',
  676. 'format_size',
  677. 'hexdump',
  678. 'imagej_description',
  679. 'imagej_metadata_tag',
  680. 'imread',
  681. 'imshow',
  682. 'imwrite',
  683. 'logger',
  684. 'lsm2bin',
  685. 'matlabstr2py',
  686. 'memmap',
  687. 'natural_sorted',
  688. 'nullfunc',
  689. 'parse_filenames',
  690. 'parse_kwargs',
  691. 'pformat',
  692. 'product',
  693. 'read_gdal_structural_metadata',
  694. 'read_micromanager_metadata',
  695. 'read_ndtiff_index',
  696. 'read_scanimage_metadata',
  697. 'repeat_nd',
  698. 'reshape_axes',
  699. 'reshape_nd',
  700. 'stripnull', # deprecated
  701. 'strptime',
  702. 'tiff2fsspec',
  703. 'tiffcomment',
  704. 'transpose_axes',
  705. 'update_kwargs',
  706. 'validate_jhove',
  707. 'xml2dict',
  708. ]
  709. import binascii
  710. import collections
  711. import enum
  712. import glob
  713. import io
  714. import json
  715. import logging
  716. import math
  717. import os
  718. import re
  719. import struct
  720. import sys
  721. import threading
  722. import time
  723. import warnings
  724. from collections.abc import Callable, Iterable, Mapping, Sequence
  725. from concurrent.futures import ThreadPoolExecutor
  726. from datetime import datetime as DateTime # noqa: N812
  727. from datetime import timedelta as TimeDelta # noqa: N812
  728. from functools import cached_property
  729. import numpy
  730. try:
  731. import imagecodecs
  732. except ImportError:
  733. # load pure Python implementation of some codecs
  734. try:
  735. from . import _imagecodecs as imagecodecs # type: ignore[no-redef]
  736. except ImportError:
  737. import _imagecodecs as imagecodecs # type: ignore[no-redef]
  738. from typing import IO, TYPE_CHECKING, cast, final, overload
  739. if TYPE_CHECKING:
  740. from collections.abc import Collection, Container, Iterator
  741. from types import TracebackType
  742. from typing import Any, Literal, Self, TypeAlias
  743. from numpy.typing import ArrayLike, DTypeLike, NDArray
  744. ByteOrder: TypeAlias = Literal['>', '<']
  745. OutputType: TypeAlias = str | IO[bytes] | NDArray[Any] | None
  746. TagTuple: TypeAlias = tuple[int | str, int | str, int | None, Any, bool]
  747. @overload
  748. def imread(
  749. files: (
  750. str
  751. | os.PathLike[Any]
  752. | FileHandle
  753. | IO[bytes]
  754. | Sequence[str | os.PathLike[Any]]
  755. | None
  756. ) = None,
  757. *,
  758. selection: Any | None = None, # TODO: type this
  759. aszarr: Literal[False] = ...,
  760. key: int | slice | Iterable[int] | None = None,
  761. series: int | None = None,
  762. level: int | None = None,
  763. squeeze: bool | None = None,
  764. maxworkers: int | None = None,
  765. buffersize: int | None = None,
  766. mode: Literal['r', 'r+'] | None = None,
  767. name: str | None = None,
  768. offset: int | None = None,
  769. size: int | None = None,
  770. pattern: str | None = None,
  771. axesorder: Sequence[int] | None = None,
  772. categories: dict[str, dict[str, int]] | None = None,
  773. imread: Callable[..., NDArray[Any]] | None = None,
  774. sort: Callable[..., Any] | bool | None = None,
  775. container: str | os.PathLike[Any] | None = None,
  776. chunkshape: tuple[int, ...] | None = None,
  777. dtype: DTypeLike | None = None,
  778. axestiled: dict[int, int] | Sequence[tuple[int, int]] | None = None,
  779. ioworkers: int | None = 1,
  780. chunkmode: CHUNKMODE | int | str | None = None,
  781. fillvalue: float | None = None,
  782. zattrs: dict[str, Any] | None = None,
  783. multiscales: bool | None = None,
  784. omexml: str | None = None,
  785. superres: int | None = None,
  786. out: OutputType = None,
  787. out_inplace: bool | None = None,
  788. _multifile: bool | None = None,
  789. _useframes: bool | None = None,
  790. **kwargs: Any,
  791. ) -> NDArray[Any]: ...
  792. @overload
  793. def imread(
  794. files: (
  795. str
  796. | os.PathLike[Any]
  797. | FileHandle
  798. | IO[bytes]
  799. | Sequence[str | os.PathLike[Any]]
  800. | None
  801. ) = None,
  802. *,
  803. selection: Any | None = None, # TODO: type this
  804. aszarr: Literal[True],
  805. key: int | slice | Iterable[int] | None = None,
  806. series: int | None = None,
  807. level: int | None = None,
  808. squeeze: bool | None = None,
  809. maxworkers: int | None = None,
  810. buffersize: int | None = None,
  811. mode: Literal['r', 'r+'] | None = None,
  812. name: str | None = None,
  813. offset: int | None = None,
  814. size: int | None = None,
  815. pattern: str | None = None,
  816. axesorder: Sequence[int] | None = None,
  817. categories: dict[str, dict[str, int]] | None = None,
  818. imread: Callable[..., NDArray[Any]] | None = None,
  819. imreadargs: dict[str, Any] | None = None,
  820. sort: Callable[..., Any] | bool | None = None,
  821. container: str | os.PathLike[Any] | None = None,
  822. chunkshape: tuple[int, ...] | None = None,
  823. chunkdtype: DTypeLike | None = None,
  824. axestiled: dict[int, int] | Sequence[tuple[int, int]] | None = None,
  825. ioworkers: int | None = 1,
  826. chunkmode: CHUNKMODE | int | str | None = None,
  827. fillvalue: float | None = None,
  828. zattrs: dict[str, Any] | None = None,
  829. multiscales: bool | None = None,
  830. omexml: str | None = None,
  831. superres: int | None = None,
  832. out: OutputType = None,
  833. out_inplace: bool | None = None,
  834. _multifile: bool | None = None,
  835. _useframes: bool | None = None,
  836. **kwargs: Any,
  837. ) -> ZarrTiffStore | ZarrFileSequenceStore: ...
  838. @overload
  839. def imread(
  840. files: (
  841. str
  842. | os.PathLike[Any]
  843. | FileHandle
  844. | IO[bytes]
  845. | Sequence[str | os.PathLike[Any]]
  846. | None
  847. ) = None,
  848. *,
  849. selection: Any | None = None, # TODO: type this
  850. aszarr: bool = False,
  851. key: int | slice | Iterable[int] | None = None,
  852. series: int | None = None,
  853. level: int | None = None,
  854. squeeze: bool | None = None,
  855. maxworkers: int | None = None,
  856. buffersize: int | None = None,
  857. mode: Literal['r', 'r+'] | None = None,
  858. name: str | None = None,
  859. offset: int | None = None,
  860. size: int | None = None,
  861. pattern: str | None = None,
  862. axesorder: Sequence[int] | None = None,
  863. categories: dict[str, dict[str, int]] | None = None,
  864. imread: Callable[..., NDArray[Any]] | None = None,
  865. imreadargs: dict[str, Any] | None = None,
  866. sort: Callable[..., Any] | bool | None = None,
  867. container: str | os.PathLike[Any] | None = None,
  868. chunkshape: tuple[int, ...] | None = None,
  869. chunkdtype: DTypeLike | None = None,
  870. axestiled: dict[int, int] | Sequence[tuple[int, int]] | None = None,
  871. ioworkers: int | None = 1,
  872. chunkmode: CHUNKMODE | int | str | None = None,
  873. fillvalue: float | None = None,
  874. zattrs: dict[str, Any] | None = None,
  875. multiscales: bool | None = None,
  876. omexml: str | None = None,
  877. superres: int | None = None,
  878. out: OutputType = None,
  879. out_inplace: bool | None = None,
  880. _multifile: bool | None = None,
  881. _useframes: bool | None = None,
  882. **kwargs: Any,
  883. ) -> NDArray[Any] | ZarrTiffStore | ZarrFileSequenceStore: ...
  884. def imread(
  885. files: (
  886. str
  887. | os.PathLike[Any]
  888. | FileHandle
  889. | IO[bytes]
  890. | Sequence[str | os.PathLike[Any]]
  891. | None
  892. ) = None,
  893. *,
  894. selection: Any | None = None, # TODO: type this
  895. aszarr: bool = False,
  896. key: int | slice | Iterable[int] | None = None,
  897. series: int | None = None,
  898. level: int | None = None,
  899. squeeze: bool | None = None,
  900. maxworkers: int | None = None,
  901. buffersize: int | None = None,
  902. mode: Literal['r', 'r+'] | None = None,
  903. name: str | None = None,
  904. offset: int | None = None,
  905. size: int | None = None,
  906. pattern: str | None = None,
  907. axesorder: Sequence[int] | None = None,
  908. categories: dict[str, dict[str, int]] | None = None,
  909. imread: Callable[..., NDArray[Any]] | None = None,
  910. imreadargs: dict[str, Any] | None = None,
  911. sort: Callable[..., Any] | bool | None = None,
  912. container: str | os.PathLike[Any] | None = None,
  913. chunkshape: tuple[int, ...] | None = None,
  914. chunkdtype: DTypeLike | None = None,
  915. axestiled: dict[int, int] | Sequence[tuple[int, int]] | None = None,
  916. ioworkers: int | None = 1,
  917. chunkmode: CHUNKMODE | int | str | None = None,
  918. fillvalue: float | None = None,
  919. zattrs: dict[str, Any] | None = None,
  920. multiscales: bool | None = None,
  921. omexml: str | None = None,
  922. superres: int | None = None,
  923. out: OutputType = None,
  924. out_inplace: bool | None = None,
  925. _multifile: bool | None = None,
  926. _useframes: bool | None = None,
  927. **kwargs: Any,
  928. ) -> NDArray[Any] | ZarrTiffStore | ZarrFileSequenceStore:
  929. """Return image from TIFF file(s) as NumPy array or Zarr store.
  930. The first image series in the file(s) is returned by default.
  931. Parameters:
  932. files:
  933. File name, seekable binary stream, glob pattern, or sequence of
  934. file names. May be *None* if `container` is specified.
  935. selection:
  936. Subset of image to be extracted.
  937. If not None, a Zarr array is created, indexed with the
  938. `selection` value, and returned as a NumPy array. Only segments
  939. that are part of the selection will be read from file.
  940. Refer to the Zarr documentation for valid selections.
  941. Depending on selection size, image size, and storage properties,
  942. it may be more efficient to read the whole image from file and
  943. then index it.
  944. aszarr:
  945. Return file sequences, series, or single pages as Zarr store
  946. instead of NumPy array if `selection` is None.
  947. mode, name, offset, size, superres, omexml, _multifile, _useframes:
  948. Passed to :py:class:`TiffFile`.
  949. key, series, level, squeeze, maxworkers, buffersize:
  950. Passed to :py:meth:`TiffFile.asarray`
  951. or :py:meth:`TiffFile.aszarr`.
  952. imread, container, sort, pattern, axesorder, axestiled, categories:
  953. Passed to :py:class:`FileSequence`.
  954. chunkmode, fillvalue, zattrs, multiscales:
  955. Passed to :py:class:`ZarrTiffStore`
  956. or :py:class:`ZarrFileSequenceStore`.
  957. chunkshape, chunkdtype, ioworkers:
  958. Passed to :py:meth:`FileSequence.asarray` or
  959. :py:class:`ZarrFileSequenceStore`.
  960. out_inplace:
  961. Passed to :py:meth:`FileSequence.asarray`
  962. out:
  963. Passed to :py:meth:`TiffFile.asarray`,
  964. :py:meth:`FileSequence.asarray`, or :py:func:`zarr_selection`.
  965. imreadargs:
  966. Additional arguments passed to :py:attr:`FileSequence.imread`.
  967. **kwargs:
  968. Additional arguments passed to :py:class:`TiffFile` or
  969. :py:attr:`FileSequence.imread`.
  970. Returns:
  971. Images from specified files, series, or pages.
  972. Zarr store instances must be closed after use.
  973. See :py:meth:`TiffPage.asarray` for operations that are applied
  974. (or not) to the image data stored in the file.
  975. """
  976. store: ZarrStore
  977. aszarr = aszarr or (selection is not None)
  978. is_flags = parse_kwargs(kwargs, *(k for k in kwargs if k[:3] == 'is_'))
  979. if imread is None and kwargs:
  980. raise TypeError(
  981. 'imread() got unexpected keyword arguments '
  982. + ', '.join(f"'{key}'" for key in kwargs)
  983. )
  984. glob_pattern: str | None = None
  985. if container is None:
  986. if isinstance(files, str) and ('*' in files or '?' in files):
  987. glob_pattern = files
  988. files = glob.glob(files)
  989. if not files:
  990. raise ValueError('no files found')
  991. if (
  992. isinstance(files, Sequence)
  993. and not isinstance(files, str)
  994. and len(files) == 1
  995. ):
  996. files = files[0]
  997. if isinstance(files, str) or not isinstance(files, Sequence):
  998. with TiffFile(
  999. files,
  1000. mode=mode,
  1001. name=name,
  1002. offset=offset,
  1003. size=size,
  1004. omexml=omexml,
  1005. superres=superres,
  1006. _multifile=_multifile,
  1007. _useframes=_useframes,
  1008. **is_flags,
  1009. ) as tif:
  1010. if aszarr:
  1011. assert key is None or isinstance(key, int)
  1012. store = tif.aszarr(
  1013. key=key,
  1014. series=series,
  1015. level=level,
  1016. squeeze=squeeze,
  1017. maxworkers=maxworkers,
  1018. buffersize=buffersize,
  1019. chunkmode=chunkmode,
  1020. fillvalue=fillvalue,
  1021. zattrs=zattrs,
  1022. multiscales=multiscales,
  1023. )
  1024. if selection is None:
  1025. return store
  1026. from .zarr import zarr_selection
  1027. return zarr_selection(store, selection, out=out)
  1028. return tif.asarray(
  1029. key=key,
  1030. series=series,
  1031. level=level,
  1032. squeeze=squeeze,
  1033. maxworkers=maxworkers,
  1034. buffersize=buffersize,
  1035. out=out,
  1036. )
  1037. elif isinstance(files, (FileHandle, IO)):
  1038. raise ValueError('BinaryIO not supported')
  1039. imread_kwargs = kwargs_notnone(
  1040. key=key,
  1041. series=series,
  1042. level=level,
  1043. squeeze=squeeze,
  1044. maxworkers=maxworkers,
  1045. buffersize=buffersize,
  1046. imreadargs=imreadargs,
  1047. _multifile=_multifile,
  1048. _useframes=_useframes,
  1049. **is_flags,
  1050. **kwargs,
  1051. )
  1052. if glob_pattern is not None:
  1053. # TODO: this forces glob to be executed again
  1054. files = glob_pattern
  1055. with TiffSequence(
  1056. files,
  1057. pattern=pattern,
  1058. axesorder=axesorder,
  1059. categories=categories,
  1060. container=container,
  1061. sort=sort,
  1062. **kwargs_notnone(imread=imread),
  1063. ) as imseq:
  1064. if aszarr:
  1065. store = imseq.aszarr(
  1066. axestiled=axestiled,
  1067. chunkmode=chunkmode,
  1068. chunkshape=chunkshape,
  1069. chunkdtype=chunkdtype,
  1070. fillvalue=fillvalue,
  1071. ioworkers=ioworkers,
  1072. zattrs=zattrs,
  1073. **imread_kwargs,
  1074. )
  1075. if selection is None:
  1076. return store
  1077. from .zarr import zarr_selection
  1078. return zarr_selection(store, selection, out=out)
  1079. return imseq.asarray(
  1080. axestiled=axestiled,
  1081. chunkshape=chunkshape,
  1082. chunkdtype=chunkdtype,
  1083. ioworkers=ioworkers,
  1084. out_inplace=out_inplace,
  1085. out=out,
  1086. **imread_kwargs,
  1087. )
  1088. def imwrite(
  1089. file: str | os.PathLike[Any] | FileHandle | IO[bytes],
  1090. /,
  1091. data: (
  1092. ArrayLike
  1093. | Iterator[NDArray[Any] | None]
  1094. | Iterator[bytes]
  1095. | Iterator[tuple[bytes, int]]
  1096. | None
  1097. ) = None,
  1098. *,
  1099. mode: Literal['w', 'x', 'r+'] | None = None,
  1100. bigtiff: bool | None = None,
  1101. byteorder: ByteOrder | None = None,
  1102. imagej: bool = False,
  1103. ome: bool | None = None,
  1104. shaped: bool | None = None,
  1105. append: bool = False,
  1106. shape: Sequence[int] | None = None,
  1107. dtype: DTypeLike | None = None,
  1108. photometric: PHOTOMETRIC | int | str | None = None,
  1109. planarconfig: PLANARCONFIG | int | str | None = None,
  1110. extrasamples: Sequence[EXTRASAMPLE | int | str] | None = None,
  1111. volumetric: bool = False,
  1112. tile: Sequence[int] | None = None,
  1113. rowsperstrip: int | None = None,
  1114. bitspersample: int | None = None,
  1115. compression: COMPRESSION | int | str | None = None,
  1116. compressionargs: dict[str, Any] | None = None,
  1117. predictor: PREDICTOR | int | str | bool | None = None,
  1118. subsampling: tuple[int, int] | None = None,
  1119. jpegtables: bytes | None = None,
  1120. iccprofile: bytes | None = None,
  1121. colormap: ArrayLike | None = None,
  1122. description: str | bytes | None = None,
  1123. datetime: str | bool | DateTime | None = None,
  1124. resolution: (
  1125. tuple[float | tuple[int, int], float | tuple[int, int]] | None
  1126. ) = None,
  1127. resolutionunit: RESUNIT | int | str | None = None,
  1128. subfiletype: FILETYPE | int | None = None,
  1129. software: str | bytes | bool | None = None,
  1130. # subifds: int | Sequence[int] | None = None,
  1131. metadata: dict[str, Any] | None = {}, # noqa: B006
  1132. extratags: Sequence[TagTuple] | None = None,
  1133. contiguous: bool = False,
  1134. truncate: bool = False,
  1135. align: int | None = None,
  1136. maxworkers: int | None = None,
  1137. buffersize: int | None = None,
  1138. returnoffset: bool = False,
  1139. ) -> tuple[int, int] | None:
  1140. """Write NumPy array to TIFF file.
  1141. A BigTIFF file is written if the data size is larger than 4 GB less
  1142. 32 MB for metadata, and `bigtiff` is not *False*, and `imagej`,
  1143. `truncate` and `compression` are not enabled.
  1144. Unless `byteorder` is specified, the TIFF file byte order is determined
  1145. from the dtype of `data` or the `dtype` argument.
  1146. Parameters:
  1147. file:
  1148. Passed to :py:class:`TiffWriter`.
  1149. data, shape, dtype:
  1150. Passed to :py:meth:`TiffWriter.write`.
  1151. mode, append, byteorder, bigtiff, imagej, ome, shaped:
  1152. Passed to :py:class:`TiffWriter`.
  1153. photometric, planarconfig, extrasamples, volumetric, tile,\
  1154. rowsperstrip, bitspersample, compression, compressionargs, predictor,\
  1155. subsampling, jpegtables, iccprofile, colormap, description, datetime,\
  1156. resolution, resolutionunit, subfiletype, software,\
  1157. metadata, extratags, maxworkers, buffersize, \
  1158. contiguous, truncate, align:
  1159. Passed to :py:meth:`TiffWriter.write`.
  1160. returnoffset:
  1161. Return offset and number of bytes of memory-mappable image data
  1162. in file.
  1163. Returns:
  1164. If `returnoffset` is *True* and the image data in the file are
  1165. memory-mappable, the offset and number of bytes of the image
  1166. data in the file.
  1167. """
  1168. if data is None:
  1169. # write empty file
  1170. if shape is None or dtype is None:
  1171. raise ValueError("missing required 'shape' or 'dtype' argument")
  1172. dtype = numpy.dtype(dtype)
  1173. shape = tuple(shape)
  1174. datasize = product(shape) * dtype.itemsize
  1175. if byteorder is None:
  1176. byteorder = dtype.byteorder # type: ignore[assignment]
  1177. else:
  1178. try:
  1179. datasize = data.nbytes # type: ignore[union-attr]
  1180. if byteorder is None:
  1181. byteorder = data.dtype.byteorder # type: ignore[union-attr]
  1182. except Exception:
  1183. datasize = 0
  1184. if bigtiff is None:
  1185. bigtiff = (
  1186. datasize > 2**32 - 2**25
  1187. and not imagej
  1188. and not truncate
  1189. and compression in {None, 0, 1, 'NONE', 'None', 'none'}
  1190. )
  1191. with TiffWriter(
  1192. file,
  1193. mode=mode,
  1194. bigtiff=bigtiff,
  1195. byteorder=byteorder,
  1196. append=append,
  1197. imagej=imagej,
  1198. ome=ome,
  1199. shaped=shaped,
  1200. ) as tif:
  1201. return tif.write(
  1202. data,
  1203. shape=shape,
  1204. dtype=dtype,
  1205. photometric=photometric,
  1206. planarconfig=planarconfig,
  1207. extrasamples=extrasamples,
  1208. volumetric=volumetric,
  1209. tile=tile,
  1210. rowsperstrip=rowsperstrip,
  1211. bitspersample=bitspersample,
  1212. compression=compression,
  1213. compressionargs=compressionargs,
  1214. predictor=predictor,
  1215. subsampling=subsampling,
  1216. jpegtables=jpegtables,
  1217. iccprofile=iccprofile,
  1218. colormap=colormap,
  1219. description=description,
  1220. datetime=datetime,
  1221. resolution=resolution,
  1222. resolutionunit=resolutionunit,
  1223. subfiletype=subfiletype,
  1224. software=software,
  1225. metadata=metadata,
  1226. extratags=extratags,
  1227. contiguous=contiguous,
  1228. truncate=truncate,
  1229. align=align,
  1230. maxworkers=maxworkers,
  1231. buffersize=buffersize,
  1232. returnoffset=returnoffset,
  1233. )
  1234. def memmap(
  1235. filename: str | os.PathLike[Any],
  1236. /,
  1237. *,
  1238. shape: Sequence[int] | None = None,
  1239. dtype: DTypeLike | None = None,
  1240. page: int | None = None,
  1241. series: int = 0,
  1242. level: int = 0,
  1243. mode: Literal['r+', 'r', 'c'] = 'r+',
  1244. **kwargs: Any,
  1245. ) -> numpy.memmap[Any, Any]:
  1246. """Return memory-mapped NumPy array of image data stored in TIFF file.
  1247. Memory-mapping requires the image data stored in native byte order,
  1248. without tiling, compression, predictors, etc.
  1249. If `shape` and `dtype` are provided, existing files are overwritten or
  1250. appended to depending on the `append` argument.
  1251. Else, the image data of a specified page or series in an existing
  1252. file are memory-mapped. By default, the image data of the first
  1253. series are memory-mapped.
  1254. Call `flush` to write any changes in the array to the file.
  1255. Parameters:
  1256. filename:
  1257. Name of TIFF file which stores array.
  1258. shape:
  1259. Shape of empty array.
  1260. dtype:
  1261. Datatype of empty array.
  1262. page:
  1263. Index of page which image data to memory-map.
  1264. series:
  1265. Index of page series which image data to memory-map.
  1266. level:
  1267. Index of pyramid level which image data to memory-map.
  1268. mode:
  1269. Memory-map file open mode. The default is 'r+', which opens
  1270. existing file for reading and writing.
  1271. **kwargs:
  1272. Additional arguments passed to :py:func:`imwrite` or
  1273. :py:class:`TiffFile`.
  1274. Returns:
  1275. Image in TIFF file as memory-mapped NumPy array.
  1276. Raises:
  1277. ValueError: Image data in TIFF file are not memory-mappable.
  1278. """
  1279. filename = os.fspath(filename)
  1280. if shape is not None:
  1281. shape = tuple(shape)
  1282. if shape is not None and dtype is not None:
  1283. # create a new, empty array
  1284. dtype = numpy.dtype(dtype)
  1285. if 'byteorder' in kwargs:
  1286. dtype = dtype.newbyteorder(kwargs['byteorder'])
  1287. kwargs.update(
  1288. data=None,
  1289. shape=shape,
  1290. dtype=dtype,
  1291. align=TIFF.ALLOCATIONGRANULARITY,
  1292. returnoffset=True,
  1293. )
  1294. result = imwrite(filename, **kwargs)
  1295. if result is None:
  1296. # TODO: fail before creating file or writing data
  1297. raise ValueError('image data are not memory-mappable')
  1298. offset = result[0]
  1299. else:
  1300. # use existing file
  1301. with TiffFile(filename, **kwargs) as tif:
  1302. if page is None:
  1303. tiffseries = tif.series[series].levels[level]
  1304. if tiffseries.dataoffset is None:
  1305. raise ValueError('image data are not memory-mappable')
  1306. shape = tiffseries.shape
  1307. dtype = tiffseries.dtype
  1308. offset = tiffseries.dataoffset
  1309. else:
  1310. tiffpage = tif.pages[page]
  1311. if not tiffpage.is_memmappable:
  1312. raise ValueError('image data are not memory-mappable')
  1313. offset = tiffpage.dataoffsets[0]
  1314. shape = tiffpage.shape
  1315. dtype = tiffpage.dtype
  1316. assert dtype is not None
  1317. dtype = numpy.dtype(tif.byteorder + dtype.char)
  1318. return numpy.memmap(filename, dtype, mode, offset, shape, 'C')
  1319. class TiffFileError(ValueError):
  1320. """Exception to indicate invalid TIFF structure."""
  1321. @final
  1322. class TiffWriter:
  1323. """Write NumPy arrays to TIFF file.
  1324. TiffWriter's main purpose is to save multi-dimensional NumPy arrays in
  1325. TIFF containers, not to create any possible TIFF format.
  1326. Specifically, ExifIFD and GPSIFD tags are not supported.
  1327. TiffWriter instances must be closed with :py:meth:`TiffWriter.close`,
  1328. which is automatically called when using the 'with' context manager.
  1329. TiffWriter instances are not thread-safe. All attributes are read-only.
  1330. Parameters:
  1331. file:
  1332. Specifies file to write.
  1333. mode:
  1334. Binary file open mode if `file` is file name.
  1335. The default is 'w', which opens files for writing, truncating
  1336. existing files.
  1337. 'x' opens files for exclusive creation, failing on existing files.
  1338. 'r+' opens files for updating, enabling `append`.
  1339. bigtiff:
  1340. Write 64-bit BigTIFF formatted file, which can exceed 4 GB.
  1341. By default, a classic 32-bit TIFF file is written, which is
  1342. limited to 4 GB.
  1343. If `append` is *True*, the format of the existing file is used.
  1344. byteorder:
  1345. Endianness of TIFF format. One of '<', '>', '=', or '|'.
  1346. The default is the system's native byte order.
  1347. append:
  1348. If `file` is existing standard TIFF file, append image data
  1349. and tags to file.
  1350. Parameters `bigtiff` and `byteorder` set from existing file.
  1351. Appending does not scale well with the number of pages already in
  1352. the file and may corrupt specifically formatted TIFF files such as
  1353. OME-TIFF, LSM, STK, ImageJ, or FluoView.
  1354. imagej:
  1355. Write ImageJ hyperstack compatible file if `ome` is not enabled.
  1356. This format can handle data types uint8, uint16, or float32 and
  1357. data shapes up to 6 dimensions in TZCYXS order.
  1358. RGB images (S=3 or S=4) must be `uint8`.
  1359. ImageJ's default byte order is big-endian, but this
  1360. implementation uses the system's native byte order by default.
  1361. ImageJ hyperstacks do not support BigTIFF or compression.
  1362. The ImageJ file format is undocumented.
  1363. Use FIJI's Bio-Formats import function for compressed files.
  1364. ome:
  1365. Write OME-TIFF compatible file.
  1366. By default, the OME-TIFF format is used if the file name extension
  1367. contains '.ome.', `imagej` is not enabled, and the `description`
  1368. argument in the first call of :py:meth:`TiffWriter.write` is not
  1369. specified.
  1370. The format supports multiple image series up to 9 dimensions.
  1371. The default axes order is TZC(S)YX(S).
  1372. Refer to the OME model for restrictions of this format.
  1373. shaped:
  1374. Write tifffile "shaped" compatible file.
  1375. The shape of multi-dimensional images is stored in JSON format in
  1376. a ImageDescription tag of the first page of a series.
  1377. This is the default format used by tifffile unless `imagej` or
  1378. `ome` are enabled or ``metadata=None`` is passed to
  1379. :py:meth:`TiffWriter.write`.
  1380. Raises:
  1381. ValueError:
  1382. The TIFF file cannot be appended to. Use ``append='force'`` to
  1383. force appending, which may result in a corrupted file.
  1384. """
  1385. tiff: TiffFormat
  1386. """Format of TIFF file being written."""
  1387. _fh: FileHandle
  1388. _omexml: OmeXml | None
  1389. _ome: bool | None # writing OME-TIFF format
  1390. _imagej: bool # writing ImageJ format
  1391. _tifffile: bool # writing Tifffile shaped format
  1392. _truncate: bool
  1393. _metadata: dict[str, Any] | None
  1394. _colormap: NDArray[numpy.uint16] | None
  1395. _tags: list[tuple[int, bytes, Any, bool]] | None
  1396. _datashape: tuple[int, ...] | None # shape of data in consecutive pages
  1397. _datadtype: numpy.dtype[Any] | None # data type
  1398. _dataoffset: int | None # offset to data
  1399. _databytecounts: list[int] | None # byte counts per plane
  1400. _dataoffsetstag: int | None # strip or tile offset tag code
  1401. _descriptiontag: TiffTag | None # TiffTag for updating comment
  1402. _ifdoffset: int
  1403. _subifds: int # number of subifds
  1404. _subifdslevel: int # index of current subifd level
  1405. _subifdsoffsets: list[int] # offsets to offsets to subifds
  1406. _nextifdoffsets: list[int] # offsets to offset to next ifd
  1407. _ifdindex: int # index of current ifd
  1408. _storedshape: StoredShape | None # normalized shape in consecutive pages
  1409. def __init__(
  1410. self,
  1411. file: str | os.PathLike[Any] | FileHandle | IO[bytes],
  1412. /,
  1413. *,
  1414. mode: Literal['w', 'x', 'r+'] | None = None,
  1415. bigtiff: bool = False,
  1416. byteorder: ByteOrder | None = None,
  1417. append: bool | str = False,
  1418. imagej: bool = False,
  1419. ome: bool | None = None,
  1420. shaped: bool | None = None,
  1421. ) -> None:
  1422. if mode in {'r+', 'r+b'} or (
  1423. isinstance(file, FileHandle) and file._mode == 'r+b'
  1424. ):
  1425. mode = 'r+'
  1426. append = True
  1427. if append:
  1428. # determine if file is an existing TIFF file that can be extended
  1429. try:
  1430. with FileHandle(file, mode='rb', size=0) as fh:
  1431. pos = fh.tell()
  1432. try:
  1433. with TiffFile(fh) as tif:
  1434. if append != 'force' and not tif.is_appendable:
  1435. raise ValueError(
  1436. 'cannot append to file containing metadata'
  1437. )
  1438. byteorder = tif.byteorder
  1439. bigtiff = tif.is_bigtiff
  1440. self._ifdoffset = cast(
  1441. int, tif.pages.next_page_offset
  1442. )
  1443. finally:
  1444. fh.seek(pos)
  1445. append = True
  1446. except (OSError, FileNotFoundError):
  1447. append = False
  1448. if append:
  1449. if mode not in {None, 'r+', 'r+b'}:
  1450. raise ValueError("append mode must be 'r+'")
  1451. mode = 'r+'
  1452. elif mode is None:
  1453. mode = 'w'
  1454. if byteorder is None or byteorder in {'=', '|'}:
  1455. byteorder = '<' if sys.byteorder == 'little' else '>'
  1456. elif byteorder not in {'<', '>'}:
  1457. raise ValueError(f'invalid byteorder {byteorder}')
  1458. if byteorder == '<':
  1459. self.tiff = TIFF.BIG_LE if bigtiff else TIFF.CLASSIC_LE
  1460. else:
  1461. self.tiff = TIFF.BIG_BE if bigtiff else TIFF.CLASSIC_BE
  1462. self._truncate = False
  1463. self._metadata = None
  1464. self._colormap = None
  1465. self._tags = None
  1466. self._datashape = None
  1467. self._datadtype = None
  1468. self._dataoffset = None
  1469. self._databytecounts = None
  1470. self._dataoffsetstag = None
  1471. self._descriptiontag = None
  1472. self._subifds = 0
  1473. self._subifdslevel = -1
  1474. self._subifdsoffsets = []
  1475. self._nextifdoffsets = []
  1476. self._ifdindex = 0
  1477. self._omexml = None
  1478. self._storedshape = None
  1479. self._fh = FileHandle(file, mode=mode, size=0)
  1480. if append:
  1481. self._fh.seek(0, os.SEEK_END)
  1482. else:
  1483. assert byteorder is not None
  1484. self._fh.write(b'II' if byteorder == '<' else b'MM')
  1485. if bigtiff:
  1486. self._fh.write(struct.pack(byteorder + 'HHH', 43, 8, 0))
  1487. else:
  1488. self._fh.write(struct.pack(byteorder + 'H', 42))
  1489. # first IFD
  1490. self._ifdoffset = self._fh.tell()
  1491. self._fh.write(struct.pack(self.tiff.offsetformat, 0))
  1492. self._ome = None if ome is None else bool(ome)
  1493. self._imagej = False if self._ome else bool(imagej)
  1494. if self._imagej:
  1495. self._ome = False
  1496. if self._ome or self._imagej:
  1497. self._tifffile = False
  1498. else:
  1499. self._tifffile = True if shaped is None else bool(shaped)
  1500. if imagej and bigtiff:
  1501. warnings.warn(
  1502. f'{self!r} writing nonconformant BigTIFF ImageJ',
  1503. UserWarning,
  1504. stacklevel=2,
  1505. )
  1506. def write(
  1507. self,
  1508. data: (
  1509. ArrayLike
  1510. | Iterator[NDArray[Any] | None]
  1511. | Iterator[bytes]
  1512. | Iterator[tuple[bytes, int]]
  1513. | None
  1514. ) = None,
  1515. *,
  1516. shape: Sequence[int] | None = None,
  1517. dtype: DTypeLike | None = None,
  1518. photometric: PHOTOMETRIC | int | str | None = None,
  1519. planarconfig: PLANARCONFIG | int | str | None = None,
  1520. extrasamples: Sequence[EXTRASAMPLE | int | str] | None = None,
  1521. volumetric: bool = False,
  1522. tile: Sequence[int] | None = None,
  1523. rowsperstrip: int | None = None,
  1524. bitspersample: int | None = None,
  1525. compression: COMPRESSION | int | str | bool | None = None,
  1526. compressionargs: dict[str, Any] | None = None,
  1527. predictor: PREDICTOR | int | str | bool | None = None,
  1528. subsampling: tuple[int, int] | None = None,
  1529. jpegtables: bytes | None = None,
  1530. iccprofile: bytes | None = None,
  1531. colormap: ArrayLike | None = None,
  1532. description: str | bytes | None = None,
  1533. datetime: str | bool | DateTime | None = None,
  1534. resolution: (
  1535. tuple[float | tuple[int, int], float | tuple[int, int]] | None
  1536. ) = None,
  1537. resolutionunit: RESUNIT | int | str | None = None,
  1538. subfiletype: FILETYPE | int | None = None,
  1539. software: str | bytes | bool | None = None,
  1540. subifds: int | Sequence[int] | None = None,
  1541. metadata: dict[str, Any] | None = {}, # noqa: B006
  1542. extratags: Sequence[TagTuple] | None = None,
  1543. contiguous: bool = False,
  1544. truncate: bool = False,
  1545. align: int | None = None,
  1546. maxworkers: int | None = None,
  1547. buffersize: int | None = None,
  1548. returnoffset: bool = False,
  1549. ) -> tuple[int, int] | None:
  1550. r"""Write multi-dimensional image to series of TIFF pages.
  1551. Metadata in JSON, ImageJ, or OME-XML format are written to the
  1552. ImageDescription tag of the first page of a series by default,
  1553. such that the image can later be read back as an array of the
  1554. same shape.
  1555. The values of the ImageWidth, ImageLength, ImageDepth, and
  1556. SamplesPerPixel tags are inferred from the last dimensions of the
  1557. data's shape.
  1558. The value of the SampleFormat tag is inferred from the data's dtype.
  1559. Image data are written uncompressed in one strip per plane by default.
  1560. Dimensions higher than 2 to 4 (depending on photometric mode, planar
  1561. configuration, and volumetric mode) are flattened and written as
  1562. separate pages.
  1563. If the data size is zero, write a single page with shape (0, 0).
  1564. Parameters:
  1565. data:
  1566. Specifies image to write.
  1567. If *None*, an empty image is written, which size and type must
  1568. be specified using `shape` and `dtype` arguments.
  1569. This option cannot be used with compression, predictors,
  1570. packed integers, or bilevel images.
  1571. A copy of array-like data is made if it is not a C-contiguous
  1572. numpy or dask array with the same byteorder as the TIFF file.
  1573. Iterators must yield ndarrays or bytes compatible with the
  1574. file's byteorder as well as the `shape` and `dtype` arguments.
  1575. Iterator bytes must be compatible with the `compression`,
  1576. `predictor`, `subsampling`, and `jpegtables` arguments.
  1577. If `tile` is specified, iterator items must match the tile
  1578. shape. Incomplete tiles are zero-padded.
  1579. Iterators of non-tiled images must yield ndarrays of
  1580. `shape[1:]` or strips as bytes. Iterators of strip ndarrays
  1581. are not supported.
  1582. Writing dask arrays might be excruciatingly slow for arrays
  1583. with many chunks or files with many segments.
  1584. (https://github.com/dask/dask/issues/8570).
  1585. shape:
  1586. Shape of image to write.
  1587. The default is inferred from the `data` argument if possible.
  1588. A ValueError is raised if the value is incompatible with
  1589. the `data` or other arguments.
  1590. dtype:
  1591. NumPy data type of image to write.
  1592. The default is inferred from the `data` argument if possible.
  1593. A ValueError is raised if the value is incompatible with
  1594. the `data` argument.
  1595. photometric:
  1596. Color space of image.
  1597. The default is inferred from the data shape, dtype, and the
  1598. `colormap` argument.
  1599. A UserWarning is logged if RGB color space is auto-detected.
  1600. Specify this parameter to silence the warning and to avoid
  1601. ambiguities.
  1602. *MINISBLACK*: for bilevel and grayscale images, 0 is black.
  1603. *MINISWHITE*: for bilevel and grayscale images, 0 is white.
  1604. *RGB*: the image contains red, green and blue samples.
  1605. *SEPARATED*: the image contains CMYK samples.
  1606. *PALETTE*: the image is used as an index into a colormap.
  1607. *CFA*: the image is a Color Filter Array. The
  1608. CFARepeatPatternDim, CFAPattern, and other DNG or TIFF/EP tags
  1609. must be specified in `extratags` to produce a valid file.
  1610. The value is written to the PhotometricInterpretation tag.
  1611. planarconfig:
  1612. Specifies if samples are stored interleaved or in separate
  1613. planes.
  1614. *CONTIG*: the last dimension contains samples.
  1615. *SEPARATE*: the 3rd or 4th last dimension contains samples.
  1616. The default is inferred from the data shape and `photometric`
  1617. mode.
  1618. If this parameter is set, extra samples are used to store
  1619. grayscale images.
  1620. The value is written to the PlanarConfiguration tag.
  1621. extrasamples:
  1622. Interpretation of extra components in pixels.
  1623. *UNSPECIFIED*: no transparency information (default).
  1624. *ASSOCALPHA*: true transparency with premultiplied color.
  1625. *UNASSALPHA*: independent transparency masks.
  1626. The values are written to the ExtraSamples tag.
  1627. volumetric:
  1628. Write volumetric image to single page (instead of multiple
  1629. pages) using SGI ImageDepth tag.
  1630. The volumetric format is not part of the TIFF specification,
  1631. and few software can read it.
  1632. OME and ImageJ formats are not compatible with volumetric
  1633. storage.
  1634. tile:
  1635. Shape ([depth,] length, width) of image tiles to write.
  1636. By default, image data are written in strips.
  1637. The tile length and width must be a multiple of 16.
  1638. If a tile depth is provided, the SGI ImageDepth and TileDepth
  1639. tags are used to write volumetric data.
  1640. Tiles cannot be used to write contiguous series, except if
  1641. the tile shape matches the data shape.
  1642. The values are written to the TileWidth, TileLength, and
  1643. TileDepth tags.
  1644. rowsperstrip:
  1645. Number of rows per strip.
  1646. By default, strips are about 256 KB if `compression` is
  1647. enabled, else rowsperstrip is set to the image length.
  1648. The value is written to the RowsPerStrip tag.
  1649. bitspersample:
  1650. Number of bits per sample.
  1651. The default is the number of bits of the data's dtype.
  1652. Different values per samples are not supported.
  1653. Unsigned integer data are packed into bytes as tightly as
  1654. possible.
  1655. Valid values are 1-8 for uint8, 9-16 for uint16, and 17-32
  1656. for uint32.
  1657. This setting cannot be used with compression, contiguous
  1658. series, or empty files.
  1659. The value is written to the BitsPerSample tag.
  1660. compression:
  1661. Compression scheme used on image data.
  1662. By default, image data are written uncompressed.
  1663. Compression cannot be used to write contiguous series.
  1664. Compressors may require certain data shapes, types or value
  1665. ranges. For example, JPEG compression requires grayscale or
  1666. RGB(A), uint8 or 12-bit uint16.
  1667. JPEG compression is experimental. JPEG markers and TIFF tags
  1668. may not match.
  1669. Only a limited set of compression schemes are implemented.
  1670. 'ZLIB' is short for ADOBE_DEFLATE.
  1671. The value is written to the Compression tag.
  1672. compressionargs:
  1673. Extra arguments passed to compression codec, for example,
  1674. compression level. Refer to the Imagecodecs implementation
  1675. for supported arguments.
  1676. predictor:
  1677. Horizontal differencing operator applied to image data before
  1678. compression.
  1679. By default, no operator is applied.
  1680. Predictors can only be used with certain compression schemes
  1681. and data types.
  1682. The value is written to the Predictor tag.
  1683. subsampling:
  1684. Horizontal and vertical subsampling factors used for the
  1685. chrominance components of images: (1, 1), (2, 1), (2, 2), or
  1686. (4, 1). The default is *(2, 2)*.
  1687. Currently applies to JPEG compression of RGB images only.
  1688. Images are stored in YCbCr color space, the value of the
  1689. PhotometricInterpretation tag is *YCBCR*.
  1690. Segment widths must be a multiple of 8 times the horizontal
  1691. factor. Segment lengths and rowsperstrip must be a multiple
  1692. of 8 times the vertical factor.
  1693. The values are written to the YCbCrSubSampling tag.
  1694. jpegtables:
  1695. JPEG quantization and/or Huffman tables.
  1696. Use for copying pre-compressed JPEG segments.
  1697. The value is written to the JPEGTables tag.
  1698. iccprofile:
  1699. International Color Consortium (ICC) device profile
  1700. characterizing image color space.
  1701. The value is written verbatim to the InterColorProfile tag.
  1702. colormap:
  1703. RGB color values for corresponding data value.
  1704. The colormap array must be of shape
  1705. `(3, 2\*\*(data.itemsize*8))` (or `(3, 256)` for ImageJ)
  1706. and dtype uint16.
  1707. The image's data type must be uint8 or uint16 (or float32
  1708. for ImageJ) and the values are indices into the last
  1709. dimension of the colormap.
  1710. The value is written to the ColorMap tag.
  1711. description:
  1712. Subject of image. Must be 7-bit ASCII.
  1713. Cannot be used with the ImageJ or OME formats.
  1714. The value is written to the ImageDescription tag of the
  1715. first page of a series.
  1716. datetime:
  1717. Date and time of image creation in ``%Y:%m:%d %H:%M:%S``
  1718. format or datetime object.
  1719. If *True*, the current date and time is used.
  1720. The value is written to the DateTime tag of the first page
  1721. of a series.
  1722. resolution:
  1723. Number of pixels per `resolutionunit` in X and Y directions
  1724. as float or rational numbers.
  1725. The default is (1.0, 1.0).
  1726. The values are written to the YResolution and XResolution tags.
  1727. resolutionunit:
  1728. Unit of measurement for `resolution` values.
  1729. The default is *NONE* if `resolution` is not specified and
  1730. for ImageJ format, else *INCH*.
  1731. The value is written to the ResolutionUnit tags.
  1732. subfiletype:
  1733. Bitfield to indicate kind of image.
  1734. Set bit 0 if the image is a reduced-resolution version of
  1735. another image.
  1736. Set bit 1 if the image is part of a multi-page image.
  1737. Set bit 2 if the image is transparency mask for another
  1738. image (photometric must be MASK, SamplesPerPixel and
  1739. bitspersample must be 1).
  1740. software:
  1741. Name of software used to create file.
  1742. Must be 7-bit ASCII. The default is 'tifffile.py'.
  1743. Unless *False*, the value is written to the Software tag of
  1744. the first page of a series.
  1745. subifds:
  1746. Number of child IFDs.
  1747. If greater than 0, the following `subifds` number of series
  1748. are written as child IFDs of the current series.
  1749. The number of IFDs written for each SubIFD level must match
  1750. the number of IFDs written for the current series.
  1751. All pages written to a certain SubIFD level of the current
  1752. series must have the same hash.
  1753. SubIFDs cannot be used with truncated or ImageJ files.
  1754. SubIFDs in OME-TIFF files must be sub-resolutions of the
  1755. main IFDs.
  1756. metadata:
  1757. Additional metadata describing image, written along
  1758. with shape information in JSON, OME-XML, or ImageJ formats
  1759. in ImageDescription or IJMetadata tags.
  1760. Metadata do not determine, but must match, how image data
  1761. is written to the file.
  1762. If *None*, or the `shaped` argument to :py:class:`TiffWriter`
  1763. is *False*, no information in JSON format is written to
  1764. the ImageDescription tag.
  1765. The 'axes' item defines the character codes for dimensions in
  1766. `data` or `shape`.
  1767. Refer to :py:class:`OmeXml` for supported keys when writing
  1768. OME-TIFF.
  1769. Refer to :py:func:`imagej_description` and
  1770. :py:func:`imagej_metadata_tag` for items supported
  1771. by the ImageJ format. Items 'Info', 'Labels', 'Ranges',
  1772. 'LUTs', 'Plot', 'ROI', and 'Overlays' are written to the
  1773. IJMetadata and IJMetadataByteCounts tags.
  1774. Strings must be 7-bit ASCII.
  1775. Written with the first page of a series only.
  1776. extratags:
  1777. Additional tags to write. A list of tuples with 5 items:
  1778. 0. code (int): Tag Id.
  1779. 1. dtype (:py:class:`DATATYPE`):
  1780. Data type of items in `value`.
  1781. 2. count (int): Number of data values.
  1782. Not used for string or bytes values.
  1783. 3. value (Sequence[Any]): `count` values compatible with
  1784. `dtype`. Bytes must contain count values of dtype packed
  1785. as binary data.
  1786. 4. writeonce (bool): If *True*, write tag to first page
  1787. of a series only.
  1788. Duplicate and select tags in TIFF.TAG_FILTERED are not written
  1789. if the extratag is specified by integer code.
  1790. Extratags cannot be used to write IFD type tags.
  1791. contiguous:
  1792. If *False* (default), write data to a new series.
  1793. If *True* and the data and arguments are compatible with
  1794. previous written ones (same shape, no compression, etc.),
  1795. the image data are stored contiguously after the previous one.
  1796. In that case, `photometric`, `planarconfig`, and
  1797. `rowsperstrip` are ignored.
  1798. Metadata such as `description`, `metadata`, `datetime`,
  1799. and `extratags` are written to the first page of a contiguous
  1800. series only.
  1801. Contiguous mode cannot be used with the OME or ImageJ formats.
  1802. truncate:
  1803. If *True*, only write first page of contiguous series
  1804. if possible (uncompressed, contiguous, not tiled).
  1805. Other TIFF readers will only be able to read part of the data.
  1806. Cannot be used with the OME or ImageJ formats.
  1807. align:
  1808. Byte boundary on which to align image data in file.
  1809. The default is 16.
  1810. Use mmap.ALLOCATIONGRANULARITY for memory-mapped data.
  1811. Following contiguous writes are not aligned.
  1812. maxworkers:
  1813. Maximum number of threads to concurrently compress tiles
  1814. or strips.
  1815. If *None* or *0*, use up to :py:attr:`_TIFF.MAXWORKERS` CPU
  1816. cores for compressing large segments.
  1817. Using multiple threads can significantly speed up this
  1818. function if the bottleneck is encoding the data, for example,
  1819. in case of large JPEG compressed tiles.
  1820. If the bottleneck is I/O or pure Python code, using multiple
  1821. threads might be detrimental.
  1822. buffersize:
  1823. Approximate number of bytes to compress in one pass.
  1824. The default is :py:attr:`_TIFF.BUFFERSIZE` * 2.
  1825. returnoffset:
  1826. Return offset and number of bytes of memory-mappable image
  1827. data in file.
  1828. Returns:
  1829. If `returnoffset` is *True* and the image data in the file are
  1830. memory-mappable, return the offset and number of bytes of the
  1831. image data in the file.
  1832. """
  1833. # TODO: refactor this function
  1834. fh: FileHandle
  1835. storedshape: StoredShape = StoredShape(frames=-1)
  1836. byteorder: Literal['>', '<']
  1837. inputshape: tuple[int, ...]
  1838. datashape: tuple[int, ...]
  1839. dataarray: NDArray[Any] | None = None
  1840. dataiter: Iterator[NDArray[Any] | bytes | None] | None = None
  1841. dataoffsets: list[int] | None = None
  1842. dataoffsetsoffset: tuple[int, int | None] | None = None
  1843. databytecounts: list[int]
  1844. databytecountsoffset: tuple[int, int | None] | None = None
  1845. subifdsoffsets: tuple[int, int | None] | None = None
  1846. datadtype: numpy.dtype[Any]
  1847. bilevel: bool
  1848. tiles: tuple[int, ...]
  1849. ifdpos: int
  1850. photometricsamples: int
  1851. pos: int | None = None
  1852. predictortag: int
  1853. predictorfunc: Callable[..., Any] | None = None
  1854. compressiontag: int
  1855. compressionfunc: Callable[..., Any] | None = None
  1856. tags: list[tuple[int, bytes, bytes | None, bool]]
  1857. numtiles: int
  1858. numstrips: int
  1859. fh = self._fh
  1860. byteorder = self.tiff.byteorder
  1861. if data is None:
  1862. # empty
  1863. if shape is None or dtype is None:
  1864. raise ValueError(
  1865. "missing required 'shape' or 'dtype' arguments"
  1866. )
  1867. dataarray = None
  1868. dataiter = None
  1869. datashape = tuple(shape)
  1870. datadtype = numpy.dtype(dtype).newbyteorder(byteorder)
  1871. elif hasattr(data, '__next__'):
  1872. # iterator/generator
  1873. if shape is None or dtype is None:
  1874. raise ValueError(
  1875. "missing required 'shape' or 'dtype' arguments"
  1876. )
  1877. dataiter = data # type: ignore[assignment]
  1878. datashape = tuple(shape)
  1879. datadtype = numpy.dtype(dtype).newbyteorder(byteorder)
  1880. elif hasattr(data, 'dtype'):
  1881. # numpy, zarr, or dask array
  1882. data = cast(numpy.ndarray, data)
  1883. dataarray = data
  1884. datadtype = numpy.dtype(data.dtype).newbyteorder(byteorder)
  1885. if not hasattr(data, 'reshape'):
  1886. # zarr array cannot be shape-normalized
  1887. dataarray = numpy.asarray(data, datadtype, 'C')
  1888. else:
  1889. try:
  1890. # numpy array must be C contiguous
  1891. if data.flags.f_contiguous:
  1892. dataarray = numpy.asarray(data, datadtype, 'C')
  1893. except AttributeError:
  1894. # not a numpy array
  1895. pass
  1896. datashape = dataarray.shape
  1897. dataiter = None
  1898. if dtype is not None and numpy.dtype(dtype) != datadtype:
  1899. raise ValueError(
  1900. f'dtype argument {dtype!r} does not match '
  1901. f'data dtype {datadtype}'
  1902. )
  1903. if shape is not None and shape != dataarray.shape:
  1904. raise ValueError(
  1905. f'shape argument {shape!r} does not match '
  1906. f'data shape {dataarray.shape}'
  1907. )
  1908. else:
  1909. # scalar, list, tuple, etc
  1910. # if dtype is not specified, default to float64
  1911. datadtype = numpy.dtype(dtype).newbyteorder(byteorder)
  1912. dataarray = numpy.asarray(data, datadtype, 'C')
  1913. datashape = dataarray.shape
  1914. dataiter = None
  1915. del data
  1916. if any(size >= 4294967296 for size in datashape):
  1917. raise ValueError('invalid data shape')
  1918. bilevel = datadtype.char == '?'
  1919. if bilevel:
  1920. index = -1 if datashape[-1] > 1 else -2
  1921. datasize = product(datashape[:index])
  1922. if datashape[index] % 8:
  1923. datasize *= datashape[index] // 8 + 1
  1924. else:
  1925. datasize *= datashape[index] // 8
  1926. else:
  1927. datasize = product(datashape) * datadtype.itemsize
  1928. if datasize == 0:
  1929. dataarray = None
  1930. compression = False
  1931. bitspersample = None
  1932. if metadata is not None:
  1933. truncate = True
  1934. if (
  1935. not compression
  1936. or (
  1937. not isinstance(compression, bool) # because True == 1
  1938. and compression in ('NONE', 'None', 'none', 1)
  1939. )
  1940. or (
  1941. isinstance(compression, (tuple, list))
  1942. and compression[0] in (None, 0, 1, 'NONE', 'None', 'none')
  1943. )
  1944. ):
  1945. compression = False
  1946. if not predictor or (
  1947. not isinstance(predictor, bool) # because True == 1
  1948. and predictor in {'NONE', 'None', 'none', 1}
  1949. ):
  1950. predictor = False
  1951. inputshape = datashape
  1952. packints = (
  1953. bitspersample is not None
  1954. and bitspersample != datadtype.itemsize * 8
  1955. )
  1956. # just append contiguous data if possible
  1957. if self._datashape is not None and self._datadtype is not None:
  1958. if colormap is not None:
  1959. colormap = numpy.asarray(colormap, dtype=byteorder + 'H')
  1960. if (
  1961. not contiguous
  1962. or self._datashape[1:] != datashape
  1963. or self._datadtype != datadtype
  1964. or (colormap is None and self._colormap is not None)
  1965. or (self._colormap is None and colormap is not None)
  1966. or not numpy.array_equal(
  1967. colormap, self._colormap # type: ignore[arg-type]
  1968. )
  1969. ):
  1970. # incompatible shape, dtype, or colormap
  1971. self._write_remaining_pages()
  1972. if self._imagej:
  1973. raise ValueError(
  1974. 'the ImageJ format does not support '
  1975. 'non-contiguous series'
  1976. )
  1977. if self._omexml is not None:
  1978. if self._subifdslevel < 0:
  1979. # add image to OME-XML
  1980. assert self._storedshape is not None
  1981. assert self._metadata is not None
  1982. self._omexml.addimage(
  1983. dtype=self._datadtype,
  1984. shape=self._datashape[
  1985. 0 if self._datashape[0] != 1 else 1 :
  1986. ],
  1987. storedshape=self._storedshape.shape,
  1988. **self._metadata,
  1989. )
  1990. elif metadata is not None:
  1991. self._write_image_description()
  1992. # description might have been appended to file
  1993. fh.seek(0, os.SEEK_END)
  1994. if self._subifds:
  1995. if self._truncate or truncate:
  1996. raise ValueError(
  1997. 'SubIFDs cannot be used with truncated series'
  1998. )
  1999. self._subifdslevel += 1
  2000. if self._subifdslevel == self._subifds:
  2001. # done with writing SubIFDs
  2002. self._nextifdoffsets = []
  2003. self._subifdsoffsets = []
  2004. self._subifdslevel = -1
  2005. self._subifds = 0
  2006. self._ifdindex = 0
  2007. elif subifds:
  2008. raise ValueError(
  2009. 'SubIFDs in SubIFDs are not supported'
  2010. )
  2011. self._datashape = None
  2012. self._colormap = None
  2013. elif compression or packints or tile:
  2014. raise ValueError(
  2015. 'contiguous mode cannot be used with compression or tiles'
  2016. )
  2017. else:
  2018. # consecutive mode
  2019. # write all data, write IFDs/tags later
  2020. self._datashape = (self._datashape[0] + 1, *datashape)
  2021. offset = fh.tell()
  2022. if dataarray is None:
  2023. fh.write_empty(datasize)
  2024. else:
  2025. fh.write_array(dataarray, datadtype)
  2026. if returnoffset:
  2027. return offset, datasize
  2028. return None
  2029. if self._ome is None:
  2030. if description is None:
  2031. self._ome = '.ome.' in fh.extension
  2032. else:
  2033. self._ome = False
  2034. if self._tifffile or self._imagej:
  2035. self._truncate = bool(truncate)
  2036. elif truncate:
  2037. raise ValueError(
  2038. 'truncate can only be used with imagej or shaped formats'
  2039. )
  2040. else:
  2041. self._truncate = False
  2042. if self._truncate and (compression or packints or tile):
  2043. raise ValueError(
  2044. 'truncate cannot be used with compression, packints, or tiles'
  2045. )
  2046. if datasize == 0:
  2047. # write single placeholder TiffPage for arrays with size=0
  2048. datashape = (0, 0)
  2049. warnings.warn(
  2050. f'{self!r} writing zero-size array to nonconformant TIFF',
  2051. UserWarning,
  2052. stacklevel=2,
  2053. )
  2054. # TODO: reconsider this
  2055. # raise ValueError('cannot save zero size array')
  2056. tagnoformat = self.tiff.tagnoformat
  2057. offsetformat = self.tiff.offsetformat
  2058. offsetsize = self.tiff.offsetsize
  2059. tagsize = self.tiff.tagsize
  2060. MINISBLACK = PHOTOMETRIC.MINISBLACK
  2061. MINISWHITE = PHOTOMETRIC.MINISWHITE
  2062. RGB = PHOTOMETRIC.RGB
  2063. YCBCR = PHOTOMETRIC.YCBCR
  2064. PALETTE = PHOTOMETRIC.PALETTE
  2065. CONTIG = PLANARCONFIG.CONTIG
  2066. SEPARATE = PLANARCONFIG.SEPARATE
  2067. # parse input
  2068. if photometric is not None:
  2069. photometric = enumarg(PHOTOMETRIC, photometric)
  2070. if planarconfig:
  2071. planarconfig = enumarg(PLANARCONFIG, planarconfig)
  2072. if extrasamples is not None:
  2073. # TODO: deprecate non-sequence extrasamples
  2074. extrasamples = tuple(
  2075. int(enumarg(EXTRASAMPLE, x)) for x in sequence(extrasamples)
  2076. )
  2077. if compressionargs is None:
  2078. compressionargs = {}
  2079. if compression:
  2080. if isinstance(compression, (tuple, list)):
  2081. # TODO: unreachable
  2082. raise TypeError(
  2083. "passing multiple values to the 'compression' "
  2084. 'parameter was deprecated in 2022.7.28. '
  2085. "Use 'compressionargs' to pass extra arguments to the "
  2086. 'compression codec.',
  2087. )
  2088. if isinstance(compression, str):
  2089. compression = compression.upper()
  2090. if compression == 'ZLIB':
  2091. compression = 8 # ADOBE_DEFLATE
  2092. elif isinstance(compression, bool):
  2093. compression = 8 # ADOBE_DEFLATE
  2094. compressiontag = enumarg(COMPRESSION, compression).value
  2095. compression = True
  2096. else:
  2097. compressiontag = 1
  2098. compression = False
  2099. if compressiontag == 1:
  2100. compressionargs = {}
  2101. elif compressiontag in {33003, 33004, 33005, 34712}:
  2102. # JPEG2000: use J2K instead of JP2
  2103. compressionargs['codecformat'] = 0 # OPJ_CODEC_J2K
  2104. assert compressionargs is not None
  2105. if predictor:
  2106. if not compression:
  2107. raise ValueError('cannot use predictor without compression')
  2108. if compressiontag in TIFF.IMAGE_COMPRESSIONS:
  2109. # don't use predictor with JPEG, JPEG2000, WEBP, PNG, ...
  2110. raise ValueError(
  2111. 'cannot use predictor with '
  2112. f'{COMPRESSION(compressiontag)!r}'
  2113. )
  2114. if isinstance(predictor, bool):
  2115. if datadtype.kind == 'f':
  2116. predictortag = 3
  2117. elif datadtype.kind in 'iu' and datadtype.itemsize <= 4:
  2118. predictortag = 2
  2119. else:
  2120. raise ValueError(
  2121. f'cannot use predictor with {datadtype!r}'
  2122. )
  2123. else:
  2124. predictor = enumarg(PREDICTOR, predictor)
  2125. if (
  2126. datadtype.kind in 'iu'
  2127. and predictor.value not in {2, 34892, 34893}
  2128. and datadtype.itemsize <= 4
  2129. ) or (
  2130. datadtype.kind == 'f'
  2131. and predictor.value not in {3, 34894, 34895}
  2132. ):
  2133. raise ValueError(
  2134. f'cannot use {predictor!r} with {datadtype!r}'
  2135. )
  2136. predictortag = predictor.value
  2137. else:
  2138. predictortag = 1
  2139. del predictor
  2140. predictorfunc = TIFF.PREDICTORS[predictortag]
  2141. if self._ome:
  2142. if description is not None:
  2143. warnings.warn(
  2144. f'{self!r} not writing description to OME-TIFF',
  2145. UserWarning,
  2146. stacklevel=2,
  2147. )
  2148. description = None
  2149. if self._omexml is None:
  2150. if metadata is None:
  2151. self._omexml = OmeXml()
  2152. else:
  2153. self._omexml = OmeXml(**metadata)
  2154. if volumetric or (tile and len(tile) > 2):
  2155. raise ValueError('OME-TIFF does not support ImageDepth')
  2156. volumetric = False
  2157. elif self._imagej:
  2158. # if tile is not None or predictor or compression:
  2159. # warnings.warn(
  2160. # f'{self!r} the ImageJ format does not support '
  2161. # 'tiles, predictors, compression'
  2162. # )
  2163. if description is not None:
  2164. warnings.warn(
  2165. f'{self!r} not writing description to ImageJ file',
  2166. UserWarning,
  2167. stacklevel=2,
  2168. )
  2169. description = None
  2170. if datadtype.char not in 'BHhf':
  2171. raise ValueError(
  2172. 'the ImageJ format does not support data type '
  2173. f'{datadtype.char!r}'
  2174. )
  2175. if volumetric or (tile and len(tile) > 2):
  2176. raise ValueError(
  2177. 'the ImageJ format does not support ImageDepth'
  2178. )
  2179. volumetric = False
  2180. ijrgb = photometric == RGB if photometric else None
  2181. if datadtype.char != 'B':
  2182. if photometric == RGB:
  2183. raise ValueError(
  2184. 'the ImageJ format does not support '
  2185. f'data type {datadtype!r} for RGB'
  2186. )
  2187. ijrgb = False
  2188. if colormap is not None:
  2189. ijrgb = False
  2190. axes = None if metadata is None else metadata.get('axes', None)
  2191. ijshape = imagej_shape(datashape, rgb=ijrgb, axes=axes)
  2192. if planarconfig == SEPARATE:
  2193. raise ValueError(
  2194. 'the ImageJ format does not support planar samples'
  2195. )
  2196. if ijshape[-1] in {3, 4}:
  2197. photometric = RGB
  2198. elif photometric is None:
  2199. if colormap is not None and datadtype.char == 'B':
  2200. photometric = PALETTE
  2201. else:
  2202. photometric = MINISBLACK
  2203. planarconfig = None
  2204. planarconfig = CONTIG if ijrgb else None
  2205. # verify colormap and indices
  2206. if colormap is not None:
  2207. colormap = numpy.asarray(colormap, dtype=byteorder + 'H')
  2208. self._colormap = colormap
  2209. if self._imagej:
  2210. if colormap.shape != (3, 256):
  2211. raise ValueError('invalid colormap shape for ImageJ')
  2212. if datadtype.char == 'B' and photometric in {
  2213. MINISBLACK,
  2214. MINISWHITE,
  2215. }:
  2216. photometric = PALETTE
  2217. elif not (
  2218. (datadtype.char == 'B' and photometric == PALETTE)
  2219. or (
  2220. datadtype.char in 'Hf'
  2221. and photometric in {MINISBLACK, MINISWHITE}
  2222. )
  2223. ):
  2224. warnings.warn(
  2225. f'{self!r} not writing colormap to ImageJ image with '
  2226. f'dtype={datadtype} and {photometric=}',
  2227. UserWarning,
  2228. stacklevel=2,
  2229. )
  2230. colormap = None
  2231. elif photometric is None and datadtype.char in 'BH':
  2232. photometric = PALETTE
  2233. planarconfig = None
  2234. if colormap.shape != (3, 2 ** (datadtype.itemsize * 8)):
  2235. raise ValueError('invalid colormap shape')
  2236. elif photometric == PALETTE:
  2237. planarconfig = None
  2238. if datadtype.char not in 'BH':
  2239. raise ValueError('invalid data dtype for palette-image')
  2240. if colormap.shape != (3, 2 ** (datadtype.itemsize * 8)):
  2241. raise ValueError('invalid colormap shape')
  2242. else:
  2243. warnings.warn(
  2244. f'{self!r} not writing colormap with image of '
  2245. f'dtype={datadtype} and {photometric=}',
  2246. UserWarning,
  2247. stacklevel=2,
  2248. )
  2249. colormap = None
  2250. if tile:
  2251. # verify tile shape
  2252. if (
  2253. not 1 < len(tile) < 4
  2254. or tile[-1] % 16
  2255. or tile[-2] % 16
  2256. or any(i < 1 for i in tile)
  2257. ):
  2258. raise ValueError(f'invalid tile shape {tile}')
  2259. tile = tuple(int(i) for i in tile)
  2260. if volumetric and len(tile) == 2:
  2261. tile = (1, *tile)
  2262. volumetric = len(tile) == 3
  2263. else:
  2264. tile = ()
  2265. volumetric = bool(volumetric)
  2266. assert isinstance(tile, tuple) # for mypy
  2267. # normalize data shape to 5D or 6D, depending on volume:
  2268. # (pages, separate_samples, [depth,] length, width, contig_samples)
  2269. shape = reshape_nd(
  2270. datashape,
  2271. TIFF.PHOTOMETRIC_SAMPLES.get(
  2272. photometric, 2 # type: ignore[arg-type]
  2273. ),
  2274. )
  2275. ndim = len(shape)
  2276. if volumetric and ndim < 3:
  2277. volumetric = False
  2278. if photometric is None:
  2279. deprecate = False
  2280. photometric = MINISBLACK
  2281. if bilevel:
  2282. photometric = MINISWHITE
  2283. elif planarconfig == CONTIG:
  2284. if ndim > 2 and shape[-1] in {3, 4}:
  2285. photometric = RGB
  2286. deprecate = datadtype.char not in 'BH'
  2287. elif planarconfig == SEPARATE:
  2288. if (volumetric and ndim > 3 and shape[-4] in {3, 4}) or (
  2289. ndim > 2 and shape[-3] in {3, 4}
  2290. ):
  2291. photometric = RGB
  2292. deprecate = True
  2293. elif ndim > 2 and shape[-1] in {3, 4}:
  2294. photometric = RGB
  2295. planarconfig = CONTIG
  2296. deprecate = datadtype.char not in 'BH'
  2297. elif self._imagej or self._ome:
  2298. photometric = MINISBLACK
  2299. planarconfig = None
  2300. elif (volumetric and ndim > 3 and shape[-4] in {3, 4}) or (
  2301. ndim > 2 and shape[-3] in {3, 4}
  2302. ):
  2303. photometric = RGB
  2304. planarconfig = SEPARATE
  2305. deprecate = True
  2306. if deprecate:
  2307. if planarconfig == CONTIG:
  2308. msg = 'contiguous samples', 'parameter is'
  2309. else:
  2310. msg = (
  2311. 'separate component planes',
  2312. "and 'planarconfig' parameters are",
  2313. )
  2314. warnings.warn(
  2315. f"<tifffile.TiffWriter.write> data with shape {datashape} "
  2316. f"and dtype '{datadtype}' are stored as RGB with {msg[0]}."
  2317. ' Future versions will store such data as MINISBLACK in '
  2318. "separate pages by default, unless the 'photometric' "
  2319. f"{msg[1]} specified.",
  2320. DeprecationWarning,
  2321. stacklevel=2,
  2322. )
  2323. del msg
  2324. del deprecate
  2325. del datashape
  2326. assert photometric is not None
  2327. photometricsamples = TIFF.PHOTOMETRIC_SAMPLES[photometric]
  2328. if planarconfig and len(shape) <= (3 if volumetric else 2):
  2329. # TODO: raise error?
  2330. planarconfig = None
  2331. if photometricsamples > 1:
  2332. photometric = MINISBLACK
  2333. if photometricsamples > 1:
  2334. if len(shape) < 3:
  2335. raise ValueError(f'not a {photometric!r} image')
  2336. if len(shape) < 4:
  2337. volumetric = False
  2338. if planarconfig is None:
  2339. if photometric == RGB:
  2340. samples_set = {photometricsamples, 4} # allow common alpha
  2341. else:
  2342. samples_set = {photometricsamples}
  2343. if shape[-1] in samples_set:
  2344. planarconfig = CONTIG
  2345. elif shape[-4 if volumetric else -3] in samples_set:
  2346. planarconfig = SEPARATE
  2347. elif shape[-1] > shape[-4 if volumetric else -3]:
  2348. # TODO: deprecated this?
  2349. planarconfig = SEPARATE
  2350. else:
  2351. planarconfig = CONTIG
  2352. if planarconfig == CONTIG:
  2353. storedshape.contig_samples = shape[-1]
  2354. storedshape.width = shape[-2]
  2355. storedshape.length = shape[-3]
  2356. if volumetric:
  2357. storedshape.depth = shape[-4]
  2358. else:
  2359. storedshape.width = shape[-1]
  2360. storedshape.length = shape[-2]
  2361. if volumetric:
  2362. storedshape.depth = shape[-3]
  2363. storedshape.separate_samples = shape[-4]
  2364. else:
  2365. storedshape.separate_samples = shape[-3]
  2366. if storedshape.samples > photometricsamples:
  2367. storedshape.extrasamples = (
  2368. storedshape.samples - photometricsamples
  2369. )
  2370. elif photometric == PHOTOMETRIC.CFA:
  2371. if len(shape) != 2:
  2372. raise ValueError('invalid CFA image')
  2373. volumetric = False
  2374. planarconfig = None
  2375. storedshape.width = shape[-1]
  2376. storedshape.length = shape[-2]
  2377. # if all(et[0] != 50706 for et in extratags):
  2378. # raise ValueError('must specify DNG tags for CFA image')
  2379. elif planarconfig and len(shape) > (3 if volumetric else 2):
  2380. if planarconfig == CONTIG:
  2381. if extrasamples is None or len(extrasamples) > 0:
  2382. # use extrasamples
  2383. storedshape.contig_samples = shape[-1]
  2384. storedshape.width = shape[-2]
  2385. storedshape.length = shape[-3]
  2386. if volumetric:
  2387. storedshape.depth = shape[-4]
  2388. else:
  2389. planarconfig = None
  2390. storedshape.contig_samples = 1
  2391. storedshape.width = shape[-1]
  2392. storedshape.length = shape[-2]
  2393. if volumetric:
  2394. storedshape.depth = shape[-3]
  2395. else:
  2396. storedshape.width = shape[-1]
  2397. storedshape.length = shape[-2]
  2398. if extrasamples is None or len(extrasamples) > 0:
  2399. # use extrasamples
  2400. if volumetric:
  2401. storedshape.depth = shape[-3]
  2402. storedshape.separate_samples = shape[-4]
  2403. else:
  2404. storedshape.separate_samples = shape[-3]
  2405. else:
  2406. planarconfig = None
  2407. storedshape.separate_samples = 1
  2408. if volumetric:
  2409. storedshape.depth = shape[-3]
  2410. storedshape.extrasamples = storedshape.samples - 1
  2411. else:
  2412. # photometricsamples == 1
  2413. planarconfig = None
  2414. if self._tifffile and (metadata or metadata == {}):
  2415. # remove trailing 1s in shaped series
  2416. while len(shape) > 2 and shape[-1] == 1:
  2417. shape = shape[:-1]
  2418. elif self._imagej and len(shape) > 2 and shape[-1] == 1:
  2419. # TODO: remove this and sync with ImageJ shape
  2420. shape = shape[:-1]
  2421. if len(shape) < 3:
  2422. volumetric = False
  2423. if not extrasamples:
  2424. storedshape.width = shape[-1]
  2425. storedshape.length = shape[-2]
  2426. if volumetric:
  2427. storedshape.depth = shape[-3]
  2428. else:
  2429. storedshape.contig_samples = shape[-1]
  2430. storedshape.width = shape[-2]
  2431. storedshape.length = shape[-3]
  2432. if volumetric:
  2433. storedshape.depth = shape[-4]
  2434. storedshape.extrasamples = storedshape.samples - 1
  2435. if not volumetric and tile and len(tile) == 3 and tile[0] > 1:
  2436. raise ValueError(
  2437. f'<tifffile.TiffWriter.write> cannot write {storedshape!r} '
  2438. f'using volumetric tiles {tile}'
  2439. )
  2440. if subfiletype is not None and subfiletype & 0b100:
  2441. # FILETYPE_MASK
  2442. if not (
  2443. bilevel
  2444. and storedshape.samples == 1
  2445. and photometric in {0, 1, 4}
  2446. ):
  2447. raise ValueError('invalid SubfileType MASK')
  2448. photometric = PHOTOMETRIC.MASK
  2449. packints = False
  2450. if bilevel:
  2451. if bitspersample is not None and bitspersample != 1:
  2452. raise ValueError(f'{bitspersample=} must be 1 for bilevel')
  2453. bitspersample = 1
  2454. elif compressiontag in {6, 7, 34892, 33007}:
  2455. # JPEG
  2456. # TODO: add bitspersample to compressionargs?
  2457. if bitspersample is None:
  2458. if 'bitspersample' in compressionargs:
  2459. bitspersample = compressionargs['bitspersample']
  2460. else:
  2461. bitspersample = 12 if datadtype == 'uint16' else 8
  2462. if not 2 <= bitspersample <= 16:
  2463. raise ValueError(
  2464. f'{bitspersample=} invalid for JPEG compression'
  2465. )
  2466. elif compressiontag in {33003, 33004, 33005, 34712, 50002, 52546}:
  2467. # JPEG2K, JPEGXL
  2468. # TODO: unify with JPEG?
  2469. if bitspersample is None:
  2470. if 'bitspersample' in compressionargs:
  2471. bitspersample = compressionargs['bitspersample']
  2472. else:
  2473. bitspersample = datadtype.itemsize * 8
  2474. if not (
  2475. bitspersample > {1: 0, 2: 8, 4: 16}[datadtype.itemsize]
  2476. and bitspersample <= datadtype.itemsize * 8
  2477. ):
  2478. raise ValueError(
  2479. f'{bitspersample=} out of range of {datadtype=}'
  2480. )
  2481. elif bitspersample is None:
  2482. bitspersample = datadtype.itemsize * 8
  2483. elif (
  2484. datadtype.kind != 'u' or datadtype.itemsize > 4
  2485. ) and bitspersample != datadtype.itemsize * 8:
  2486. raise ValueError(f'{bitspersample=} does not match {datadtype=}')
  2487. elif not (
  2488. bitspersample > {1: 0, 2: 8, 4: 16}[datadtype.itemsize]
  2489. and bitspersample <= datadtype.itemsize * 8
  2490. ):
  2491. raise ValueError(f'{bitspersample=} out of range of {datadtype=}')
  2492. elif compression:
  2493. if bitspersample != datadtype.itemsize * 8:
  2494. raise ValueError(
  2495. f'{bitspersample=} cannot be used with compression'
  2496. )
  2497. elif bitspersample != datadtype.itemsize * 8:
  2498. packints = True
  2499. if storedshape.frames == -1:
  2500. s0 = storedshape.page_size
  2501. storedshape.frames = 1 if s0 == 0 else product(inputshape) // s0
  2502. if datasize > 0 and not storedshape.is_valid:
  2503. raise RuntimeError(f'invalid {storedshape!r}')
  2504. if photometric == PALETTE:
  2505. if storedshape.samples != 1 or storedshape.extrasamples > 0:
  2506. raise ValueError(f'invalid {storedshape!r} for palette mode')
  2507. elif storedshape.samples < photometricsamples:
  2508. raise ValueError(
  2509. f'not enough samples for {photometric!r}: '
  2510. f'expected {photometricsamples}, got {storedshape.samples}'
  2511. )
  2512. if (
  2513. planarconfig is not None
  2514. and storedshape.planarconfig != planarconfig
  2515. ):
  2516. raise ValueError(
  2517. f'{planarconfig!r} does not match {storedshape!r}'
  2518. )
  2519. del planarconfig
  2520. if dataarray is not None:
  2521. dataarray = dataarray.reshape(storedshape.shape)
  2522. tags = [] # list of (code, ifdentry, ifdvalue, writeonce)
  2523. if tile:
  2524. tagbytecounts = 325 # TileByteCounts
  2525. tagoffsets = 324 # TileOffsets
  2526. else:
  2527. tagbytecounts = 279 # StripByteCounts
  2528. tagoffsets = 273 # StripOffsets
  2529. self._dataoffsetstag = tagoffsets
  2530. pack = self._pack
  2531. addtag = self._addtag
  2532. if extratags is None:
  2533. extratags = ()
  2534. if description is not None:
  2535. # ImageDescription: user provided description
  2536. addtag(tags, 270, 2, 0, description, True)
  2537. # write shape and metadata to ImageDescription
  2538. self._metadata = {} if not metadata else metadata.copy()
  2539. if self._omexml is not None:
  2540. if len(self._omexml.images) == 0:
  2541. # rewritten later at end of file
  2542. description = '\x00\x00\x00\x00'
  2543. else:
  2544. description = None
  2545. elif self._imagej:
  2546. ijmetadata = parse_kwargs(
  2547. self._metadata,
  2548. 'Info',
  2549. 'Labels',
  2550. 'Ranges',
  2551. 'LUTs',
  2552. 'Plot',
  2553. 'ROI',
  2554. 'Overlays',
  2555. 'Properties',
  2556. 'info',
  2557. 'labels',
  2558. 'ranges',
  2559. 'luts',
  2560. 'plot',
  2561. 'roi',
  2562. 'overlays',
  2563. 'prop',
  2564. )
  2565. for t in imagej_metadata_tag(ijmetadata, byteorder):
  2566. addtag(tags, *t)
  2567. description = imagej_description(
  2568. inputshape,
  2569. rgb=storedshape.contig_samples in {3, 4},
  2570. colormaped=self._colormap is not None,
  2571. **self._metadata,
  2572. )
  2573. description += '\x00' * 64 # add buffer for in-place update
  2574. elif self._tifffile and (metadata or metadata == {}):
  2575. if self._truncate:
  2576. self._metadata.update(truncated=True)
  2577. description = shaped_description(inputshape, **self._metadata)
  2578. description += '\x00' * 16 # add buffer for in-place update
  2579. # elif metadata is None and self._truncate:
  2580. # raise ValueError('cannot truncate without writing metadata')
  2581. elif description is not None:
  2582. if not isinstance(description, bytes):
  2583. description = description.encode('ascii')
  2584. self._descriptiontag = TiffTag(
  2585. self, 0, 270, 2, len(description), description, 0
  2586. )
  2587. description = None
  2588. if description is None:
  2589. # disable shaped format if user disabled metadata
  2590. self._tifffile = False
  2591. else:
  2592. description = description.encode('ascii')
  2593. addtag(tags, 270, 2, 0, description, True)
  2594. self._descriptiontag = TiffTag(
  2595. self, 0, 270, 2, len(description), description, 0
  2596. )
  2597. del description
  2598. if software is None:
  2599. software = 'tifffile.py'
  2600. if software:
  2601. addtag(tags, 305, 2, 0, software, True)
  2602. if datetime:
  2603. if isinstance(datetime, str):
  2604. if len(datetime) != 19 or datetime[16] != ':':
  2605. raise ValueError('invalid datetime string')
  2606. elif isinstance(datetime, DateTime):
  2607. datetime = datetime.strftime('%Y:%m:%d %H:%M:%S')
  2608. else:
  2609. datetime = DateTime.now().strftime('%Y:%m:%d %H:%M:%S')
  2610. addtag(tags, 306, 2, 0, datetime, True)
  2611. addtag(tags, 259, 3, 1, compressiontag) # Compression
  2612. if compressiontag == 34887:
  2613. # LERC
  2614. if (
  2615. 'compression' not in compressionargs
  2616. or compressionargs['compression'] is None
  2617. ):
  2618. lerc_compression = 0
  2619. elif compressionargs['compression'] == 'deflate':
  2620. lerc_compression = 1
  2621. elif compressionargs['compression'] == 'zstd':
  2622. lerc_compression = 2
  2623. else:
  2624. raise ValueError(
  2625. 'invalid LERC compression '
  2626. f'{compressionargs["compression"]!r}'
  2627. )
  2628. addtag(tags, 50674, 4, 2, (4, lerc_compression))
  2629. del lerc_compression
  2630. if predictortag != 1:
  2631. addtag(tags, 317, 3, 1, predictortag)
  2632. addtag(tags, 256, 4, 1, storedshape.width) # ImageWidth
  2633. addtag(tags, 257, 4, 1, storedshape.length) # ImageLength
  2634. if tile:
  2635. addtag(tags, 322, 4, 1, tile[-1]) # TileWidth
  2636. addtag(tags, 323, 4, 1, tile[-2]) # TileLength
  2637. if volumetric:
  2638. addtag(tags, 32997, 4, 1, storedshape.depth) # ImageDepth
  2639. if tile:
  2640. addtag(tags, 32998, 4, 1, tile[0]) # TileDepth
  2641. if subfiletype is not None:
  2642. addtag(tags, 254, 4, 1, subfiletype) # NewSubfileType
  2643. if (subifds or self._subifds) and self._subifdslevel < 0:
  2644. if self._subifds:
  2645. subifds = self._subifds
  2646. elif hasattr(subifds, '__len__'):
  2647. # allow TiffPage.subifds tuple
  2648. subifds = len(subifds) # type: ignore[arg-type]
  2649. else:
  2650. subifds = int(subifds) # type: ignore[arg-type]
  2651. self._subifds = subifds
  2652. addtag(
  2653. tags, 330, 18 if offsetsize > 4 else 13, subifds, [0] * subifds
  2654. )
  2655. if not bilevel and datadtype.kind != 'u':
  2656. # SampleFormat
  2657. sampleformat = {'u': 1, 'i': 2, 'f': 3, 'c': 6}[datadtype.kind]
  2658. addtag(
  2659. tags,
  2660. 339,
  2661. 3,
  2662. storedshape.samples,
  2663. (sampleformat,) * storedshape.samples,
  2664. )
  2665. if colormap is not None:
  2666. addtag(tags, 320, 3, colormap.size, colormap)
  2667. if iccprofile is not None:
  2668. addtag(tags, 34675, 7, len(iccprofile), iccprofile)
  2669. addtag(tags, 277, 3, 1, storedshape.samples)
  2670. if bilevel:
  2671. # PlanarConfiguration
  2672. if storedshape.samples > 1:
  2673. addtag(tags, 284, 3, 1, storedshape.planarconfig)
  2674. elif storedshape.samples > 1:
  2675. # PlanarConfiguration
  2676. addtag(tags, 284, 3, 1, storedshape.planarconfig)
  2677. # BitsPerSample
  2678. addtag(
  2679. tags,
  2680. 258,
  2681. 3,
  2682. storedshape.samples,
  2683. (bitspersample,) * storedshape.samples,
  2684. )
  2685. else:
  2686. addtag(tags, 258, 3, 1, bitspersample)
  2687. if storedshape.extrasamples > 0:
  2688. if extrasamples is not None:
  2689. if storedshape.extrasamples != len(extrasamples):
  2690. raise ValueError(
  2691. 'wrong number of extrasamples '
  2692. f'{storedshape.extrasamples} != {len(extrasamples)}'
  2693. )
  2694. addtag(tags, 338, 3, len(extrasamples), extrasamples)
  2695. elif photometric == RGB and storedshape.extrasamples == 1:
  2696. # Unassociated alpha channel
  2697. addtag(tags, 338, 3, 1, 2)
  2698. else:
  2699. # Unspecified alpha channel
  2700. addtag(
  2701. tags,
  2702. 338,
  2703. 3,
  2704. storedshape.extrasamples,
  2705. (0,) * storedshape.extrasamples,
  2706. )
  2707. if jpegtables is not None:
  2708. addtag(tags, 347, 7, len(jpegtables), jpegtables)
  2709. if (
  2710. compressiontag == 7
  2711. and storedshape.planarconfig == 1
  2712. and photometric in {RGB, YCBCR}
  2713. ):
  2714. # JPEG compression with subsampling
  2715. # TODO: use JPEGTables for multiple tiles or strips
  2716. if subsampling is None:
  2717. subsampling = (2, 2)
  2718. elif subsampling not in {(1, 1), (2, 1), (2, 2), (4, 1)}:
  2719. raise ValueError(
  2720. f'invalid subsampling factors {subsampling!r}'
  2721. )
  2722. maxsampling = max(subsampling) * 8
  2723. if tile and (tile[-1] % maxsampling or tile[-2] % maxsampling):
  2724. raise ValueError(f'tile shape not a multiple of {maxsampling}')
  2725. if storedshape.extrasamples > 1:
  2726. raise ValueError('JPEG subsampling requires RGB(A) images')
  2727. addtag(tags, 530, 3, 2, subsampling) # YCbCrSubSampling
  2728. # use PhotometricInterpretation YCBCR by default
  2729. outcolorspace = enumarg(
  2730. PHOTOMETRIC, compressionargs.get('outcolorspace', 6)
  2731. )
  2732. compressionargs['subsampling'] = subsampling
  2733. compressionargs['colorspace'] = photometric.name
  2734. compressionargs['outcolorspace'] = outcolorspace.name
  2735. addtag(tags, 262, 3, 1, outcolorspace)
  2736. if outcolorspace == YCBCR and all(
  2737. et[0] != 532 for et in extratags
  2738. ):
  2739. # ReferenceBlackWhite is required for YCBCR
  2740. addtag(
  2741. tags,
  2742. 532,
  2743. 5,
  2744. 6,
  2745. (0, 1, 255, 1, 128, 1, 255, 1, 128, 1, 255, 1),
  2746. )
  2747. else:
  2748. if subsampling not in {None, (1, 1)}:
  2749. logger().warning(
  2750. f'{self!r} cannot apply subsampling {subsampling!r}'
  2751. )
  2752. subsampling = None
  2753. maxsampling = 1
  2754. addtag(
  2755. tags, 262, 3, 1, photometric.value
  2756. ) # PhotometricInterpretation
  2757. if photometric == YCBCR:
  2758. # YCbCrSubSampling and ReferenceBlackWhite
  2759. addtag(tags, 530, 3, 2, (1, 1))
  2760. if all(et[0] != 532 for et in extratags):
  2761. addtag(
  2762. tags,
  2763. 532,
  2764. 5,
  2765. 6,
  2766. (0, 1, 255, 1, 128, 1, 255, 1, 128, 1, 255, 1),
  2767. )
  2768. if resolutionunit is not None:
  2769. resolutionunit = enumarg(RESUNIT, resolutionunit)
  2770. elif self._imagej or resolution is None:
  2771. resolutionunit = RESUNIT.NONE
  2772. else:
  2773. resolutionunit = RESUNIT.INCH
  2774. if resolution is not None:
  2775. addtag(tags, 282, 5, 1, rational(resolution[0])) # XResolution
  2776. addtag(tags, 283, 5, 1, rational(resolution[1])) # YResolution
  2777. if len(resolution) > 2:
  2778. # TODO: unreachable
  2779. raise ValueError(
  2780. "passing a unit along with the 'resolution' parameter "
  2781. 'was deprecated in 2022.7.28. '
  2782. "Use the 'resolutionunit' parameter.",
  2783. )
  2784. addtag(tags, 296, 3, 1, resolutionunit) # ResolutionUnit
  2785. else:
  2786. addtag(tags, 282, 5, 1, (1, 1)) # XResolution
  2787. addtag(tags, 283, 5, 1, (1, 1)) # YResolution
  2788. addtag(tags, 296, 3, 1, resolutionunit) # ResolutionUnit
  2789. # can save data array contiguous
  2790. contiguous = not (compression or packints or bilevel)
  2791. if tile:
  2792. # one chunk per tile per plane
  2793. if len(tile) == 2:
  2794. tiles = (
  2795. (storedshape.length + tile[0] - 1) // tile[0],
  2796. (storedshape.width + tile[1] - 1) // tile[1],
  2797. )
  2798. contiguous = (
  2799. contiguous
  2800. and storedshape.length == tile[0]
  2801. and storedshape.width == tile[1]
  2802. )
  2803. else:
  2804. tiles = (
  2805. (storedshape.depth + tile[0] - 1) // tile[0],
  2806. (storedshape.length + tile[1] - 1) // tile[1],
  2807. (storedshape.width + tile[2] - 1) // tile[2],
  2808. )
  2809. contiguous = (
  2810. contiguous
  2811. and storedshape.depth == tile[0]
  2812. and storedshape.length == tile[1]
  2813. and storedshape.width == tile[2]
  2814. )
  2815. numtiles = product(tiles) * storedshape.separate_samples
  2816. databytecounts = [
  2817. product(tile) * storedshape.contig_samples * datadtype.itemsize
  2818. ] * numtiles
  2819. bytecountformat = self._bytecount_format(
  2820. databytecounts, compressiontag
  2821. )
  2822. addtag(
  2823. tags, tagbytecounts, bytecountformat, numtiles, databytecounts
  2824. )
  2825. addtag(tags, tagoffsets, offsetformat, numtiles, [0] * numtiles)
  2826. bytecountformat = f'{numtiles}{bytecountformat}'
  2827. if not contiguous:
  2828. if dataarray is not None:
  2829. dataiter = iter_tiles(dataarray, tile, tiles)
  2830. elif dataiter is None and not (
  2831. compression or packints or bilevel
  2832. ):
  2833. def dataiter_(
  2834. numtiles: int = numtiles * storedshape.frames,
  2835. bytecount: int = databytecounts[0],
  2836. ) -> Iterator[bytes]:
  2837. # yield empty tiles
  2838. chunk = bytes(bytecount)
  2839. for _ in range(numtiles):
  2840. yield chunk
  2841. dataiter = dataiter_()
  2842. rowsperstrip = 0
  2843. elif contiguous and (
  2844. rowsperstrip is None or rowsperstrip >= storedshape.length
  2845. ):
  2846. count = storedshape.separate_samples * storedshape.depth
  2847. databytecounts = [
  2848. storedshape.length
  2849. * storedshape.width
  2850. * storedshape.contig_samples
  2851. * datadtype.itemsize
  2852. ] * count
  2853. bytecountformat = self._bytecount_format(
  2854. databytecounts, compressiontag
  2855. )
  2856. addtag(tags, tagbytecounts, bytecountformat, count, databytecounts)
  2857. addtag(tags, tagoffsets, offsetformat, count, [0] * count)
  2858. addtag(tags, 278, 4, 1, storedshape.length) # RowsPerStrip
  2859. bytecountformat = f'{count}{bytecountformat}'
  2860. rowsperstrip = storedshape.length
  2861. numstrips = count
  2862. else:
  2863. # use rowsperstrip
  2864. rowsize = (
  2865. storedshape.width
  2866. * storedshape.contig_samples
  2867. * datadtype.itemsize
  2868. )
  2869. if compressiontag == 48124:
  2870. # Jetraw works on whole camera frame
  2871. rowsperstrip = storedshape.length
  2872. if rowsperstrip is None:
  2873. # compress ~256 KB chunks by default
  2874. # TIFF-EP requires <= 64 KB
  2875. if compression:
  2876. rowsperstrip = 262144 // rowsize
  2877. else:
  2878. rowsperstrip = storedshape.length
  2879. if rowsperstrip < 1:
  2880. rowsperstrip = maxsampling
  2881. elif rowsperstrip > storedshape.length:
  2882. rowsperstrip = storedshape.length
  2883. elif subsampling and rowsperstrip % maxsampling:
  2884. rowsperstrip = (
  2885. math.ceil(rowsperstrip / maxsampling) * maxsampling
  2886. )
  2887. assert rowsperstrip is not None
  2888. addtag(tags, 278, 4, 1, rowsperstrip) # RowsPerStrip
  2889. numstrips1 = (
  2890. storedshape.length + rowsperstrip - 1
  2891. ) // rowsperstrip
  2892. numstrips = (
  2893. numstrips1 * storedshape.separate_samples * storedshape.depth
  2894. )
  2895. # TODO: save bilevel data with rowsperstrip
  2896. stripsize = rowsperstrip * rowsize
  2897. databytecounts = [stripsize] * numstrips
  2898. laststripsize = stripsize - rowsize * (
  2899. numstrips1 * rowsperstrip - storedshape.length
  2900. )
  2901. for i in range(numstrips1 - 1, numstrips, numstrips1):
  2902. databytecounts[i] = laststripsize
  2903. bytecountformat = self._bytecount_format(
  2904. databytecounts, compressiontag
  2905. )
  2906. addtag(
  2907. tags, tagbytecounts, bytecountformat, numstrips, databytecounts
  2908. )
  2909. addtag(tags, tagoffsets, offsetformat, numstrips, [0] * numstrips)
  2910. bytecountformat = bytecountformat * numstrips
  2911. if dataarray is not None and not contiguous:
  2912. dataiter = iter_images(dataarray)
  2913. if dataiter is None and not contiguous:
  2914. raise ValueError('cannot write non-contiguous empty file')
  2915. # add extra tags from user; filter duplicate and select tags
  2916. extratag: TagTuple
  2917. tagset = {t[0] for t in tags}
  2918. tagset.update(TIFF.TAG_FILTERED)
  2919. for extratag in extratags:
  2920. if extratag[0] in tagset:
  2921. logger().warning(
  2922. f'{self!r} not writing extratag {extratag[0]}'
  2923. )
  2924. else:
  2925. addtag(tags, *extratag)
  2926. del tagset
  2927. del extratags
  2928. # TODO: check TIFFReadDirectoryCheckOrder warning in files containing
  2929. # multiple tags of same code
  2930. # the entries in an IFD must be sorted in ascending order by tag code
  2931. tags = sorted(tags, key=lambda x: x[0])
  2932. # define compress function
  2933. compressionaxis: int = -2
  2934. bytesiter: bool = False
  2935. tupleiter: bool = False
  2936. iteritem: NDArray[Any] | tuple[bytes, int] | bytes | None
  2937. if dataiter is not None:
  2938. iteritem, dataiter = peek_iterator(dataiter)
  2939. if isinstance(iteritem, tuple):
  2940. tupleiter = True
  2941. iteritem, bytecount = iteritem
  2942. if not isinstance(iteritem, bytes):
  2943. raise TypeError(f'{type(iteritem)=} != bytes')
  2944. bytesiter = isinstance(iteritem, bytes)
  2945. if not bytesiter:
  2946. iteritem = numpy.asarray(iteritem)
  2947. if (
  2948. tile
  2949. and storedshape.contig_samples == 1
  2950. and iteritem.shape[-1] != 1
  2951. ):
  2952. # issue 185
  2953. compressionaxis = -1
  2954. if iteritem.dtype.char != datadtype.char:
  2955. raise ValueError(
  2956. f'dtype of iterator {iteritem.dtype!r} '
  2957. f'does not match dtype {datadtype!r}'
  2958. )
  2959. else:
  2960. iteritem = None
  2961. if bilevel:
  2962. if compressiontag == 1:
  2963. def compressionfunc1(
  2964. data: Any, axis: int = compressionaxis
  2965. ) -> bytes:
  2966. return numpy.packbits(data, axis=axis).tobytes()
  2967. compressionfunc = compressionfunc1
  2968. elif compressiontag in {5, 32773, 8, 32946, 50013, 34925, 50000}:
  2969. # LZW, PackBits, deflate, LZMA, ZSTD
  2970. def compressionfunc2(
  2971. data: Any,
  2972. compressor: Any = TIFF.COMPRESSORS[compressiontag],
  2973. axis: int = compressionaxis,
  2974. kwargs: Any = compressionargs,
  2975. ) -> bytes:
  2976. data = numpy.packbits(data, axis=axis).tobytes()
  2977. return compressor(data, **kwargs)
  2978. compressionfunc = compressionfunc2
  2979. else:
  2980. raise NotImplementedError('cannot compress bilevel image')
  2981. elif compression:
  2982. compressor = TIFF.COMPRESSORS[compressiontag]
  2983. if compressiontag == 32773:
  2984. # PackBits
  2985. compressionargs['axis'] = compressionaxis
  2986. # elif compressiontag == 48124:
  2987. # # Jetraw
  2988. # imagecodecs.jetraw_init(
  2989. # parameters=compressionargs.pop('parameters', None),
  2990. # verbose=compressionargs.pop('verbose', None),
  2991. # )
  2992. # if not 'identifier' in compressionargs:
  2993. # raise ValueError(
  2994. # "jetraw_encode() missing argument: 'identifier'"
  2995. # )
  2996. if subsampling:
  2997. # JPEG with subsampling
  2998. def compressionfunc(
  2999. data: Any,
  3000. compressor: Any = compressor,
  3001. kwargs: Any = compressionargs,
  3002. ) -> bytes:
  3003. return compressor(data, **kwargs)
  3004. elif predictorfunc is not None:
  3005. def compressionfunc(
  3006. data: Any,
  3007. predictorfunc: Any = predictorfunc,
  3008. compressor: Any = compressor,
  3009. axis: int = compressionaxis,
  3010. kwargs: Any = compressionargs,
  3011. ) -> bytes:
  3012. data = predictorfunc(data, axis=axis)
  3013. return compressor(data, **kwargs)
  3014. elif compressionargs:
  3015. def compressionfunc(
  3016. data: Any,
  3017. compressor: Any = compressor,
  3018. kwargs: Any = compressionargs,
  3019. ) -> bytes:
  3020. return compressor(data, **kwargs)
  3021. elif compressiontag > 1:
  3022. compressionfunc = compressor
  3023. else:
  3024. compressionfunc = None
  3025. elif packints:
  3026. def compressionfunc(
  3027. data: Any,
  3028. bps: Any = bitspersample,
  3029. axis: int = compressionaxis,
  3030. ) -> bytes:
  3031. return imagecodecs.packints_encode(
  3032. data, bps, axis=axis
  3033. ) # type: ignore[return-value]
  3034. else:
  3035. compressionfunc = None
  3036. del compression
  3037. if not contiguous and not bytesiter and compressionfunc is not None:
  3038. # create iterator of encoded tiles or strips
  3039. bytesiter = True
  3040. if tile:
  3041. # dataiter yields tiles
  3042. tileshape = (*tile, storedshape.contig_samples)
  3043. tilesize = product(tileshape) * datadtype.itemsize
  3044. maxworkers = TiffWriter._maxworkers(
  3045. maxworkers,
  3046. numtiles * storedshape.frames,
  3047. tilesize,
  3048. compressiontag,
  3049. )
  3050. # yield encoded tiles
  3051. dataiter = encode_chunks(
  3052. numtiles * storedshape.frames,
  3053. dataiter, # type: ignore[arg-type]
  3054. compressionfunc,
  3055. tileshape,
  3056. datadtype,
  3057. maxworkers,
  3058. buffersize,
  3059. True,
  3060. )
  3061. else:
  3062. # dataiter yields frames
  3063. maxworkers = TiffWriter._maxworkers(
  3064. maxworkers,
  3065. numstrips * storedshape.frames,
  3066. stripsize,
  3067. compressiontag,
  3068. )
  3069. # yield strips
  3070. dataiter = iter_strips(
  3071. dataiter, # type: ignore[arg-type]
  3072. storedshape.page_shape,
  3073. datadtype,
  3074. rowsperstrip,
  3075. )
  3076. # yield encoded strips
  3077. dataiter = encode_chunks(
  3078. numstrips * storedshape.frames,
  3079. dataiter,
  3080. compressionfunc,
  3081. (
  3082. rowsperstrip,
  3083. storedshape.width,
  3084. storedshape.contig_samples,
  3085. ),
  3086. datadtype,
  3087. maxworkers,
  3088. buffersize,
  3089. False,
  3090. )
  3091. fhpos = fh.tell()
  3092. # commented out to allow image data beyond 4GB in classic TIFF
  3093. # if (
  3094. # not (
  3095. # offsetsize > 4
  3096. # or self._imagej or compressionfunc is not None
  3097. # )
  3098. # and fhpos + datasize > 2**32 - 1
  3099. # ):
  3100. # raise ValueError('data too large for classic TIFF format')
  3101. dataoffset: int = 0
  3102. # if not compressed or multi-tiled, write the first IFD and then
  3103. # all data contiguously; else, write all IFDs and data interleaved
  3104. for pageindex in range(1 if contiguous else storedshape.frames):
  3105. ifdpos = fhpos
  3106. if ifdpos % 2:
  3107. # position of IFD must begin on a word boundary
  3108. fh.write(b'\x00')
  3109. ifdpos += 1
  3110. if self._subifdslevel < 0:
  3111. # update pointer at ifdoffset
  3112. fh.seek(self._ifdoffset)
  3113. fh.write(pack(offsetformat, ifdpos))
  3114. fh.seek(ifdpos)
  3115. # create IFD in memory
  3116. if pageindex < 2:
  3117. subifdsoffsets = None
  3118. ifd = io.BytesIO()
  3119. ifd.write(pack(tagnoformat, len(tags)))
  3120. tagoffset = ifd.tell()
  3121. ifd.write(b''.join(t[1] for t in tags))
  3122. ifdoffset = ifd.tell()
  3123. ifd.write(pack(offsetformat, 0)) # offset to next IFD
  3124. # write tag values and patch offsets in ifdentries
  3125. for tagindex, tag in enumerate(tags):
  3126. offset = tagoffset + tagindex * tagsize + 4 + offsetsize
  3127. code = tag[0]
  3128. value = tag[2]
  3129. if value:
  3130. pos = ifd.tell()
  3131. if pos % 2:
  3132. # tag value is expected to begin on word boundary
  3133. ifd.write(b'\x00')
  3134. pos += 1
  3135. ifd.seek(offset)
  3136. ifd.write(pack(offsetformat, ifdpos + pos))
  3137. ifd.seek(pos)
  3138. ifd.write(value)
  3139. if code == tagoffsets:
  3140. dataoffsetsoffset = offset, pos
  3141. elif code == tagbytecounts:
  3142. databytecountsoffset = offset, pos
  3143. elif code == 270:
  3144. if (
  3145. self._descriptiontag is not None
  3146. and self._descriptiontag.offset == 0
  3147. and value.startswith(
  3148. self._descriptiontag.value
  3149. )
  3150. ):
  3151. self._descriptiontag.offset = (
  3152. ifdpos + tagoffset + tagindex * tagsize
  3153. )
  3154. self._descriptiontag.valueoffset = ifdpos + pos
  3155. elif code == 330:
  3156. subifdsoffsets = offset, pos
  3157. elif code == tagoffsets:
  3158. dataoffsetsoffset = offset, None
  3159. elif code == tagbytecounts:
  3160. databytecountsoffset = offset, None
  3161. elif code == 270:
  3162. if (
  3163. self._descriptiontag is not None
  3164. and self._descriptiontag.offset == 0
  3165. and self._descriptiontag.value in tag[1][-4:]
  3166. ):
  3167. self._descriptiontag.offset = (
  3168. ifdpos + tagoffset + tagindex * tagsize
  3169. )
  3170. self._descriptiontag.valueoffset = (
  3171. self._descriptiontag.offset + offsetsize + 4
  3172. )
  3173. elif code == 330:
  3174. subifdsoffsets = offset, None
  3175. ifdsize = ifd.tell()
  3176. if ifdsize % 2:
  3177. ifd.write(b'\x00')
  3178. ifdsize += 1
  3179. # write IFD later when strip/tile bytecounts and offsets are known
  3180. fh.seek(ifdsize, os.SEEK_CUR)
  3181. # write image data
  3182. dataoffset = fh.tell()
  3183. if align is None:
  3184. align = 16
  3185. skip = (align - (dataoffset % align)) % align
  3186. fh.seek(skip, os.SEEK_CUR)
  3187. dataoffset += skip
  3188. if contiguous:
  3189. # write all image data contiguously
  3190. if dataiter is not None:
  3191. byteswritten = 0
  3192. if bytesiter:
  3193. for iteritem in dataiter:
  3194. # assert isinstance(iteritem, bytes)
  3195. byteswritten += fh.write(
  3196. iteritem # type: ignore[arg-type]
  3197. )
  3198. del iteritem
  3199. else:
  3200. pagesize = storedshape.page_size * datadtype.itemsize
  3201. for iteritem in dataiter:
  3202. if iteritem is None:
  3203. byteswritten += fh.write_empty(pagesize)
  3204. else:
  3205. # assert isinstance(iteritem, numpy.ndarray)
  3206. byteswritten += fh.write_array(
  3207. iteritem, # type: ignore[arg-type]
  3208. datadtype,
  3209. )
  3210. del iteritem
  3211. if byteswritten != datasize:
  3212. raise ValueError(
  3213. 'iterator contains wrong number of bytes '
  3214. f'{byteswritten} != {datasize}'
  3215. )
  3216. elif dataarray is None:
  3217. fh.write_empty(datasize)
  3218. else:
  3219. fh.write_array(dataarray, datadtype)
  3220. elif tupleiter:
  3221. # write tiles or strips from iterator of tuples
  3222. assert dataiter is not None
  3223. dataoffsets = [0] * (numtiles if tile else numstrips)
  3224. offset = dataoffset
  3225. for chunkindex in range(numtiles if tile else numstrips):
  3226. iteritem, bytecount = cast(
  3227. tuple[bytes, int], next(dataiter)
  3228. )
  3229. # assert bytecount >= len(iteritem)
  3230. databytecounts[chunkindex] = bytecount
  3231. dataoffsets[chunkindex] = offset
  3232. offset += len(iteritem)
  3233. fh.write(iteritem)
  3234. del iteritem
  3235. elif bytesiter:
  3236. # write tiles or strips
  3237. assert dataiter is not None
  3238. for chunkindex in range(numtiles if tile else numstrips):
  3239. iteritem = cast(bytes, next(dataiter))
  3240. # assert isinstance(iteritem, bytes)
  3241. databytecounts[chunkindex] = len(iteritem)
  3242. fh.write(iteritem)
  3243. del iteritem
  3244. elif tile:
  3245. # write uncompressed tiles
  3246. assert dataiter is not None
  3247. tileshape = (*tile, storedshape.contig_samples)
  3248. tilesize = product(tileshape) * datadtype.itemsize
  3249. for tileindex in range(numtiles):
  3250. iteritem = next(dataiter)
  3251. if iteritem is None:
  3252. databytecounts[tileindex] = 0
  3253. # fh.write_empty(tilesize)
  3254. continue
  3255. # assert not isinstance(iteritem, bytes)
  3256. iteritem = numpy.ascontiguousarray(iteritem, datadtype)
  3257. if iteritem.nbytes != tilesize:
  3258. # if iteritem.dtype != datadtype:
  3259. # raise ValueError(
  3260. # 'dtype of tile does not match data'
  3261. # )
  3262. if iteritem.nbytes > tilesize:
  3263. raise ValueError('tile is too large')
  3264. pad = tuple(
  3265. (0, i - j)
  3266. for i, j in zip(
  3267. tileshape, iteritem.shape, strict=False
  3268. )
  3269. )
  3270. iteritem = numpy.pad(iteritem, pad)
  3271. fh.write_array(iteritem)
  3272. del iteritem
  3273. else:
  3274. raise RuntimeError('unreachable code')
  3275. # update strip/tile offsets
  3276. assert dataoffsetsoffset is not None
  3277. offset, pos = dataoffsetsoffset
  3278. ifd.seek(offset)
  3279. if pos is not None:
  3280. ifd.write(pack(offsetformat, ifdpos + pos))
  3281. ifd.seek(pos)
  3282. if dataoffsets is None:
  3283. offset = dataoffset
  3284. for size in databytecounts:
  3285. ifd.write(
  3286. pack(offsetformat, offset if size > 0 else 0)
  3287. )
  3288. offset += size
  3289. else:
  3290. for offset in dataoffsets:
  3291. ifd.write(pack(offsetformat, offset))
  3292. else:
  3293. ifd.write(pack(offsetformat, dataoffset))
  3294. if compressionfunc is not None or (tile and dataarray is None):
  3295. # update strip/tile bytecounts
  3296. assert databytecountsoffset is not None
  3297. offset, pos = databytecountsoffset
  3298. ifd.seek(offset)
  3299. if pos is not None:
  3300. ifd.write(pack(offsetformat, ifdpos + pos))
  3301. ifd.seek(pos)
  3302. ifd.write(pack(bytecountformat, *databytecounts))
  3303. if subifdsoffsets is not None:
  3304. # update and save pointer to SubIFDs tag values if necessary
  3305. offset, pos = subifdsoffsets
  3306. if pos is not None:
  3307. ifd.seek(offset)
  3308. ifd.write(pack(offsetformat, ifdpos + pos))
  3309. self._subifdsoffsets.append(ifdpos + pos)
  3310. else:
  3311. self._subifdsoffsets.append(ifdpos + offset)
  3312. fhpos = fh.tell()
  3313. fh.seek(ifdpos)
  3314. fh.write(ifd.getbuffer())
  3315. fh.flush()
  3316. if self._subifdslevel < 0:
  3317. self._ifdoffset = ifdpos + ifdoffset
  3318. else:
  3319. # update SubIFDs tag values
  3320. fh.seek(
  3321. self._subifdsoffsets[self._ifdindex]
  3322. + self._subifdslevel * offsetsize
  3323. )
  3324. fh.write(pack(offsetformat, ifdpos))
  3325. # update SubIFD chain offsets
  3326. if self._subifdslevel == 0:
  3327. self._nextifdoffsets.append(ifdpos + ifdoffset)
  3328. else:
  3329. fh.seek(self._nextifdoffsets[self._ifdindex])
  3330. fh.write(pack(offsetformat, ifdpos))
  3331. self._nextifdoffsets[self._ifdindex] = ifdpos + ifdoffset
  3332. self._ifdindex += 1
  3333. self._ifdindex %= len(self._subifdsoffsets)
  3334. fh.seek(fhpos)
  3335. # remove tags that should be written only once
  3336. if pageindex == 0:
  3337. tags = [tag for tag in tags if not tag[-1]]
  3338. assert dataoffset > 0
  3339. self._datashape = (1, *inputshape)
  3340. self._datadtype = datadtype
  3341. self._dataoffset = dataoffset
  3342. self._databytecounts = databytecounts
  3343. self._storedshape = storedshape
  3344. if contiguous:
  3345. # write remaining IFDs/tags later
  3346. self._tags = tags
  3347. # return offset and size of image data
  3348. if returnoffset:
  3349. return dataoffset, sum(databytecounts)
  3350. return None
  3351. def overwrite_description(self, description: str, /) -> None:
  3352. """Overwrite value of last ImageDescription tag.
  3353. Can be used to write OME-XML after writing images.
  3354. Ends a contiguous series.
  3355. """
  3356. if self._descriptiontag is None:
  3357. raise ValueError('no ImageDescription tag found')
  3358. self._write_remaining_pages()
  3359. self._descriptiontag.overwrite(description, erase=False)
  3360. self._descriptiontag = None
  3361. def close(self) -> None:
  3362. """Write remaining pages and close file handle."""
  3363. try:
  3364. if not self._truncate:
  3365. self._write_remaining_pages()
  3366. self._write_image_description()
  3367. finally:
  3368. try:
  3369. self._fh.close()
  3370. except Exception: # noqa: S110
  3371. pass
  3372. @property
  3373. def filehandle(self) -> FileHandle:
  3374. """File handle to write file."""
  3375. return self._fh
  3376. def _write_remaining_pages(self) -> None:
  3377. """Write outstanding IFDs and tags to file."""
  3378. if not self._tags or self._truncate or self._datashape is None:
  3379. return
  3380. assert self._storedshape is not None
  3381. assert self._databytecounts is not None
  3382. assert self._dataoffset is not None
  3383. pageno: int = self._storedshape.frames * self._datashape[0] - 1
  3384. if pageno < 1:
  3385. self._tags = None
  3386. self._dataoffset = None
  3387. self._databytecounts = None
  3388. return
  3389. fh = self._fh
  3390. fhpos: int = fh.tell()
  3391. if fhpos % 2:
  3392. fh.write(b'\x00')
  3393. fhpos += 1
  3394. pack = struct.pack
  3395. offsetformat: str = self.tiff.offsetformat
  3396. offsetsize: int = self.tiff.offsetsize
  3397. tagnoformat: str = self.tiff.tagnoformat
  3398. tagsize: int = self.tiff.tagsize
  3399. dataoffset: int = self._dataoffset
  3400. pagedatasize: int = sum(self._databytecounts)
  3401. subifdsoffsets: tuple[int, int | None] | None = None
  3402. dataoffsetsoffset: tuple[int, int | None]
  3403. pos: int | None
  3404. offset: int
  3405. # construct template IFD in memory
  3406. # must patch offsets to next IFD and data before writing to file
  3407. ifd = io.BytesIO()
  3408. ifd.write(pack(tagnoformat, len(self._tags)))
  3409. tagoffset = ifd.tell()
  3410. ifd.write(b''.join(t[1] for t in self._tags))
  3411. ifdoffset = ifd.tell()
  3412. ifd.write(pack(offsetformat, 0)) # offset to next IFD
  3413. # tag values
  3414. for tagindex, tag in enumerate(self._tags):
  3415. offset = tagoffset + tagindex * tagsize + offsetsize + 4
  3416. code = tag[0]
  3417. value = tag[2]
  3418. if value:
  3419. pos = ifd.tell()
  3420. if pos % 2:
  3421. # tag value is expected to begin on word boundary
  3422. ifd.write(b'\x00')
  3423. pos += 1
  3424. ifd.seek(offset)
  3425. try:
  3426. ifd.write(pack(offsetformat, fhpos + pos))
  3427. except Exception as exc: # struct.error
  3428. if self._imagej:
  3429. warnings.warn(
  3430. f'{self!r} truncating ImageJ file',
  3431. UserWarning,
  3432. stacklevel=2,
  3433. )
  3434. self._truncate = True
  3435. return
  3436. raise ValueError(
  3437. 'data too large for non-BigTIFF file'
  3438. ) from exc
  3439. ifd.seek(pos)
  3440. ifd.write(value)
  3441. if code == self._dataoffsetstag:
  3442. # save strip/tile offsets for later updates
  3443. dataoffsetsoffset = offset, pos
  3444. elif code == 330:
  3445. # save subifds offsets for later updates
  3446. subifdsoffsets = offset, pos
  3447. elif code == self._dataoffsetstag:
  3448. dataoffsetsoffset = offset, None
  3449. elif code == 330:
  3450. subifdsoffsets = offset, None
  3451. ifdsize = ifd.tell()
  3452. if ifdsize % 2:
  3453. ifd.write(b'\x00')
  3454. ifdsize += 1
  3455. # check if all IFDs fit in file
  3456. if offsetsize < 8 and fhpos + ifdsize * pageno > 2**32 - 32:
  3457. if self._imagej:
  3458. warnings.warn(
  3459. f'{self!r} truncating ImageJ file',
  3460. UserWarning,
  3461. stacklevel=2,
  3462. )
  3463. self._truncate = True
  3464. return
  3465. raise ValueError('data too large for non-BigTIFF file')
  3466. # assemble IFD chain in memory from IFD template
  3467. ifds = io.BytesIO(bytes(ifdsize * pageno))
  3468. ifdpos = fhpos
  3469. for _ in range(pageno):
  3470. # update strip/tile offsets in IFD
  3471. dataoffset += pagedatasize # offset to image data
  3472. offset, pos = dataoffsetsoffset
  3473. ifd.seek(offset)
  3474. if pos is not None:
  3475. ifd.write(pack(offsetformat, ifdpos + pos))
  3476. ifd.seek(pos)
  3477. offset = dataoffset
  3478. for size in self._databytecounts:
  3479. ifd.write(pack(offsetformat, offset))
  3480. offset += size
  3481. else:
  3482. ifd.write(pack(offsetformat, dataoffset))
  3483. if subifdsoffsets is not None:
  3484. offset, pos = subifdsoffsets
  3485. self._subifdsoffsets.append(
  3486. ifdpos + (pos if pos is not None else offset)
  3487. )
  3488. if self._subifdslevel < 0:
  3489. if subifdsoffsets is not None:
  3490. # update pointer to SubIFDs tag values if necessary
  3491. offset, pos = subifdsoffsets
  3492. if pos is not None:
  3493. ifd.seek(offset)
  3494. ifd.write(pack(offsetformat, ifdpos + pos))
  3495. # update pointer at ifdoffset to point to next IFD in file
  3496. ifdpos += ifdsize
  3497. ifd.seek(ifdoffset)
  3498. ifd.write(pack(offsetformat, ifdpos))
  3499. else:
  3500. # update SubIFDs tag values in file
  3501. fh.seek(
  3502. self._subifdsoffsets[self._ifdindex]
  3503. + self._subifdslevel * offsetsize
  3504. )
  3505. fh.write(pack(offsetformat, ifdpos))
  3506. # update SubIFD chain
  3507. if self._subifdslevel == 0:
  3508. self._nextifdoffsets.append(ifdpos + ifdoffset)
  3509. else:
  3510. fh.seek(self._nextifdoffsets[self._ifdindex])
  3511. fh.write(pack(offsetformat, ifdpos))
  3512. self._nextifdoffsets[self._ifdindex] = ifdpos + ifdoffset
  3513. self._ifdindex += 1
  3514. self._ifdindex %= len(self._subifdsoffsets)
  3515. ifdpos += ifdsize
  3516. # write IFD entry
  3517. ifds.write(ifd.getbuffer())
  3518. # terminate IFD chain
  3519. ifdoffset += ifdsize * (pageno - 1)
  3520. ifds.seek(ifdoffset)
  3521. ifds.write(pack(offsetformat, 0))
  3522. # write IFD chain to file
  3523. fh.seek(fhpos)
  3524. fh.write(ifds.getbuffer())
  3525. if self._subifdslevel < 0:
  3526. # update file to point to new IFD chain
  3527. pos = fh.tell()
  3528. fh.seek(self._ifdoffset)
  3529. fh.write(pack(offsetformat, fhpos))
  3530. fh.flush()
  3531. fh.seek(pos)
  3532. self._ifdoffset = fhpos + ifdoffset
  3533. self._tags = None
  3534. self._dataoffset = None
  3535. self._databytecounts = None
  3536. # do not reset _storedshape, _datashape, _datadtype
  3537. def _write_image_description(self) -> None:
  3538. """Write metadata to ImageDescription tag."""
  3539. if self._datashape is None or self._descriptiontag is None:
  3540. self._descriptiontag = None
  3541. return
  3542. assert self._storedshape is not None
  3543. assert self._datadtype is not None
  3544. if self._omexml is not None:
  3545. if self._subifdslevel < 0:
  3546. assert self._metadata is not None
  3547. self._omexml.addimage(
  3548. dtype=self._datadtype,
  3549. shape=self._datashape[
  3550. 0 if self._datashape[0] != 1 else 1 :
  3551. ],
  3552. storedshape=self._storedshape.shape,
  3553. **self._metadata,
  3554. )
  3555. description = self._omexml.tostring(declaration=True)
  3556. elif self._datashape[0] == 1:
  3557. # description already up-to-date
  3558. self._descriptiontag = None
  3559. return
  3560. # elif self._subifdslevel >= 0:
  3561. # # don't write metadata to SubIFDs
  3562. # return
  3563. elif self._imagej:
  3564. assert self._metadata is not None
  3565. colormapped = self._colormap is not None
  3566. isrgb = self._storedshape.samples in {3, 4}
  3567. description = imagej_description(
  3568. self._datashape,
  3569. rgb=isrgb,
  3570. colormaped=colormapped,
  3571. **self._metadata,
  3572. )
  3573. elif not self._tifffile:
  3574. self._descriptiontag = None
  3575. return
  3576. else:
  3577. assert self._metadata is not None
  3578. description = shaped_description(self._datashape, **self._metadata)
  3579. self._descriptiontag.overwrite(description.encode(), erase=False)
  3580. self._descriptiontag = None
  3581. def _addtag(
  3582. self,
  3583. tags: list[tuple[int, bytes, bytes | None, bool]],
  3584. code: int | str,
  3585. dtype: int | str,
  3586. count: int | None,
  3587. value: Any,
  3588. writeonce: bool = False, # noqa: FBT001, FBT002
  3589. /,
  3590. ) -> None:
  3591. """Append (code, ifdentry, ifdvalue, writeonce) to tags list.
  3592. Compute ifdentry and ifdvalue bytes from code, dtype, count, value.
  3593. """
  3594. pack = self._pack
  3595. if not isinstance(code, int):
  3596. code = TIFF.TAGS[code]
  3597. try:
  3598. datatype = cast(int, dtype)
  3599. dataformat = TIFF.DATA_FORMATS[datatype][-1]
  3600. except KeyError as exc:
  3601. try:
  3602. dataformat = cast(str, dtype)
  3603. if dataformat[0] in '<>':
  3604. dataformat = dataformat[1:]
  3605. datatype = TIFF.DATA_DTYPES[dataformat]
  3606. except (KeyError, TypeError):
  3607. raise ValueError(f'unknown dtype {dtype}') from exc
  3608. del dtype
  3609. rawcount = count
  3610. if datatype == 2:
  3611. # string
  3612. if isinstance(value, str):
  3613. # enforce 7-bit ASCII on Unicode strings
  3614. try:
  3615. value = value.encode('ascii')
  3616. except UnicodeEncodeError as exc:
  3617. raise ValueError(
  3618. 'TIFF strings must be 7-bit ASCII'
  3619. ) from exc
  3620. elif not isinstance(value, bytes):
  3621. raise ValueError('TIFF strings must be 7-bit ASCII')
  3622. if len(value) == 0 or value[-1:] != b'\x00':
  3623. value += b'\x00'
  3624. count = len(value)
  3625. if code == 270:
  3626. rawcount = int(value.find(b'\x00\x00'))
  3627. if rawcount < 0:
  3628. rawcount = count
  3629. else:
  3630. # length of string without buffer
  3631. rawcount = max(self.tiff.offsetsize + 1, rawcount + 1)
  3632. rawcount = min(count, rawcount)
  3633. else:
  3634. rawcount = count
  3635. value = (value,)
  3636. elif isinstance(value, bytes):
  3637. # packed binary data
  3638. itemsize = struct.calcsize(dataformat)
  3639. if len(value) % itemsize:
  3640. raise ValueError('invalid packed binary data')
  3641. count = len(value) // itemsize
  3642. rawcount = count
  3643. elif count is None:
  3644. raise ValueError('invalid count')
  3645. else:
  3646. count = int(count)
  3647. if datatype in {5, 10}: # rational
  3648. count *= 2
  3649. dataformat = dataformat[-1]
  3650. ifdentry = [
  3651. pack('HH', code, datatype),
  3652. pack(self.tiff.offsetformat, rawcount),
  3653. ]
  3654. ifdvalue = None
  3655. if struct.calcsize(dataformat) * count <= self.tiff.offsetsize:
  3656. # value(s) can be written directly
  3657. valueformat = f'{self.tiff.offsetsize}s'
  3658. if isinstance(value, bytes):
  3659. ifdentry.append(pack(valueformat, value))
  3660. elif count == 1:
  3661. if isinstance(value, (tuple, list, numpy.ndarray)):
  3662. value = value[0]
  3663. ifdentry.append(pack(valueformat, pack(dataformat, value)))
  3664. else:
  3665. ifdentry.append(
  3666. pack(valueformat, pack(f'{count}{dataformat}', *value))
  3667. )
  3668. else:
  3669. # use offset to value(s)
  3670. ifdentry.append(pack(self.tiff.offsetformat, 0))
  3671. if isinstance(value, bytes):
  3672. ifdvalue = value
  3673. elif isinstance(value, numpy.ndarray):
  3674. if value.size != count:
  3675. raise RuntimeError('value.size != count')
  3676. if value.dtype.char != dataformat:
  3677. raise RuntimeError('value.dtype.char != dtype')
  3678. ifdvalue = value.tobytes()
  3679. elif isinstance(value, (tuple, list)):
  3680. ifdvalue = pack(f'{count}{dataformat}', *value)
  3681. else:
  3682. ifdvalue = pack(dataformat, value)
  3683. tags.append((code, b''.join(ifdentry), ifdvalue, writeonce))
  3684. def _pack(self, fmt: str, *val: Any) -> bytes:
  3685. """Return values packed to bytes according to format."""
  3686. if fmt[0] not in '<>':
  3687. fmt = self.tiff.byteorder + fmt
  3688. return struct.pack(fmt, *val)
  3689. def _bytecount_format(
  3690. self, bytecounts: Sequence[int], compression: int, /
  3691. ) -> str:
  3692. """Return small bytecount format."""
  3693. if len(bytecounts) == 1:
  3694. return self.tiff.offsetformat[1]
  3695. bytecount = bytecounts[0]
  3696. if compression > 1:
  3697. bytecount = bytecount * 10
  3698. if bytecount < 2**16:
  3699. return 'H'
  3700. if bytecount < 2**32:
  3701. return 'I'
  3702. return self.tiff.offsetformat[1]
  3703. @staticmethod
  3704. def _maxworkers(
  3705. maxworkers: int | None,
  3706. numchunks: int,
  3707. chunksize: int,
  3708. compression: int,
  3709. ) -> int:
  3710. """Return number of threads to encode segments."""
  3711. if maxworkers is not None:
  3712. return maxworkers
  3713. if (
  3714. # imagecodecs is None or
  3715. compression <= 1
  3716. or numchunks < 2
  3717. or chunksize < 1024
  3718. or compression == 48124 # Jetraw is not thread-safe?
  3719. ):
  3720. return 1
  3721. # the following is based on benchmarking RGB tile sizes vs maxworkers
  3722. # using a (8228, 11500, 3) uint8 WSI slide:
  3723. if chunksize < 131072 and compression in {
  3724. 7, # JPEG
  3725. 33007, # ALT_JPG
  3726. 34892, # JPEG_LOSSY
  3727. 32773, # PackBits
  3728. 34887, # LERC
  3729. }:
  3730. return 1
  3731. if chunksize < 32768 and compression in {
  3732. 5, # LZW
  3733. 8, # zlib
  3734. 32946, # zlib
  3735. 50000, # zstd
  3736. 50013, # zlib/pixtiff
  3737. }:
  3738. # zlib,
  3739. return 1
  3740. if chunksize < 8192 and compression in {
  3741. 34934, # JPEG XR
  3742. 22610, # JPEG XR
  3743. 34933, # PNG
  3744. }:
  3745. return 1
  3746. if chunksize < 2048 and compression in {
  3747. 33003, # JPEG2000
  3748. 33004, # JPEG2000
  3749. 33005, # JPEG2000
  3750. 34712, # JPEG2000
  3751. 50002, # JPEG XL
  3752. 52546, # JPEG XL DNG
  3753. }:
  3754. return 1
  3755. if chunksize < 1024 and compression in {
  3756. 34925, # LZMA
  3757. 50001, # WebP
  3758. }:
  3759. return 1
  3760. if compression == 34887: # LERC
  3761. # limit to 4 threads
  3762. return min(numchunks, 4)
  3763. return min(numchunks, TIFF.MAXWORKERS)
  3764. def __enter__(self) -> Self:
  3765. return self
  3766. def __exit__(
  3767. self,
  3768. exc_type: type[BaseException] | None,
  3769. exc_value: BaseException | None,
  3770. traceback: TracebackType | None,
  3771. ) -> None:
  3772. self.close()
  3773. def __repr__(self) -> str:
  3774. return f'<tifffile.TiffWriter {snipstr(self.filehandle.name, 32)!r}>'
  3775. @final
  3776. class TiffFile:
  3777. """Read image and metadata from TIFF file.
  3778. TiffFile instances must be closed with :py:meth:`TiffFile.close`, which
  3779. is automatically called when using the 'with' context manager.
  3780. TiffFile instances are not thread-safe. All attributes are read-only.
  3781. Parameters:
  3782. file:
  3783. Specifies TIFF file to read.
  3784. File objects must be open in binary mode and positioned at the
  3785. TIFF header.
  3786. mode:
  3787. File open mode if `file` is file name. The default is 'rb'.
  3788. name:
  3789. Name of file if `file` is file handle.
  3790. offset:
  3791. Start position of embedded file.
  3792. The default is the current file position.
  3793. size:
  3794. Size of embedded file. The default is the number of bytes
  3795. from the `offset` to the end of the file.
  3796. omexml:
  3797. OME metadata in XML format, for example, from external companion
  3798. file or sanitized XML overriding XML in file.
  3799. superres:
  3800. EER super-resolution level to decode.
  3801. The default is 0 (no super-resolution).
  3802. _multifile, _useframes, _parent:
  3803. Internal use.
  3804. **is_flags:
  3805. Override `TiffFile.is_` flags, for example:
  3806. ``is_ome=False``: disable processing of OME-XML metadata.
  3807. ``is_lsm=False``: disable special handling of LSM files.
  3808. ``is_ndpi=True``: force file to be NDPI format.
  3809. Raises:
  3810. TiffFileError: Invalid TIFF structure.
  3811. """
  3812. tiff: TiffFormat
  3813. """Properties of TIFF file format."""
  3814. pages: TiffPages
  3815. """Sequence of pages in TIFF file."""
  3816. _fh: FileHandle
  3817. _multifile: bool
  3818. _parent: TiffFile # OME master file
  3819. _files: dict[str | None, TiffFile] # cache of TiffFile instances
  3820. _omexml: str | None # external OME-XML
  3821. _superres: int # EER super-resolution level
  3822. _decoders: dict[ # cache of TiffPage.decode functions
  3823. int,
  3824. Callable[
  3825. ...,
  3826. tuple[
  3827. NDArray[Any] | None,
  3828. tuple[int, int, int, int, int],
  3829. tuple[int, int, int, int],
  3830. ],
  3831. ],
  3832. ]
  3833. def __init__(
  3834. self,
  3835. file: str | os.PathLike[Any] | FileHandle | IO[bytes],
  3836. /,
  3837. *,
  3838. mode: Literal['r', 'r+'] | None = None,
  3839. name: str | None = None,
  3840. offset: int | None = None,
  3841. size: int | None = None,
  3842. omexml: str | None = None,
  3843. superres: int | None = None,
  3844. _multifile: bool | None = None,
  3845. _useframes: bool | None = None,
  3846. _parent: TiffFile | None = None,
  3847. **is_flags: bool | None,
  3848. ) -> None:
  3849. for key, value in is_flags.items():
  3850. if key[:3] == 'is_' and key[3:] in TIFF.FILE_FLAGS:
  3851. if value is not None:
  3852. setattr(self, key, bool(value))
  3853. else:
  3854. raise TypeError(f'unexpected keyword argument: {key}')
  3855. if mode not in {None, 'r', 'r+', 'rb', 'r+b'}:
  3856. raise ValueError(f'invalid mode {mode!r}')
  3857. self._omexml = None
  3858. if omexml:
  3859. if omexml.strip()[-4:] != 'OME>':
  3860. raise ValueError('invalid OME-XML')
  3861. self._omexml = omexml
  3862. self.is_ome = True
  3863. fh = FileHandle(file, mode=mode, name=name, offset=offset, size=size)
  3864. self._fh = fh
  3865. self._multifile = True if _multifile is None else bool(_multifile)
  3866. self._files = {fh.name: self}
  3867. self._decoders = {}
  3868. self._parent = self if _parent is None else _parent
  3869. self._superres = 0 if superres is None else max(0, int(superres))
  3870. try:
  3871. fh.seek(0)
  3872. header = fh.read(4)
  3873. try:
  3874. byteorder = {b'II': '<', b'MM': '>', b'EP': '<'}[header[:2]]
  3875. except KeyError as exc:
  3876. raise TiffFileError(f'not a TIFF file {header!r}') from exc
  3877. version = struct.unpack(byteorder + 'H', header[2:4])[0]
  3878. if version == 43:
  3879. # BigTiff
  3880. offsetsize, zero = struct.unpack(byteorder + 'HH', fh.read(4))
  3881. if zero != 0 or offsetsize != 8:
  3882. raise TiffFileError(
  3883. f'invalid BigTIFF offset size {(offsetsize, zero)}'
  3884. )
  3885. if byteorder == '>':
  3886. self.tiff = TIFF.BIG_BE
  3887. else:
  3888. self.tiff = TIFF.BIG_LE
  3889. elif version == 42:
  3890. # Classic TIFF
  3891. if byteorder == '>':
  3892. self.tiff = TIFF.CLASSIC_BE
  3893. elif is_flags.get('is_ndpi', fh.extension == '.ndpi'):
  3894. # NDPI uses 64 bit IFD offsets
  3895. if is_flags.get('is_ndpi', True):
  3896. self.tiff = TIFF.NDPI_LE
  3897. else:
  3898. self.tiff = TIFF.CLASSIC_LE
  3899. else:
  3900. self.tiff = TIFF.CLASSIC_LE
  3901. elif version == 0x4352:
  3902. # DNG DCP
  3903. if byteorder == '>':
  3904. self.tiff = TIFF.CLASSIC_BE
  3905. else:
  3906. self.tiff = TIFF.CLASSIC_LE
  3907. elif version == 0x4E31:
  3908. # NIFF
  3909. if byteorder == '>':
  3910. raise TiffFileError('invalid NIFF file')
  3911. logger().error(f'{self!r} NIFF format not supported')
  3912. self.tiff = TIFF.CLASSIC_LE
  3913. elif version in {0x55, 0x4F52, 0x5352}:
  3914. # Panasonic or Olympus RAW
  3915. logger().error(
  3916. f'{self!r} RAW format 0x{version:04X} not supported'
  3917. )
  3918. if byteorder == '>':
  3919. self.tiff = TIFF.CLASSIC_BE
  3920. else:
  3921. self.tiff = TIFF.CLASSIC_LE
  3922. else:
  3923. raise TiffFileError(f'invalid TIFF version {version}')
  3924. # file handle is at offset to offset to first page
  3925. self.pages = TiffPages(self)
  3926. if self.is_lsm and (
  3927. self.filehandle.size >= 2**32
  3928. or self.pages[0].compression != 1
  3929. or self.pages[1].compression != 1
  3930. ):
  3931. self._lsm_load_pages()
  3932. elif self.is_scanimage and not self.is_bigtiff:
  3933. # ScanImage <= 2015
  3934. try:
  3935. self.pages._load_virtual_frames()
  3936. except Exception as exc:
  3937. logger().error(
  3938. f'{self!r} <TiffPages._load_virtual_frames> '
  3939. f'raised {exc!r:.128}'
  3940. )
  3941. elif self.is_ndpi:
  3942. try:
  3943. self._ndpi_load_pages()
  3944. except Exception as exc:
  3945. logger().error(
  3946. f'{self!r} <_ndpi_load_pages> raised {exc!r:.128}'
  3947. )
  3948. elif _useframes:
  3949. self.pages.useframes = True
  3950. except Exception:
  3951. fh.close()
  3952. raise
  3953. @property
  3954. def byteorder(self) -> Literal['>', '<']:
  3955. """Byteorder of TIFF file."""
  3956. return self.tiff.byteorder
  3957. @property
  3958. def filehandle(self) -> FileHandle:
  3959. """File handle."""
  3960. return self._fh
  3961. @property
  3962. def filename(self) -> str:
  3963. """Name of file handle."""
  3964. return self._fh.name
  3965. @cached_property
  3966. def fstat(self) -> Any:
  3967. """Status of file handle's descriptor, if any."""
  3968. try:
  3969. return os.fstat(self._fh.fileno())
  3970. except Exception: # io.UnsupportedOperation
  3971. return None
  3972. def close(self) -> None:
  3973. """Close open file handle(s)."""
  3974. for tif in self._files.values():
  3975. tif.filehandle.close()
  3976. def asarray(
  3977. self,
  3978. key: int | slice | Iterable[int] | None = None,
  3979. *,
  3980. series: int | TiffPageSeries | None = None,
  3981. level: int | None = None,
  3982. squeeze: bool | None = None,
  3983. out: OutputType = None,
  3984. maxworkers: int | None = None,
  3985. buffersize: int | None = None,
  3986. ) -> NDArray[Any]:
  3987. """Return images from select pages as NumPy array.
  3988. By default, the image array from the first level of the first series
  3989. is returned.
  3990. Parameters:
  3991. key:
  3992. Specifies which pages to return as array.
  3993. By default, the image of the specified `series` and `level`
  3994. is returned.
  3995. If not *None*, the images from the specified pages in the
  3996. whole file (if `series` is *None*) or a specified series are
  3997. returned as a stacked array.
  3998. Requesting an array from multiple pages that are not
  3999. compatible with respect to shape, dtype, compression etc.
  4000. is undefined, that is, it may crash or return incorrect values.
  4001. series:
  4002. Specifies which series of pages to return as array.
  4003. The default is 0.
  4004. level:
  4005. Specifies which level of multi-resolution series to return
  4006. as array. The default is 0.
  4007. squeeze:
  4008. If *True*, remove all length-1 dimensions (except X and Y)
  4009. from array.
  4010. If *False*, single pages are returned as 5D array of shape
  4011. :py:attr:`TiffPage.shaped`.
  4012. For series, the shape of the returned array also includes
  4013. singlet dimensions specified in some file formats.
  4014. For example, ImageJ series and most commonly also OME series,
  4015. are returned in TZCYXS order.
  4016. By default, all but `"shaped"` series are squeezed.
  4017. out:
  4018. Specifies how image array is returned.
  4019. By default, a new NumPy array is created.
  4020. If a *numpy.ndarray*, a writable array to which the image
  4021. is copied.
  4022. If *'memmap'*, directly memory-map the image data in the
  4023. file if possible; else create a memory-mapped array in a
  4024. temporary file.
  4025. If a *string* or *open file*, the file used to create a
  4026. memory-mapped array.
  4027. maxworkers:
  4028. Maximum number of threads to concurrently decode data from
  4029. multiple pages or compressed segments.
  4030. If *None* or *0*, use up to :py:attr:`_TIFF.MAXWORKERS`
  4031. threads. Reading data from file is limited to the main thread.
  4032. Using multiple threads can significantly speed up this
  4033. function if the bottleneck is decoding compressed data,
  4034. for example, in case of large LZW compressed LSM files or
  4035. JPEG compressed tiled slides.
  4036. If the bottleneck is I/O or pure Python code, using multiple
  4037. threads might be detrimental.
  4038. buffersize:
  4039. Approximate number of bytes to read from file in one pass.
  4040. The default is :py:attr:`_TIFF.BUFFERSIZE`.
  4041. Returns:
  4042. Images from specified pages. See `TiffPage.asarray`
  4043. for operations that are applied (or not) to the image data
  4044. stored in the file.
  4045. """
  4046. if not self.pages:
  4047. return numpy.array([])
  4048. if key is None and series is None:
  4049. series = 0
  4050. pages: Any # TiffPages | TiffPageSeries | list[TiffPage | TiffFrame]
  4051. page0: TiffPage | TiffFrame | None
  4052. if series is None:
  4053. pages = self.pages
  4054. else:
  4055. if not isinstance(series, TiffPageSeries):
  4056. series = self.series[series]
  4057. if level is not None:
  4058. series = series.levels[level]
  4059. pages = series
  4060. if key is None:
  4061. pass
  4062. elif series is None:
  4063. pages = pages._getlist(key)
  4064. elif isinstance(key, (int, numpy.integer)):
  4065. pages = [pages[int(key)]]
  4066. elif isinstance(key, slice):
  4067. pages = pages[key]
  4068. elif isinstance(key, Iterable) and not isinstance(key, str):
  4069. pages = [pages[k] for k in key]
  4070. else:
  4071. raise TypeError(
  4072. f'key must be an integer, slice, or sequence, not {type(key)}'
  4073. )
  4074. if pages is None or len(pages) == 0:
  4075. raise ValueError('no pages selected')
  4076. if (
  4077. key is None
  4078. and series is not None
  4079. and series.dataoffset is not None
  4080. ):
  4081. typecode = self.byteorder + series.dtype.char
  4082. if (
  4083. series.keyframe.is_memmappable
  4084. and isinstance(out, str)
  4085. and out == 'memmap'
  4086. ):
  4087. # direct mapping
  4088. shape = series.get_shape(squeeze=squeeze)
  4089. result = self.filehandle.memmap_array(
  4090. typecode, shape, series.dataoffset
  4091. )
  4092. else:
  4093. # read into output
  4094. shape = series.get_shape(squeeze=squeeze)
  4095. if out is not None:
  4096. out = create_output(out, shape, series.dtype)
  4097. result = self.filehandle.read_array(
  4098. typecode,
  4099. series.size,
  4100. series.dataoffset,
  4101. out=out,
  4102. )
  4103. elif len(pages) == 1:
  4104. page0 = pages[0]
  4105. if page0 is None:
  4106. raise ValueError('page is None')
  4107. result = page0.asarray(
  4108. out=out, maxworkers=maxworkers, buffersize=buffersize
  4109. )
  4110. else:
  4111. result = stack_pages(
  4112. pages, out=out, maxworkers=maxworkers, buffersize=buffersize
  4113. )
  4114. assert result is not None
  4115. if key is None:
  4116. assert series is not None # TODO: ?
  4117. shape = series.get_shape(squeeze=squeeze)
  4118. try:
  4119. result.shape = shape
  4120. except ValueError as exc:
  4121. try:
  4122. logger().warning(
  4123. f'{self!r} <asarray> failed to reshape '
  4124. f'{result.shape} to {shape}, raised {exc!r:.128}'
  4125. )
  4126. # try series of expected shapes
  4127. result.shape = (-1, *shape)
  4128. except ValueError:
  4129. # revert to generic shape
  4130. result.shape = (-1, *series.keyframe.shape)
  4131. elif len(pages) == 1:
  4132. if squeeze is None:
  4133. squeeze = True
  4134. page0 = pages[0]
  4135. if page0 is None:
  4136. raise ValueError('page is None')
  4137. result = result.reshape(page0.shape if squeeze else page0.shaped)
  4138. else:
  4139. if squeeze is None:
  4140. squeeze = True
  4141. try:
  4142. page0 = next(p for p in pages if p is not None)
  4143. except StopIteration as exc:
  4144. raise ValueError('pages are all None') from exc
  4145. assert page0 is not None
  4146. result = result.reshape(
  4147. (-1, *page0.shape) if squeeze else (-1, *page0.shaped)
  4148. )
  4149. return result
  4150. def aszarr(
  4151. self,
  4152. key: int | None = None,
  4153. *,
  4154. series: int | TiffPageSeries | None = None,
  4155. level: int | None = None,
  4156. **kwargs: Any,
  4157. ) -> ZarrTiffStore:
  4158. """Return images from select pages as Zarr store.
  4159. By default, the images from the first series, including all levels,
  4160. are wrapped as a Zarr store.
  4161. Parameters:
  4162. key:
  4163. Index of page in file (if `series` is None) or series to wrap
  4164. as Zarr store.
  4165. By default, a series is wrapped.
  4166. series:
  4167. Index of series to wrap as Zarr store.
  4168. The default is 0 (if `key` is None).
  4169. level:
  4170. Index of pyramid level in series to wrap as Zarr store.
  4171. By default, all levels are included as a multi-scale group.
  4172. **kwargs:
  4173. Additional arguments passed to :py:meth:`TiffPage.aszarr`
  4174. or :py:meth:`TiffPageSeries.aszarr`.
  4175. """
  4176. if not self.pages:
  4177. raise NotImplementedError('empty Zarr arrays not supported')
  4178. if key is None and series is None:
  4179. return self.series[0].aszarr(level=level, **kwargs)
  4180. pages: Any
  4181. if series is None:
  4182. pages = self.pages
  4183. else:
  4184. if not isinstance(series, TiffPageSeries):
  4185. series = self.series[series]
  4186. if key is None:
  4187. return series.aszarr(level=level, **kwargs)
  4188. if level is not None:
  4189. series = series.levels[level]
  4190. pages = series
  4191. if isinstance(key, (int, numpy.integer)):
  4192. page: TiffPage | TiffFrame = pages[key]
  4193. return page.aszarr(**kwargs)
  4194. raise TypeError('key must be an integer index')
  4195. @cached_property
  4196. def series(self) -> list[TiffPageSeries]:
  4197. """Series of pages with compatible shape and data type.
  4198. Side effect: after accessing this property, `TiffFile.pages` might
  4199. contain `TiffPage` and `TiffFrame` instead of only `TiffPage`
  4200. instances.
  4201. """
  4202. if not self.pages:
  4203. return []
  4204. assert self.pages.keyframe is not None
  4205. useframes = self.pages.useframes
  4206. keyframe = self.pages.keyframe.index
  4207. series: list[TiffPageSeries] | None = None
  4208. for kind in (
  4209. 'shaped',
  4210. 'lsm',
  4211. 'mmstack',
  4212. 'ome',
  4213. 'imagej',
  4214. 'ndtiff',
  4215. 'fluoview',
  4216. 'stk',
  4217. 'sis',
  4218. 'svs',
  4219. 'scn',
  4220. 'qpi',
  4221. 'ndpi',
  4222. 'bif',
  4223. 'avs',
  4224. 'eer',
  4225. 'philips',
  4226. 'scanimage',
  4227. # 'indica', # TODO: rewrite _series_indica()
  4228. 'nih',
  4229. 'mdgel', # adds second page to cache
  4230. 'uniform',
  4231. ):
  4232. if getattr(self, 'is_' + kind, False):
  4233. series = getattr(self, '_series_' + kind)()
  4234. if not series:
  4235. if kind == 'ome' and self.is_imagej:
  4236. # try ImageJ series if OME series fails.
  4237. # clear pages cache since _series_ome() might leave
  4238. # some frames without keyframe
  4239. self.pages._clear()
  4240. continue
  4241. if kind == 'mmstack':
  4242. # try OME, ImageJ, uniform
  4243. continue
  4244. break
  4245. if not series:
  4246. series = self._series_generic()
  4247. self.pages.useframes = useframes
  4248. self.pages.set_keyframe(keyframe)
  4249. # remove empty series, for example, in MD Gel files
  4250. # series = [s for s in series if product(s.shape) > 0]
  4251. assert series is not None
  4252. for i, s in enumerate(series):
  4253. s._index = i
  4254. return series
  4255. def _series_uniform(self) -> list[TiffPageSeries] | None:
  4256. """Return all images in file as single series."""
  4257. self.pages.useframes = True
  4258. self.pages.set_keyframe(0)
  4259. page = self.pages.first
  4260. validate = not (page.is_scanimage or page.is_nih)
  4261. pages = self.pages._getlist(validate=validate)
  4262. if len(pages) == 1:
  4263. shape = page.shape
  4264. axes = page.axes
  4265. else:
  4266. shape = (len(pages), *page.shape)
  4267. axes = 'I' + page.axes
  4268. dtype = page.dtype
  4269. return [TiffPageSeries(pages, shape, dtype, axes, kind='uniform')]
  4270. def _series_generic(self) -> list[TiffPageSeries] | None:
  4271. """Return image series in file.
  4272. A series is a sequence of TiffPages with the same hash.
  4273. """
  4274. pages = self.pages
  4275. pages._clear(fully=False)
  4276. pages.useframes = False
  4277. if pages.cache:
  4278. pages._load()
  4279. series = []
  4280. keys = []
  4281. seriesdict: dict[int, list[TiffPage | TiffFrame]] = {}
  4282. def addpage(page: TiffPage | TiffFrame, /) -> None:
  4283. # add page to seriesdict
  4284. if not page.shape: # or product(page.shape) == 0:
  4285. return
  4286. key = page.hash
  4287. if key in seriesdict:
  4288. for p in seriesdict[key]:
  4289. if p.offset == page.offset:
  4290. break # remove duplicate page
  4291. else:
  4292. seriesdict[key].append(page)
  4293. else:
  4294. keys.append(key)
  4295. seriesdict[key] = [page]
  4296. for page in pages:
  4297. addpage(page)
  4298. if page.subifds is not None:
  4299. for i, offset in enumerate(page.subifds):
  4300. if offset < 8:
  4301. continue
  4302. try:
  4303. self._fh.seek(offset)
  4304. subifd = TiffPage(self, (page.index, i))
  4305. except Exception as exc:
  4306. logger().warning(
  4307. f'{self!r} generic series raised {exc!r:.128}'
  4308. )
  4309. else:
  4310. addpage(subifd)
  4311. for key in keys:
  4312. pagelist = seriesdict[key]
  4313. page = pagelist[0]
  4314. shape = (len(pagelist), *page.shape)
  4315. axes = 'I' + page.axes
  4316. if 'S' not in axes:
  4317. shape += (1,)
  4318. axes += 'S'
  4319. series.append(
  4320. TiffPageSeries(
  4321. pagelist, shape, page.dtype, axes, kind='generic'
  4322. )
  4323. )
  4324. self.is_uniform = len(series) == 1 # replaces is_uniform method
  4325. if not self.is_agilent:
  4326. pyramidize_series(series)
  4327. return series
  4328. def _series_shaped(self) -> list[TiffPageSeries] | None:
  4329. """Return image series in tifffile "shaped" formatted file."""
  4330. # TODO: all series need to have JSON metadata for this to succeed
  4331. def append(
  4332. series: list[TiffPageSeries],
  4333. pages: list[TiffPage | TiffFrame | None],
  4334. axes: str | None,
  4335. shape: tuple[int, ...] | None,
  4336. reshape: tuple[int, ...],
  4337. name: str,
  4338. truncated: bool | None, # noqa: FBT001
  4339. /,
  4340. ) -> None:
  4341. # append TiffPageSeries to series
  4342. assert isinstance(pages[0], TiffPage)
  4343. page = pages[0]
  4344. if not check_shape(page.shape, reshape):
  4345. logger().warning(
  4346. f'{self!r} shaped series metadata does not match '
  4347. f'page shape {page.shape} != {tuple(reshape)}'
  4348. )
  4349. failed = True
  4350. else:
  4351. failed = False
  4352. if failed or axes is None or shape is None:
  4353. shape = page.shape
  4354. axes = page.axes
  4355. if len(pages) > 1:
  4356. shape = (len(pages), *shape)
  4357. axes = 'Q' + axes
  4358. if failed:
  4359. reshape = shape
  4360. size = product(shape)
  4361. resize = product(reshape)
  4362. if page.is_contiguous and resize > size and resize % size == 0:
  4363. if truncated is None:
  4364. truncated = True
  4365. axes = 'Q' + axes
  4366. shape = (resize // size, *shape)
  4367. try:
  4368. axes = reshape_axes(axes, shape, reshape)
  4369. shape = reshape
  4370. except ValueError as exc:
  4371. logger().error(
  4372. f'{self!r} shaped series failed to reshape, '
  4373. f'raised {exc!r:.128}'
  4374. )
  4375. series.append(
  4376. TiffPageSeries(
  4377. pages,
  4378. shape,
  4379. page.dtype,
  4380. axes,
  4381. name=name,
  4382. kind='shaped',
  4383. truncated=bool(truncated),
  4384. squeeze=False,
  4385. )
  4386. )
  4387. def detect_series(
  4388. pages: TiffPages | list[TiffPage | TiffFrame | None],
  4389. series: list[TiffPageSeries],
  4390. /,
  4391. ) -> list[TiffPageSeries] | None:
  4392. shape: tuple[int, ...] | None
  4393. reshape: tuple[int, ...]
  4394. page: TiffPage | TiffFrame | None
  4395. keyframe: TiffPage
  4396. subifds: list[TiffPage | TiffFrame | None] = []
  4397. subifd: TiffPage | TiffFrame
  4398. keysubifd: TiffPage
  4399. axes: str | None
  4400. name: str
  4401. lenpages = len(pages)
  4402. index = 0
  4403. while True:
  4404. if index >= lenpages:
  4405. break
  4406. if isinstance(pages, TiffPages):
  4407. # new keyframe; start of new series
  4408. pages.set_keyframe(index)
  4409. keyframe = cast(TiffPage, pages.keyframe)
  4410. else:
  4411. # pages is list of SubIFDs
  4412. keyframe = cast(TiffPage, pages[0])
  4413. if keyframe.shaped_description is None:
  4414. logger().error(
  4415. f'{self!r} '
  4416. 'invalid shaped series metadata or corrupted file'
  4417. )
  4418. return None
  4419. # read metadata
  4420. axes = None
  4421. shape = None
  4422. metadata = shaped_description_metadata(
  4423. keyframe.shaped_description
  4424. )
  4425. name = metadata.get('name', '')
  4426. reshape = metadata['shape']
  4427. truncated = None if keyframe.subifds is None else False
  4428. truncated = metadata.get('truncated', truncated)
  4429. if 'axes' in metadata:
  4430. axes = cast(str, metadata['axes'])
  4431. if len(axes) == len(reshape):
  4432. shape = reshape
  4433. else:
  4434. axes = ''
  4435. logger().error(
  4436. f'{self!r} shaped series axes do not match shape'
  4437. )
  4438. # skip pages if possible
  4439. spages: list[TiffPage | TiffFrame | None] = [keyframe]
  4440. size = product(reshape)
  4441. if size > 0:
  4442. npages, mod = divmod(size, product(keyframe.shape))
  4443. else:
  4444. npages = 1
  4445. mod = 0
  4446. if mod:
  4447. logger().error(
  4448. f'{self!r} '
  4449. 'shaped series shape does not match page shape'
  4450. )
  4451. return None
  4452. if 1 < npages <= lenpages - index:
  4453. assert keyframe._dtype is not None
  4454. size *= keyframe._dtype.itemsize
  4455. if truncated:
  4456. npages = 1
  4457. else:
  4458. page = pages[index + 1]
  4459. if (
  4460. keyframe.is_final
  4461. and page is not None
  4462. and keyframe.offset + size < page.offset
  4463. and keyframe.subifds is None
  4464. ):
  4465. truncated = False
  4466. else:
  4467. # must read all pages for series
  4468. truncated = False
  4469. for j in range(index + 1, index + npages):
  4470. page = pages[j]
  4471. assert page is not None
  4472. page.keyframe = keyframe
  4473. spages.append(page)
  4474. append(series, spages, axes, shape, reshape, name, truncated)
  4475. index += npages
  4476. # create series from SubIFDs
  4477. if keyframe.subifds:
  4478. subifds_size = len(keyframe.subifds)
  4479. for i, offset in enumerate(keyframe.subifds):
  4480. if offset < 8:
  4481. continue
  4482. subifds = []
  4483. for j, page in enumerate(spages):
  4484. # if page.subifds is not None:
  4485. try:
  4486. if (
  4487. page is None
  4488. or page.subifds is None
  4489. or len(page.subifds) < subifds_size
  4490. ):
  4491. raise ValueError(
  4492. f'{page!r} contains invalid subifds'
  4493. )
  4494. self._fh.seek(page.subifds[i])
  4495. if j == 0:
  4496. subifd = TiffPage(self, (page.index, i))
  4497. keysubifd = subifd
  4498. else:
  4499. subifd = TiffFrame(
  4500. self,
  4501. (page.index, i),
  4502. keyframe=keysubifd,
  4503. )
  4504. except Exception as exc:
  4505. logger().error(
  4506. f'{self!r} shaped series '
  4507. f'raised {exc!r:.128}'
  4508. )
  4509. return None
  4510. subifds.append(subifd)
  4511. if subifds:
  4512. series_or_none = detect_series(subifds, series)
  4513. if series_or_none is None:
  4514. return None
  4515. series = series_or_none
  4516. return series
  4517. self.pages.useframes = True
  4518. series = detect_series(self.pages, [])
  4519. if series is None:
  4520. return None
  4521. self.is_uniform = len(series) == 1
  4522. pyramidize_series(series, reduced=True)
  4523. return series
  4524. def _series_imagej(self) -> list[TiffPageSeries] | None:
  4525. """Return image series in ImageJ file."""
  4526. # ImageJ's dimension order is TZCYXS
  4527. # TODO: fix loading of color, composite, or palette images
  4528. meta = self.imagej_metadata
  4529. if meta is None:
  4530. return None
  4531. pages = self.pages
  4532. pages.useframes = True
  4533. pages.set_keyframe(0)
  4534. page = self.pages.first
  4535. order = meta.get('order', 'czt').lower()
  4536. frames = meta.get('frames', 1)
  4537. slices = meta.get('slices', 1)
  4538. channels = meta.get('channels', 1)
  4539. images = meta.get('images', 1) # not reliable
  4540. if images < 1 or frames < 1 or slices < 1 or channels < 1:
  4541. logger().warning(
  4542. f'{self!r} ImageJ series metadata invalid or corrupted file'
  4543. )
  4544. return None
  4545. if channels == 1:
  4546. images = frames * slices
  4547. elif page.shaped[0] > 1 and page.shaped[0] == channels:
  4548. # Bio-Formats declares separate samples as channels
  4549. images = frames * slices
  4550. elif images == frames * slices and page.shaped[4] == channels:
  4551. # RGB contig samples declared as channel
  4552. channels = 1
  4553. else:
  4554. images = frames * slices * channels
  4555. if images == 1 and pages.is_multipage:
  4556. images = len(pages)
  4557. nbytes = images * page.nbytes
  4558. # ImageJ virtual hyperstacks store all image metadata in the first
  4559. # page and image data are stored contiguously before the second
  4560. # page, if any
  4561. if not page.is_final:
  4562. isvirtual = False
  4563. elif page.dataoffsets[0] + nbytes > self.filehandle.size:
  4564. logger().error(
  4565. f'{self!r} ImageJ series metadata invalid or corrupted file'
  4566. )
  4567. return None
  4568. elif images <= 1:
  4569. isvirtual = True
  4570. elif (
  4571. pages.is_multipage
  4572. and page.dataoffsets[0] + nbytes > pages[1].offset
  4573. ):
  4574. # next page is not stored after data
  4575. isvirtual = False
  4576. else:
  4577. isvirtual = True
  4578. page_list: list[TiffPage | TiffFrame]
  4579. # no need to read other pages if virtual
  4580. page_list = [page] if isvirtual else pages[:]
  4581. shape: tuple[int, ...]
  4582. axes: str
  4583. if order in {'czt', 'default'}:
  4584. axes = 'TZC'
  4585. shape = (frames, slices, channels)
  4586. elif order == 'ctz':
  4587. axes = 'ZTC'
  4588. shape = (slices, frames, channels)
  4589. elif order == 'zct':
  4590. axes = 'TCZ'
  4591. shape = (frames, channels, slices)
  4592. elif order == 'ztc':
  4593. axes = 'CTZ'
  4594. shape = (channels, frames, slices)
  4595. elif order == 'tcz':
  4596. axes = 'ZCT'
  4597. shape = (slices, channels, frames)
  4598. elif order == 'tzc':
  4599. axes = 'CZT'
  4600. shape = (channels, slices, frames)
  4601. else:
  4602. axes = 'TZC'
  4603. shape = (frames, slices, channels)
  4604. logger().warning(
  4605. f'{self!r} ImageJ series of unknown order {order!r}'
  4606. )
  4607. remain = images // product(shape)
  4608. if remain > 1:
  4609. logger().debug(
  4610. f'{self!r} ImageJ series contains unidentified dimension'
  4611. )
  4612. shape = (remain, *shape)
  4613. axes = 'I' + axes
  4614. if page.shaped[0] > 1:
  4615. # Bio-Formats declares separate samples as channels
  4616. assert axes[-1] == 'C'
  4617. shape = shape[:-1] + page.shape
  4618. axes += page.axes[1:]
  4619. else:
  4620. shape += page.shape
  4621. axes += page.axes
  4622. if 'S' not in axes:
  4623. shape += (1,)
  4624. axes += 'S'
  4625. # assert axes.endswith('TZCYXS'), axes
  4626. truncated = (
  4627. isvirtual and not pages.is_multipage and page.nbytes != nbytes
  4628. )
  4629. self.is_uniform = True
  4630. return [
  4631. TiffPageSeries(
  4632. page_list,
  4633. shape,
  4634. page.dtype,
  4635. axes,
  4636. kind='imagej',
  4637. truncated=truncated,
  4638. )
  4639. ]
  4640. def _series_nih(self) -> list[TiffPageSeries] | None:
  4641. """Return all images in NIH Image file as single series."""
  4642. series = self._series_uniform()
  4643. if series is not None:
  4644. for s in series:
  4645. s.kind = 'nih'
  4646. return series
  4647. def _series_scanimage(self) -> list[TiffPageSeries] | None:
  4648. """Return image series in ScanImage file."""
  4649. pages = self.pages._getlist(validate=False)
  4650. page = self.pages.first
  4651. dtype = page.dtype
  4652. shape = None
  4653. meta = self.scanimage_metadata
  4654. framedata = {} if meta is None else meta.get('FrameData', {})
  4655. if 'SI.hChannels.channelSave' in framedata:
  4656. try:
  4657. channels = framedata['SI.hChannels.channelSave']
  4658. try:
  4659. # channelSave is a list of channel IDs
  4660. channels = len(channels)
  4661. except TypeError:
  4662. # channelSave is a single channel ID
  4663. channels = 1
  4664. # slices = framedata.get(
  4665. # 'SI.hStackManager.actualNumSlices',
  4666. # framedata.get('SI.hStackManager.numSlices', None),
  4667. # )
  4668. # if slices is None:
  4669. # raise ValueError('unable to determine numSlices')
  4670. slices = None
  4671. try:
  4672. frames = int(framedata['SI.hStackManager.framesPerSlice'])
  4673. except Exception as exc:
  4674. # framesPerSlice is inf
  4675. slices = 1
  4676. if len(pages) % channels:
  4677. raise ValueError(
  4678. 'unable to determine framesPerSlice'
  4679. ) from exc
  4680. frames = len(pages) // channels
  4681. if slices is None:
  4682. slices = max(len(pages) // (frames * channels), 1)
  4683. shape = (slices, frames, channels, *page.shape)
  4684. axes = 'ZTC' + page.axes
  4685. except Exception as exc:
  4686. logger().warning(
  4687. f'{self!r} ScanImage series raised {exc!r:.128}'
  4688. )
  4689. # TODO: older versions of ScanImage store non-varying frame data in
  4690. # the ImageDescription tag. Candidates are scanimage.SI5.channelsSave,
  4691. # scanimage.SI5.stackNumSlices, scanimage.SI5.acqNumFrames
  4692. # scanimage.SI4., state.acq.numberOfFrames, state.acq.numberOfFrames...
  4693. if shape is None:
  4694. shape = (len(pages), *page.shape)
  4695. axes = 'I' + page.axes
  4696. return [TiffPageSeries(pages, shape, dtype, axes, kind='scanimage')]
  4697. def _series_fluoview(self) -> list[TiffPageSeries] | None:
  4698. """Return image series in FluoView file."""
  4699. meta = self.fluoview_metadata
  4700. if meta is None:
  4701. return None
  4702. pages = self.pages._getlist(validate=False)
  4703. mmhd = list(reversed(meta['Dimensions']))
  4704. axes = ''.join(TIFF.MM_DIMENSIONS.get(i[0].upper(), 'Q') for i in mmhd)
  4705. shape = tuple(int(i[1]) for i in mmhd)
  4706. self.is_uniform = True
  4707. return [
  4708. TiffPageSeries(
  4709. pages,
  4710. shape,
  4711. pages[0].dtype,
  4712. axes,
  4713. name=meta['ImageName'],
  4714. kind='fluoview',
  4715. )
  4716. ]
  4717. def _series_mdgel(self) -> list[TiffPageSeries] | None:
  4718. """Return image series in MD Gel file."""
  4719. # only a single page, scaled according to metadata in second page
  4720. meta = self.mdgel_metadata
  4721. if meta is None:
  4722. return None
  4723. transform: Callable[[NDArray[Any]], NDArray[Any]] | None
  4724. self.pages.useframes = False
  4725. self.pages.set_keyframe(0)
  4726. if meta['FileTag'] in {2, 128}:
  4727. dtype = numpy.dtype(numpy.float32)
  4728. scale = meta['ScalePixel']
  4729. scale = scale[0] / scale[1] # rational
  4730. if meta['FileTag'] == 2:
  4731. # squary root data format
  4732. def transform(a: NDArray[Any], /) -> NDArray[Any]:
  4733. return a.astype(numpy.float32) ** 2 * scale
  4734. else:
  4735. def transform(a: NDArray[Any], /) -> NDArray[Any]:
  4736. return a.astype(numpy.float32) * scale
  4737. else:
  4738. transform = None
  4739. page = self.pages.first
  4740. self.is_uniform = False
  4741. return [
  4742. TiffPageSeries(
  4743. [page],
  4744. page.shape,
  4745. dtype,
  4746. page.axes,
  4747. transform=transform,
  4748. kind='mdgel',
  4749. )
  4750. ]
  4751. def _series_eer(self) -> list[TiffPageSeries] | None:
  4752. """Return image series in EER file."""
  4753. series = []
  4754. page = self.pages.first
  4755. if page.compression == 1:
  4756. # integrated/final image
  4757. if len(self.pages) < 2:
  4758. return None
  4759. series.append(
  4760. TiffPageSeries(
  4761. [page],
  4762. page.shape,
  4763. page.dtype,
  4764. page.axes,
  4765. name='integrated',
  4766. kind='eer',
  4767. )
  4768. )
  4769. self.is_uniform = False
  4770. page = self.pages[1].aspage() # first EER frame
  4771. assert page.compression in {65000, 65001, 65002}
  4772. self.pages.useframes = True
  4773. self.pages.set_keyframe(page.index)
  4774. pages = self.pages._getlist(slice(page.index, None), validate=False)
  4775. if len(pages) == 1:
  4776. shape = page.shape
  4777. axes = page.axes
  4778. else:
  4779. shape = (len(pages), *page.shape)
  4780. axes = 'I' + page.axes
  4781. series.insert(
  4782. 0,
  4783. TiffPageSeries(
  4784. pages, shape, page.dtype, axes, name='frames', kind='eer'
  4785. ),
  4786. )
  4787. return series
  4788. def _series_ndpi(self) -> list[TiffPageSeries] | None:
  4789. """Return pyramidal image series in NDPI file."""
  4790. series = self._series_generic()
  4791. if series is None:
  4792. return None
  4793. for s in series:
  4794. s.kind = 'ndpi'
  4795. if s.axes[0] == 'I':
  4796. s._set_dimensions(s.shape, 'Z' + s.axes[1:], None, True)
  4797. if s.is_pyramidal:
  4798. name = s.keyframe.tags.valueof(65427)
  4799. s.name = 'Baseline' if name is None else name
  4800. continue
  4801. mag = s.keyframe.tags.valueof(65421)
  4802. if mag is not None:
  4803. if mag == -1.0:
  4804. s.name = 'Macro'
  4805. # s.kind += '_macro'
  4806. elif mag == -2.0:
  4807. s.name = 'Map'
  4808. # s.kind += '_map'
  4809. self.is_uniform = False
  4810. return series
  4811. def _series_avs(self) -> list[TiffPageSeries] | None:
  4812. """Return pyramidal image series in AVS file."""
  4813. series = self._series_generic()
  4814. if series is None:
  4815. return None
  4816. if len(series) != 3:
  4817. logger().warning(
  4818. f'{self!r} AVS series expected 3 series, got {len(series)}'
  4819. )
  4820. s = series[0]
  4821. s.kind = 'avs'
  4822. if s.axes[0] == 'I':
  4823. s._set_dimensions(s.shape, 'Z' + s.axes[1:], None, True)
  4824. if s.is_pyramidal:
  4825. s.name = 'Baseline'
  4826. if len(series) == 3:
  4827. series[1].name = 'Map'
  4828. series[1].kind = 'avs'
  4829. series[2].name = 'Macro'
  4830. series[2].kind = 'avs'
  4831. self.is_uniform = False
  4832. return series
  4833. def _series_philips(self) -> list[TiffPageSeries] | None:
  4834. """Return pyramidal image series in Philips DP file."""
  4835. from xml.etree import ElementTree
  4836. series = []
  4837. pages = self.pages
  4838. pages.cache = False
  4839. pages.useframes = False
  4840. pages.set_keyframe(0)
  4841. pages._load()
  4842. meta = self.philips_metadata
  4843. assert meta is not None
  4844. try:
  4845. tree = ElementTree.fromstring(meta)
  4846. except ElementTree.ParseError as exc:
  4847. logger().error(f'{self!r} Philips series raised {exc!r:.128}')
  4848. return None
  4849. pixel_spacing = [
  4850. tuple(float(v) for v in elem.text.replace('"', '').split())
  4851. for elem in tree.findall(
  4852. './/*'
  4853. '/DataObject[@ObjectType="PixelDataRepresentation"]'
  4854. '/Attribute[@Name="DICOM_PIXEL_SPACING"]'
  4855. )
  4856. if elem.text is not None
  4857. ]
  4858. if len(pixel_spacing) < 2:
  4859. logger().error(
  4860. f'{self!r} Philips series {len(pixel_spacing)=} < 2'
  4861. )
  4862. return None
  4863. series_dict: dict[str, list[TiffPage]] = {}
  4864. series_dict['Level'] = []
  4865. series_dict['Other'] = []
  4866. for page in pages:
  4867. assert isinstance(page, TiffPage)
  4868. if page.description.startswith('Macro'):
  4869. series_dict['Macro'] = [page]
  4870. elif page.description.startswith('Label'):
  4871. series_dict['Label'] = [page]
  4872. elif not page.is_tiled:
  4873. series_dict['Other'].append(page)
  4874. else:
  4875. series_dict['Level'].append(page)
  4876. levels = series_dict.pop('Level')
  4877. if len(levels) != len(pixel_spacing):
  4878. logger().error(
  4879. f'{self!r} Philips series '
  4880. f'{len(levels)=} != {len(pixel_spacing)=}'
  4881. )
  4882. return None
  4883. # fix padding of sublevels
  4884. imagewidth0 = levels[0].imagewidth
  4885. imagelength0 = levels[0].imagelength
  4886. h0, w0 = pixel_spacing[0]
  4887. for serie, (h, w) in zip(levels[1:], pixel_spacing[1:], strict=True):
  4888. page = serie.keyframe
  4889. # if page.dtype.itemsize == 1:
  4890. # page.nodata = 255
  4891. imagewidth = imagewidth0 // round(w / w0)
  4892. imagelength = imagelength0 // round(h / h0)
  4893. if page.imagewidth - page.tilewidth >= imagewidth:
  4894. logger().warning(
  4895. f'{self!r} Philips series {page.index=} '
  4896. f'{page.imagewidth=}-{page.tilewidth=} >= {imagewidth=}'
  4897. )
  4898. page.imagewidth -= page.tilewidth - 1
  4899. elif page.imagewidth < imagewidth:
  4900. logger().warning(
  4901. f'{self!r} Philips series {page.index=} '
  4902. f'{page.imagewidth=} < {imagewidth=}'
  4903. )
  4904. else:
  4905. page.imagewidth = imagewidth
  4906. imagewidth = page.imagewidth
  4907. if page.imagelength - page.tilelength >= imagelength:
  4908. logger().warning(
  4909. f'{self!r} Philips series {page.index=} '
  4910. f'{page.imagelength=}-{page.tilelength=} >= {imagelength=}'
  4911. )
  4912. page.imagelength -= page.tilelength - 1
  4913. # elif page.imagelength < imagelength:
  4914. # # in this case image is padded with zero
  4915. else:
  4916. page.imagelength = imagelength
  4917. imagelength = page.imagelength
  4918. if page.shaped[-1] > 1:
  4919. page.shape = (imagelength, imagewidth, page.shape[-1])
  4920. elif page.shaped[0] > 1:
  4921. page.shape = (page.shape[0], imagelength, imagewidth)
  4922. else:
  4923. page.shape = (imagelength, imagewidth)
  4924. page.shaped = (
  4925. *page.shaped[:2],
  4926. imagelength,
  4927. imagewidth,
  4928. *page.shaped[-1:],
  4929. )
  4930. series = [TiffPageSeries([levels[0]], name='Baseline', kind='philips')]
  4931. for i, page in enumerate(levels[1:]):
  4932. series[0].levels.append(
  4933. TiffPageSeries([page], name=f'Level{i + 1}', kind='philips')
  4934. )
  4935. for key, value in series_dict.items():
  4936. for page in value:
  4937. series.append(TiffPageSeries([page], name=key, kind='philips'))
  4938. self.is_uniform = False
  4939. return series
  4940. def _series_indica(self) -> list[TiffPageSeries] | None:
  4941. """Return pyramidal image series in IndicaLabs file."""
  4942. # TODO: need more IndicaLabs sample files
  4943. # TODO: parse indica series from XML
  4944. # TODO: alpha channels in SubIFDs or main IFDs
  4945. from xml.etree import ElementTree
  4946. series = self._series_generic()
  4947. if series is None or len(series) != 1:
  4948. return series
  4949. try:
  4950. tree = ElementTree.fromstring(self.pages.first.description)
  4951. except ElementTree.ParseError as exc:
  4952. logger().error(f'{self!r} Indica series raised {exc!r:.128}')
  4953. return series
  4954. channel_names = [
  4955. channel.attrib['name'] for channel in tree.iter('channel')
  4956. ]
  4957. for s in series:
  4958. s.kind = 'indica'
  4959. # TODO: identify other dimensions
  4960. if s.axes[0] == 'I' and s.shape[0] == len(channel_names):
  4961. s._set_dimensions(s.shape, 'C' + s.axes[1:], None, True)
  4962. if s.is_pyramidal:
  4963. s.name = 'Baseline'
  4964. self.is_uniform = False
  4965. return series
  4966. def _series_sis(self) -> list[TiffPageSeries] | None:
  4967. """Return image series in Olympus SIS file."""
  4968. meta = self.sis_metadata
  4969. if meta is None:
  4970. return None
  4971. pages = self.pages._getlist(validate=False) # TODO: this fails for VSI
  4972. page = pages[0]
  4973. lenpages = len(pages)
  4974. if 'shape' in meta and 'axes' in meta:
  4975. shape = meta['shape'] + page.shape
  4976. axes = meta['axes'] + page.axes
  4977. else:
  4978. shape = (lenpages, *page.shape)
  4979. axes = 'I' + page.axes
  4980. self.is_uniform = True
  4981. return [TiffPageSeries(pages, shape, page.dtype, axes, kind='sis')]
  4982. def _series_qpi(self) -> list[TiffPageSeries] | None:
  4983. """Return image series in PerkinElmer QPI file."""
  4984. series = []
  4985. pages = self.pages
  4986. pages.cache = True
  4987. pages.useframes = False
  4988. pages.set_keyframe(0)
  4989. pages._load()
  4990. page0 = self.pages.first
  4991. # Baseline
  4992. # TODO: get name from ImageDescription XML
  4993. ifds = []
  4994. index = 0
  4995. axes = 'C' + page0.axes
  4996. dtype = page0.dtype
  4997. pshape = page0.shape
  4998. while index < len(pages):
  4999. page = pages[index]
  5000. if page.shape != pshape:
  5001. break
  5002. ifds.append(page)
  5003. index += 1
  5004. shape = (len(ifds), *pshape)
  5005. series.append(
  5006. TiffPageSeries(
  5007. ifds, shape, dtype, axes, name='Baseline', kind='qpi'
  5008. )
  5009. )
  5010. if index < len(pages):
  5011. # Thumbnail
  5012. page = pages[index]
  5013. series.append(
  5014. TiffPageSeries(
  5015. [page],
  5016. page.shape,
  5017. page.dtype,
  5018. page.axes,
  5019. name='Thumbnail',
  5020. kind='qpi',
  5021. )
  5022. )
  5023. index += 1
  5024. if page0.is_tiled:
  5025. # Resolutions
  5026. while index < len(pages):
  5027. pshape = (pshape[0] // 2, pshape[1] // 2, *pshape[2:])
  5028. ifds = []
  5029. while index < len(pages):
  5030. page = pages[index]
  5031. if page.shape != pshape:
  5032. break
  5033. ifds.append(page)
  5034. index += 1
  5035. if len(ifds) != len(series[0].pages):
  5036. break
  5037. shape = (len(ifds), *pshape)
  5038. series[0].levels.append(
  5039. TiffPageSeries(
  5040. ifds, shape, dtype, axes, name='Resolution', kind='qpi'
  5041. )
  5042. )
  5043. if series[0].is_pyramidal and index < len(pages):
  5044. # Macro
  5045. page = pages[index]
  5046. series.append(
  5047. TiffPageSeries(
  5048. [page],
  5049. page.shape,
  5050. page.dtype,
  5051. page.axes,
  5052. name='Macro',
  5053. kind='qpi',
  5054. )
  5055. )
  5056. index += 1
  5057. # Label
  5058. if index < len(pages):
  5059. page = pages[index]
  5060. series.append(
  5061. TiffPageSeries(
  5062. [page],
  5063. page.shape,
  5064. page.dtype,
  5065. page.axes,
  5066. name='Label',
  5067. kind='qpi',
  5068. )
  5069. )
  5070. self.is_uniform = False
  5071. return series
  5072. def _series_svs(self) -> list[TiffPageSeries] | None:
  5073. """Return image series in Aperio SVS file."""
  5074. if not self.pages.first.is_tiled:
  5075. return None
  5076. series = []
  5077. self.pages.cache = True
  5078. self.pages.useframes = False
  5079. self.pages.set_keyframe(0)
  5080. self.pages._load()
  5081. # baseline
  5082. firstpage = self.pages.first
  5083. if len(self.pages) == 1:
  5084. self.is_uniform = False
  5085. return [
  5086. TiffPageSeries(
  5087. [firstpage],
  5088. firstpage.shape,
  5089. firstpage.dtype,
  5090. firstpage.axes,
  5091. name='Baseline',
  5092. kind='svs',
  5093. )
  5094. ]
  5095. # thumbnail
  5096. page = self.pages[1]
  5097. thumbnail = TiffPageSeries(
  5098. [page],
  5099. page.shape,
  5100. page.dtype,
  5101. page.axes,
  5102. name='Thumbnail',
  5103. kind='svs',
  5104. )
  5105. # resolutions and focal planes
  5106. levels = {firstpage.shape: [firstpage]}
  5107. index = 2
  5108. while index < len(self.pages):
  5109. page = cast(TiffPage, self.pages[index])
  5110. if not page.is_tiled or page.is_reduced:
  5111. break
  5112. if page.shape in levels:
  5113. levels[page.shape].append(page)
  5114. else:
  5115. levels[page.shape] = [page]
  5116. index += 1
  5117. zsize = len(levels[firstpage.shape])
  5118. if not all(len(level) == zsize for level in levels.values()):
  5119. logger().warning(f'{self!r} SVS series focal planes do not match')
  5120. zsize = 1
  5121. baseline = TiffPageSeries(
  5122. levels[firstpage.shape],
  5123. (zsize, *firstpage.shape),
  5124. firstpage.dtype,
  5125. 'Z' + firstpage.axes,
  5126. name='Baseline',
  5127. kind='svs',
  5128. )
  5129. for shape, level in levels.items():
  5130. if shape == firstpage.shape:
  5131. continue
  5132. page = level[0]
  5133. baseline.levels.append(
  5134. TiffPageSeries(
  5135. level,
  5136. (zsize, *page.shape),
  5137. page.dtype,
  5138. 'Z' + page.axes,
  5139. name='Resolution',
  5140. kind='svs',
  5141. )
  5142. )
  5143. series.append(baseline)
  5144. series.append(thumbnail)
  5145. # Label, Macro; subfiletype 1, 9
  5146. for _ in range(2):
  5147. if index == len(self.pages):
  5148. break
  5149. page = self.pages[index]
  5150. assert isinstance(page, TiffPage)
  5151. name = 'Macro' if page.subfiletype == 9 else 'Label'
  5152. series.append(
  5153. TiffPageSeries(
  5154. [page],
  5155. page.shape,
  5156. page.dtype,
  5157. page.axes,
  5158. name=name,
  5159. kind='svs',
  5160. )
  5161. )
  5162. index += 1
  5163. self.is_uniform = False
  5164. return series
  5165. def _series_scn(self) -> list[TiffPageSeries] | None:
  5166. """Return pyramidal image series in Leica SCN file."""
  5167. # TODO: support collections
  5168. from xml.etree import ElementTree
  5169. scnxml = self.pages.first.description
  5170. root = ElementTree.fromstring(scnxml)
  5171. series = []
  5172. self.pages.cache = True
  5173. self.pages.useframes = False
  5174. self.pages.set_keyframe(0)
  5175. self.pages._load()
  5176. for collection in root:
  5177. if not collection.tag.endswith('collection'):
  5178. continue
  5179. for image in collection:
  5180. if not image.tag.endswith('image'):
  5181. continue
  5182. name = image.attrib.get('name', 'Unknown')
  5183. for pixels in image:
  5184. if not pixels.tag.endswith('pixels'):
  5185. continue
  5186. resolutions: dict[int, dict[str, Any]] = {}
  5187. for dimension in pixels:
  5188. if not dimension.tag.endswith('dimension'):
  5189. continue
  5190. if int(image.attrib.get('sizeZ', 1)) > 1:
  5191. raise NotImplementedError(
  5192. 'SCN series: Z-Stacks not supported. '
  5193. 'Please submit a sample file.'
  5194. )
  5195. sizex = int(dimension.attrib['sizeX'])
  5196. sizey = int(dimension.attrib['sizeY'])
  5197. c = int(dimension.attrib.get('c', 0))
  5198. z = int(dimension.attrib.get('z', 0))
  5199. r = int(dimension.attrib.get('r', 0))
  5200. ifd = int(dimension.attrib['ifd'])
  5201. if r in resolutions:
  5202. level = resolutions[r]
  5203. level['channels'] = max(level['channels'], c)
  5204. level['sizez'] = max(level['sizez'], z)
  5205. level['ifds'][(c, z)] = ifd
  5206. else:
  5207. resolutions[r] = {
  5208. 'size': [sizey, sizex],
  5209. 'channels': c,
  5210. 'sizez': z,
  5211. 'ifds': {(c, z): ifd},
  5212. }
  5213. if not resolutions:
  5214. continue
  5215. levels = []
  5216. for _r, level in sorted(resolutions.items()):
  5217. shape: tuple[int, ...] = (
  5218. level['channels'] + 1,
  5219. level['sizez'] + 1,
  5220. )
  5221. axes = 'CZ'
  5222. ifds: list[TiffPage | TiffFrame | None] = [
  5223. None
  5224. ] * product(shape)
  5225. for (c, z), ifd in sorted(level['ifds'].items()):
  5226. ifds[c * shape[1] + z] = self.pages[ifd]
  5227. assert ifds[0] is not None
  5228. axes += ifds[0].axes
  5229. shape += ifds[0].shape
  5230. dtype = ifds[0].dtype
  5231. levels.append(
  5232. TiffPageSeries(
  5233. ifds,
  5234. shape,
  5235. dtype,
  5236. axes,
  5237. parent=self,
  5238. name=name,
  5239. kind='scn',
  5240. )
  5241. )
  5242. levels[0].levels.extend(levels[1:])
  5243. series.append(levels[0])
  5244. self.is_uniform = False
  5245. return series
  5246. def _series_bif(self) -> list[TiffPageSeries] | None:
  5247. """Return image series in Ventana/Roche BIF file."""
  5248. series = []
  5249. baseline: TiffPageSeries | None = None
  5250. self.pages.cache = True
  5251. self.pages.useframes = False
  5252. self.pages.set_keyframe(0)
  5253. self.pages._load()
  5254. for page in self.pages:
  5255. page = cast(TiffPage, page)
  5256. if page.description[:5] == 'Label':
  5257. series.append(
  5258. TiffPageSeries(
  5259. [page],
  5260. page.shape,
  5261. page.dtype,
  5262. page.axes,
  5263. name='Label',
  5264. kind='bif',
  5265. )
  5266. )
  5267. elif (
  5268. page.description == 'Thumbnail'
  5269. or page.description[:11] == 'Probability'
  5270. ):
  5271. series.append(
  5272. TiffPageSeries(
  5273. [page],
  5274. page.shape,
  5275. page.dtype,
  5276. page.axes,
  5277. name='Thumbnail',
  5278. kind='bif',
  5279. )
  5280. )
  5281. elif 'level' not in page.description:
  5282. # TODO: is this necessary?
  5283. series.append(
  5284. TiffPageSeries(
  5285. [page],
  5286. page.shape,
  5287. page.dtype,
  5288. page.axes,
  5289. name='Unknown',
  5290. kind='bif',
  5291. )
  5292. )
  5293. elif baseline is None:
  5294. baseline = TiffPageSeries(
  5295. [page],
  5296. page.shape,
  5297. page.dtype,
  5298. page.axes,
  5299. name='Baseline',
  5300. kind='bif',
  5301. )
  5302. series.insert(0, baseline)
  5303. else:
  5304. baseline.levels.append(
  5305. TiffPageSeries(
  5306. [page],
  5307. page.shape,
  5308. page.dtype,
  5309. page.axes,
  5310. name='Resolution',
  5311. kind='bif',
  5312. )
  5313. )
  5314. logger().warning(f'{self!r} BIF series tiles are not stitched')
  5315. self.is_uniform = False
  5316. return series
  5317. def _series_ome(self) -> list[TiffPageSeries] | None:
  5318. """Return image series in OME-TIFF file(s)."""
  5319. # xml.etree found to be faster than lxml
  5320. from xml.etree import ElementTree
  5321. omexml = self.ome_metadata
  5322. if omexml is None:
  5323. return None
  5324. try:
  5325. root = ElementTree.fromstring(omexml)
  5326. except ElementTree.ParseError as exc:
  5327. # TODO: test badly encoded OME-XML
  5328. logger().error(f'{self!r} OME series raised {exc!r:.128}')
  5329. return None
  5330. keyframe: TiffPage
  5331. ifds: list[TiffPage | TiffFrame | None]
  5332. size: int = -1
  5333. def load_pages(tif: TiffFile, /) -> None:
  5334. tif.pages.cache = True
  5335. tif.pages.useframes = True
  5336. tif.pages.set_keyframe(0)
  5337. tif.pages._load(None)
  5338. load_pages(self)
  5339. root_uuid = root.attrib.get('UUID', None)
  5340. self._files = {root_uuid: self}
  5341. dirname = self._fh.dirname
  5342. files_missing = 0
  5343. moduloref = []
  5344. modulo: dict[str, dict[str, tuple[str, int]]] = {}
  5345. series: list[TiffPageSeries] = []
  5346. for element in root:
  5347. if element.tag.endswith('BinaryOnly'):
  5348. # TODO: load OME-XML from master or companion file
  5349. logger().debug(
  5350. f'{self!r} OME series is BinaryOnly, '
  5351. 'not an OME-TIFF master file'
  5352. )
  5353. break
  5354. if element.tag.endswith('StructuredAnnotations'):
  5355. for annot in element:
  5356. if not annot.attrib.get('Namespace', '').endswith(
  5357. 'modulo'
  5358. ):
  5359. continue
  5360. modulo[annot.attrib['ID']] = mod = {}
  5361. for value in annot:
  5362. for modulo_ns in value:
  5363. for along in modulo_ns:
  5364. if not along.tag[:-1].endswith('Along'):
  5365. continue
  5366. axis = along.tag[-1]
  5367. newaxis = along.attrib.get('Type', 'other')
  5368. newaxis = TIFF.AXES_CODES[newaxis]
  5369. if 'Start' in along.attrib:
  5370. step = float(along.attrib.get('Step', 1))
  5371. start = float(along.attrib['Start'])
  5372. stop = float(along.attrib['End']) + step
  5373. labels = len(
  5374. numpy.arange(start, stop, step)
  5375. )
  5376. else:
  5377. labels = len(
  5378. [
  5379. label
  5380. for label in along
  5381. if label.tag.endswith('Label')
  5382. ]
  5383. )
  5384. mod[axis] = (newaxis, labels)
  5385. if not element.tag.endswith('Image'):
  5386. continue
  5387. for annot in element:
  5388. if annot.tag.endswith('AnnotationRef'):
  5389. annotationref = annot.attrib['ID']
  5390. break
  5391. else:
  5392. annotationref = None
  5393. attr = element.attrib
  5394. name = attr.get('Name', None)
  5395. for pixels in element:
  5396. if not pixels.tag.endswith('Pixels'):
  5397. continue
  5398. attr = pixels.attrib
  5399. # dtype = attr.get('PixelType', None)
  5400. axes = ''.join(reversed(attr['DimensionOrder']))
  5401. shape = [int(attr['Size' + ax]) for ax in axes]
  5402. ifds = []
  5403. spp = 1 # samples per pixel
  5404. first = True
  5405. for data in pixels:
  5406. if data.tag.endswith('Channel'):
  5407. attr = data.attrib
  5408. if first:
  5409. first = False
  5410. spp = int(attr.get('SamplesPerPixel', spp))
  5411. if spp > 1:
  5412. # correct channel dimension for spp
  5413. shape = [
  5414. shape[i] // spp if ax == 'C' else shape[i]
  5415. for i, ax in enumerate(axes)
  5416. ]
  5417. elif int(attr.get('SamplesPerPixel', 1)) != spp:
  5418. raise ValueError(
  5419. 'OME series cannot handle differing '
  5420. 'SamplesPerPixel'
  5421. )
  5422. continue
  5423. if not data.tag.endswith('TiffData'):
  5424. continue
  5425. attr = data.attrib
  5426. ifd_index = int(attr.get('IFD', 0))
  5427. num = int(attr.get('NumPlanes', 1 if 'IFD' in attr else 0))
  5428. num = int(attr.get('PlaneCount', num))
  5429. idxs = [int(attr.get('First' + ax, 0)) for ax in axes[:-2]]
  5430. try:
  5431. idx = int(numpy.ravel_multi_index(idxs, shape[:-2]))
  5432. except ValueError as exc:
  5433. # ImageJ produces invalid ome-xml when cropping
  5434. logger().warning(
  5435. f'{self!r} '
  5436. 'OME series contains invalid TiffData index, '
  5437. f'raised {exc!r:.128}',
  5438. )
  5439. continue
  5440. for uuid in data:
  5441. if not uuid.tag.endswith('UUID'):
  5442. continue
  5443. if (
  5444. root_uuid is None
  5445. and uuid.text is not None
  5446. and (
  5447. uuid.attrib.get('FileName', '').lower()
  5448. == self.filename.lower()
  5449. )
  5450. ):
  5451. # no global UUID, use this file
  5452. root_uuid = uuid.text
  5453. self._files[root_uuid] = self._files[None]
  5454. del self._files[None]
  5455. elif uuid.text not in self._files:
  5456. if not self._multifile:
  5457. # abort reading multifile OME series
  5458. # and fall back to generic series
  5459. return []
  5460. fname = uuid.attrib['FileName']
  5461. try:
  5462. if not self.filehandle.is_file:
  5463. raise ValueError
  5464. tif = TiffFile(
  5465. os.path.join(dirname, fname), _parent=self
  5466. )
  5467. load_pages(tif)
  5468. except (
  5469. OSError,
  5470. FileNotFoundError,
  5471. ValueError,
  5472. ) as exc:
  5473. if files_missing == 0:
  5474. logger().warning(
  5475. f'{self!r} OME series failed to read '
  5476. f'{fname!r}, raised {exc!r:.128}. '
  5477. 'Missing data are zeroed'
  5478. )
  5479. files_missing += 1
  5480. # assume that size is same as in previous file
  5481. # if no NumPlanes or PlaneCount are given
  5482. if num:
  5483. size = num
  5484. elif size == -1:
  5485. raise ValueError(
  5486. 'OME series missing '
  5487. 'NumPlanes or PlaneCount'
  5488. ) from exc
  5489. ifds.extend([None] * (size + idx - len(ifds)))
  5490. break
  5491. self._files[uuid.text] = tif
  5492. tif.close()
  5493. pages = self._files[uuid.text].pages
  5494. try:
  5495. size = num if num else len(pages)
  5496. ifds.extend([None] * (size + idx - len(ifds)))
  5497. for i in range(size):
  5498. ifds[idx + i] = pages[ifd_index + i]
  5499. except IndexError as exc:
  5500. logger().warning(
  5501. f'{self!r} '
  5502. 'OME series contains index out of range, '
  5503. f'raised {exc!r:.128}'
  5504. )
  5505. # only process first UUID
  5506. break
  5507. else:
  5508. # no uuid found
  5509. pages = self.pages
  5510. try:
  5511. size = num if num else len(pages)
  5512. ifds.extend([None] * (size + idx - len(ifds)))
  5513. for i in range(size):
  5514. ifds[idx + i] = pages[ifd_index + i]
  5515. except IndexError as exc:
  5516. logger().warning(
  5517. f'{self!r} '
  5518. 'OME series contains index out of range, '
  5519. f'raised {exc!r:.128}'
  5520. )
  5521. if not ifds or all(i is None for i in ifds):
  5522. # skip images without data
  5523. continue
  5524. # find a keyframe
  5525. for ifd in ifds:
  5526. # try find a TiffPage
  5527. if ifd is not None and ifd == ifd.keyframe:
  5528. keyframe = cast(TiffPage, ifd)
  5529. break
  5530. else:
  5531. # reload a TiffPage from file
  5532. for i, ifd in enumerate(ifds):
  5533. if ifd is not None:
  5534. isclosed = ifd.parent.filehandle.closed
  5535. if isclosed:
  5536. ifd.parent.filehandle.open()
  5537. ifd.parent.pages.set_keyframe(ifd.index)
  5538. keyframe = cast(
  5539. TiffPage, ifd.parent.pages[ifd.index]
  5540. )
  5541. ifds[i] = keyframe
  5542. if isclosed:
  5543. keyframe.parent.filehandle.close()
  5544. break
  5545. # does the series spawn multiple files
  5546. multifile = False
  5547. for ifd in ifds:
  5548. if ifd and ifd.parent != keyframe.parent:
  5549. multifile = True
  5550. break
  5551. if spp > 1:
  5552. if keyframe.planarconfig == 1:
  5553. shape += [spp]
  5554. axes += 'S'
  5555. else:
  5556. shape = [*shape[:-2], spp, *shape[-2:]]
  5557. axes = axes[:-2] + 'S' + axes[-2:]
  5558. if 'S' not in axes:
  5559. shape += [1]
  5560. axes += 'S'
  5561. # number of pages in the file might mismatch XML metadata, for
  5562. # example Nikon-cell011.ome.tif or stack_t24_y2048_x2448.tiff
  5563. size = max(product(shape) // keyframe.size, 1)
  5564. if size < len(ifds):
  5565. logger().warning(
  5566. f'{self!r} '
  5567. f'OME series expected {size} frames, got {len(ifds)}'
  5568. )
  5569. ifds = ifds[:size]
  5570. elif size > len(ifds):
  5571. logger().warning(
  5572. f'{self!r} '
  5573. f'OME series is missing {size - len(ifds)} frames.'
  5574. ' Missing data are zeroed'
  5575. )
  5576. ifds.extend([None] * (size - len(ifds)))
  5577. # FIXME: this implementation assumes the last dimensions are
  5578. # stored in TIFF pages. Apparently that is not always the case.
  5579. # For example, TCX (20000, 2, 500) is stored in 2 pages of
  5580. # (20000, 500) in 'Image 7.ome_h00.tiff'.
  5581. # For now, verify that shapes of keyframe and series match.
  5582. # If not, skip series.
  5583. squeezed = squeeze_axes(shape, axes)[0]
  5584. if keyframe.shape != tuple(squeezed[-len(keyframe.shape) :]):
  5585. logger().warning(
  5586. f'{self!r} OME series cannot handle discontiguous '
  5587. f'storage ({keyframe.shape} != '
  5588. f'{tuple(squeezed[-len(keyframe.shape) :])})',
  5589. )
  5590. del ifds
  5591. continue
  5592. # set keyframe on all IFDs
  5593. # each series must contain a TiffPage used as keyframe
  5594. keyframes: dict[str, TiffPage] = {
  5595. keyframe.parent.filehandle.name: keyframe
  5596. }
  5597. for i, page in enumerate(ifds):
  5598. if page is None:
  5599. continue
  5600. fh = page.parent.filehandle
  5601. if fh.name not in keyframes:
  5602. if page.keyframe != page:
  5603. # reload TiffPage from file
  5604. isclosed = fh.closed
  5605. if isclosed:
  5606. fh.open()
  5607. page.parent.pages.set_keyframe(page.index)
  5608. page = page.parent.pages[ # noqa: PLW2901
  5609. page.index
  5610. ]
  5611. ifds[i] = page
  5612. if isclosed:
  5613. fh.close()
  5614. keyframes[fh.name] = cast(TiffPage, page)
  5615. if page.keyframe != page:
  5616. page.keyframe = keyframes[fh.name]
  5617. moduloref.append(annotationref)
  5618. series.append(
  5619. TiffPageSeries(
  5620. ifds,
  5621. shape,
  5622. keyframe.dtype,
  5623. axes,
  5624. parent=self,
  5625. name=name,
  5626. multifile=multifile,
  5627. kind='ome',
  5628. )
  5629. )
  5630. del ifds
  5631. if files_missing > 1:
  5632. logger().warning(
  5633. f'{self!r} OME series failed to read {files_missing} files'
  5634. )
  5635. # apply modulo according to AnnotationRef
  5636. for aseries, annotationref in zip(series, moduloref, strict=True):
  5637. if annotationref not in modulo:
  5638. continue
  5639. shape = list(aseries.get_shape(squeeze=False))
  5640. axes = aseries.get_axes(squeeze=False)
  5641. for axis, (newaxis, size) in modulo[annotationref].items():
  5642. i = axes.index(axis)
  5643. if shape[i] == size:
  5644. axes = axes.replace(axis, newaxis, 1)
  5645. else:
  5646. shape[i] //= size
  5647. shape.insert(i + 1, size)
  5648. axes = axes.replace(axis, axis + newaxis, 1)
  5649. aseries._set_dimensions(shape, axes, None)
  5650. # pyramids
  5651. for aseries in series:
  5652. keyframe = aseries.keyframe
  5653. if keyframe.subifds is None:
  5654. continue
  5655. if len(self._files) > 1:
  5656. # TODO: support multi-file pyramids; must re-open/close
  5657. logger().warning(
  5658. f'{self!r} OME series cannot read multi-file pyramids'
  5659. )
  5660. break
  5661. for level in range(len(keyframe.subifds)):
  5662. found_keyframe = False
  5663. ifds = []
  5664. for page in aseries.pages:
  5665. if (
  5666. page is None
  5667. or page.subifds is None
  5668. or page.subifds[level] < 8
  5669. ):
  5670. ifds.append(None)
  5671. continue
  5672. page.parent.filehandle.seek(page.subifds[level])
  5673. if page.keyframe == page:
  5674. ifd = keyframe = TiffPage(
  5675. self, (page.index, level + 1)
  5676. )
  5677. found_keyframe = True
  5678. elif not found_keyframe:
  5679. raise RuntimeError('no keyframe found')
  5680. else:
  5681. ifd = TiffFrame(
  5682. self, (page.index, level + 1), keyframe=keyframe
  5683. )
  5684. ifds.append(ifd)
  5685. if all(ifd_or_none is None for ifd_or_none in ifds):
  5686. logger().warning(
  5687. f'{self!r} OME series level {level + 1} is empty'
  5688. )
  5689. break
  5690. # fix shape
  5691. shape = list(aseries.get_shape(squeeze=False))
  5692. axes = aseries.get_axes(squeeze=False)
  5693. for i, ax in enumerate(axes):
  5694. if ax == 'X':
  5695. shape[i] = keyframe.imagewidth
  5696. elif ax == 'Y':
  5697. shape[i] = keyframe.imagelength
  5698. # add series
  5699. aseries.levels.append(
  5700. TiffPageSeries(
  5701. ifds,
  5702. tuple(shape),
  5703. keyframe.dtype,
  5704. axes,
  5705. parent=self,
  5706. name=f'level {level + 1}',
  5707. kind='ome',
  5708. )
  5709. )
  5710. self.is_uniform = len(series) == 1 and len(series[0].levels) == 1
  5711. return series
  5712. def _series_mmstack(self) -> list[TiffPageSeries] | None:
  5713. """Return series in Micro-Manager stack file(s)."""
  5714. settings = self.micromanager_metadata
  5715. if (
  5716. settings is None
  5717. or 'Summary' not in settings
  5718. or 'IndexMap' not in settings
  5719. ):
  5720. return None
  5721. pages: list[TiffPage | TiffFrame | None]
  5722. page_count: int
  5723. summary = settings['Summary']
  5724. indexmap = settings['IndexMap']
  5725. indexmap = indexmap[indexmap[:, 4].argsort()]
  5726. if 'MicroManagerVersion' not in summary or 'Frames' not in summary:
  5727. # TODO: handle MagellanStack?
  5728. return None
  5729. # determine CZTR shape from indexmap; TODO: is this necessary?
  5730. indexmap_shape = (numpy.max(indexmap[:, :4], axis=0) + 1).tolist()
  5731. indexmap_index = {'C': 0, 'Z': 1, 'T': 2, 'R': 3}
  5732. # TODO: activate this?
  5733. # if 'AxisOrder' in summary:
  5734. # axesorder = summary['AxisOrder']
  5735. # keys = {
  5736. # 'channel': 'C',
  5737. # 'z': 'Z',
  5738. # 'slice': 'Z',
  5739. # 'position': 'R',
  5740. # 'time': 'T',
  5741. # }
  5742. # axes = ''.join(keys[ax] for ax in reversed(axesorder))
  5743. axes = 'TR' if summary.get('TimeFirst', True) else 'RT'
  5744. axes += 'ZC' if summary.get('SlicesFirst', True) else 'CZ'
  5745. keys = {
  5746. 'C': 'Channels',
  5747. 'Z': 'Slices',
  5748. 'R': 'Positions',
  5749. 'T': 'Frames',
  5750. }
  5751. shape = tuple(
  5752. max(
  5753. indexmap_shape[indexmap_index[ax]],
  5754. int(summary.get(keys[ax], 1)),
  5755. )
  5756. for ax in axes
  5757. )
  5758. size = product(shape)
  5759. indexmap_order = tuple(indexmap_index[ax] for ax in axes)
  5760. def add_file(tif: TiffFile, indexmap: NDArray[Any]) -> int:
  5761. # add virtual TiffFrames to pages list
  5762. page_count = 0
  5763. offsets: list[int]
  5764. offsets = indexmap[:, 4].tolist()
  5765. indices = numpy.ravel_multi_index(
  5766. indexmap[:, indexmap_order].T, shape
  5767. ).tolist()
  5768. keyframe = tif.pages.first
  5769. filesize = tif.filehandle.size - keyframe.databytecounts[0] - 162
  5770. index: int
  5771. offset: int
  5772. for item in zip(indices, offsets, strict=True):
  5773. index, offset = item
  5774. if offset == keyframe.offset:
  5775. pages[index] = keyframe
  5776. page_count += 1
  5777. continue
  5778. if 0 < offset <= filesize:
  5779. dataoffsets = (offset + 162,)
  5780. databytecounts = keyframe.databytecounts
  5781. page_count += 1
  5782. else:
  5783. # assume file is truncated
  5784. dataoffsets = databytecounts = (0,)
  5785. offset = 0
  5786. pages[index] = TiffFrame(
  5787. tif,
  5788. index=index,
  5789. offset=offset,
  5790. dataoffsets=dataoffsets,
  5791. databytecounts=databytecounts,
  5792. keyframe=keyframe,
  5793. )
  5794. return page_count
  5795. multifile = size > indexmap.shape[0]
  5796. if multifile:
  5797. # get multifile prefix
  5798. if not self.filehandle.is_file:
  5799. logger().warning(
  5800. f'{self!r} MMStack multi-file series cannot be read from '
  5801. f'{self.filehandle._fh!r}'
  5802. )
  5803. multifile = False
  5804. elif '_MMStack' not in self.filename:
  5805. logger().warning(f'{self!r} MMStack file name is invalid')
  5806. multifile = False
  5807. elif 'Prefix' in summary:
  5808. prefix = summary['Prefix']
  5809. if not self.filename.startswith(prefix):
  5810. logger().warning(f'{self!r} MMStack file name is invalid')
  5811. multifile = False
  5812. else:
  5813. prefix = self.filename.split('_MMStack')[0]
  5814. if multifile:
  5815. # read other files
  5816. pattern = os.path.join(
  5817. self.filehandle.dirname, prefix + '_MMStack*.tif'
  5818. )
  5819. filenames = glob.glob(pattern)
  5820. if len(filenames) == 1:
  5821. multifile = False
  5822. else:
  5823. pages = [None] * size
  5824. page_count = add_file(self, indexmap)
  5825. for fname in filenames:
  5826. if self.filename == os.path.split(fname)[-1]:
  5827. continue
  5828. with TiffFile(fname) as tif:
  5829. indexmap = read_micromanager_metadata(
  5830. tif.filehandle, {'IndexMap'}
  5831. )['IndexMap']
  5832. indexmap = indexmap[indexmap[:, 4].argsort()]
  5833. page_count += add_file(tif, indexmap)
  5834. if multifile:
  5835. pass
  5836. elif size > indexmap.shape[0]:
  5837. # other files missing: squeeze shape
  5838. old_shape = shape
  5839. min_index = numpy.min(indexmap[:, :4], axis=0)
  5840. max_index = numpy.max(indexmap[:, :4], axis=0)
  5841. indexmap = indexmap.copy()
  5842. indexmap[:, :4] -= min_index
  5843. shape = tuple(
  5844. j - i + 1
  5845. for i, j in zip(
  5846. min_index.tolist(), max_index.tolist(), strict=True
  5847. )
  5848. )
  5849. shape = tuple(shape[i] for i in indexmap_order)
  5850. size = product(shape)
  5851. pages = [None] * size
  5852. page_count = add_file(self, indexmap)
  5853. logger().warning(
  5854. f'{self!r} MMStack series is missing files. '
  5855. f'Returning subset {shape!r} of {old_shape!r}'
  5856. )
  5857. else:
  5858. # single file
  5859. pages = [None] * size
  5860. page_count = add_file(self, indexmap)
  5861. if page_count != size:
  5862. logger().warning(
  5863. f'{self!r} MMStack is missing {size - page_count} pages.'
  5864. ' Missing data are zeroed'
  5865. )
  5866. keyframe = self.pages.first
  5867. return [
  5868. TiffPageSeries(
  5869. pages,
  5870. shape=shape + keyframe.shape,
  5871. dtype=keyframe.dtype,
  5872. axes=axes + keyframe.axes,
  5873. # axestiled=axestiled,
  5874. # axesoverlap=axesoverlap,
  5875. # coords=coords,
  5876. parent=self,
  5877. kind='mmstack',
  5878. multifile=multifile,
  5879. squeeze=True,
  5880. )
  5881. ]
  5882. def _series_ndtiff(self) -> list[TiffPageSeries] | None:
  5883. """Return series in NDTiff v2 and v3 files."""
  5884. # TODO: implement fallback for missing index file, versions 0 and 1
  5885. if not self.filehandle.is_file:
  5886. logger().warning(
  5887. f'{self!r} NDTiff.index not found for {self.filehandle._fh!r}'
  5888. )
  5889. return None
  5890. indexfile = os.path.join(self.filehandle.dirname, 'NDTiff.index')
  5891. if not os.path.exists(indexfile):
  5892. logger().warning(f'{self!r} NDTiff.index not found')
  5893. return None
  5894. keyframes: dict[str, TiffPage] = {}
  5895. shape: tuple[int, ...]
  5896. dims: tuple[str, ...]
  5897. page: TiffPage | TiffFrame
  5898. pageindex = 0
  5899. pixel_types = {
  5900. 0: ('uint8', 8), # 8bit monochrome
  5901. 1: ('uint16', 16), # 16bit monochrome
  5902. 2: ('uint8', 8), # 8bit RGB
  5903. 3: ('uint16', 10), # 10bit monochrome
  5904. 4: ('uint16', 12), # 12bit monochrome
  5905. 5: ('uint16', 14), # 14bit monochrome
  5906. 6: ('uint16', 11), # 11bit monochrome
  5907. }
  5908. indices: dict[tuple[int, ...], TiffPage | TiffFrame] = {}
  5909. categories: dict[str, dict[str, int]] = {}
  5910. first = True
  5911. for (
  5912. axes_dict,
  5913. filename,
  5914. dataoffset,
  5915. width,
  5916. height,
  5917. pixeltype,
  5918. compression,
  5919. _metaoffset,
  5920. _metabytecount,
  5921. _metacompression,
  5922. ) in read_ndtiff_index(indexfile):
  5923. if filename in keyframes:
  5924. # create virtual frame from index
  5925. pageindex += 1 # TODO
  5926. keyframe = keyframes[filename]
  5927. page = TiffFrame(
  5928. keyframe.parent,
  5929. pageindex,
  5930. offset=None, # virtual frame
  5931. keyframe=keyframe,
  5932. dataoffsets=(dataoffset,),
  5933. databytecounts=keyframe.databytecounts,
  5934. )
  5935. if page.shape[:2] != (height, width):
  5936. raise ValueError(
  5937. 'NDTiff.index does not match TIFF shape '
  5938. f'{page.shape[:2]} != {(height, width)}'
  5939. )
  5940. if compression != 0:
  5941. raise ValueError(
  5942. 'NDTiff.index compression {compression} not supported'
  5943. )
  5944. if page.compression != 1:
  5945. raise ValueError(
  5946. 'NDTiff.index does not match TIFF compression '
  5947. f'{page.compression!r}'
  5948. )
  5949. if pixeltype not in pixel_types:
  5950. raise ValueError(
  5951. f'NDTiff.index unknown pixel type {pixeltype}'
  5952. )
  5953. dtype, _ = pixel_types[pixeltype]
  5954. if page.dtype != dtype:
  5955. raise ValueError(
  5956. 'NDTiff.index pixeltype does not match TIFF dtype '
  5957. f'{page.dtype} != {dtype}'
  5958. )
  5959. elif filename == self.filename:
  5960. # use first page as keyframe
  5961. pageindex = 0
  5962. page = self.pages.first
  5963. keyframes[filename] = page
  5964. else:
  5965. # read keyframe from file
  5966. pageindex = 0
  5967. with TiffFile(
  5968. os.path.join(self.filehandle.dirname, filename)
  5969. ) as tif:
  5970. page = tif.pages.first
  5971. keyframes[filename] = page
  5972. # replace string with integer indices
  5973. index: int | str
  5974. if first:
  5975. for axis, index in axes_dict.items():
  5976. if isinstance(index, str):
  5977. categories[axis] = {index: 0}
  5978. axes_dict[axis] = 0
  5979. first = False
  5980. dims = tuple(axes_dict.keys())
  5981. elif categories:
  5982. for axis, values in categories.items():
  5983. index = axes_dict[axis]
  5984. assert isinstance(index, str)
  5985. if index not in values:
  5986. values[index] = max(values.values()) + 1
  5987. axes_dict[axis] = values[index]
  5988. if tuple(axes_dict.keys()) != dims:
  5989. dims_ = tuple(axes_dict.keys())
  5990. logger().warning(
  5991. f'{self!r} NDTiff.index axes_dict.keys={dims_} != {dims}'
  5992. )
  5993. indices[tuple(int(axes_dict[dim]) for dim in dims)] = page
  5994. # indices may be negative or missing
  5995. indices_array = numpy.array(list(indices.keys()), dtype=numpy.int32)
  5996. min_index = numpy.min(indices_array, axis=0).tolist()
  5997. max_index = numpy.max(indices_array, axis=0).tolist()
  5998. shape = tuple(
  5999. j - i + 1 for i, j in zip(min_index, max_index, strict=True)
  6000. )
  6001. # change axes to match storage order
  6002. order = order_axes(indices_array, squeeze=False)
  6003. shape = tuple(shape[i] for i in order)
  6004. dims = tuple(dims[i] for i in order)
  6005. indices = {
  6006. tuple(index[i] - min_index[i] for i in order): value
  6007. for index, value in indices.items()
  6008. }
  6009. pages: list[TiffPage | TiffFrame | None] = [
  6010. indices.get(idx) for idx in numpy.ndindex(shape)
  6011. ]
  6012. keyframe = next(i for i in keyframes.values())
  6013. shape += keyframe.shape
  6014. dims += keyframe.dims
  6015. axes = ''.join(TIFF.AXES_CODES.get(i.lower(), 'Q') for i in dims)
  6016. # TODO: support tiled axes and overlap
  6017. # meta: Any = self.micromanager_metadata
  6018. # if meta is None:
  6019. # meta = {}
  6020. # elif 'Summary' in meta:
  6021. # meta = meta['Summary']
  6022. # # map axes column->x, row->y
  6023. # axestiled: dict[int, int] = {}
  6024. # axesoverlap: dict[int, int] = {}
  6025. # if 'column' in dims:
  6026. # key = dims.index('column')
  6027. # axestiled[key] = keyframe.axes.index('X')
  6028. # axesoverlap[key] = meta.get('GridPixelOverlapX', 0)
  6029. # if 'row' in dims:
  6030. # key = dims.index('row')
  6031. # axestiled[key] = keyframe.axes.index('Y')
  6032. # axesoverlap[key] = meta.get('GridPixelOverlapY', 0)
  6033. # if all(i == 0 for i in axesoverlap.values()):
  6034. # axesoverlap = {}
  6035. self.is_uniform = True
  6036. return [
  6037. TiffPageSeries(
  6038. pages,
  6039. shape=shape,
  6040. dtype=keyframe.dtype,
  6041. axes=axes,
  6042. # axestiled=axestiled,
  6043. # axesoverlap=axesoverlap,
  6044. # coords=coords,
  6045. parent=self,
  6046. kind='ndtiff',
  6047. multifile=len(keyframes) > 1,
  6048. squeeze=True,
  6049. )
  6050. ]
  6051. def _series_stk(self) -> list[TiffPageSeries] | None:
  6052. """Return series in STK file."""
  6053. meta = self.stk_metadata
  6054. if meta is None:
  6055. return None
  6056. page = self.pages.first
  6057. planes = meta['NumberPlanes']
  6058. name = meta.get('Name', '')
  6059. if planes == 1:
  6060. shape = (1, *page.shape)
  6061. axes = 'I' + page.axes
  6062. elif numpy.all(meta['ZDistance'] != 0):
  6063. shape = (planes, *page.shape)
  6064. axes = 'Z' + page.axes
  6065. elif numpy.all(numpy.diff(meta['TimeCreated']) != 0):
  6066. shape = (planes, *page.shape)
  6067. axes = 'T' + page.axes
  6068. else:
  6069. # TODO: determine other/combinations of dimensions
  6070. shape = (planes, *page.shape)
  6071. axes = 'I' + page.axes
  6072. self.is_uniform = True
  6073. series = TiffPageSeries(
  6074. [page],
  6075. shape,
  6076. page.dtype,
  6077. axes,
  6078. name=name,
  6079. truncated=planes > 1,
  6080. kind='stk',
  6081. )
  6082. return [series]
  6083. def _series_lsm(self) -> list[TiffPageSeries] | None:
  6084. """Return main and thumbnail series in LSM file."""
  6085. lsmi = self.lsm_metadata
  6086. if lsmi is None:
  6087. return None
  6088. axes = TIFF.CZ_LSMINFO_SCANTYPE[lsmi['ScanType']]
  6089. if self.pages.first.planarconfig == 1:
  6090. axes = axes.replace('C', '').replace('X', 'XC')
  6091. elif self.pages.first.planarconfig == 2:
  6092. # keep axis for `get_shape(False)`
  6093. pass
  6094. elif self.pages.first.samplesperpixel == 1:
  6095. axes = axes.replace('C', '')
  6096. if lsmi.get('DimensionP', 0) > 0:
  6097. axes = 'P' + axes
  6098. if lsmi.get('DimensionM', 0) > 0:
  6099. axes = 'M' + axes
  6100. shape = tuple(int(lsmi[TIFF.CZ_LSMINFO_DIMENSIONS[i]]) for i in axes)
  6101. name = lsmi.get('Name', '')
  6102. pages = self.pages._getlist(slice(0, None, 2), validate=False)
  6103. dtype = pages[0].dtype
  6104. series = [
  6105. TiffPageSeries(pages, shape, dtype, axes, name=name, kind='lsm')
  6106. ]
  6107. page = cast(TiffPage, self.pages[1])
  6108. if page.is_reduced:
  6109. pages = self.pages._getlist(slice(1, None, 2), validate=False)
  6110. dtype = page.dtype
  6111. cp = 1
  6112. i = 0
  6113. while cp < len(pages) and i < len(shape) - 2:
  6114. cp *= shape[i]
  6115. i += 1
  6116. shape = shape[:i] + page.shape
  6117. axes = axes[:i] + page.axes
  6118. series.append(
  6119. TiffPageSeries(
  6120. pages, shape, dtype, axes, name=name, kind='lsm'
  6121. )
  6122. )
  6123. self.is_uniform = False
  6124. return series
  6125. def _lsm_load_pages(self) -> None:
  6126. """Read and fix all pages from LSM file."""
  6127. # cache all pages to preserve corrected values
  6128. pages = self.pages
  6129. pages.cache = True
  6130. pages.useframes = True
  6131. # use first and second page as keyframes
  6132. pages.set_keyframe(1)
  6133. pages.set_keyframe(0)
  6134. # load remaining pages as frames
  6135. pages._load(None)
  6136. # fix offsets and bytecounts first
  6137. # TODO: fix multiple conversions between lists and tuples
  6138. self._lsm_fix_strip_offsets()
  6139. self._lsm_fix_strip_bytecounts()
  6140. # assign keyframes for data and thumbnail series
  6141. keyframe = self.pages.first
  6142. for page in pages._pages[::2]:
  6143. page.keyframe = keyframe # type: ignore[union-attr]
  6144. keyframe = cast(TiffPage, pages[1])
  6145. for page in pages._pages[1::2]:
  6146. page.keyframe = keyframe # type: ignore[union-attr]
  6147. def _lsm_fix_strip_offsets(self) -> None:
  6148. """Unwrap strip offsets for LSM files greater than 4 GB.
  6149. Each series and position require separate unwrapping (undocumented).
  6150. """
  6151. if self.filehandle.size < 2**32:
  6152. return
  6153. indices: NDArray[Any]
  6154. pages = self.pages
  6155. npages = len(pages)
  6156. series = self.series[0]
  6157. axes = series.axes
  6158. # find positions
  6159. positions = 1
  6160. for i in 0, 1:
  6161. if series.axes[i] in 'PM':
  6162. positions *= series.shape[i]
  6163. # make time axis first
  6164. if positions > 1:
  6165. ntimes = 0
  6166. for i in 1, 2:
  6167. if axes[i] == 'T':
  6168. ntimes = series.shape[i]
  6169. break
  6170. if ntimes:
  6171. div, mod = divmod(npages, 2 * positions * ntimes)
  6172. if mod != 0:
  6173. raise RuntimeError('mod != 0')
  6174. shape = (positions, ntimes, div, 2)
  6175. indices = numpy.arange(product(shape)).reshape(shape)
  6176. indices = numpy.moveaxis(indices, 1, 0)
  6177. else:
  6178. indices = numpy.arange(npages).reshape((-1, 2))
  6179. else:
  6180. indices = numpy.arange(npages).reshape((-1, 2))
  6181. # images of reduced page might be stored first
  6182. if pages[0].dataoffsets[0] > pages[1].dataoffsets[0]:
  6183. indices = indices[..., ::-1]
  6184. # unwrap offsets
  6185. wrap = 0
  6186. previousoffset = 0
  6187. for npi in indices.flat:
  6188. page = pages[int(npi)]
  6189. dataoffsets = []
  6190. if all(i <= 0 for i in page.dataoffsets):
  6191. logger().warning(
  6192. f'{self!r} LSM file incompletely written at {page}'
  6193. )
  6194. break
  6195. for currentoffset in page.dataoffsets:
  6196. if currentoffset < previousoffset:
  6197. wrap += 2**32
  6198. dataoffsets.append(currentoffset + wrap)
  6199. previousoffset = currentoffset
  6200. page.dataoffsets = tuple(dataoffsets)
  6201. def _lsm_fix_strip_bytecounts(self) -> None:
  6202. """Set databytecounts to size of compressed data.
  6203. The StripByteCounts tag in LSM files contains the number of bytes
  6204. for the uncompressed data.
  6205. """
  6206. if self.pages.first.compression == 1:
  6207. return
  6208. # sort pages by first strip offset
  6209. pages = sorted(self.pages, key=lambda p: p.dataoffsets[0])
  6210. npages = len(pages) - 1
  6211. for i, page in enumerate(pages):
  6212. if page.index % 2:
  6213. continue
  6214. offsets = page.dataoffsets
  6215. bytecounts = page.databytecounts
  6216. if i < npages:
  6217. lastoffset = pages[i + 1].dataoffsets[0]
  6218. else:
  6219. # LZW compressed strips might be longer than uncompressed
  6220. lastoffset = min(
  6221. offsets[-1] + 2 * bytecounts[-1], self._fh.size
  6222. )
  6223. bytecount_list = list(bytecounts)
  6224. for j in range(len(bytecounts) - 1):
  6225. bytecount_list[j] = offsets[j + 1] - offsets[j]
  6226. bytecount_list[-1] = lastoffset - offsets[-1]
  6227. page.databytecounts = tuple(bytecount_list)
  6228. def _ndpi_load_pages(self) -> None:
  6229. """Read and fix pages from NDPI slide file if CaptureMode > 6.
  6230. If the value of the CaptureMode tag is greater than 6, change the
  6231. attributes of TiffPage instances that are part of the pyramid to
  6232. match 16-bit grayscale data. TiffTag values are not corrected.
  6233. """
  6234. pages = self.pages
  6235. capturemode = self.pages.first.tags.valueof(65441)
  6236. if capturemode is None or capturemode < 6:
  6237. return
  6238. pages.cache = True
  6239. pages.useframes = False
  6240. pages._load()
  6241. for page in pages:
  6242. assert isinstance(page, TiffPage)
  6243. mag = page.tags.valueof(65421)
  6244. if mag is None or mag > 0:
  6245. page.photometric = PHOTOMETRIC.MINISBLACK
  6246. page.sampleformat = SAMPLEFORMAT.UINT
  6247. page.samplesperpixel = 1
  6248. page.bitspersample = 16
  6249. page.dtype = page._dtype = numpy.dtype(numpy.uint16)
  6250. if page.shaped[-1] > 1:
  6251. page.axes = page.axes[:-1]
  6252. page.shape = page.shape[:-1]
  6253. page.shaped = (*page.shaped[:-1], 1)
  6254. def __getattr__(self, name: str, /) -> bool:
  6255. """Return `is_flag` attributes from first page."""
  6256. if name[3:] in TIFF.PAGE_FLAGS:
  6257. if not self.pages:
  6258. return False
  6259. value = bool(getattr(self.pages.first, name))
  6260. setattr(self, name, value)
  6261. return value
  6262. raise AttributeError(
  6263. f'{self.__class__.__name__!r} object has no attribute {name!r}'
  6264. )
  6265. def __enter__(self) -> Self:
  6266. return self
  6267. def __exit__(
  6268. self,
  6269. exc_type: type[BaseException] | None,
  6270. exc_value: BaseException | None,
  6271. traceback: TracebackType | None,
  6272. ) -> None:
  6273. self.close()
  6274. def __repr__(self) -> str:
  6275. return f'<tifffile.TiffFile {snipstr(self._fh.name, 32)!r}>'
  6276. def __str__(self) -> str:
  6277. return self._str()
  6278. def _str(self, detail: int = 0, width: int = 79) -> str:
  6279. """Return string containing information about TiffFile.
  6280. The `detail` parameter specifies the level of detail returned:
  6281. 0: file only.
  6282. 1: all series, first page of series and its tags.
  6283. 2: large tag values and file metadata.
  6284. 3: all pages.
  6285. """
  6286. info_list = [
  6287. "TiffFile '{}'",
  6288. format_size(self._fh.size),
  6289. (
  6290. ''
  6291. if byteorder_isnative(self.byteorder)
  6292. else {'<': 'little-endian', '>': 'big-endian'}[self.byteorder]
  6293. ),
  6294. ]
  6295. if self.is_bigtiff:
  6296. info_list.append('BigTiff')
  6297. if len(self.pages) > 1:
  6298. info_list.append(f'{len(self.pages)} Pages')
  6299. if len(self.series) > 1:
  6300. info_list.append(f'{len(self.series)} Series')
  6301. if len(self._files) > 1:
  6302. info_list.append(f'{len(self._files)} Files')
  6303. flags = self.flags
  6304. if 'uniform' in flags and len(self.pages) == 1:
  6305. flags.discard('uniform')
  6306. info_list.append('|'.join(f.lower() for f in sorted(flags)))
  6307. info = ' '.join(info_list)
  6308. info = info.replace(' ', ' ').replace(' ', ' ')
  6309. info = info.format(
  6310. snipstr(self._fh.name, max(12, width + 2 - len(info)))
  6311. )
  6312. if detail <= 0:
  6313. return info
  6314. info_list = [info]
  6315. info_list.append('\n'.join(str(s) for s in self.series))
  6316. if detail >= 3:
  6317. for page in self.pages:
  6318. if page is None:
  6319. continue
  6320. info_list.append(page._str(detail=detail, width=width))
  6321. if page.pages is not None:
  6322. for subifd in page.pages:
  6323. info_list.append(
  6324. subifd._str(detail=detail, width=width)
  6325. )
  6326. elif self.series:
  6327. info_list.extend(
  6328. s.keyframe._str(detail=detail, width=width)
  6329. for s in self.series
  6330. if not s.keyframe.parent.filehandle.closed # avoid warning
  6331. )
  6332. elif self.pages: # and self.pages.first:
  6333. info_list.append(self.pages.first._str(detail=detail, width=width))
  6334. if detail >= 2:
  6335. for name in sorted(self.flags):
  6336. if hasattr(self, name + '_metadata'):
  6337. m = getattr(self, name + '_metadata')
  6338. if m:
  6339. info_list.append(
  6340. f'{name.upper()}_METADATA\n'
  6341. f'{pformat(m, width=width, height=detail * 24)}'
  6342. )
  6343. return '\n\n'.join(info_list).replace('\n\n\n', '\n\n')
  6344. @cached_property
  6345. def flags(self) -> set[str]:
  6346. """Set of file flags (a potentially expensive operation)."""
  6347. return {
  6348. name.lower()
  6349. for name in TIFF.FILE_FLAGS
  6350. if getattr(self, 'is_' + name)
  6351. }
  6352. @cached_property
  6353. def is_uniform(self) -> bool:
  6354. """File contains uniform series of pages."""
  6355. # the hashes of IFDs 0, 7, and -1 are the same
  6356. pages = self.pages
  6357. try:
  6358. page = self.pages.first
  6359. except IndexError:
  6360. return False
  6361. if page.subifds:
  6362. return False
  6363. if page.is_scanimage or page.is_nih:
  6364. return True
  6365. i = 0
  6366. useframes = pages.useframes
  6367. try:
  6368. pages.useframes = False
  6369. h = page.hash
  6370. for i in (1, 7, -1):
  6371. if pages[i].aspage().hash != h:
  6372. return False
  6373. except IndexError:
  6374. return i == 1 # single page TIFF is uniform
  6375. finally:
  6376. pages.useframes = useframes
  6377. return True
  6378. @property
  6379. def is_appendable(self) -> bool:
  6380. """Pages can be appended to file without corrupting."""
  6381. # TODO: check other formats
  6382. return not (
  6383. self.is_ome
  6384. or self.is_lsm
  6385. or self.is_stk
  6386. or self.is_imagej
  6387. or self.is_fluoview
  6388. or self.is_micromanager
  6389. )
  6390. @property
  6391. def is_bigtiff(self) -> bool:
  6392. """File has BigTIFF format."""
  6393. return self.tiff.is_bigtiff
  6394. @cached_property
  6395. def is_ndtiff(self) -> bool:
  6396. """File has NDTiff format."""
  6397. # file should be accompanied by NDTiff.index
  6398. meta = self.micromanager_metadata
  6399. if meta is not None and meta.get('MajorVersion', 0) >= 2:
  6400. self.is_uniform = True
  6401. return True
  6402. return False
  6403. @cached_property
  6404. def is_mmstack(self) -> bool:
  6405. """File has Micro-Manager stack format."""
  6406. meta = self.micromanager_metadata
  6407. if (
  6408. meta is not None
  6409. and 'Summary' in meta
  6410. and 'IndexMap' in meta
  6411. and meta.get('MajorVersion', 1) == 0
  6412. # and 'MagellanStack' not in self.filename:
  6413. ):
  6414. self.is_uniform = True
  6415. return True
  6416. return False
  6417. @cached_property
  6418. def is_mdgel(self) -> bool:
  6419. """File has MD Gel format."""
  6420. # side effect: add second page, if exists, to cache
  6421. try:
  6422. ismdgel = (
  6423. self.pages.first.is_mdgel
  6424. or self.pages.get(1, cache=True).is_mdgel
  6425. )
  6426. except IndexError:
  6427. return False
  6428. if ismdgel:
  6429. self.is_uniform = False
  6430. return ismdgel
  6431. @property
  6432. def is_sis(self) -> bool:
  6433. """File is Olympus SIS format."""
  6434. try:
  6435. return (
  6436. self.pages.first.is_sis
  6437. and not self.filename.lower().endswith('.vsi')
  6438. )
  6439. except IndexError:
  6440. return False
  6441. @cached_property
  6442. def shaped_metadata(self) -> tuple[dict[str, Any], ...] | None:
  6443. """Tifffile metadata from JSON formatted ImageDescription tags."""
  6444. if not self.is_shaped:
  6445. return None
  6446. result = []
  6447. for s in self.series:
  6448. if s.kind.lower() != 'shaped':
  6449. continue
  6450. page = s.pages[0]
  6451. if (
  6452. not isinstance(page, TiffPage)
  6453. or page.shaped_description is None
  6454. ):
  6455. continue
  6456. result.append(shaped_description_metadata(page.shaped_description))
  6457. return tuple(result)
  6458. @property
  6459. def ome_metadata(self) -> str | None:
  6460. """OME XML metadata from ImageDescription tag."""
  6461. if not self.is_ome:
  6462. return None
  6463. # return xml2dict(self.pages.first.description)['OME']
  6464. if self._omexml:
  6465. return self._omexml
  6466. return self.pages.first.description
  6467. @property
  6468. def scn_metadata(self) -> str | None:
  6469. """Leica SCN XML metadata from ImageDescription tag."""
  6470. if not self.is_scn:
  6471. return None
  6472. return self.pages.first.description
  6473. @property
  6474. def philips_metadata(self) -> str | None:
  6475. """Philips DP XML metadata from ImageDescription tag."""
  6476. if not self.is_philips:
  6477. return None
  6478. return self.pages.first.description
  6479. @property
  6480. def indica_metadata(self) -> str | None:
  6481. """IndicaLabs XML metadata from ImageDescription tag."""
  6482. if not self.is_indica:
  6483. return None
  6484. return self.pages.first.description
  6485. @property
  6486. def avs_metadata(self) -> str | None:
  6487. """Argos AVS XML metadata from tag 65000."""
  6488. if not self.is_avs:
  6489. return None
  6490. return self.pages.first.tags.valueof(65000)
  6491. @property
  6492. def lsm_metadata(self) -> dict[str, Any] | None:
  6493. """LSM metadata from CZ_LSMINFO tag."""
  6494. if not self.is_lsm:
  6495. return None
  6496. return self.pages.first.tags.valueof(34412) # CZ_LSMINFO
  6497. @cached_property
  6498. def stk_metadata(self) -> dict[str, Any] | None:
  6499. """STK metadata from UIC tags."""
  6500. if not self.is_stk:
  6501. return None
  6502. page = self.pages.first
  6503. tags = page.tags
  6504. result: dict[str, Any] = {}
  6505. if page.description:
  6506. result['PlaneDescriptions'] = page.description.split('\x00')
  6507. tag = tags.get(33629) # UIC2tag
  6508. result['NumberPlanes'] = 1 if tag is None else tag.count
  6509. value = tags.valueof(33628) # UIC1tag
  6510. if value is not None:
  6511. result.update(value)
  6512. value = tags.valueof(33630) # UIC3tag
  6513. if value is not None:
  6514. result.update(value) # wavelengths
  6515. value = tags.valueof(33631) # UIC4tag
  6516. if value is not None:
  6517. result.update(value) # override UIC1 tags
  6518. uic2tag = tags.valueof(33629)
  6519. if uic2tag is not None:
  6520. result['ZDistance'] = uic2tag['ZDistance']
  6521. result['TimeCreated'] = uic2tag['TimeCreated']
  6522. result['TimeModified'] = uic2tag['TimeModified']
  6523. for key in ('Created', 'Modified'):
  6524. try:
  6525. result['Datetime' + key] = numpy.array(
  6526. [
  6527. julian_datetime(*dt)
  6528. for dt in zip(
  6529. uic2tag['Date' + key],
  6530. uic2tag['Time' + key],
  6531. strict=True,
  6532. )
  6533. ],
  6534. dtype='datetime64[ns]',
  6535. )
  6536. except Exception as exc:
  6537. result['Datetime' + key] = None
  6538. logger().warning(
  6539. f'{self!r} STK Datetime{key} raised {exc!r:.128}'
  6540. )
  6541. return result
  6542. @cached_property
  6543. def imagej_metadata(self) -> dict[str, Any] | None:
  6544. """ImageJ metadata from ImageDescription and IJMetadata tags."""
  6545. if not self.is_imagej:
  6546. return None
  6547. page = self.pages.first
  6548. if page.imagej_description is None:
  6549. return None
  6550. result = imagej_description_metadata(page.imagej_description)
  6551. value = page.tags.valueof(50839) # IJMetadata
  6552. if value is not None:
  6553. try:
  6554. result.update(value)
  6555. except TypeError:
  6556. pass
  6557. return result
  6558. @cached_property
  6559. def fluoview_metadata(self) -> dict[str, Any] | None:
  6560. """FluoView metadata from MM_Header and MM_Stamp tags."""
  6561. if not self.is_fluoview:
  6562. return None
  6563. result = {}
  6564. page = self.pages.first
  6565. value = page.tags.valueof(34361) # MM_Header
  6566. if value is not None:
  6567. result.update(value)
  6568. # TODO: read stamps from all pages
  6569. value = page.tags.valueof(34362) # MM_Stamp
  6570. if value is not None:
  6571. result['Stamp'] = value
  6572. # skip parsing image description; not reliable
  6573. # try:
  6574. # t = fluoview_description_metadata(page.image_description)
  6575. # if t is not None:
  6576. # result['ImageDescription'] = t
  6577. # except Exception as exc:
  6578. # logger().warning(
  6579. # f'{self!r} <fluoview_description_metadata> '
  6580. # f'raised {exc!r:.128}'
  6581. # )
  6582. return result
  6583. @property
  6584. def nih_metadata(self) -> dict[str, Any] | None:
  6585. """NIHImage metadata from NIHImageHeader tag."""
  6586. if not self.is_nih:
  6587. return None
  6588. return self.pages.first.tags.valueof(43314) # NIHImageHeader
  6589. @property
  6590. def fei_metadata(self) -> dict[str, Any] | None:
  6591. """FEI metadata from SFEG or HELIOS tags."""
  6592. if not self.is_fei:
  6593. return None
  6594. tags = self.pages.first.tags
  6595. result = {}
  6596. try:
  6597. result.update(tags.valueof(34680)) # FEI_SFEG
  6598. except TypeError:
  6599. pass
  6600. try:
  6601. result.update(tags.valueof(34682)) # FEI_HELIOS
  6602. except TypeError:
  6603. pass
  6604. return result
  6605. @property
  6606. def sem_metadata(self) -> dict[str, Any] | None:
  6607. """SEM metadata from CZ_SEM tag."""
  6608. if not self.is_sem:
  6609. return None
  6610. return self.pages.first.tags.valueof(34118)
  6611. @property
  6612. def sis_metadata(self) -> dict[str, Any] | None:
  6613. """Olympus SIS metadata from OlympusSIS and OlympusINI tags."""
  6614. if not self.pages.first.is_sis:
  6615. return None
  6616. tags = self.pages.first.tags
  6617. result = {}
  6618. try:
  6619. result.update(tags.valueof(33471)) # OlympusINI
  6620. except TypeError:
  6621. pass
  6622. try:
  6623. result.update(tags.valueof(33560)) # OlympusSIS
  6624. except TypeError:
  6625. pass
  6626. return result if result else None
  6627. @cached_property
  6628. def mdgel_metadata(self) -> dict[str, Any] | None:
  6629. """MD-GEL metadata from MDFileTag tags."""
  6630. if not self.is_mdgel:
  6631. return None
  6632. if 33445 in self.pages.first.tags:
  6633. tags = self.pages.first.tags
  6634. else:
  6635. page = cast(TiffPage, self.pages[1])
  6636. if 33445 in page.tags:
  6637. tags = page.tags
  6638. else:
  6639. return None
  6640. result = {}
  6641. for code in range(33445, 33453):
  6642. if code not in tags:
  6643. continue
  6644. name = TIFF.TAGS[code]
  6645. result[name[2:]] = tags.valueof(code)
  6646. return result
  6647. @property
  6648. def eer_metadata(self) -> dict[str, Any] | None:
  6649. """EER metadata from tags 65001-65009."""
  6650. if not self.is_eer:
  6651. return None
  6652. return self.pages.first.eer_tags
  6653. @property
  6654. def nuvu_metadata(self) -> dict[str, Any] | None:
  6655. """Nuvu metadata from tags >= 65000."""
  6656. if not self.is_nuvu:
  6657. return None
  6658. return self.pages.first.nuvu_tags
  6659. @property
  6660. def andor_metadata(self) -> dict[str, Any] | None:
  6661. """Andor metadata from Andor tags."""
  6662. return self.pages.first.andor_tags
  6663. @property
  6664. def epics_metadata(self) -> dict[str, Any] | None:
  6665. """EPICS metadata from areaDetector tags."""
  6666. return self.pages.first.epics_tags
  6667. @property
  6668. def tvips_metadata(self) -> dict[str, Any] | None:
  6669. """TVIPS metadata from tag."""
  6670. if not self.is_tvips:
  6671. return None
  6672. return self.pages.first.tags.valueof(37706)
  6673. @cached_property
  6674. def metaseries_metadata(self) -> dict[str, Any] | None:
  6675. """MetaSeries metadata from ImageDescription tag of first tag."""
  6676. # TODO: remove this? It is a per page property
  6677. if not self.is_metaseries:
  6678. return None
  6679. return metaseries_description_metadata(self.pages.first.description)
  6680. @cached_property
  6681. def pilatus_metadata(self) -> dict[str, Any] | None:
  6682. """Pilatus metadata from ImageDescription tag."""
  6683. if not self.is_pilatus:
  6684. return None
  6685. return pilatus_description_metadata(self.pages.first.description)
  6686. @cached_property
  6687. def micromanager_metadata(self) -> dict[str, Any] | None:
  6688. """Non-TIFF Micro-Manager metadata."""
  6689. if not self.is_micromanager:
  6690. return None
  6691. return read_micromanager_metadata(self._fh)
  6692. @cached_property
  6693. def gdal_structural_metadata(self) -> dict[str, Any] | None:
  6694. """Non-TIFF GDAL structural metadata."""
  6695. return read_gdal_structural_metadata(self._fh)
  6696. @cached_property
  6697. def scanimage_metadata(self) -> dict[str, Any] | None:
  6698. """ScanImage non-varying frame and ROI metadata.
  6699. The returned dict may contain 'FrameData', 'RoiGroups', and 'version'
  6700. keys.
  6701. Varying frame data can be found in the ImageDescription tags.
  6702. """
  6703. if not self.is_scanimage:
  6704. return None
  6705. result: dict[str, Any] = {}
  6706. try:
  6707. framedata, roidata, version = read_scanimage_metadata(self._fh)
  6708. result['version'] = version
  6709. result['FrameData'] = framedata
  6710. result.update(roidata)
  6711. except (TypeError, ValueError):
  6712. pass
  6713. return result
  6714. @property
  6715. def geotiff_metadata(self) -> dict[str, Any] | None:
  6716. """GeoTIFF metadata from tags."""
  6717. if not self.is_geotiff:
  6718. return None
  6719. return self.pages.first.geotiff_tags
  6720. @property
  6721. def gdal_metadata(self) -> dict[str, Any] | None:
  6722. """GDAL XML metadata from GDAL_METADATA tag."""
  6723. if not self.is_gdal:
  6724. return None
  6725. return self.pages.first.tags.valueof(42112)
  6726. @cached_property
  6727. def astrotiff_metadata(self) -> dict[str, Any] | None:
  6728. """AstroTIFF metadata from ImageDescription tag."""
  6729. if not self.is_astrotiff:
  6730. return None
  6731. return astrotiff_description_metadata(self.pages.first.description)
  6732. @cached_property
  6733. def streak_metadata(self) -> dict[str, Any] | None:
  6734. """Hamamatsu streak metadata from ImageDescription tag."""
  6735. if not self.is_streak:
  6736. return None
  6737. return streak_description_metadata(
  6738. self.pages.first.description, self.filehandle
  6739. )
  6740. @final
  6741. class TiffFormat:
  6742. """TIFF format properties."""
  6743. __slots__ = (
  6744. '_hash',
  6745. 'byteorder',
  6746. 'offsetformat',
  6747. 'offsetsize',
  6748. 'tagformat1',
  6749. 'tagformat2',
  6750. 'tagnoformat',
  6751. 'tagnosize',
  6752. 'tagoffsetthreshold',
  6753. 'tagsize',
  6754. 'version',
  6755. )
  6756. version: int
  6757. """Version of TIFF header."""
  6758. byteorder: Literal['>', '<']
  6759. """Byteorder of TIFF header."""
  6760. offsetsize: int
  6761. """Size of offsets."""
  6762. offsetformat: str
  6763. """Struct format for offset values."""
  6764. tagnosize: int
  6765. """Size of `tagnoformat`."""
  6766. tagnoformat: str
  6767. """Struct format for number of TIFF tags."""
  6768. tagsize: int
  6769. """Size of `tagformat1` and `tagformat2`."""
  6770. tagformat1: str
  6771. """Struct format for code and dtype of TIFF tag."""
  6772. tagformat2: str
  6773. """Struct format for count and value of TIFF tag."""
  6774. tagoffsetthreshold: int
  6775. """Size of inline tag values."""
  6776. _hash: int
  6777. def __init__(
  6778. self,
  6779. version: int,
  6780. byteorder: Literal['>', '<'],
  6781. offsetsize: int,
  6782. offsetformat: str,
  6783. tagnosize: int,
  6784. tagnoformat: str,
  6785. tagsize: int,
  6786. tagformat1: str,
  6787. tagformat2: str,
  6788. tagoffsetthreshold: int,
  6789. ) -> None:
  6790. self.version = version
  6791. self.byteorder = byteorder
  6792. self.offsetsize = offsetsize
  6793. self.offsetformat = offsetformat
  6794. self.tagnosize = tagnosize
  6795. self.tagnoformat = tagnoformat
  6796. self.tagsize = tagsize
  6797. self.tagformat1 = tagformat1
  6798. self.tagformat2 = tagformat2
  6799. self.tagoffsetthreshold = tagoffsetthreshold
  6800. self._hash = hash((version, byteorder, offsetsize))
  6801. @property
  6802. def is_bigtiff(self) -> bool:
  6803. """Format is 64-bit BigTIFF."""
  6804. return self.version == 43
  6805. @property
  6806. def is_ndpi(self) -> bool:
  6807. """Format is 32-bit TIFF with 64-bit offsets used by NDPI."""
  6808. return self.version == 42 and self.offsetsize == 8
  6809. def __hash__(self) -> int:
  6810. return self._hash
  6811. def __repr__(self) -> str:
  6812. bits = '32' if self.version == 42 else '64'
  6813. endian = 'little' if self.byteorder == '<' else 'big'
  6814. ndpi = ' with 64-bit offsets' if self.is_ndpi else ''
  6815. return f'<tifffile.TiffFormat {bits}-bit {endian}-endian{ndpi}>'
  6816. def __str__(self) -> str:
  6817. return indent(
  6818. repr(self),
  6819. *(
  6820. f'{attr}: {getattr(self, attr)!r}'
  6821. for attr in TiffFormat.__slots__
  6822. ),
  6823. )
  6824. @final
  6825. class TiffPage:
  6826. """TIFF image file directory (IFD).
  6827. TiffPage instances are not thread-safe. All attributes are read-only.
  6828. Parameters:
  6829. parent:
  6830. TiffFile instance to read page from.
  6831. The file handle position must be at an offset to an IFD structure.
  6832. index:
  6833. Index of page in IFD tree.
  6834. keyframe:
  6835. Not used.
  6836. Raises:
  6837. TiffFileError: Invalid TIFF structure.
  6838. """
  6839. # instance attributes
  6840. tags: TiffTags
  6841. """Tags belonging to page."""
  6842. parent: TiffFile
  6843. """TiffFile instance page belongs to."""
  6844. offset: int
  6845. """Position of page in file."""
  6846. shape: tuple[int, ...]
  6847. """Shape of image array in page."""
  6848. dtype: numpy.dtype[Any] | None
  6849. """Data type of image array in page."""
  6850. shaped: tuple[int, int, int, int, int]
  6851. """Normalized 5-dimensional shape of image array in page:
  6852. 0. separate samplesperpixel or 1.
  6853. 1. imagedepth or 1.
  6854. 2. imagelength.
  6855. 3. imagewidth.
  6856. 4. contig samplesperpixel or 1.
  6857. """
  6858. axes: str
  6859. """Character codes for dimensions in image array:
  6860. 'S' sample, 'X' width, 'Y' length, 'Z' depth.
  6861. """
  6862. dataoffsets: tuple[int, ...]
  6863. """Positions of strips or tiles in file."""
  6864. databytecounts: tuple[int, ...]
  6865. """Size of strips or tiles in file."""
  6866. _dtype: numpy.dtype[Any] | None
  6867. _index: tuple[int, ...] # index of page in IFD tree
  6868. # default properties; might be updated from tags
  6869. subfiletype: int = 0
  6870. """:py:class:`FILETYPE` kind of image."""
  6871. imagewidth: int = 0
  6872. """Number of columns (pixels per row) in image."""
  6873. imagelength: int = 0
  6874. """Number of rows in image."""
  6875. imagedepth: int = 1
  6876. """Number of Z slices in image."""
  6877. tilewidth: int = 0
  6878. """Number of columns in each tile."""
  6879. tilelength: int = 0
  6880. """Number of rows in each tile."""
  6881. tiledepth: int = 1
  6882. """Number of Z slices in each tile."""
  6883. samplesperpixel: int = 1
  6884. """Number of components per pixel."""
  6885. bitspersample: int = 1
  6886. """Number of bits per pixel component."""
  6887. sampleformat: int = 1
  6888. """:py:class:`SAMPLEFORMAT` type of pixel components."""
  6889. rowsperstrip: int = 2**32 - 1
  6890. """Number of rows per strip."""
  6891. compression: int = 1
  6892. """:py:class:`COMPRESSION` scheme used on image data."""
  6893. planarconfig: int = 1
  6894. """:py:class:`PLANARCONFIG` type of storage of components in pixel."""
  6895. fillorder: int = 1
  6896. """Logical order of bits within byte of image data."""
  6897. photometric: int = 0
  6898. """:py:class:`PHOTOMETRIC` color space of image."""
  6899. predictor: int = 1
  6900. """:py:class:`PREDICTOR` applied to image data before compression."""
  6901. extrasamples: tuple[int, ...] = ()
  6902. """:py:class:`EXTRASAMPLE` interpretation of extra components in pixel."""
  6903. subsampling: tuple[int, int] | None = None
  6904. """Subsampling factors used for chrominance components."""
  6905. subifds: tuple[int, ...] | None = None
  6906. """Positions of SubIFDs in file."""
  6907. jpegtables: bytes | None = None
  6908. """JPEG quantization and Huffman tables."""
  6909. jpegheader: bytes | None = None
  6910. """JPEG header for NDPI."""
  6911. software: str = ''
  6912. """Software used to create image."""
  6913. description: str = ''
  6914. """Subject of image."""
  6915. description1: str = ''
  6916. """Value of second ImageDescription tag."""
  6917. nodata: float = 0
  6918. """Value used for missing data. The value of the GDAL_NODATA tag or 0."""
  6919. def __init__(
  6920. self,
  6921. parent: TiffFile,
  6922. /,
  6923. index: int | Sequence[int],
  6924. *,
  6925. keyframe: TiffPage | None = None,
  6926. ) -> None:
  6927. tag: TiffTag | None
  6928. tiff = parent.tiff
  6929. self.parent = parent
  6930. self.shape = ()
  6931. self.shaped = (0, 0, 0, 0, 0)
  6932. self.dtype = self._dtype = None
  6933. self.axes = ''
  6934. self.tags = tags = TiffTags()
  6935. self.dataoffsets = ()
  6936. self.databytecounts = ()
  6937. if isinstance(index, int):
  6938. self._index = (index,)
  6939. else:
  6940. self._index = tuple(index)
  6941. # read IFD structure and its tags from file
  6942. fh = parent.filehandle
  6943. self.offset = fh.tell() # offset to this IFD
  6944. try:
  6945. tagno: int = struct.unpack(
  6946. tiff.tagnoformat, fh.read(tiff.tagnosize)
  6947. )[0]
  6948. if tagno > 4096:
  6949. raise ValueError(f'suspicious number of tags {tagno}')
  6950. except Exception as exc:
  6951. raise TiffFileError(f'corrupted tag list @{self.offset}') from exc
  6952. tagoffset = self.offset + tiff.tagnosize # fh.tell()
  6953. tagsize = tagsize_ = tiff.tagsize
  6954. data = fh.read(tagsize * tagno)
  6955. if len(data) != tagsize * tagno:
  6956. raise TiffFileError('corrupted IFD structure')
  6957. if tiff.is_ndpi:
  6958. # patch offsets/values for 64-bit NDPI file
  6959. tagsize = 16
  6960. fh.seek(8, os.SEEK_CUR)
  6961. ext = fh.read(4 * tagno) # high bits
  6962. data = b''.join(
  6963. data[i * 12 : i * 12 + 12] + ext[i * 4 : i * 4 + 4]
  6964. for i in range(tagno)
  6965. )
  6966. tagindex = -tagsize
  6967. for i in range(tagno):
  6968. tagindex += tagsize
  6969. tagdata = data[tagindex : tagindex + tagsize]
  6970. try:
  6971. tag = TiffTag.fromfile(
  6972. parent, offset=tagoffset + i * tagsize_, header=tagdata
  6973. )
  6974. except TiffFileError as exc:
  6975. logger().error(f'<TiffTag.fromfile> raised {exc!r:.128}')
  6976. continue
  6977. tags.add(tag)
  6978. if not tags:
  6979. return # found in FIBICS
  6980. for code, name in TIFF.TAG_ATTRIBUTES.items():
  6981. value = tags.valueof(code)
  6982. if value is None:
  6983. continue
  6984. if code in {270, 305} and not isinstance(value, str):
  6985. # wrong string type for software or description
  6986. continue
  6987. setattr(self, name, value)
  6988. value = tags.valueof(270, index=1)
  6989. if isinstance(value, str):
  6990. self.description1 = value
  6991. if self.subfiletype == 0:
  6992. value = tags.valueof(255) # SubfileType
  6993. if value == 2:
  6994. self.subfiletype = 0b1 # reduced image
  6995. elif value == 3:
  6996. self.subfiletype = 0b10 # multi-page
  6997. elif not isinstance(self.subfiletype, int):
  6998. # files created by IDEAS
  6999. logger().warning(f'{self!r} invalid {self.subfiletype=}')
  7000. self.subfiletype = 0
  7001. # consolidate private tags; remove them from self.tags
  7002. # if self.is_andor:
  7003. # self.andor_tags
  7004. # elif self.is_epics:
  7005. # self.epics_tags
  7006. # elif self.is_ndpi:
  7007. # self.ndpi_tags
  7008. # if self.is_sis and 34853 in tags:
  7009. # # TODO: cannot change tag.name
  7010. # tags[34853].name = 'OlympusSIS2'
  7011. # dataoffsets and databytecounts
  7012. # TileOffsets
  7013. self.dataoffsets = tags.valueof(324)
  7014. if self.dataoffsets is None:
  7015. # StripOffsets
  7016. self.dataoffsets = tags.valueof(273)
  7017. if self.dataoffsets is None:
  7018. # JPEGInterchangeFormat et al.
  7019. self.dataoffsets = tags.valueof(513)
  7020. if self.dataoffsets is None:
  7021. self.dataoffsets = ()
  7022. logger().error(f'{self!r} missing data offset tag')
  7023. # TileByteCounts
  7024. self.databytecounts = tags.valueof(325)
  7025. if self.databytecounts is None:
  7026. # StripByteCounts
  7027. self.databytecounts = tags.valueof(279)
  7028. if self.databytecounts is None:
  7029. # JPEGInterchangeFormatLength et al.
  7030. self.databytecounts = tags.valueof(514)
  7031. if (
  7032. self.imagewidth == 0
  7033. and self.imagelength == 0
  7034. and self.dataoffsets
  7035. and self.databytecounts
  7036. ):
  7037. # dimensions may be missing in some RAW formats
  7038. # read dimensions from assumed JPEG encoded segment
  7039. try:
  7040. fh.seek(self.dataoffsets[0])
  7041. (
  7042. precision,
  7043. imagelength,
  7044. imagewidth,
  7045. samplesperpixel,
  7046. ) = jpeg_shape(fh.read(min(self.databytecounts[0], 4096)))
  7047. except Exception: # noqa: S110
  7048. pass
  7049. else:
  7050. self.imagelength = imagelength
  7051. self.imagewidth = imagewidth
  7052. self.samplesperpixel = samplesperpixel
  7053. if 258 not in tags:
  7054. self.bitspersample = 8 if precision <= 8 else 16
  7055. if 262 not in tags and samplesperpixel == 3:
  7056. self.photometric = PHOTOMETRIC.YCBCR
  7057. if 259 not in tags:
  7058. self.compression = COMPRESSION.OJPEG
  7059. if 278 not in tags:
  7060. self.rowsperstrip = imagelength
  7061. elif self.compression == 6:
  7062. # OJPEG hack. See libtiff v4.2.0 tif_dirread.c#L4082
  7063. if 262 not in tags:
  7064. # PhotometricInterpretation missing
  7065. self.photometric = PHOTOMETRIC.YCBCR
  7066. elif self.photometric == 2:
  7067. # RGB -> YCbCr
  7068. self.photometric = PHOTOMETRIC.YCBCR
  7069. if 258 not in tags:
  7070. # BitsPerSample missing
  7071. self.bitspersample = 8
  7072. if 277 not in tags and self.photometric in {0, 1, 2, 6}:
  7073. # SamplesPerPixel missing
  7074. self.samplesperpixel = 3
  7075. elif self.is_lsm or (self.index != 0 and self.parent.is_lsm):
  7076. # correct non standard LSM bitspersample tags
  7077. tags[258]._fix_lsm_bitspersample()
  7078. if self.compression == 1 and self.predictor != 1:
  7079. # work around bug in LSM510 software
  7080. self.predictor = PREDICTOR.NONE
  7081. elif self.is_vista or (self.index != 0 and self.parent.is_vista):
  7082. # ISS Vista writes wrong ImageDepth tag
  7083. self.imagedepth = 1
  7084. elif self.is_philips or (self.index != 0 and self.parent.is_philips):
  7085. # Philips (DP v1.1) writes wrong ImageDepth and TileDepth tags
  7086. self.imagedepth = 1
  7087. self.tiledepth = 1
  7088. elif self.is_stk:
  7089. # read UIC1tag again now that plane count is known
  7090. tag = tags.get(33628) # UIC1tag
  7091. assert tag is not None
  7092. fh.seek(tag.valueoffset)
  7093. uic2tag = tags.get(33629) # UIC2tag
  7094. try:
  7095. tag.value = read_uic1tag(
  7096. fh,
  7097. tiff.byteorder,
  7098. tag.dtype,
  7099. tag.count,
  7100. 0,
  7101. planecount=uic2tag.count if uic2tag is not None else 1,
  7102. )
  7103. except Exception as exc:
  7104. logger().warning(
  7105. f'{self!r} <tifffile.read_uic1tag> raised {exc!r:.128}'
  7106. )
  7107. elif parent._superres and self.compression in {65000, 65001, 65002}:
  7108. horzbits = vertbits = 2
  7109. if self.compression == 65002:
  7110. horzbits = int(self.tags.valueof(65008, 2))
  7111. vertbits = int(self.tags.valueof(65009, 2))
  7112. self.imagewidth *= 2 ** (min(horzbits, parent._superres))
  7113. self.imagelength *= 2 ** (min(vertbits, parent._superres))
  7114. self.rowsperstrip *= 2 ** (min(vertbits, parent._superres))
  7115. tag = tags.get(50839)
  7116. if tag is not None:
  7117. # decode IJMetadata tag
  7118. try:
  7119. tag.value = imagej_metadata(
  7120. tag.value,
  7121. tags[50838].value, # IJMetadataByteCounts
  7122. tiff.byteorder,
  7123. )
  7124. except Exception as exc:
  7125. logger().warning(
  7126. f'{self!r} <tifffile.imagej_metadata> raised {exc!r:.128}'
  7127. )
  7128. # BitsPerSample
  7129. value = tags.valueof(258)
  7130. if value is not None:
  7131. if self.bitspersample != 1:
  7132. pass # bitspersample was set by ojpeg hack
  7133. elif tags[258].count == 1:
  7134. self.bitspersample = int(value)
  7135. else:
  7136. # LSM might list more items than samplesperpixel
  7137. value = value[: self.samplesperpixel]
  7138. if any(v - value[0] for v in value):
  7139. self.bitspersample = value
  7140. else:
  7141. self.bitspersample = int(value[0])
  7142. # SampleFormat
  7143. value = tags.valueof(339)
  7144. if value is not None:
  7145. if tags[339].count == 1:
  7146. try:
  7147. self.sampleformat = SAMPLEFORMAT(value)
  7148. except ValueError:
  7149. self.sampleformat = int(value)
  7150. else:
  7151. value = value[: self.samplesperpixel]
  7152. if any(v - value[0] for v in value):
  7153. try:
  7154. self.sampleformat = SAMPLEFORMAT(value)
  7155. except ValueError:
  7156. self.sampleformat = int(value)
  7157. else:
  7158. try:
  7159. self.sampleformat = SAMPLEFORMAT(value[0])
  7160. except ValueError:
  7161. self.sampleformat = int(value[0])
  7162. elif self.bitspersample == 32 and (
  7163. self.is_indica or (self.index != 0 and self.parent.is_indica)
  7164. ):
  7165. # IndicaLabsImageWriter does not write SampleFormat tag
  7166. self.sampleformat = SAMPLEFORMAT.IEEEFP
  7167. if 322 in tags: # TileWidth
  7168. self.rowsperstrip = 0
  7169. elif 257 in tags: # ImageLength
  7170. if 278 not in tags or tags[278].count > 1: # RowsPerStrip
  7171. self.rowsperstrip = self.imagelength
  7172. self.rowsperstrip = min(self.rowsperstrip, self.imagelength)
  7173. # self.stripsperimage = math.floor(
  7174. # float(self.imagelength + self.rowsperstrip - 1) /
  7175. # self.rowsperstrip)
  7176. # determine dtype
  7177. dtypestr = TIFF.SAMPLE_DTYPES.get(
  7178. (self.sampleformat, self.bitspersample), None
  7179. )
  7180. dtype = numpy.dtype(dtypestr) if dtypestr is not None else None
  7181. self.dtype = self._dtype = dtype
  7182. # determine shape of data
  7183. imagelength = self.imagelength
  7184. imagewidth = self.imagewidth
  7185. imagedepth = self.imagedepth
  7186. samplesperpixel = self.samplesperpixel
  7187. if self.photometric == 2 or samplesperpixel > 1: # PHOTOMETRIC.RGB
  7188. if self.planarconfig == 1:
  7189. self.shaped = (
  7190. 1,
  7191. imagedepth,
  7192. imagelength,
  7193. imagewidth,
  7194. samplesperpixel,
  7195. )
  7196. if imagedepth == 1:
  7197. self.shape = (imagelength, imagewidth, samplesperpixel)
  7198. self.axes = 'YXS'
  7199. else:
  7200. self.shape = (
  7201. imagedepth,
  7202. imagelength,
  7203. imagewidth,
  7204. samplesperpixel,
  7205. )
  7206. self.axes = 'ZYXS'
  7207. else:
  7208. self.shaped = (
  7209. samplesperpixel,
  7210. imagedepth,
  7211. imagelength,
  7212. imagewidth,
  7213. 1,
  7214. )
  7215. if imagedepth == 1:
  7216. self.shape = (samplesperpixel, imagelength, imagewidth)
  7217. self.axes = 'SYX'
  7218. else:
  7219. self.shape = (
  7220. samplesperpixel,
  7221. imagedepth,
  7222. imagelength,
  7223. imagewidth,
  7224. )
  7225. self.axes = 'SZYX'
  7226. else:
  7227. self.shaped = (1, imagedepth, imagelength, imagewidth, 1)
  7228. if imagedepth == 1:
  7229. self.shape = (imagelength, imagewidth)
  7230. self.axes = 'YX'
  7231. else:
  7232. self.shape = (imagedepth, imagelength, imagewidth)
  7233. self.axes = 'ZYX'
  7234. if not self.databytecounts:
  7235. self.databytecounts = (
  7236. product(self.shape) * (self.bitspersample // 8),
  7237. )
  7238. if self.compression != 1:
  7239. logger().error(f'{self!r} missing ByteCounts tag')
  7240. if imagelength and self.rowsperstrip and not self.is_lsm:
  7241. # fix incorrect number of strip bytecounts and offsets
  7242. maxstrips = (
  7243. int(
  7244. math.floor(imagelength + self.rowsperstrip - 1)
  7245. / self.rowsperstrip
  7246. )
  7247. * self.imagedepth
  7248. )
  7249. if self.planarconfig == 2:
  7250. maxstrips *= self.samplesperpixel
  7251. if maxstrips != len(self.databytecounts):
  7252. logger().error(
  7253. f'{self!r} incorrect StripByteCounts count '
  7254. f'({len(self.databytecounts)} != {maxstrips})'
  7255. )
  7256. self.databytecounts = self.databytecounts[:maxstrips]
  7257. if maxstrips != len(self.dataoffsets):
  7258. logger().error(
  7259. f'{self!r} incorrect StripOffsets count '
  7260. f'({len(self.dataoffsets)} != {maxstrips})'
  7261. )
  7262. self.dataoffsets = self.dataoffsets[:maxstrips]
  7263. value = tags.valueof(42113) # GDAL_NODATA
  7264. if value is not None and dtype is not None:
  7265. try:
  7266. pytype = type(dtype.type(0).item())
  7267. value = value.replace(',', '.') # comma decimal separator
  7268. self.nodata = pytype(value)
  7269. if not numpy.can_cast(
  7270. numpy.min_scalar_type(self.nodata), dtype
  7271. ):
  7272. raise ValueError(
  7273. f'{self.nodata} is not castable to {dtype}'
  7274. )
  7275. except Exception as exc:
  7276. logger().warning(
  7277. f'{self!r} parsing GDAL_NODATA tag raised {exc!r:.128}'
  7278. )
  7279. self.nodata = 0
  7280. mcustarts = tags.valueof(65426)
  7281. if mcustarts is not None and self.is_ndpi:
  7282. # use NDPI JPEG McuStarts as tile offsets
  7283. mcustarts = mcustarts.astype(numpy.int64)
  7284. high = tags.valueof(65432)
  7285. if high is not None:
  7286. # McuStartsHighBytes
  7287. high = high.astype(numpy.uint64)
  7288. high <<= 32
  7289. mcustarts += high.astype(numpy.int64)
  7290. fh.seek(self.dataoffsets[0])
  7291. jpegheader = fh.read(mcustarts[0])
  7292. try:
  7293. (
  7294. self.tilelength,
  7295. self.tilewidth,
  7296. self.jpegheader,
  7297. ) = ndpi_jpeg_tile(jpegheader)
  7298. except ValueError as exc:
  7299. logger().warning(
  7300. f'{self!r} <tifffile.ndpi_jpeg_tile> raised {exc!r:.128}'
  7301. )
  7302. else:
  7303. # TODO: optimize tuple(ndarray.tolist())
  7304. databytecounts = numpy.diff(
  7305. mcustarts, append=self.databytecounts[0]
  7306. )
  7307. self.databytecounts = tuple(databytecounts.tolist())
  7308. mcustarts += self.dataoffsets[0]
  7309. self.dataoffsets = tuple(mcustarts.tolist())
  7310. @cached_property
  7311. def decode(
  7312. self,
  7313. ) -> Callable[
  7314. ...,
  7315. tuple[
  7316. NDArray[Any] | None,
  7317. tuple[int, int, int, int, int],
  7318. tuple[int, int, int, int],
  7319. ],
  7320. ]:
  7321. """Return decoded segment, its shape, and indices in image.
  7322. The decode function is implemented as a closure and has the following
  7323. signature:
  7324. Parameters:
  7325. data (Union[bytes, None]):
  7326. Encoded bytes of segment (strip or tile) or None for empty
  7327. segments.
  7328. index (int):
  7329. Index of segment in Offsets and Bytecount tag values.
  7330. jpegtables (Optional[bytes]):
  7331. For JPEG compressed segments only, value of JPEGTables tag
  7332. if any.
  7333. Returns:
  7334. - Decoded segment or None for empty segments.
  7335. - Position of segment in image array of normalized shape
  7336. (separate sample, depth, length, width, contig sample).
  7337. - Shape of segment (depth, length, width, contig samples).
  7338. The shape of strips depends on their linear index.
  7339. Raises:
  7340. ValueError or NotImplementedError:
  7341. Decoding is not supported.
  7342. TiffFileError:
  7343. Invalid TIFF structure.
  7344. """
  7345. if self.hash in self.parent._parent._decoders:
  7346. return self.parent._parent._decoders[self.hash]
  7347. def cache(decode, /):
  7348. self.parent._parent._decoders[self.hash] = decode
  7349. return decode
  7350. if self.dtype is None or self._dtype is None:
  7351. def decode_raise_dtype(*args, **kwargs):
  7352. raise ValueError(
  7353. 'data type not supported '
  7354. f'(SampleFormat {self.sampleformat}, '
  7355. f'{self.bitspersample}-bit)'
  7356. )
  7357. return cache(decode_raise_dtype)
  7358. if 0 in self.shaped:
  7359. def decode_raise_empty(*args, **kwargs):
  7360. raise ValueError('empty image')
  7361. return cache(decode_raise_empty)
  7362. decompress: Callable[..., Any] | None
  7363. try:
  7364. if self.compression == 1:
  7365. decompress = None
  7366. else:
  7367. decompress = TIFF.DECOMPRESSORS[self.compression]
  7368. if (
  7369. self.compression in {65000, 65001, 65002}
  7370. and not self.parent.is_eer
  7371. ):
  7372. raise KeyError(self.compression)
  7373. except KeyError as exc:
  7374. def decode_raise_compression(*args, exc=str(exc)[1:-1], **kwargs):
  7375. raise ValueError(f'{exc}')
  7376. return cache(decode_raise_compression)
  7377. try:
  7378. if self.predictor == 1:
  7379. unpredict = None
  7380. else:
  7381. unpredict = TIFF.UNPREDICTORS[self.predictor]
  7382. except KeyError as exc:
  7383. if self.compression in TIFF.IMAGE_COMPRESSIONS:
  7384. logger().warning(
  7385. f'{self!r} ignoring predictor {self.predictor}'
  7386. )
  7387. unpredict = None
  7388. else:
  7389. def decode_raise_predictor(
  7390. *args, exc=str(exc)[1:-1], **kwargs
  7391. ):
  7392. raise ValueError(f'{exc}')
  7393. return cache(decode_raise_predictor)
  7394. if self.tags.get(339) is not None:
  7395. tag = self.tags[339] # SampleFormat
  7396. if tag.count != 1 and any(i - tag.value[0] for i in tag.value):
  7397. def decode_raise_sampleformat(*args, **kwargs):
  7398. raise ValueError(
  7399. f'sample formats do not match {tag.value}'
  7400. )
  7401. return cache(decode_raise_sampleformat)
  7402. if self.is_subsampled and (
  7403. self.compression not in {6, 7, 34892, 33007}
  7404. or self.planarconfig == 2
  7405. ):
  7406. def decode_raise_subsampling(*args, **kwargs):
  7407. raise NotImplementedError(
  7408. 'chroma subsampling not supported without JPEG compression'
  7409. )
  7410. return cache(decode_raise_subsampling)
  7411. if self.compression == 50001 and self.samplesperpixel == 4:
  7412. # WebP segments may be missing all-opaque alpha channel
  7413. def decompress_webp_rgba(data, out=None, **kwargs):
  7414. return imagecodecs.webp_decode(data, hasalpha=True, out=out)
  7415. decompress = decompress_webp_rgba
  7416. # normalize segments shape to [depth, length, width, contig]
  7417. if self.is_tiled:
  7418. stshape = (
  7419. self.tiledepth,
  7420. self.tilelength,
  7421. self.tilewidth,
  7422. self.samplesperpixel if self.planarconfig == 1 else 1,
  7423. )
  7424. else:
  7425. stshape = (
  7426. 1,
  7427. self.rowsperstrip,
  7428. self.imagewidth,
  7429. self.samplesperpixel if self.planarconfig == 1 else 1,
  7430. )
  7431. stdepth, stlength, stwidth, samples = stshape
  7432. _, imdepth, imlength, imwidth, samples = self.shaped
  7433. if self.is_tiled:
  7434. width = (imwidth + stwidth - 1) // stwidth
  7435. length = (imlength + stlength - 1) // stlength
  7436. depth = (imdepth + stdepth - 1) // stdepth
  7437. def indices(
  7438. segmentindex: int, /
  7439. ) -> tuple[
  7440. tuple[int, int, int, int, int], tuple[int, int, int, int]
  7441. ]:
  7442. # return indices and shape of tile in image array
  7443. return (
  7444. (
  7445. segmentindex // (width * length * depth),
  7446. (segmentindex // (width * length)) % depth * stdepth,
  7447. (segmentindex // width) % length * stlength,
  7448. segmentindex % width * stwidth,
  7449. 0,
  7450. ),
  7451. stshape,
  7452. )
  7453. def reshape(
  7454. data: NDArray[Any],
  7455. indices: tuple[int, int, int, int, int],
  7456. shape: tuple[int, int, int, int],
  7457. /,
  7458. ) -> NDArray[Any]:
  7459. # return reshaped tile or raise TiffFileError
  7460. size = shape[0] * shape[1] * shape[2] * shape[3]
  7461. if data.ndim == 1 and data.size > size:
  7462. # decompression / unpacking might return too many bytes
  7463. data = data[:size]
  7464. if data.size == size:
  7465. # complete tile
  7466. # data might be non-contiguous; cannot reshape inplace
  7467. return data.reshape(shape)
  7468. try:
  7469. # data fills remaining space
  7470. # found in JPEG/PNG compressed tiles
  7471. return data.reshape(
  7472. (
  7473. min(imdepth - indices[1], shape[0]),
  7474. min(imlength - indices[2], shape[1]),
  7475. min(imwidth - indices[3], shape[2]),
  7476. samples,
  7477. )
  7478. )
  7479. except ValueError:
  7480. pass
  7481. try:
  7482. # data fills remaining horizontal space
  7483. # found in tiled GeoTIFF
  7484. return data.reshape(
  7485. (
  7486. min(imdepth - indices[1], shape[0]),
  7487. min(imlength - indices[2], shape[1]),
  7488. shape[2],
  7489. samples,
  7490. )
  7491. )
  7492. except ValueError:
  7493. pass
  7494. raise TiffFileError(
  7495. f'corrupted tile @ {indices} cannot be reshaped from '
  7496. f'{data.shape} to {shape}'
  7497. )
  7498. def pad(
  7499. data: NDArray[Any], shape: tuple[int, int, int, int], /
  7500. ) -> tuple[NDArray[Any], tuple[int, int, int, int]]:
  7501. # pad tile to shape
  7502. if data.shape == shape:
  7503. return data, shape
  7504. padwidth = [
  7505. (0, i - j) for i, j in zip(shape, data.shape, strict=True)
  7506. ]
  7507. data = numpy.pad(data, padwidth, constant_values=self.nodata)
  7508. return data, shape
  7509. def pad_none(
  7510. shape: tuple[int, int, int, int], /
  7511. ) -> tuple[int, int, int, int]:
  7512. # return shape of tile
  7513. return shape
  7514. else:
  7515. # strips
  7516. length = (imlength + stlength - 1) // stlength
  7517. def indices(
  7518. segmentindex: int, /
  7519. ) -> tuple[
  7520. tuple[int, int, int, int, int], tuple[int, int, int, int]
  7521. ]:
  7522. # return indices and shape of strip in image array
  7523. indices = (
  7524. segmentindex // (length * imdepth),
  7525. (segmentindex // length) % imdepth * stdepth,
  7526. segmentindex % length * stlength,
  7527. 0,
  7528. 0,
  7529. )
  7530. shape = (
  7531. stdepth,
  7532. min(stlength, imlength - indices[2]),
  7533. stwidth,
  7534. samples,
  7535. )
  7536. return indices, shape
  7537. def reshape(
  7538. data: NDArray[Any],
  7539. indices: tuple[int, int, int, int, int],
  7540. shape: tuple[int, int, int, int],
  7541. /,
  7542. ) -> NDArray[Any]:
  7543. # return reshaped strip or raise TiffFileError
  7544. size = shape[0] * shape[1] * shape[2] * shape[3]
  7545. if data.ndim == 1 and data.size > size:
  7546. # decompression / unpacking might return too many bytes
  7547. data = data[:size]
  7548. if data.size == size:
  7549. # expected size
  7550. try:
  7551. data.shape = shape
  7552. except AttributeError:
  7553. # incompatible shape for in-place modification
  7554. # decoder returned non-contiguous array
  7555. data = data.reshape(shape)
  7556. return data
  7557. datashape = data.shape
  7558. try:
  7559. # too many rows?
  7560. data.shape = shape[0], -1, shape[2], shape[3]
  7561. data = data[:, : shape[1]]
  7562. data.shape = shape
  7563. except ValueError:
  7564. pass
  7565. else:
  7566. return data
  7567. raise TiffFileError(
  7568. 'corrupted strip cannot be reshaped from '
  7569. f'{datashape} to {shape}'
  7570. )
  7571. def pad(
  7572. data: NDArray[Any], shape: tuple[int, int, int, int], /
  7573. ) -> tuple[NDArray[Any], tuple[int, int, int, int]]:
  7574. # pad strip length to rowsperstrip
  7575. shape = (shape[0], stlength, shape[2], shape[3])
  7576. if data.shape == shape:
  7577. return data, shape
  7578. padwidth = [
  7579. (0, 0),
  7580. (0, stlength - data.shape[1]),
  7581. (0, 0),
  7582. (0, 0),
  7583. ]
  7584. data = numpy.pad(data, padwidth, constant_values=self.nodata)
  7585. return data, shape
  7586. def pad_none(
  7587. shape: tuple[int, int, int, int], /
  7588. ) -> tuple[int, int, int, int]:
  7589. # return shape of strip
  7590. return (shape[0], stlength, shape[2], shape[3])
  7591. if self.compression in {6, 7, 34892, 33007}:
  7592. # JPEG needs special handling
  7593. if self.fillorder == 2:
  7594. logger().debug(f'{self!r} disabling LSB2MSB for JPEG')
  7595. if unpredict:
  7596. logger().debug(f'{self!r} disabling predictor for JPEG')
  7597. if 28672 in self.tags: # SonyRawFileType
  7598. logger().warning(
  7599. f'{self!r} SonyRawFileType might need additional '
  7600. 'unpacking (see issue #95)'
  7601. )
  7602. # 0 = Sony Uncompressed 14-bit RAW
  7603. # 1 = Sony Uncompressed 12-bit RAW
  7604. # 2 = Sony Compressed RAW
  7605. # 3 = Sony Lossless Compressed RAW
  7606. # 4 = Sony Lossless Compressed RAW 2
  7607. colorspace, outcolorspace = jpeg_decode_colorspace(
  7608. self.photometric,
  7609. self.planarconfig,
  7610. self.extrasamples,
  7611. self.is_jfif,
  7612. )
  7613. def decode_jpeg(
  7614. data: bytes | None,
  7615. index: int,
  7616. /,
  7617. *,
  7618. jpegtables: bytes | None = None,
  7619. jpegheader: bytes | None = None,
  7620. _fullsize: bool = False,
  7621. ) -> tuple[
  7622. NDArray[Any] | None,
  7623. tuple[int, int, int, int, int],
  7624. tuple[int, int, int, int],
  7625. ]:
  7626. # return decoded segment, its shape, and indices in image
  7627. segmentindex, shape = indices(index)
  7628. if data is None:
  7629. if _fullsize:
  7630. shape = pad_none(shape)
  7631. return data, segmentindex, shape
  7632. data_array: NDArray[Any] = imagecodecs.jpeg_decode(
  7633. data,
  7634. bitspersample=self.bitspersample,
  7635. tables=jpegtables,
  7636. header=jpegheader,
  7637. colorspace=colorspace,
  7638. outcolorspace=outcolorspace,
  7639. shape=shape[1:3],
  7640. )
  7641. data_array = reshape(data_array, segmentindex, shape)
  7642. if _fullsize:
  7643. data_array, shape = pad(data_array, shape)
  7644. return data_array, segmentindex, shape
  7645. return cache(decode_jpeg)
  7646. if self.compression in {65000, 65001, 65002}:
  7647. # EER decoder requires shape and extra args
  7648. horzbits = vertbits = 2
  7649. if self.compression == 65002:
  7650. skipbits = int(self.tags.valueof(65007, 7))
  7651. horzbits = int(self.tags.valueof(65008, 2))
  7652. vertbits = int(self.tags.valueof(65009, 2))
  7653. elif self.compression == 65001:
  7654. skipbits = 7
  7655. else:
  7656. skipbits = 8
  7657. superres = self.parent._superres
  7658. def decode_eer(
  7659. data: bytes | None,
  7660. index: int,
  7661. /,
  7662. *,
  7663. jpegtables: bytes | None = None,
  7664. jpegheader: bytes | None = None,
  7665. _fullsize: bool = False,
  7666. ) -> tuple[
  7667. NDArray[Any] | None,
  7668. tuple[int, int, int, int, int],
  7669. tuple[int, int, int, int],
  7670. ]:
  7671. # return decoded eer segment, its shape, and indices in image
  7672. segmentindex, shape = indices(index)
  7673. if data is None:
  7674. if _fullsize:
  7675. shape = pad_none(shape)
  7676. return data, segmentindex, shape
  7677. data_array = imagecodecs.eer_decode(
  7678. data,
  7679. shape[1:3],
  7680. skipbits,
  7681. horzbits,
  7682. vertbits,
  7683. superres=superres,
  7684. )
  7685. return data_array.reshape(shape), segmentindex, shape
  7686. return cache(decode_eer)
  7687. if self.compression == 48124:
  7688. # Jetraw requires pre-allocated output buffer
  7689. assert decompress is not None
  7690. def decode_jetraw(
  7691. data: bytes | None,
  7692. index: int,
  7693. /,
  7694. *,
  7695. jpegtables: bytes | None = None,
  7696. jpegheader: bytes | None = None,
  7697. _fullsize: bool = False,
  7698. ) -> tuple[
  7699. NDArray[Any] | None,
  7700. tuple[int, int, int, int, int],
  7701. tuple[int, int, int, int],
  7702. ]:
  7703. # return decoded segment, its shape, and indices in image
  7704. segmentindex, shape = indices(index)
  7705. if data is None:
  7706. if _fullsize:
  7707. shape = pad_none(shape)
  7708. return data, segmentindex, shape
  7709. data_array = numpy.zeros(shape, numpy.uint16)
  7710. decompress(data, out=data_array)
  7711. return data_array.reshape(shape), segmentindex, shape
  7712. return cache(decode_jetraw)
  7713. if self.compression in TIFF.IMAGE_COMPRESSIONS:
  7714. # presume codecs always return correct dtype, native byte order...
  7715. if self.fillorder == 2:
  7716. logger().debug(
  7717. f'{self!r} '
  7718. f'disabling LSB2MSB for compression {self.compression}'
  7719. )
  7720. if unpredict:
  7721. logger().debug(
  7722. f'{self!r} '
  7723. f'disabling predictor for compression {self.compression}'
  7724. )
  7725. assert decompress is not None
  7726. def decode_image(
  7727. data: bytes | None,
  7728. index: int,
  7729. /,
  7730. *,
  7731. jpegtables: bytes | None = None,
  7732. jpegheader: bytes | None = None,
  7733. _fullsize: bool = False,
  7734. ) -> tuple[
  7735. NDArray[Any] | None,
  7736. tuple[int, int, int, int, int],
  7737. tuple[int, int, int, int],
  7738. ]:
  7739. # return decoded segment, its shape, and indices in image
  7740. segmentindex, shape = indices(index)
  7741. if data is None:
  7742. if _fullsize:
  7743. shape = pad_none(shape)
  7744. return data, segmentindex, shape
  7745. data_array: NDArray[Any]
  7746. data_array = decompress(data)
  7747. # del data
  7748. data_array = reshape(data_array, segmentindex, shape)
  7749. if _fullsize:
  7750. data_array, shape = pad(data_array, shape)
  7751. return data_array, segmentindex, shape
  7752. return cache(decode_image)
  7753. dtype = numpy.dtype(self.parent.byteorder + self._dtype.char)
  7754. if self.sampleformat == 5:
  7755. # complex integer
  7756. if unpredict is not None:
  7757. raise NotImplementedError(
  7758. 'unpredicting complex integers not supported'
  7759. )
  7760. itype = numpy.dtype(
  7761. f'{self.parent.byteorder}i{self.bitspersample // 16}'
  7762. )
  7763. ftype = numpy.dtype(
  7764. f'{self.parent.byteorder}f{dtype.itemsize // 2}'
  7765. )
  7766. def unpack(data: bytes, /) -> NDArray[Any]:
  7767. # return complex integer as numpy.complex
  7768. return numpy.frombuffer(data, itype).astype(ftype).view(dtype)
  7769. elif self.bitspersample in {8, 16, 32, 64, 128}:
  7770. # regular data types
  7771. if (self.bitspersample * stwidth * samples) % 8:
  7772. raise ValueError('data and sample size mismatch')
  7773. if self.predictor in {3, 34894, 34895}: # PREDICTOR.FLOATINGPOINT
  7774. # floating-point horizontal differencing decoder needs
  7775. # raw byte order
  7776. dtype = numpy.dtype(self._dtype.char)
  7777. def unpack(data: bytes, /) -> NDArray[Any]:
  7778. # return numpy array from buffer
  7779. try:
  7780. # read only numpy array
  7781. return numpy.frombuffer(data, dtype)
  7782. except ValueError:
  7783. # for example, LZW strips may be missing EOI
  7784. bps = self.bitspersample // 8
  7785. size = (len(data) // bps) * bps
  7786. return numpy.frombuffer(data[:size], dtype)
  7787. elif isinstance(self.bitspersample, tuple):
  7788. # for example, RGB 565
  7789. def unpack(data: bytes, /) -> NDArray[Any]:
  7790. # return numpy array from packed integers
  7791. return unpack_rgb(data, dtype, self.bitspersample)
  7792. elif self.bitspersample == 24 and dtype.char == 'f':
  7793. # float24
  7794. if unpredict is not None:
  7795. # floatpred_decode requires numpy.float24, which does not exist
  7796. raise NotImplementedError('unpredicting float24 not supported')
  7797. def unpack(data: bytes, /) -> NDArray[Any]:
  7798. # return numpy.float32 array from float24
  7799. return imagecodecs.float24_decode(
  7800. data, byteorder=self.parent.byteorder
  7801. )
  7802. else:
  7803. # bilevel and packed integers
  7804. def unpack(data: bytes, /) -> NDArray[Any]:
  7805. # return NumPy array from packed integers
  7806. return imagecodecs.packints_decode(
  7807. data, dtype, self.bitspersample, runlen=stwidth * samples
  7808. )
  7809. def decode_other(
  7810. data: bytes | None,
  7811. index: int,
  7812. /,
  7813. *,
  7814. jpegtables: bytes | None = None,
  7815. jpegheader: bytes | None = None,
  7816. _fullsize: bool = False,
  7817. ) -> tuple[
  7818. NDArray[Any] | None,
  7819. tuple[int, int, int, int, int],
  7820. tuple[int, int, int, int],
  7821. ]:
  7822. # return decoded segment, its shape, and indices in image
  7823. segmentindex, shape = indices(index)
  7824. if data is None:
  7825. if _fullsize:
  7826. shape = pad_none(shape)
  7827. return data, segmentindex, shape
  7828. if self.fillorder == 2:
  7829. data = imagecodecs.bitorder_decode(data)
  7830. if decompress is not None:
  7831. # TODO: calculate correct size for packed integers
  7832. size = shape[0] * shape[1] * shape[2] * shape[3]
  7833. data = decompress(data, out=size * dtype.itemsize)
  7834. data_array = unpack(data)
  7835. # del data
  7836. data_array = reshape(data_array, segmentindex, shape)
  7837. data_array = data_array.astype('=' + dtype.char, copy=False)
  7838. if unpredict is not None:
  7839. # unpredict is faster with native byte order
  7840. data_array = unpredict(data_array, axis=-2, out=data_array)
  7841. if _fullsize:
  7842. data_array, shape = pad(data_array, shape)
  7843. return data_array, segmentindex, shape
  7844. return cache(decode_other)
  7845. def segments(
  7846. self,
  7847. *,
  7848. lock: threading.RLock | NullContext | None = None,
  7849. maxworkers: int | None = None,
  7850. func: Callable[..., Any] | None = None, # TODO: type this
  7851. sort: bool = False,
  7852. buffersize: int | None = None,
  7853. _fullsize: bool | None = None,
  7854. ) -> Iterator[
  7855. tuple[
  7856. NDArray[Any] | None,
  7857. tuple[int, int, int, int, int],
  7858. tuple[int, int, int, int],
  7859. ]
  7860. ]:
  7861. """Return iterator over decoded tiles or strips.
  7862. Parameters:
  7863. lock:
  7864. Reentrant lock to synchronize file seeks and reads.
  7865. maxworkers:
  7866. Maximum number of threads to concurrently decode segments.
  7867. func:
  7868. Function to process decoded segment.
  7869. sort:
  7870. Read segments from file in order of their offsets.
  7871. buffersize:
  7872. Approximate number of bytes to read from file in one pass.
  7873. The default is :py:attr:`_TIFF.BUFFERSIZE`.
  7874. _fullsize:
  7875. Internal use.
  7876. Yields:
  7877. - Decoded segment or None for empty segments.
  7878. - Position of segment in image array of normalized shape
  7879. (separate sample, depth, length, width, contig sample).
  7880. - Shape of segment (depth, length, width, contig samples).
  7881. The shape of strips depends on their linear index.
  7882. """
  7883. keyframe = self.keyframe # self or keyframe
  7884. fh = self.parent.filehandle
  7885. if lock is None:
  7886. lock = fh.lock
  7887. if _fullsize is None:
  7888. _fullsize = keyframe.is_tiled
  7889. decodeargs: dict[str, Any] = {'_fullsize': bool(_fullsize)}
  7890. if keyframe.compression in {6, 7, 34892, 33007}: # JPEG
  7891. decodeargs['jpegtables'] = self.jpegtables
  7892. decodeargs['jpegheader'] = keyframe.jpegheader
  7893. if func is None:
  7894. def decode(args, decodeargs=decodeargs, decode=keyframe.decode):
  7895. return decode(*args, **decodeargs)
  7896. else:
  7897. def decode(args, decodeargs=decodeargs, decode=keyframe.decode):
  7898. return func(decode(*args, **decodeargs))
  7899. number_segments = product(self.chunked)
  7900. if maxworkers is None or maxworkers < 1:
  7901. maxworkers = keyframe.maxworkers
  7902. if maxworkers < 2:
  7903. for segment in fh.read_segments(
  7904. self.dataoffsets,
  7905. self.databytecounts,
  7906. length=number_segments,
  7907. lock=lock,
  7908. sort=sort,
  7909. buffersize=buffersize,
  7910. flat=True,
  7911. ):
  7912. yield decode(segment)
  7913. else:
  7914. # reduce memory overhead by processing chunks of up to
  7915. # buffersize of segments because ThreadPoolExecutor.map is not
  7916. # collecting iterables lazily
  7917. with ThreadPoolExecutor(maxworkers) as executor:
  7918. for segments in fh.read_segments(
  7919. self.dataoffsets,
  7920. self.databytecounts,
  7921. length=number_segments,
  7922. lock=lock,
  7923. sort=sort,
  7924. buffersize=buffersize,
  7925. flat=False,
  7926. ):
  7927. yield from executor.map(decode, segments)
  7928. def asarray(
  7929. self,
  7930. *,
  7931. out: OutputType = None,
  7932. squeeze: bool = True,
  7933. lock: threading.RLock | NullContext | None = None,
  7934. maxworkers: int | None = None,
  7935. buffersize: int | None = None,
  7936. ) -> NDArray[Any]:
  7937. """Return image from page as NumPy array.
  7938. Parameters:
  7939. out:
  7940. Specifies how image array is returned.
  7941. By default, a new NumPy array is created.
  7942. If a *numpy.ndarray*, a writable array to which the image
  7943. is copied.
  7944. If *'memmap'*, directly memory-map the image data in the
  7945. file if possible; else create a memory-mapped array in a
  7946. temporary file.
  7947. If a *string* or *open file*, the file used to create a
  7948. memory-mapped array.
  7949. squeeze:
  7950. Remove all length-1 dimensions (except X and Y) from
  7951. image array.
  7952. If *False*, return the image array with normalized
  7953. 5-dimensional shape :py:attr:`TiffPage.shaped`.
  7954. lock:
  7955. Reentrant lock to synchronize seeks and reads from file.
  7956. The default is the lock of the parent's file handle.
  7957. maxworkers:
  7958. Maximum number of threads to concurrently decode segments.
  7959. If *None* or *0*, use up to :py:attr:`_TIFF.MAXWORKERS`
  7960. threads. See remarks in :py:meth:`TiffFile.asarray`.
  7961. buffersize:
  7962. Approximate number of bytes to read from file in one pass.
  7963. The default is :py:attr:`_TIFF.BUFFERSIZE`.
  7964. Returns:
  7965. NumPy array of decompressed, unpredicted, and unpacked image data
  7966. read from Strip/Tile Offsets/ByteCounts, formatted according to
  7967. shape and dtype metadata found in tags and arguments.
  7968. Photometric conversion, premultiplied alpha, orientation, and
  7969. colorimetry corrections are not applied.
  7970. Specifically, CMYK images are not converted to RGB, MinIsWhite
  7971. images are not inverted, color palettes are not applied,
  7972. gamma is not corrected, and CFA images are not demosaciced.
  7973. Exception are YCbCr JPEG compressed images, which are converted to
  7974. RGB.
  7975. Raises:
  7976. ValueError:
  7977. Format of image in file is not supported and cannot be decoded.
  7978. """
  7979. keyframe = self.keyframe # self or keyframe
  7980. if 0 in keyframe.shaped or keyframe._dtype is None:
  7981. return numpy.empty((0,), keyframe.dtype)
  7982. if len(self.dataoffsets) == 0:
  7983. raise TiffFileError('missing data offset')
  7984. fh = self.parent.filehandle
  7985. if lock is None:
  7986. lock = fh.lock
  7987. if (
  7988. isinstance(out, str)
  7989. and out == 'memmap'
  7990. and keyframe.is_memmappable
  7991. ):
  7992. # direct memory map array in file
  7993. with lock:
  7994. closed = fh.closed
  7995. if closed:
  7996. warnings.warn(
  7997. f'{self!r} reading array from closed file',
  7998. UserWarning,
  7999. stacklevel=2,
  8000. )
  8001. fh.open()
  8002. result = fh.memmap_array(
  8003. keyframe.parent.byteorder + keyframe._dtype.char,
  8004. keyframe.shaped,
  8005. offset=self.dataoffsets[0],
  8006. )
  8007. elif keyframe.is_contiguous:
  8008. # read contiguous bytes to array
  8009. if keyframe.is_subsampled:
  8010. raise NotImplementedError('chroma subsampling not supported')
  8011. if out is not None:
  8012. out = create_output(out, keyframe.shaped, keyframe._dtype)
  8013. with lock:
  8014. closed = fh.closed
  8015. if closed:
  8016. warnings.warn(
  8017. f'{self!r} reading array from closed file',
  8018. UserWarning,
  8019. stacklevel=2,
  8020. )
  8021. fh.open()
  8022. fh.seek(self.dataoffsets[0])
  8023. result = fh.read_array(
  8024. keyframe.parent.byteorder + keyframe._dtype.char,
  8025. product(keyframe.shaped),
  8026. out=out,
  8027. )
  8028. if keyframe.fillorder == 2:
  8029. result = imagecodecs.bitorder_decode(result, out=result)
  8030. if keyframe.predictor != 1:
  8031. # predictors without compression
  8032. unpredict = TIFF.UNPREDICTORS[keyframe.predictor]
  8033. if keyframe.predictor == 1:
  8034. result = unpredict(result, axis=-2, out=result)
  8035. else:
  8036. # floatpred cannot decode in-place
  8037. out = unpredict(result, axis=-2, out=result)
  8038. result[:] = out
  8039. elif (
  8040. keyframe.jpegheader is not None
  8041. and keyframe is self
  8042. and 273 in self.tags # striped ...
  8043. and self.is_tiled # but reported as tiled
  8044. # TODO: imagecodecs can decode larger JPEG
  8045. and self.imagewidth <= 65500
  8046. and self.imagelength <= 65500
  8047. ):
  8048. # decode the whole NDPI JPEG strip
  8049. with lock:
  8050. closed = fh.closed
  8051. if closed:
  8052. warnings.warn(
  8053. f'{self!r} reading array from closed file',
  8054. UserWarning,
  8055. stacklevel=2,
  8056. )
  8057. fh.open()
  8058. fh.seek(self.tags[273].value[0]) # StripOffsets
  8059. data = fh.read(self.tags[279].value[0]) # StripByteCounts
  8060. decompress = TIFF.DECOMPRESSORS[self.compression]
  8061. result = decompress(
  8062. data,
  8063. bitspersample=self.bitspersample,
  8064. out=out,
  8065. # shape=(self.imagelength, self.imagewidth)
  8066. )
  8067. del data
  8068. else:
  8069. # decode individual strips or tiles
  8070. with lock:
  8071. closed = fh.closed
  8072. if closed:
  8073. warnings.warn(
  8074. f'{self!r} reading array from closed file',
  8075. UserWarning,
  8076. stacklevel=2,
  8077. )
  8078. fh.open()
  8079. # init TiffPage.decode function under lock
  8080. keyframe.decode # noqa: B018
  8081. result = create_output(out, keyframe.shaped, keyframe._dtype)
  8082. def func(
  8083. decoderesult: tuple[
  8084. NDArray[Any] | None,
  8085. tuple[int, int, int, int, int],
  8086. tuple[int, int, int, int],
  8087. ],
  8088. keyframe: TiffPage = keyframe,
  8089. out: NDArray[Any] = result,
  8090. ) -> None:
  8091. # copy decoded segments to output array
  8092. segment, (s, d, h, w, _), shape = decoderesult
  8093. if segment is None:
  8094. out[
  8095. s, d : d + shape[0], h : h + shape[1], w : w + shape[2]
  8096. ] = keyframe.nodata
  8097. else:
  8098. out[
  8099. s, d : d + shape[0], h : h + shape[1], w : w + shape[2]
  8100. ] = segment[
  8101. : keyframe.imagedepth - d,
  8102. : keyframe.imagelength - h,
  8103. : keyframe.imagewidth - w,
  8104. ]
  8105. # except IndexError:
  8106. # pass # corrupted file, for example, with too many strips
  8107. for _ in self.segments(
  8108. func=func,
  8109. lock=lock,
  8110. maxworkers=maxworkers,
  8111. buffersize=buffersize,
  8112. sort=True,
  8113. _fullsize=False,
  8114. ):
  8115. pass
  8116. result.shape = keyframe.shaped
  8117. if squeeze:
  8118. try:
  8119. result.shape = keyframe.shape
  8120. except ValueError as exc:
  8121. logger().warning(
  8122. f'{self!r} <asarray> failed to reshape '
  8123. f'{result.shape} to {keyframe.shape}, raised {exc!r:.128}'
  8124. )
  8125. if closed:
  8126. # TODO: close file if an exception occurred above
  8127. fh.close()
  8128. return result
  8129. def aszarr(self, **kwargs: Any) -> ZarrTiffStore:
  8130. """Return image from page as Zarr store.
  8131. Parameters:
  8132. **kwarg: Passed to :py:class:`ZarrTiffStore`.
  8133. """
  8134. from .zarr import ZarrTiffStore
  8135. return ZarrTiffStore(self, **kwargs)
  8136. def asrgb(
  8137. self,
  8138. *,
  8139. uint8: bool = False,
  8140. alpha: Container[int] | None = None,
  8141. **kwargs: Any,
  8142. ) -> NDArray[Any]:
  8143. """Return image as RGB(A). Work in progress. Do not use.
  8144. :meta private:
  8145. """
  8146. data = self.asarray(**kwargs)
  8147. keyframe = self.keyframe # self or keyframe
  8148. if keyframe.photometric == PHOTOMETRIC.PALETTE:
  8149. colormap = keyframe.colormap
  8150. if colormap is None:
  8151. raise ValueError('no colormap')
  8152. if (
  8153. colormap.shape[1] < 2**keyframe.bitspersample
  8154. or keyframe.dtype is None
  8155. or keyframe.dtype.char not in 'BH'
  8156. ):
  8157. raise ValueError('cannot apply colormap')
  8158. if uint8:
  8159. if colormap.max() > 255:
  8160. colormap >>= 8
  8161. colormap = colormap.astype(numpy.uint8)
  8162. if 'S' in keyframe.axes:
  8163. data = data[..., 0] if keyframe.planarconfig == 1 else data[0]
  8164. data = apply_colormap(data, colormap)
  8165. elif keyframe.photometric == PHOTOMETRIC.RGB:
  8166. if keyframe.extrasamples:
  8167. if alpha is None:
  8168. alpha = EXTRASAMPLE
  8169. for i, exs in enumerate(keyframe.extrasamples):
  8170. if exs in EXTRASAMPLE:
  8171. if keyframe.planarconfig == 1:
  8172. data = data[..., [0, 1, 2, 3 + i]]
  8173. else:
  8174. data = data[:, [0, 1, 2, 3 + i]]
  8175. break
  8176. elif keyframe.planarconfig == 1:
  8177. data = data[..., :3]
  8178. else:
  8179. data = data[:, :3]
  8180. # TODO: convert to uint8?
  8181. # elif keyframe.photometric == PHOTOMETRIC.MINISBLACK:
  8182. # raise NotImplementedError
  8183. # elif keyframe.photometric == PHOTOMETRIC.MINISWHITE:
  8184. # raise NotImplementedError
  8185. # elif keyframe.photometric == PHOTOMETRIC.SEPARATED:
  8186. # raise NotImplementedError
  8187. else:
  8188. raise NotImplementedError
  8189. return data
  8190. def _gettags(
  8191. self,
  8192. codes: Container[int] | None = None,
  8193. /,
  8194. lock: threading.RLock | None = None,
  8195. ) -> list[tuple[int, TiffTag]]:
  8196. """Return list of (code, TiffTag)."""
  8197. return [
  8198. (tag.code, tag)
  8199. for tag in self.tags
  8200. if codes is None or tag.code in codes
  8201. ]
  8202. def _nextifd(self) -> int:
  8203. """Return offset to next IFD from file."""
  8204. fh = self.parent.filehandle
  8205. tiff = self.parent.tiff
  8206. fh.seek(self.offset)
  8207. tagno = struct.unpack(tiff.tagnoformat, fh.read(tiff.tagnosize))[0]
  8208. fh.seek(self.offset + tiff.tagnosize + tagno * tiff.tagsize)
  8209. return int(
  8210. struct.unpack(tiff.offsetformat, fh.read(tiff.offsetsize))[0]
  8211. )
  8212. def aspage(self) -> TiffPage:
  8213. """Return TiffPage instance."""
  8214. return self
  8215. @property
  8216. def index(self) -> int:
  8217. """Index of page in IFD chain."""
  8218. return self._index[-1]
  8219. @property
  8220. def treeindex(self) -> tuple[int, ...]:
  8221. """Index of page in IFD tree."""
  8222. return self._index
  8223. @property
  8224. def keyframe(self) -> TiffPage:
  8225. """Self."""
  8226. return self
  8227. @keyframe.setter
  8228. def keyframe(self, index: TiffPage) -> None:
  8229. return
  8230. @property
  8231. def name(self) -> str:
  8232. """Name of image array."""
  8233. index = self._index if len(self._index) > 1 else self._index[0]
  8234. return f'TiffPage {index}'
  8235. @property
  8236. def ndim(self) -> int:
  8237. """Number of dimensions in image array."""
  8238. return len(self.shape)
  8239. @cached_property
  8240. def dims(self) -> tuple[str, ...]:
  8241. """Names of dimensions in image array."""
  8242. names = TIFF.AXES_NAMES
  8243. return tuple(names[ax] for ax in self.axes)
  8244. @cached_property
  8245. def sizes(self) -> dict[str, int]:
  8246. """Ordered map of dimension names to lengths."""
  8247. shape = self.shape
  8248. names = TIFF.AXES_NAMES
  8249. return {names[ax]: shape[i] for i, ax in enumerate(self.axes)}
  8250. @cached_property
  8251. def coords(self) -> dict[str, NDArray[Any]]:
  8252. """Ordered map of dimension names to coordinate arrays."""
  8253. resolution = self.get_resolution()
  8254. coords: dict[str, NDArray[Any]] = {}
  8255. for ax, size in zip(self.axes, self.shape, strict=True):
  8256. name = TIFF.AXES_NAMES[ax]
  8257. value = None
  8258. step: float = 1
  8259. if ax == 'X':
  8260. step = resolution[0]
  8261. elif ax == 'Y':
  8262. step = resolution[1]
  8263. elif ax == 'S':
  8264. value = self._sample_names()
  8265. elif ax == 'Z' and resolution[0] == resolution[1]:
  8266. # a ZResolution tag doesn't exist
  8267. # use XResolution if it agrees with YResolution
  8268. step = resolution[0]
  8269. if value is not None:
  8270. coords[name] = numpy.asarray(value)
  8271. elif step == 0 or step == 1 or size == 0: # noqa: PLR1714
  8272. coords[name] = numpy.arange(size)
  8273. else:
  8274. coords[name] = numpy.linspace(
  8275. 0, size / step, size, endpoint=False, dtype=numpy.float32
  8276. )
  8277. assert len(coords[name]) == size
  8278. return coords
  8279. @cached_property
  8280. def attr(self) -> dict[str, Any]:
  8281. """Arbitrary metadata associated with image array."""
  8282. # TODO: what to return?
  8283. return {}
  8284. @cached_property
  8285. def size(self) -> int:
  8286. """Number of elements in image array."""
  8287. return product(self.shape)
  8288. @cached_property
  8289. def nbytes(self) -> int:
  8290. """Number of bytes in image array."""
  8291. if self.dtype is None:
  8292. return 0
  8293. return self.size * self.dtype.itemsize
  8294. @property
  8295. def colormap(self) -> NDArray[numpy.uint16] | None:
  8296. """Value of Colormap tag."""
  8297. return self.tags.valueof(320)
  8298. @property
  8299. def iccprofile(self) -> bytes | None:
  8300. """Value of InterColorProfile tag."""
  8301. return self.tags.valueof(34675)
  8302. @property
  8303. def transferfunction(self) -> NDArray[numpy.uint16] | None:
  8304. """Value of TransferFunction tag."""
  8305. return self.tags.valueof(301)
  8306. def get_resolution(
  8307. self,
  8308. unit: RESUNIT | int | str | None = None,
  8309. scale: float | None = None,
  8310. ) -> tuple[float, float]:
  8311. """Return number of pixels per unit in X and Y dimensions.
  8312. By default, the XResolution and YResolution tag values are returned.
  8313. Missing tag values are set to 1.
  8314. Parameters:
  8315. unit:
  8316. Unit of measurement of returned values.
  8317. The default is the value of the ResolutionUnit tag.
  8318. scale:
  8319. Factor to convert resolution values to meter unit.
  8320. The default is determined from the ResolutionUnit tag.
  8321. """
  8322. scales = {
  8323. 1: 1, # meter, no unit
  8324. 2: 100 / 2.54, # INCH
  8325. 3: 100, # CENTIMETER
  8326. 4: 1000, # MILLIMETER
  8327. 5: 1000000, # MICROMETER
  8328. }
  8329. if unit is not None:
  8330. unit = enumarg(RESUNIT, unit)
  8331. try:
  8332. if scale is None:
  8333. resolutionunit = self.tags.valueof(296, default=2)
  8334. scale = scales[resolutionunit]
  8335. except Exception as exc:
  8336. logger().warning(
  8337. f'{self!r} <get_resolution> raised {exc!r:.128}'
  8338. )
  8339. scale = 1
  8340. else:
  8341. scale2 = scales[unit]
  8342. if scale % scale2 == 0:
  8343. scale //= scale2
  8344. else:
  8345. scale /= scale2
  8346. elif scale is None:
  8347. scale = 1
  8348. resolution: list[float] = []
  8349. n: int
  8350. d: int
  8351. for code in 282, 283:
  8352. try:
  8353. n, d = self.tags.valueof(code, default=(1, 1))
  8354. if d == 0:
  8355. value = n * scale
  8356. elif n % d == 0:
  8357. value = n // d * scale
  8358. else:
  8359. value = n / d * scale
  8360. except Exception:
  8361. value = 1
  8362. resolution.append(value)
  8363. return resolution[0], resolution[1]
  8364. @cached_property
  8365. def resolution(self) -> tuple[float, float]:
  8366. """Number of pixels per resolutionunit in X and Y directions."""
  8367. # values are returned in (somewhat unexpected) XY order to
  8368. # keep symmetry with the TiffWriter.write resolution argument
  8369. resolution = self.get_resolution()
  8370. return float(resolution[0]), float(resolution[1])
  8371. @property
  8372. def resolutionunit(self) -> int:
  8373. """Unit of measurement for X and Y resolutions."""
  8374. return self.tags.valueof(296, default=2)
  8375. @property
  8376. def datetime(self) -> DateTime | None:
  8377. """Date and time of image creation."""
  8378. value = self.tags.valueof(306)
  8379. if value is None:
  8380. return None
  8381. try:
  8382. return strptime(value)
  8383. except (TypeError, ValueError):
  8384. pass
  8385. return None
  8386. @property
  8387. def tile(self) -> tuple[int, ...] | None:
  8388. """Tile depth, length, and width."""
  8389. if not self.is_tiled:
  8390. return None
  8391. if self.tiledepth > 1:
  8392. return (self.tiledepth, self.tilelength, self.tilewidth)
  8393. return (self.tilelength, self.tilewidth)
  8394. @cached_property
  8395. def chunks(self) -> tuple[int, ...]:
  8396. """Shape of images in tiles or strips."""
  8397. shape: list[int] = []
  8398. if self.tiledepth > 1:
  8399. shape.append(self.tiledepth)
  8400. if self.is_tiled:
  8401. shape.extend((self.tilelength, self.tilewidth))
  8402. else:
  8403. shape.extend((self.rowsperstrip, self.imagewidth))
  8404. if self.planarconfig == 1 and self.samplesperpixel > 1:
  8405. shape.append(self.samplesperpixel)
  8406. return tuple(shape)
  8407. @cached_property
  8408. def chunked(self) -> tuple[int, ...]:
  8409. """Shape of chunked image."""
  8410. shape: list[int] = []
  8411. if self.planarconfig == 2 and self.samplesperpixel > 1:
  8412. shape.append(self.samplesperpixel)
  8413. if self.is_tiled:
  8414. if self.imagedepth > 1:
  8415. shape.append(
  8416. (self.imagedepth + self.tiledepth - 1) // self.tiledepth
  8417. )
  8418. shape.append(
  8419. (self.imagelength + self.tilelength - 1) // self.tilelength
  8420. )
  8421. shape.append(
  8422. (self.imagewidth + self.tilewidth - 1) // self.tilewidth
  8423. )
  8424. else:
  8425. if self.imagedepth > 1:
  8426. shape.append(self.imagedepth)
  8427. shape.append(
  8428. (self.imagelength + self.rowsperstrip - 1) // self.rowsperstrip
  8429. )
  8430. shape.append(1)
  8431. if self.planarconfig == 1 and self.samplesperpixel > 1:
  8432. shape.append(1)
  8433. return tuple(shape)
  8434. @cached_property
  8435. def hash(self) -> int:
  8436. """Checksum to identify pages in same series.
  8437. Pages with the same hash can use the same decode function.
  8438. The hash is calculated from the following properties:
  8439. :py:attr:`TiffFile.tiff`,
  8440. :py:attr:`TiffPage.shaped`,
  8441. :py:attr:`TiffPage.rowsperstrip`,
  8442. :py:attr:`TiffPage.tilewidth`,
  8443. :py:attr:`TiffPage.tilelength`,
  8444. :py:attr:`TiffPage.tiledepth`,
  8445. :py:attr:`TiffPage.sampleformat`,
  8446. :py:attr:`TiffPage.bitspersample`,
  8447. :py:attr:`TiffPage.fillorder`,
  8448. :py:attr:`TiffPage.predictor`,
  8449. :py:attr:`TiffPage.compression`,
  8450. :py:attr:`TiffPage.extrasamples`, and
  8451. :py:attr:`TiffPage.photometric`.
  8452. """
  8453. return hash(
  8454. (
  8455. *self.shaped,
  8456. self.parent.tiff,
  8457. self.rowsperstrip,
  8458. self.tilewidth,
  8459. self.tilelength,
  8460. self.tiledepth,
  8461. self.sampleformat,
  8462. self.bitspersample,
  8463. self.fillorder,
  8464. self.predictor,
  8465. self.compression,
  8466. self.extrasamples,
  8467. self.photometric,
  8468. )
  8469. )
  8470. @cached_property
  8471. def pages(self) -> TiffPages | None:
  8472. """Sequence of sub-pages, SubIFDs."""
  8473. if 330 not in self.tags:
  8474. return None
  8475. return TiffPages(self, index=self.index)
  8476. @cached_property
  8477. def maxworkers(self) -> int:
  8478. """Maximum number of threads for decoding segments.
  8479. A value of 0 disables multi-threading also when stacking pages.
  8480. """
  8481. if self.is_contiguous or self.dtype is None:
  8482. return 0
  8483. if self.compression in TIFF.IMAGE_COMPRESSIONS:
  8484. return min(TIFF.MAXWORKERS, len(self.dataoffsets))
  8485. bytecount = product(self.chunks) * self.dtype.itemsize
  8486. if bytecount < 2048:
  8487. # disable multi-threading for small segments
  8488. return 0
  8489. if self.compression == 5 and bytecount < 14336:
  8490. # disable multi-threading for small LZW compressed segments
  8491. return 0
  8492. if len(self.dataoffsets) < 4:
  8493. return 1
  8494. if imagecodecs is not None and (
  8495. self.compression != 1 or self.fillorder != 1 or self.predictor != 1
  8496. ):
  8497. return min(TIFF.MAXWORKERS, len(self.dataoffsets))
  8498. return 2 # optimum for large number of uncompressed tiles
  8499. @cached_property
  8500. def is_contiguous(self) -> bool:
  8501. """Image data is stored contiguously.
  8502. Contiguous image data can be read from
  8503. ``offset=TiffPage.dataoffsets[0]`` with ``size=TiffPage.nbytes``.
  8504. Excludes prediction and fillorder.
  8505. """
  8506. if (
  8507. self.sampleformat == 5
  8508. or self.compression != 1
  8509. or self.bitspersample not in {8, 16, 32, 64}
  8510. ):
  8511. return False
  8512. if 322 in self.tags: # TileWidth
  8513. if (
  8514. self.imagewidth != self.tilewidth
  8515. or self.imagelength % self.tilelength
  8516. or self.tilewidth % 16
  8517. or self.tilelength % 16
  8518. ):
  8519. return False
  8520. if (
  8521. 32997 in self.tags # ImageDepth
  8522. and 32998 in self.tags # TileDepth
  8523. and (
  8524. self.imagelength != self.tilelength
  8525. or self.imagedepth % self.tiledepth
  8526. )
  8527. ):
  8528. return False
  8529. offsets = self.dataoffsets
  8530. bytecounts = self.databytecounts
  8531. if len(offsets) == 0:
  8532. return False
  8533. if len(offsets) == 1:
  8534. return True
  8535. if self.is_stk or self.is_lsm:
  8536. return True
  8537. if sum(bytecounts) != self.nbytes:
  8538. return False
  8539. return all(
  8540. bytecounts[i] != 0 and offsets[i] + bytecounts[i] == offsets[i + 1]
  8541. for i in range(len(offsets) - 1)
  8542. )
  8543. @cached_property
  8544. def is_final(self) -> bool:
  8545. """Image data are stored in final form. Excludes byte-swapping."""
  8546. return (
  8547. self.is_contiguous
  8548. and self.fillorder == 1
  8549. and self.predictor == 1
  8550. and not self.is_subsampled
  8551. )
  8552. @cached_property
  8553. def is_memmappable(self) -> bool:
  8554. """Image data in file can be memory-mapped to NumPy array."""
  8555. return (
  8556. self.parent.filehandle.is_file
  8557. and self.is_final
  8558. # and (self.bitspersample == 8 or self.parent.isnative)
  8559. # aligned?
  8560. and self.dtype is not None
  8561. and self.dataoffsets[0] % self.dtype.itemsize == 0
  8562. )
  8563. def __repr__(self) -> str:
  8564. index = self._index if len(self._index) > 1 else self._index[0]
  8565. return f'<tifffile.TiffPage {index} @{self.offset}>'
  8566. def __str__(self) -> str:
  8567. return self._str()
  8568. def _str(self, detail: int = 0, width: int = 79) -> str:
  8569. """Return string containing information about TiffPage."""
  8570. if self.keyframe != self:
  8571. return TiffFrame._str(
  8572. self, detail, width # type: ignore[arg-type]
  8573. )
  8574. attr = ''
  8575. for name in ('memmappable', 'final', 'contiguous'):
  8576. attr = getattr(self, 'is_' + name)
  8577. if attr:
  8578. attr = name.upper()
  8579. break
  8580. def tostr(name: str, /, skip: int = 1) -> str:
  8581. obj = getattr(self, name)
  8582. if obj == skip:
  8583. return ''
  8584. try:
  8585. value = obj.name
  8586. except AttributeError:
  8587. return ''
  8588. return str(value)
  8589. info = ' '.join(
  8590. s.lower()
  8591. for s in (
  8592. 'x'.join(str(i) for i in self.shape),
  8593. f'{SAMPLEFORMAT(self.sampleformat).name}{self.bitspersample}',
  8594. ' '.join(
  8595. i
  8596. for i in (
  8597. PHOTOMETRIC(self.photometric).name,
  8598. 'REDUCED' if self.is_reduced else '',
  8599. 'MASK' if self.is_mask else '',
  8600. 'TILED' if self.is_tiled else '',
  8601. tostr('compression'),
  8602. tostr('planarconfig'),
  8603. tostr('predictor'),
  8604. tostr('fillorder'),
  8605. attr,
  8606. )
  8607. if i
  8608. ),
  8609. '|'.join(f.upper() for f in sorted(self.flags)),
  8610. )
  8611. if s
  8612. )
  8613. index = self._index if len(self._index) > 1 else self._index[0]
  8614. info = f'TiffPage {index} @{self.offset} {info}'
  8615. if detail <= 0:
  8616. return info
  8617. info_list = [info, self.tags._str(detail + 1, width=width)]
  8618. if detail > 1:
  8619. for name in ('ndpi_tags',):
  8620. attr = getattr(self, name, '')
  8621. if attr:
  8622. info_list.append(
  8623. f'{name.upper()}\n'
  8624. f'{pformat(attr, width=width, height=detail * 8)}'
  8625. )
  8626. if detail > 3:
  8627. try:
  8628. data = self.asarray()
  8629. info_list.append(
  8630. f'DATA\n{pformat(data, width=width, height=detail * 8)}'
  8631. )
  8632. except Exception: # noqa: S110
  8633. pass
  8634. return '\n\n'.join(info_list)
  8635. def _sample_names(self) -> list[str] | None:
  8636. """Return names of samples."""
  8637. if 'S' not in self.axes:
  8638. return None
  8639. samples = self.shape[self.axes.find('S')]
  8640. extrasamples = len(self.extrasamples)
  8641. if samples < 1 or extrasamples > 2:
  8642. return None
  8643. if self.photometric == 0:
  8644. names = ['WhiteIsZero']
  8645. elif self.photometric == 1:
  8646. names = ['BlackIsZero']
  8647. elif self.photometric == 2:
  8648. names = ['Red', 'Green', 'Blue']
  8649. elif self.photometric == 5:
  8650. names = ['Cyan', 'Magenta', 'Yellow', 'Black']
  8651. elif self.photometric == 6:
  8652. if self.compression in {6, 7, 34892, 33007}:
  8653. # YCBCR -> RGB for JPEG
  8654. names = ['Red', 'Green', 'Blue']
  8655. else:
  8656. names = ['Luma', 'Cb', 'Cr']
  8657. else:
  8658. return None
  8659. if extrasamples > 0:
  8660. names += [enumarg(EXTRASAMPLE, self.extrasamples[0]).name.title()]
  8661. if extrasamples > 1:
  8662. names += [enumarg(EXTRASAMPLE, self.extrasamples[1]).name.title()]
  8663. if len(names) != samples:
  8664. return None
  8665. return names
  8666. @cached_property
  8667. def flags(self) -> set[str]:
  8668. r"""Set of ``is\_\*`` properties that are True."""
  8669. return {
  8670. name.lower()
  8671. for name in TIFF.PAGE_FLAGS
  8672. if getattr(self, 'is_' + name)
  8673. }
  8674. @cached_property
  8675. def eer_tags(self) -> dict[str, Any] | None:
  8676. """Consolidated metadata from EER tags 65001-65009."""
  8677. if not self.is_eer:
  8678. return None
  8679. result = {}
  8680. for code in range(65001, 65007):
  8681. value = self.tags.valueof(code)
  8682. if (
  8683. value is None
  8684. or not isinstance(value, bytes)
  8685. or not value.startswith(b'<metadata>')
  8686. ):
  8687. continue
  8688. try:
  8689. result.update(eer_xml_metadata(value.decode()))
  8690. except Exception as exc:
  8691. logger().warning(
  8692. f'{self!r} eer_xml_metadata failed for tag {code}'
  8693. f'{exc!r:.128}'
  8694. )
  8695. return result
  8696. @cached_property
  8697. def nuvu_tags(self) -> dict[str, Any] | None:
  8698. """Consolidated metadata from Nuvu tags."""
  8699. if not self.is_nuvu:
  8700. return None
  8701. result: dict[str, Any] = {}
  8702. used: set[int] = set()
  8703. for tag in self.tags:
  8704. if (
  8705. tag.code < 65000
  8706. or tag.code in used
  8707. or tag.dtype != 2
  8708. or tag.value[:7] != "Field '"
  8709. ):
  8710. continue
  8711. try:
  8712. value = tag.value.split("'")
  8713. name = value[3]
  8714. code = int(value[1])
  8715. except Exception as exc:
  8716. logger().warning(
  8717. f'{self!r} corrupted Nuvu tag {tag.code} ({exc})'
  8718. )
  8719. continue
  8720. result[name] = self.tags.valueof(code)
  8721. used.add(code)
  8722. return result
  8723. @cached_property
  8724. def andor_tags(self) -> dict[str, Any] | None:
  8725. """Consolidated metadata from Andor tags."""
  8726. if not self.is_andor:
  8727. return None
  8728. result = {'Id': self.tags[4864].value} # AndorId
  8729. for tag in self.tags: # list(self.tags.values()):
  8730. code = tag.code
  8731. if not 4864 < code < 5031:
  8732. continue
  8733. name = tag.name
  8734. name = name[5:] if len(name) > 5 else name
  8735. result[name] = tag.value
  8736. # del self.tags[code]
  8737. return result
  8738. @cached_property
  8739. def epics_tags(self) -> dict[str, Any] | None:
  8740. """Consolidated metadata from EPICS areaDetector tags.
  8741. Use the :py:func:`epics_datetime` function to get a datetime object
  8742. from the epicsTSSec and epicsTSNsec tags.
  8743. """
  8744. if not self.is_epics:
  8745. return None
  8746. result = {}
  8747. for tag in self.tags: # list(self.tags.values()):
  8748. code = tag.code
  8749. if not 65000 <= code < 65500:
  8750. continue
  8751. value = tag.value
  8752. if code == 65000:
  8753. # not a POSIX timestamp
  8754. # https://github.com/bluesky/area-detector-handlers/issues/20
  8755. result['timeStamp'] = float(value)
  8756. elif code == 65001:
  8757. result['uniqueID'] = int(value)
  8758. elif code == 65002:
  8759. result['epicsTSSec'] = int(value)
  8760. elif code == 65003:
  8761. result['epicsTSNsec'] = int(value)
  8762. else:
  8763. key, value = value.split(':', 1)
  8764. result[key] = astype(value)
  8765. # del self.tags[code]
  8766. return result
  8767. @cached_property
  8768. def ndpi_tags(self) -> dict[str, Any] | None:
  8769. """Consolidated metadata from Hamamatsu NDPI tags."""
  8770. # TODO: parse 65449 ini style comments
  8771. if not self.is_ndpi:
  8772. return None
  8773. tags = self.tags
  8774. result = {}
  8775. for name in ('Make', 'Model', 'Software'):
  8776. result[name] = tags[name].value
  8777. for code, name in TIFF.NDPI_TAGS.items():
  8778. if code in tags:
  8779. result[name] = tags[code].value
  8780. # del tags[code]
  8781. if 'McuStarts' in result:
  8782. mcustarts = result['McuStarts']
  8783. if 'McuStartsHighBytes' in result:
  8784. high = result['McuStartsHighBytes'].astype(numpy.uint64)
  8785. high <<= 32
  8786. mcustarts = mcustarts.astype(numpy.uint64)
  8787. mcustarts += high
  8788. del result['McuStartsHighBytes']
  8789. result['McuStarts'] = mcustarts
  8790. return result
  8791. @cached_property
  8792. def geotiff_tags(self) -> dict[str, Any] | None:
  8793. """Consolidated metadata from GeoTIFF tags."""
  8794. if not self.is_geotiff:
  8795. return None
  8796. tags = self.tags
  8797. gkd = tags.valueof(34735) # GeoKeyDirectoryTag
  8798. if gkd is None or len(gkd) < 2 or gkd[0] != 1:
  8799. logger().warning(f'{self!r} invalid GeoKeyDirectoryTag')
  8800. return {}
  8801. result = {
  8802. 'KeyDirectoryVersion': gkd[0],
  8803. 'KeyRevision': gkd[1],
  8804. 'KeyRevisionMinor': gkd[2],
  8805. # 'NumberOfKeys': gkd[3],
  8806. }
  8807. # deltags = ['GeoKeyDirectoryTag']
  8808. geokeys = TIFF.GEO_KEYS
  8809. geocodes = TIFF.GEO_CODES
  8810. for index in range(gkd[3]):
  8811. try:
  8812. keyid, tagid, count, offset = gkd[
  8813. 4 + index * 4 : index * 4 + 8
  8814. ]
  8815. except Exception as exc:
  8816. logger().warning(
  8817. f'{self!r} corrupted GeoKeyDirectoryTag '
  8818. f'raised {exc!r:.128}'
  8819. )
  8820. continue
  8821. if tagid == 0:
  8822. value = offset
  8823. else:
  8824. try:
  8825. value = tags[tagid].value[offset : offset + count]
  8826. except TiffFileError as exc:
  8827. logger().warning(
  8828. f'{self!r} corrupted GeoKeyDirectoryTag {tagid} '
  8829. f'raised {exc!r:.128}'
  8830. )
  8831. continue
  8832. except KeyError as exc:
  8833. logger().warning(
  8834. f'{self!r} GeoKeyDirectoryTag {tagid} not found, '
  8835. f'raised {exc!r:.128}'
  8836. )
  8837. continue
  8838. if tagid == 34737 and count > 1 and value[-1] == '|':
  8839. value = value[:-1]
  8840. value = value if count > 1 else value[0]
  8841. if keyid in geocodes:
  8842. try:
  8843. value = geocodes[keyid](value)
  8844. except ValueError:
  8845. pass
  8846. try:
  8847. key = geokeys(keyid).name
  8848. except ValueError:
  8849. key = keyid
  8850. result[key] = value
  8851. value = tags.valueof(33920) # IntergraphMatrixTag
  8852. if value is not None:
  8853. value = numpy.array(value)
  8854. if value.size == 16:
  8855. value = value.reshape((4, 4)).tolist()
  8856. result['IntergraphMatrix'] = value
  8857. value = tags.valueof(33550) # ModelPixelScaleTag
  8858. if value is not None:
  8859. result['ModelPixelScale'] = numpy.array(value).tolist()
  8860. value = tags.valueof(33922) # ModelTiepointTag
  8861. if value is not None:
  8862. value = numpy.array(value).reshape((-1, 6)).squeeze().tolist()
  8863. result['ModelTiepoint'] = value
  8864. value = tags.valueof(34264) # ModelTransformationTag
  8865. if value is not None:
  8866. value = numpy.array(value).reshape((4, 4)).tolist()
  8867. result['ModelTransformation'] = value
  8868. # if 33550 in tags and 33922 in tags:
  8869. # sx, sy, sz = tags[33550].value # ModelPixelScaleTag
  8870. # tiepoints = tags[33922].value # ModelTiepointTag
  8871. # transforms = []
  8872. # for tp in range(0, len(tiepoints), 6):
  8873. # i, j, k, x, y, z = tiepoints[tp : tp + 6]
  8874. # transforms.append(
  8875. # [
  8876. # [sx, 0.0, 0.0, x - i * sx],
  8877. # [0.0, -sy, 0.0, y + j * sy],
  8878. # [0.0, 0.0, sz, z - k * sz],
  8879. # [0.0, 0.0, 0.0, 1.0],
  8880. # ]
  8881. # )
  8882. # if len(tiepoints) == 6:
  8883. # transforms = transforms[0]
  8884. # result['ModelTransformation'] = transforms
  8885. rpcc = tags.valueof(50844) # RPCCoefficientTag
  8886. if rpcc is not None:
  8887. result['RPCCoefficient'] = {
  8888. 'ERR_BIAS': rpcc[0],
  8889. 'ERR_RAND': rpcc[1],
  8890. 'LINE_OFF': rpcc[2],
  8891. 'SAMP_OFF': rpcc[3],
  8892. 'LAT_OFF': rpcc[4],
  8893. 'LONG_OFF': rpcc[5],
  8894. 'HEIGHT_OFF': rpcc[6],
  8895. 'LINE_SCALE': rpcc[7],
  8896. 'SAMP_SCALE': rpcc[8],
  8897. 'LAT_SCALE': rpcc[9],
  8898. 'LONG_SCALE': rpcc[10],
  8899. 'HEIGHT_SCALE': rpcc[11],
  8900. 'LINE_NUM_COEFF': rpcc[12:33],
  8901. 'LINE_DEN_COEFF ': rpcc[33:53],
  8902. 'SAMP_NUM_COEFF': rpcc[53:73],
  8903. 'SAMP_DEN_COEFF': rpcc[73:],
  8904. }
  8905. return result
  8906. @cached_property
  8907. def shaped_description(self) -> str | None:
  8908. """Description containing array shape if exists, else None."""
  8909. for description in (self.description, self.description1):
  8910. if not description or '"mibi.' in description:
  8911. return None
  8912. if description[:1] == '{' and '"shape":' in description:
  8913. return description
  8914. if description[:6] == 'shape=':
  8915. return description
  8916. return None
  8917. @cached_property
  8918. def imagej_description(self) -> str | None:
  8919. """ImageJ description if exists, else None."""
  8920. for description in (self.description, self.description1):
  8921. if not description:
  8922. return None
  8923. if description[:7] == 'ImageJ=' or description[:7] == 'SCIFIO=':
  8924. return description
  8925. return None
  8926. @cached_property
  8927. def is_jfif(self) -> bool:
  8928. """JPEG compressed segments contain JFIF metadata."""
  8929. if (
  8930. self.compression not in {6, 7, 34892, 33007}
  8931. or len(self.dataoffsets) < 1
  8932. or self.dataoffsets[0] == 0
  8933. or len(self.databytecounts) < 1
  8934. or self.databytecounts[0] < 11
  8935. ):
  8936. return False
  8937. fh = self.parent.filehandle
  8938. fh.seek(self.dataoffsets[0] + 6)
  8939. data = fh.read(4)
  8940. return data == b'JFIF' # or data == b'Exif'
  8941. @property
  8942. def is_frame(self) -> bool:
  8943. """Object is :py:class:`TiffFrame` instance."""
  8944. return False
  8945. @property
  8946. def is_virtual(self) -> bool:
  8947. """Page does not have IFD structure in file."""
  8948. return False
  8949. @property
  8950. def is_subifd(self) -> bool:
  8951. """Page is SubIFD of another page."""
  8952. return len(self._index) > 1
  8953. @property
  8954. def is_reduced(self) -> bool:
  8955. """Page is reduced image of another image."""
  8956. return bool(self.subfiletype & 0b1)
  8957. @property
  8958. def is_multipage(self) -> bool:
  8959. """Page is part of multi-page image."""
  8960. return bool(self.subfiletype & 0b10)
  8961. @property
  8962. def is_mask(self) -> bool:
  8963. """Page is transparency mask for another image."""
  8964. return bool(self.subfiletype & 0b100)
  8965. @property
  8966. def is_mrc(self) -> bool:
  8967. """Page is part of Mixed Raster Content."""
  8968. return bool(self.subfiletype & 0b1000)
  8969. @property
  8970. def is_tiled(self) -> bool:
  8971. """Page contains tiled image."""
  8972. return self.tilewidth > 0 # return 322 in self.tags # TileWidth
  8973. @property
  8974. def is_subsampled(self) -> bool:
  8975. """Page contains chroma subsampled image."""
  8976. if self.subsampling is not None:
  8977. return self.subsampling != (1, 1)
  8978. return self.photometric == 6 # YCbCr
  8979. # RGB JPEG usually stored as subsampled YCbCr
  8980. # self.compression == 7
  8981. # and self.photometric == 2
  8982. # and self.planarconfig == 1
  8983. @property
  8984. def is_imagej(self) -> bool:
  8985. """Page contains ImageJ description metadata."""
  8986. return self.imagej_description is not None
  8987. @property
  8988. def is_shaped(self) -> bool:
  8989. """Page contains Tifffile JSON metadata."""
  8990. return self.shaped_description is not None
  8991. @property
  8992. def is_mdgel(self) -> bool:
  8993. """Page contains MDFileTag tag."""
  8994. return (
  8995. 37701 not in self.tags # AgilentBinary
  8996. and 33445 in self.tags # MDFileTag
  8997. )
  8998. @property
  8999. def is_agilent(self) -> bool:
  9000. """Page contains Agilent Technologies tags."""
  9001. # tag 270 and 285 contain color names
  9002. return 285 in self.tags and 37701 in self.tags # AgilentBinary
  9003. @property
  9004. def is_mediacy(self) -> bool:
  9005. """Page contains Media Cybernetics Id tag."""
  9006. tag = self.tags.get(50288) # MC_Id
  9007. try:
  9008. return tag is not None and tag.value[:7] == b'MC TIFF'
  9009. except Exception:
  9010. return False
  9011. @property
  9012. def is_stk(self) -> bool:
  9013. """Page contains UIC1Tag tag."""
  9014. return 33628 in self.tags
  9015. @property
  9016. def is_lsm(self) -> bool:
  9017. """Page contains CZ_LSMINFO tag."""
  9018. return 34412 in self.tags
  9019. @property
  9020. def is_fluoview(self) -> bool:
  9021. """Page contains FluoView MM_STAMP tag."""
  9022. return 34362 in self.tags
  9023. @property
  9024. def is_nih(self) -> bool:
  9025. """Page contains NIHImageHeader tag."""
  9026. return 43314 in self.tags
  9027. @property
  9028. def is_volumetric(self) -> bool:
  9029. """Page contains SGI ImageDepth tag with value > 1."""
  9030. return self.imagedepth > 1
  9031. @property
  9032. def is_vista(self) -> bool:
  9033. """Software tag is 'ISS Vista'."""
  9034. return self.software == 'ISS Vista'
  9035. @property
  9036. def is_metaseries(self) -> bool:
  9037. """Page contains MDS MetaSeries metadata in ImageDescription tag."""
  9038. if self.index != 0 or self.software != 'MetaSeries':
  9039. return False
  9040. d = self.description
  9041. return d.startswith('<MetaData>') and d.endswith('</MetaData>')
  9042. @property
  9043. def is_ome(self) -> bool:
  9044. """Page contains OME-XML in ImageDescription tag."""
  9045. if self.index != 0 or not self.description:
  9046. return False
  9047. return self.description[-10:].strip().endswith('OME>')
  9048. @property
  9049. def is_scn(self) -> bool:
  9050. """Page contains Leica SCN XML in ImageDescription tag."""
  9051. if self.index != 0 or not self.description:
  9052. return False
  9053. return self.description[-10:].strip().endswith('</scn>')
  9054. @property
  9055. def is_micromanager(self) -> bool:
  9056. """Page contains MicroManagerMetadata tag."""
  9057. return 51123 in self.tags
  9058. @property
  9059. def is_andor(self) -> bool:
  9060. """Page contains Andor Technology tags 4864-5030."""
  9061. return 4864 in self.tags
  9062. @property
  9063. def is_nuvu(self) -> bool:
  9064. """Page contains Nuvu cameras tags >= 65000."""
  9065. return (
  9066. 65000 in self.tags
  9067. and 65001 in self.tags
  9068. and self.tags[65000].dtype == 2
  9069. and self.tags[65000].value.startswith("Field '65001' is ")
  9070. )
  9071. @property
  9072. def is_pilatus(self) -> bool:
  9073. """Page contains Pilatus tags."""
  9074. return self.software[:8] == 'TVX TIFF' and self.description[:2] == '# '
  9075. @property
  9076. def is_epics(self) -> bool:
  9077. """Page contains EPICS areaDetector tags."""
  9078. return (
  9079. self.description == 'EPICS areaDetector'
  9080. or self.software == 'EPICS areaDetector'
  9081. )
  9082. @property
  9083. def is_tvips(self) -> bool:
  9084. """Page contains TVIPS metadata."""
  9085. return 37706 in self.tags
  9086. @property
  9087. def is_fei(self) -> bool:
  9088. """Page contains FEI_SFEG or FEI_HELIOS tags."""
  9089. return 34680 in self.tags or 34682 in self.tags
  9090. @property
  9091. def is_sem(self) -> bool:
  9092. """Page contains CZ_SEM tag."""
  9093. return 34118 in self.tags
  9094. @property
  9095. def is_svs(self) -> bool:
  9096. """Page contains Aperio metadata."""
  9097. return self.description[:7] == 'Aperio '
  9098. @property
  9099. def is_bif(self) -> bool:
  9100. """Page contains Ventana metadata."""
  9101. try:
  9102. return 700 in self.tags and (
  9103. # avoid reading XMP tag from file at this point
  9104. # b'<iScan' in self.tags[700].value[:4096]
  9105. 'Ventana' in self.software
  9106. or self.software[:17] == 'ScanOutputManager'
  9107. or self.description
  9108. in {'Label Image', 'Label_Image', 'Probability_Image'}
  9109. )
  9110. except Exception:
  9111. return False
  9112. @property
  9113. def is_scanimage(self) -> bool:
  9114. """Page contains ScanImage metadata."""
  9115. return (
  9116. self.software[:3] == 'SI.'
  9117. or self.description[:6] == 'state.'
  9118. or 'scanimage.SI' in self.description[-256:]
  9119. )
  9120. @property
  9121. def is_indica(self) -> bool:
  9122. """Page contains IndicaLabs metadata."""
  9123. return self.software[:21] == 'IndicaLabsImageWriter'
  9124. @property
  9125. def is_avs(self) -> bool:
  9126. """Page contains Argos AVS XML metadata."""
  9127. try:
  9128. return (
  9129. 65000 in self.tags and self.tags.valueof(65000)[:6] == '<Argos'
  9130. )
  9131. except Exception:
  9132. return False
  9133. @property
  9134. def is_qpi(self) -> bool:
  9135. """Page contains PerkinElmer tissue images metadata."""
  9136. # The ImageDescription tag contains XML with a top-level
  9137. # <PerkinElmer-QPI-ImageDescription> element
  9138. return self.software[:15] == 'PerkinElmer-QPI'
  9139. @property
  9140. def is_geotiff(self) -> bool:
  9141. """Page contains GeoTIFF metadata."""
  9142. return 34735 in self.tags # GeoKeyDirectoryTag
  9143. @property
  9144. def is_gdal(self) -> bool:
  9145. """Page contains GDAL metadata."""
  9146. # startswith '<GDALMetadata>'
  9147. return 42112 in self.tags # GDAL_METADATA
  9148. @property
  9149. def is_astrotiff(self) -> bool:
  9150. """Page contains AstroTIFF FITS metadata."""
  9151. return (
  9152. self.description[:7] == 'SIMPLE '
  9153. and self.description[-3:] == 'END'
  9154. )
  9155. @property
  9156. def is_streak(self) -> bool:
  9157. """Page contains Hamamatsu streak metadata."""
  9158. return (
  9159. self.description[:1] == '['
  9160. and '],' in self.description[1:32]
  9161. # and self.tags.get(315, '').value[:19] == 'Copyright Hamamatsu'
  9162. )
  9163. @property
  9164. def is_dng(self) -> bool:
  9165. """Page contains DNG metadata."""
  9166. return 50706 in self.tags # DNGVersion
  9167. @property
  9168. def is_tiffep(self) -> bool:
  9169. """Page contains TIFF/EP metadata."""
  9170. return 37398 in self.tags # TIFF/EPStandardID
  9171. @property
  9172. def is_sis(self) -> bool:
  9173. """Page contains Olympus SIS metadata."""
  9174. return 33560 in self.tags or 33471 in self.tags
  9175. @property
  9176. def is_ndpi(self) -> bool:
  9177. """Page contains NDPI metadata."""
  9178. return 65420 in self.tags and 271 in self.tags
  9179. @property
  9180. def is_philips(self) -> bool:
  9181. """Page contains Philips DP metadata."""
  9182. return self.software[:10] == 'Philips DP' and self.description[
  9183. -16:
  9184. ].strip().endswith('</DataObject>')
  9185. @property
  9186. def is_eer(self) -> bool:
  9187. """Page contains EER acquisition metadata."""
  9188. return (
  9189. self.parent.is_bigtiff
  9190. # and self.compression in {1, 65000, 65001, 65002}
  9191. and 65001 in self.tags
  9192. and self.tags[65001].dtype == 7
  9193. and self.tags[65001].value[:10] == b'<metadata>'
  9194. )
  9195. @final
  9196. class TiffFrame:
  9197. """Lightweight TIFF image file directory (IFD).
  9198. The purpose of TiffFrame is to reduce resource usage and speed up reading
  9199. image data from file compared to TiffPage.
  9200. Properties other than `offset`, `index`, `dataoffsets`, `databytecounts`,
  9201. `subifds`, and `jpegtables` are assumed to be identical with a specified
  9202. TiffPage instance, the keyframe.
  9203. TiffFrame instances have no `tags` property.
  9204. Virtual frames just reference the image data in the file. They may not
  9205. have an IFD structure in the file.
  9206. TiffFrame instances are not thread-safe. All attributes are read-only.
  9207. Parameters:
  9208. parent:
  9209. TiffFile instance to read frame from.
  9210. The file handle position must be at an offset to an IFD structure.
  9211. Only a limited number of tag values are read from file.
  9212. index:
  9213. Index of frame in IFD tree.
  9214. offset:
  9215. Position of frame in file.
  9216. keyframe:
  9217. TiffPage instance with same hash as frame.
  9218. dataoffsets:
  9219. Data offsets of "virtual frame".
  9220. databytecounts:
  9221. Data bytecounts of "virtual frame".
  9222. """
  9223. __slots__ = (
  9224. '_index',
  9225. '_keyframe',
  9226. 'databytecounts',
  9227. 'dataoffsets',
  9228. 'jpegtables',
  9229. 'offset',
  9230. 'parent',
  9231. 'subifds',
  9232. )
  9233. is_mdgel: bool = False
  9234. pages: TiffPages | None = None
  9235. # tags = {}
  9236. parent: TiffFile
  9237. """TiffFile instance frame belongs to."""
  9238. offset: int
  9239. """Position of frame in file."""
  9240. dataoffsets: tuple[int, ...]
  9241. """Positions of strips or tiles in file."""
  9242. databytecounts: tuple[int, ...]
  9243. """Size of strips or tiles in file."""
  9244. subifds: tuple[int, ...] | None
  9245. """Positions of SubIFDs in file."""
  9246. jpegtables: bytes | None
  9247. """JPEG quantization and/or Huffman tables."""
  9248. _keyframe: TiffPage | None
  9249. _index: tuple[int, ...] # index of frame in IFD tree.
  9250. def __init__(
  9251. self,
  9252. parent: TiffFile,
  9253. /,
  9254. index: int | Sequence[int],
  9255. *,
  9256. offset: int | None = None,
  9257. keyframe: TiffPage | None = None,
  9258. dataoffsets: tuple[int, ...] | None = None,
  9259. databytecounts: tuple[int, ...] | None = None,
  9260. ):
  9261. self._keyframe = None
  9262. self.parent = parent
  9263. self.offset = int(offset) if offset else 0
  9264. self.subifds = None
  9265. self.jpegtables = None
  9266. self.dataoffsets = ()
  9267. self.databytecounts = ()
  9268. if isinstance(index, int):
  9269. self._index = (index,)
  9270. else:
  9271. self._index = tuple(index)
  9272. if dataoffsets is not None and databytecounts is not None:
  9273. # initialize "virtual frame" from offsets and bytecounts
  9274. self.offset = 0 if offset is None else offset
  9275. self.dataoffsets = dataoffsets
  9276. self.databytecounts = databytecounts
  9277. self._keyframe = keyframe
  9278. return
  9279. if offset is None:
  9280. self.offset = parent.filehandle.tell()
  9281. else:
  9282. parent.filehandle.seek(offset)
  9283. if keyframe is None:
  9284. tags = {273, 279, 324, 325, 330, 347}
  9285. elif keyframe.is_contiguous:
  9286. # use databytecounts from keyframe
  9287. tags = {256, 273, 324, 330}
  9288. self.databytecounts = keyframe.databytecounts
  9289. else:
  9290. tags = {256, 273, 279, 324, 325, 330, 347}
  9291. for code, tag in self._gettags(tags):
  9292. if code in {273, 324}:
  9293. self.dataoffsets = tag.value
  9294. elif code in {279, 325}:
  9295. self.databytecounts = tag.value
  9296. elif code == 330:
  9297. self.subifds = tag.value
  9298. elif code == 347:
  9299. self.jpegtables = tag.value
  9300. elif keyframe is None or (
  9301. code == 256 and keyframe.tags[256].value != tag.value
  9302. ):
  9303. raise RuntimeError('incompatible keyframe')
  9304. if not self.dataoffsets:
  9305. logger().warning(f'{self!r} is missing required tags')
  9306. elif keyframe is not None and len(self.dataoffsets) != len(
  9307. keyframe.dataoffsets
  9308. ):
  9309. raise RuntimeError('incompatible keyframe')
  9310. if keyframe is not None:
  9311. self.keyframe = keyframe
  9312. def _gettags(
  9313. self,
  9314. codes: Container[int] | None = None,
  9315. /,
  9316. lock: threading.RLock | None = None,
  9317. ) -> list[tuple[int, TiffTag]]:
  9318. """Return list of (code, TiffTag) from file."""
  9319. fh = self.parent.filehandle
  9320. tiff = self.parent.tiff
  9321. unpack = struct.unpack
  9322. rlock: Any = NullContext() if lock is None else lock
  9323. tags = []
  9324. with rlock:
  9325. fh.seek(self.offset)
  9326. try:
  9327. tagno = unpack(tiff.tagnoformat, fh.read(tiff.tagnosize))[0]
  9328. if tagno > 4096:
  9329. raise ValueError(f'suspicious number of tags {tagno}')
  9330. except Exception as exc:
  9331. raise TiffFileError(
  9332. f'corrupted tag list @{self.offset}'
  9333. ) from exc
  9334. tagoffset = self.offset + tiff.tagnosize # fh.tell()
  9335. tagsize = tiff.tagsize
  9336. tagindex = -tagsize
  9337. codeformat = tiff.tagformat1[:2]
  9338. tagbytes = fh.read(tagsize * tagno)
  9339. for _ in range(tagno):
  9340. tagindex += tagsize
  9341. code = unpack(codeformat, tagbytes[tagindex : tagindex + 2])[0]
  9342. if codes and code not in codes:
  9343. continue
  9344. try:
  9345. tag = TiffTag.fromfile(
  9346. self.parent,
  9347. offset=tagoffset + tagindex,
  9348. header=tagbytes[tagindex : tagindex + tagsize],
  9349. )
  9350. except TiffFileError as exc:
  9351. logger().error(
  9352. f'{self!r} <TiffTag.fromfile> raised {exc!r:.128}'
  9353. )
  9354. continue
  9355. tags.append((code, tag))
  9356. return tags
  9357. def _nextifd(self) -> int:
  9358. """Return offset to next IFD from file."""
  9359. return TiffPage._nextifd(self) # type: ignore[arg-type]
  9360. def aspage(self) -> TiffPage:
  9361. """Return TiffPage from file.
  9362. Raise ValueError if frame is virtual.
  9363. """
  9364. if self.is_virtual:
  9365. raise ValueError('cannot return virtual frame as page')
  9366. fh = self.parent.filehandle
  9367. closed = fh.closed
  9368. if closed:
  9369. # this is an inefficient resort in case a user calls aspage
  9370. # of a TiffFrame with a closed FileHandle.
  9371. warnings.warn(
  9372. f'{self!r} reading TiffPage from closed file',
  9373. UserWarning,
  9374. stacklevel=2,
  9375. )
  9376. fh.open()
  9377. try:
  9378. fh.seek(self.offset)
  9379. page = TiffPage(self.parent, index=self.index)
  9380. finally:
  9381. if closed:
  9382. fh.close()
  9383. return page
  9384. def asarray(self, *args: Any, **kwargs: Any) -> NDArray[Any]:
  9385. """Return image from frame as NumPy array.
  9386. Parameters:
  9387. **kwargs: Arguments passed to :py:meth:`TiffPage.asarray`.
  9388. """
  9389. return TiffPage.asarray(
  9390. self, *args, **kwargs # type: ignore[arg-type]
  9391. )
  9392. def aszarr(self, **kwargs: Any) -> ZarrTiffStore:
  9393. """Return image from frame as Zarr store.
  9394. Parameters:
  9395. **kwarg: Arguments passed to :py:class:`ZarrTiffStore`.
  9396. """
  9397. from .zarr import ZarrTiffStore
  9398. return ZarrTiffStore(self, **kwargs)
  9399. def asrgb(self, *args: Any, **kwargs: Any) -> NDArray[Any]:
  9400. """Return image from frame as RGB(A). Work in progress. Do not use.
  9401. :meta private:
  9402. """
  9403. return TiffPage.asrgb(self, *args, **kwargs) # type: ignore[arg-type]
  9404. def segments(self, *args: Any, **kwargs: Any) -> Iterator[
  9405. tuple[
  9406. NDArray[Any] | None,
  9407. tuple[int, int, int, int, int],
  9408. tuple[int, int, int, int],
  9409. ]
  9410. ]:
  9411. """Return iterator over decoded tiles or strips.
  9412. Parameters:
  9413. **kwargs: Arguments passed to :py:meth:`TiffPage.segments`.
  9414. :meta private:
  9415. """
  9416. return TiffPage.segments(
  9417. self, *args, **kwargs # type: ignore[arg-type]
  9418. )
  9419. @property
  9420. def index(self) -> int:
  9421. """Index of frame in IFD chain."""
  9422. return self._index[-1]
  9423. @property
  9424. def treeindex(self) -> tuple[int, ...]:
  9425. """Index of frame in IFD tree."""
  9426. return self._index
  9427. @property
  9428. def keyframe(self) -> TiffPage | None:
  9429. """TiffPage with same properties as this frame."""
  9430. return self._keyframe
  9431. @keyframe.setter
  9432. def keyframe(self, keyframe: TiffPage, /) -> None:
  9433. if self._keyframe == keyframe:
  9434. return
  9435. if self._keyframe is not None:
  9436. raise RuntimeError('cannot reset keyframe')
  9437. if len(self.dataoffsets) != len(keyframe.dataoffsets):
  9438. raise RuntimeError('incompatible keyframe')
  9439. if keyframe.is_contiguous:
  9440. self.databytecounts = keyframe.databytecounts
  9441. self._keyframe = keyframe
  9442. @property
  9443. def is_frame(self) -> bool:
  9444. """Object is :py:class:`TiffFrame` instance."""
  9445. return True
  9446. @property
  9447. def is_virtual(self) -> bool:
  9448. """Frame does not have IFD structure in file."""
  9449. return self.offset <= 0
  9450. @property
  9451. def is_subifd(self) -> bool:
  9452. """Frame is SubIFD of another page."""
  9453. return len(self._index) > 1
  9454. @property
  9455. def is_final(self) -> bool:
  9456. assert self._keyframe is not None
  9457. return self._keyframe.is_final
  9458. @property
  9459. def is_contiguous(self) -> bool:
  9460. assert self._keyframe is not None
  9461. return self._keyframe.is_contiguous
  9462. @property
  9463. def is_memmappable(self) -> bool:
  9464. assert self._keyframe is not None
  9465. return self._keyframe.is_memmappable
  9466. @property
  9467. def hash(self) -> int:
  9468. assert self._keyframe is not None
  9469. return self._keyframe.hash
  9470. @property
  9471. def shape(self) -> tuple[int, ...]:
  9472. assert self._keyframe is not None
  9473. return self._keyframe.shape
  9474. @property
  9475. def shaped(self) -> tuple[int, int, int, int, int]:
  9476. assert self._keyframe is not None
  9477. return self._keyframe.shaped
  9478. @property
  9479. def chunks(self) -> tuple[int, ...]:
  9480. assert self._keyframe is not None
  9481. return self._keyframe.chunks
  9482. @property
  9483. def chunked(self) -> tuple[int, ...]:
  9484. assert self._keyframe is not None
  9485. return self._keyframe.chunked
  9486. @property
  9487. def tile(self) -> tuple[int, ...] | None:
  9488. assert self._keyframe is not None
  9489. return self._keyframe.tile
  9490. @property
  9491. def name(self) -> str:
  9492. index = self._index if len(self._index) > 1 else self._index[0]
  9493. return f'TiffFrame {index}'
  9494. @property
  9495. def ndim(self) -> int:
  9496. assert self._keyframe is not None
  9497. return self._keyframe.ndim
  9498. @property
  9499. def dims(self) -> tuple[str, ...]:
  9500. assert self._keyframe is not None
  9501. return self._keyframe.dims
  9502. @property
  9503. def sizes(self) -> dict[str, int]:
  9504. assert self._keyframe is not None
  9505. return self._keyframe.sizes
  9506. @property
  9507. def coords(self) -> dict[str, NDArray[Any]]:
  9508. assert self._keyframe is not None
  9509. return self._keyframe.coords
  9510. @property
  9511. def size(self) -> int:
  9512. assert self._keyframe is not None
  9513. return self._keyframe.size
  9514. @property
  9515. def nbytes(self) -> int:
  9516. assert self._keyframe is not None
  9517. return self._keyframe.nbytes
  9518. @property
  9519. def dtype(self) -> numpy.dtype[Any] | None:
  9520. assert self._keyframe is not None
  9521. return self._keyframe.dtype
  9522. @property
  9523. def axes(self) -> str:
  9524. assert self._keyframe is not None
  9525. return self._keyframe.axes
  9526. def get_resolution(
  9527. self,
  9528. unit: RESUNIT | int | None = None,
  9529. scale: float | None = None,
  9530. ) -> tuple[float, float]:
  9531. assert self._keyframe is not None
  9532. return self._keyframe.get_resolution(unit, scale)
  9533. @property
  9534. def resolution(self) -> tuple[float, float]:
  9535. assert self._keyframe is not None
  9536. return self._keyframe.resolution
  9537. @property
  9538. def resolutionunit(self) -> int:
  9539. assert self._keyframe is not None
  9540. return self._keyframe.resolutionunit
  9541. @property
  9542. def datetime(self) -> DateTime | None:
  9543. # TODO: TiffFrame.datetime can differ from TiffPage.datetime?
  9544. assert self._keyframe is not None
  9545. return self._keyframe.datetime
  9546. @property
  9547. def compression(self) -> int:
  9548. assert self._keyframe is not None
  9549. return self._keyframe.compression
  9550. @property
  9551. def decode(
  9552. self,
  9553. ) -> Callable[
  9554. ...,
  9555. tuple[
  9556. NDArray[Any] | None,
  9557. tuple[int, int, int, int, int],
  9558. tuple[int, int, int, int],
  9559. ],
  9560. ]:
  9561. assert self._keyframe is not None
  9562. return self._keyframe.decode
  9563. def __repr__(self) -> str:
  9564. index = self._index if len(self._index) > 1 else self._index[0]
  9565. return f'<tifffile.TiffFrame {index} @{self.offset}>'
  9566. def __str__(self) -> str:
  9567. return self._str()
  9568. def _str(self, detail: int = 0, width: int = 79) -> str:
  9569. """Return string containing information about TiffFrame."""
  9570. if self._keyframe is None:
  9571. info = ''
  9572. kf = None
  9573. else:
  9574. info = ' '.join(
  9575. s
  9576. for s in (
  9577. 'x'.join(str(i) for i in self.shape),
  9578. str(self.dtype),
  9579. )
  9580. )
  9581. kf = self._keyframe._str(width=width - 11)
  9582. if detail > 3:
  9583. of = pformat(self.dataoffsets, width=width - 9, height=detail - 3)
  9584. bc = pformat(
  9585. self.databytecounts, width=width - 13, height=detail - 3
  9586. )
  9587. info = f'\n Keyframe {kf}\n Offsets {of}\n Bytecounts {bc}'
  9588. index = self._index if len(self._index) > 1 else self._index[0]
  9589. return f'TiffFrame {index} @{self.offset} {info}'
  9590. @final
  9591. class TiffPages(Sequence[TiffPage | TiffFrame]):
  9592. """Sequence of TIFF image file directories (IFD chain).
  9593. TiffPages instances have a state, such as a cache and keyframe, and are not
  9594. thread-safe. All attributes are read-only.
  9595. Parameters:
  9596. arg:
  9597. If a *TiffFile*, the file position must be at offset to offset to
  9598. TiffPage.
  9599. If a *TiffPage* or *TiffFrame*, page offsets are read from the
  9600. SubIFDs tag.
  9601. Only the first page is initially read from the file.
  9602. index:
  9603. Position of IFD chain in IFD tree.
  9604. """
  9605. parent: TiffFile | None = None
  9606. """TiffFile instance pages belongs to."""
  9607. _pages: list[TiffPage | TiffFrame | int] # list of pages
  9608. _keyframe: TiffPage | None
  9609. _tiffpage: type[TiffPage | TiffFrame] # class used for reading pages
  9610. _indexed: bool
  9611. _cached: bool
  9612. _cache: bool
  9613. _offset: int
  9614. _nextpageoffset: int | None
  9615. _index: tuple[int, ...] | None
  9616. def __init__(
  9617. self,
  9618. arg: TiffFile | TiffPage | TiffFrame,
  9619. /,
  9620. *,
  9621. index: Sequence[int] | int | None = None,
  9622. ) -> None:
  9623. offset: int
  9624. self.parent = None
  9625. self._pages = [] # cache of TiffPages, TiffFrames, or their offsets
  9626. self._indexed = False # True if offsets to all pages were read
  9627. self._cached = False # True if all pages were read into cache
  9628. self._tiffpage = TiffPage # class used for reading pages
  9629. self._keyframe = None # page that is currently used as keyframe
  9630. self._cache = False # do not cache frames or pages (if not keyframe)
  9631. self._offset = 0
  9632. self._nextpageoffset = None
  9633. if index is None:
  9634. self._index = None
  9635. elif isinstance(index, (int, numpy.integer)):
  9636. self._index = (int(index),)
  9637. else:
  9638. self._index = tuple(index)
  9639. if isinstance(arg, TiffFile):
  9640. # read offset to first page from current file position
  9641. self.parent = arg
  9642. fh = self.parent.filehandle
  9643. self._nextpageoffset = fh.tell()
  9644. offset = struct.unpack(
  9645. self.parent.tiff.offsetformat,
  9646. fh.read(self.parent.tiff.offsetsize),
  9647. )[0]
  9648. if offset == 0:
  9649. logger().warning(f'{arg!r} contains no pages')
  9650. self._indexed = True
  9651. return
  9652. elif arg.subifds is not None:
  9653. # use offsets from SubIFDs tag
  9654. offsets = arg.subifds
  9655. self.parent = arg.parent
  9656. fh = self.parent.filehandle
  9657. if len(offsets) == 0 or offsets[0] == 0:
  9658. logger().warning(f'{arg!r} contains invalid SubIFDs')
  9659. self._indexed = True
  9660. return
  9661. offset = offsets[0]
  9662. else:
  9663. self._indexed = True
  9664. return
  9665. self._offset = offset
  9666. if offset >= fh.size:
  9667. logger().warning(
  9668. f'{self!r} invalid offset to first page {offset!r}'
  9669. )
  9670. self._indexed = True
  9671. return
  9672. pageindex: int | tuple[int, ...] = (
  9673. 0 if self._index is None else (*self._index, 0)
  9674. )
  9675. # read and cache first page
  9676. fh.seek(offset)
  9677. page = TiffPage(self.parent, index=pageindex)
  9678. self._pages.append(page)
  9679. self._keyframe = page
  9680. if self._nextpageoffset is None:
  9681. # offsets from SubIFDs tag
  9682. self._pages.extend(offsets[1:])
  9683. self._indexed = True
  9684. self._cached = True
  9685. @property
  9686. def pages(self) -> list[TiffPage | TiffFrame | int]:
  9687. """Deprecated. Use the TiffPages sequence interface.
  9688. :meta private:
  9689. """
  9690. warnings.warn(
  9691. '<tifffile.TiffPages.pages> is deprecated since 2024.5.22. '
  9692. 'Use the TiffPages sequence interface.',
  9693. DeprecationWarning,
  9694. stacklevel=2,
  9695. )
  9696. return self._pages
  9697. @property
  9698. def first(self) -> TiffPage:
  9699. """First page as TiffPage if exists, else raise IndexError."""
  9700. return cast(TiffPage, self._pages[0])
  9701. @property
  9702. def is_multipage(self) -> bool:
  9703. """IFD chain contains more than one page."""
  9704. try:
  9705. self._seek(1)
  9706. except IndexError:
  9707. return False
  9708. return True
  9709. @property
  9710. def cache(self) -> bool:
  9711. """Pages and frames are being cached.
  9712. When set to *False*, the cache is cleared.
  9713. """
  9714. return self._cache
  9715. @cache.setter
  9716. def cache(self, value: bool, /) -> None:
  9717. value = bool(value)
  9718. if self._cache and not value:
  9719. self._clear()
  9720. self._cache = value
  9721. @property
  9722. def useframes(self) -> bool:
  9723. """Use TiffFrame (True) or TiffPage (False)."""
  9724. return self._tiffpage == TiffFrame
  9725. @useframes.setter
  9726. def useframes(self, value: bool, /) -> None:
  9727. self._tiffpage = TiffFrame if value else TiffPage
  9728. @property
  9729. def keyframe(self) -> TiffPage | None:
  9730. """TiffPage used as keyframe for new TiffFrames."""
  9731. return self._keyframe
  9732. def set_keyframe(self, index: int, /) -> None:
  9733. """Set keyframe to TiffPage specified by `index`.
  9734. If not found in the cache, the TiffPage at `index` is loaded from file
  9735. and added to the cache.
  9736. """
  9737. if not isinstance(index, (int, numpy.integer)):
  9738. raise TypeError(f'indices must be integers, not {type(index)}')
  9739. index = int(index)
  9740. if index < 0:
  9741. index %= len(self)
  9742. if self._keyframe is not None and self._keyframe.index == index:
  9743. return
  9744. if index == 0:
  9745. self._keyframe = cast(TiffPage, self._pages[0])
  9746. return
  9747. if self._indexed or index < len(self._pages):
  9748. page = self._pages[index]
  9749. if isinstance(page, TiffPage):
  9750. self._keyframe = page
  9751. return
  9752. if isinstance(page, TiffFrame):
  9753. # remove existing TiffFrame
  9754. self._pages[index] = page.offset
  9755. # load TiffPage from file
  9756. tiffpage = self._tiffpage
  9757. self._tiffpage = TiffPage
  9758. try:
  9759. self._keyframe = cast(TiffPage, self._getitem(index))
  9760. finally:
  9761. self._tiffpage = tiffpage
  9762. # always cache keyframes
  9763. self._pages[index] = self._keyframe
  9764. @property
  9765. def next_page_offset(self) -> int | None:
  9766. """Offset where offset to new page can be stored."""
  9767. if not self._indexed:
  9768. self._seek(-1)
  9769. return self._nextpageoffset
  9770. def get(
  9771. self,
  9772. key: int,
  9773. /,
  9774. default: TiffPage | TiffFrame | None = None,
  9775. *,
  9776. validate: int = 0,
  9777. cache: bool = False,
  9778. aspage: bool = True,
  9779. ) -> TiffPage | TiffFrame:
  9780. """Return specified page from cache or file.
  9781. The specified TiffPage or TiffFrame is read from file if it is not
  9782. found in the cache.
  9783. Parameters:
  9784. key:
  9785. Index of requested page in IFD chain.
  9786. default:
  9787. Page or frame to return if key is out of bounds.
  9788. By default, an IndexError is raised if key is out of bounds.
  9789. validate:
  9790. If non-zero, raise RuntimeError if value does not match hash
  9791. of TiffPage or TiffFrame.
  9792. cache:
  9793. Store returned page in cache for future use.
  9794. aspage:
  9795. Return TiffPage instance.
  9796. """
  9797. try:
  9798. return self._getitem(
  9799. key, validate=validate, cache=cache, aspage=aspage
  9800. )
  9801. except IndexError:
  9802. if default is None:
  9803. raise
  9804. return default
  9805. def _load(
  9806. self,
  9807. keyframe: TiffPage | bool | None = True, # noqa: FBT001, FBT002
  9808. /,
  9809. ) -> None:
  9810. """Read all remaining pages from file."""
  9811. assert self.parent is not None
  9812. if self._cached:
  9813. return
  9814. pages = self._pages
  9815. if not pages:
  9816. return
  9817. if not self._indexed:
  9818. self._seek(-1)
  9819. if not self._cache:
  9820. return
  9821. fh = self.parent.filehandle
  9822. if keyframe is not None:
  9823. keyframe = self._keyframe
  9824. for i, page in enumerate(pages):
  9825. if isinstance(page, (int, numpy.integer)):
  9826. pageindex: int | tuple[int, ...] = (
  9827. i if self._index is None else (*self._index, i)
  9828. )
  9829. fh.seek(page)
  9830. pages[i] = self._tiffpage(
  9831. self.parent, index=pageindex, keyframe=keyframe
  9832. )
  9833. self._cached = True
  9834. def _load_virtual_frames(self) -> None:
  9835. """Calculate virtual TiffFrames."""
  9836. assert self.parent is not None
  9837. pages = self._pages
  9838. try:
  9839. if len(pages) > 1:
  9840. raise ValueError('pages already loaded')
  9841. page = cast(TiffPage, pages[0])
  9842. if not page.is_contiguous:
  9843. raise ValueError('data not contiguous')
  9844. self._seek(4)
  9845. # following pages are int
  9846. delta = cast(int, pages[2]) - cast(int, pages[1])
  9847. if (
  9848. cast(int, pages[3]) - cast(int, pages[2]) != delta
  9849. or cast(int, pages[4]) - cast(int, pages[3]) != delta
  9850. ):
  9851. raise ValueError('page offsets not equidistant')
  9852. page1 = self._getitem(1, validate=page.hash)
  9853. offsetoffset = page1.dataoffsets[0] - page1.offset
  9854. if offsetoffset < 0 or offsetoffset > delta:
  9855. raise ValueError('page offsets not equidistant')
  9856. pages = [page, page1]
  9857. filesize = self.parent.filehandle.size - delta
  9858. for index, offset in enumerate(
  9859. range(page1.offset + delta, filesize, delta)
  9860. ):
  9861. index += 2 # noqa: PLW2901
  9862. d = index * delta
  9863. dataoffsets = tuple(i + d for i in page.dataoffsets)
  9864. offset_or_none = offset if offset < 2**31 - 1 else None
  9865. pages.append(
  9866. TiffFrame(
  9867. page.parent,
  9868. index=(
  9869. index
  9870. if self._index is None
  9871. else (*self._index, index)
  9872. ),
  9873. offset=offset_or_none,
  9874. dataoffsets=dataoffsets,
  9875. databytecounts=page.databytecounts,
  9876. keyframe=page,
  9877. )
  9878. )
  9879. self._pages = pages
  9880. self._cache = True
  9881. self._cached = True
  9882. self._indexed = True
  9883. except Exception as exc:
  9884. if self.parent.filehandle.size >= 2147483648:
  9885. logger().warning(
  9886. f'{self!r} <_load_virtual_frames> raised {exc!r:.128}'
  9887. )
  9888. def _clear(self, /, *, fully: bool = True) -> None:
  9889. """Delete all but first page from cache. Set keyframe to first page."""
  9890. pages = self._pages
  9891. if not pages:
  9892. return
  9893. self._keyframe = cast(TiffPage, pages[0])
  9894. if fully:
  9895. # delete all but first TiffPage/TiffFrame
  9896. for i, page in enumerate(pages[1:]):
  9897. if not isinstance(page, int) and page.offset is not None:
  9898. pages[i + 1] = page.offset
  9899. else:
  9900. # delete only TiffFrames
  9901. for i, page in enumerate(pages):
  9902. if isinstance(page, TiffFrame) and page.offset is not None:
  9903. pages[i] = page.offset
  9904. self._cached = False
  9905. def _seek(self, index: int, /) -> int:
  9906. """Seek file to offset of page specified by index and return offset."""
  9907. assert self.parent is not None
  9908. pages = self._pages
  9909. lenpages = len(pages)
  9910. if lenpages == 0:
  9911. raise IndexError('index out of range')
  9912. fh = self.parent.filehandle
  9913. if fh.closed:
  9914. raise ValueError('seek of closed file')
  9915. if self._indexed or 0 <= index < lenpages:
  9916. page = pages[index]
  9917. offset = page if isinstance(page, int) else page.offset
  9918. return fh.seek(offset)
  9919. tiff = self.parent.tiff
  9920. offsetformat = tiff.offsetformat
  9921. offsetsize = tiff.offsetsize
  9922. tagnoformat = tiff.tagnoformat
  9923. tagnosize = tiff.tagnosize
  9924. tagsize = tiff.tagsize
  9925. unpack = struct.unpack
  9926. page = pages[-1]
  9927. offset = page if isinstance(page, int) else page.offset
  9928. while lenpages < 2**32:
  9929. # read offsets to pages from file until index is reached
  9930. fh.seek(offset)
  9931. # skip tags
  9932. try:
  9933. tagno = int(unpack(tagnoformat, fh.read(tagnosize))[0])
  9934. if tagno > 4096:
  9935. raise TiffFileError(f'suspicious number of tags {tagno}')
  9936. except Exception as exc:
  9937. logger().error(
  9938. f'{self!r} corrupted tag list of page '
  9939. f'{lenpages} @{offset} raised {exc!r:.128}',
  9940. )
  9941. del pages[-1]
  9942. lenpages -= 1
  9943. self._indexed = True
  9944. break
  9945. self._nextpageoffset = offset + tagnosize + tagno * tagsize
  9946. fh.seek(self._nextpageoffset)
  9947. # read offset to next page
  9948. try:
  9949. offset = int(unpack(offsetformat, fh.read(offsetsize))[0])
  9950. except Exception as exc:
  9951. logger().error(
  9952. f'{self!r} invalid offset to page '
  9953. f'{lenpages + 1} @{self._nextpageoffset} '
  9954. f'raised {exc!r:.128}'
  9955. )
  9956. self._indexed = True
  9957. break
  9958. if offset == 0:
  9959. self._indexed = True
  9960. break
  9961. if offset >= fh.size:
  9962. logger().error(f'{self!r} invalid page offset {offset!r}')
  9963. self._indexed = True
  9964. break
  9965. pages.append(offset)
  9966. lenpages += 1
  9967. if 0 <= index < lenpages:
  9968. break
  9969. # detect some circular references
  9970. if lenpages == 100:
  9971. for i, p in enumerate(pages[:-1]):
  9972. if offset == (p if isinstance(p, int) else p.offset):
  9973. index = i
  9974. self._pages = pages[: i + 1]
  9975. self._indexed = True
  9976. logger().error(
  9977. f'{self!r} invalid circular reference to IFD '
  9978. f'{i} at {offset=}'
  9979. )
  9980. break
  9981. if index >= lenpages:
  9982. raise IndexError('index out of range')
  9983. page = pages[index]
  9984. return fh.seek(page if isinstance(page, int) else page.offset)
  9985. def _getlist(
  9986. self,
  9987. key: int | slice | Iterable[int] | None = None,
  9988. /,
  9989. *,
  9990. useframes: bool = True,
  9991. validate: bool = True,
  9992. ) -> list[TiffPage | TiffFrame]:
  9993. """Return specified pages as list of TiffPages or TiffFrames.
  9994. The first item is a TiffPage, and is used as a keyframe for
  9995. following TiffFrames.
  9996. """
  9997. getitem = self._getitem
  9998. _useframes = self.useframes
  9999. if key is None:
  10000. key = iter(range(len(self)))
  10001. elif isinstance(key, (int, numpy.integer)):
  10002. # return single TiffPage
  10003. key = int(key)
  10004. self.useframes = False
  10005. if key == 0:
  10006. return [self.first]
  10007. try:
  10008. return [getitem(key)]
  10009. finally:
  10010. self.useframes = _useframes
  10011. elif isinstance(key, slice):
  10012. start, stop, _ = key.indices(2**31 - 1)
  10013. if not self._indexed and max(stop, start) > len(self._pages):
  10014. self._seek(-1)
  10015. key = iter(range(*key.indices(len(self._pages))))
  10016. elif isinstance(key, Iterable):
  10017. key = iter(key)
  10018. else:
  10019. raise TypeError(
  10020. f'key must be an integer, slice, or iterable, not {type(key)}'
  10021. )
  10022. # use first page as keyframe
  10023. assert self._keyframe is not None
  10024. keyframe = self._keyframe
  10025. self.set_keyframe(next(key))
  10026. validhash = self._keyframe.hash if validate else 0
  10027. if useframes:
  10028. self.useframes = True
  10029. try:
  10030. pages = [getitem(i, validate=validhash) for i in key]
  10031. pages.insert(0, self._keyframe)
  10032. finally:
  10033. # restore state
  10034. self._keyframe = keyframe
  10035. if useframes:
  10036. self.useframes = _useframes
  10037. return pages
  10038. def _getitem(
  10039. self,
  10040. key: int,
  10041. /,
  10042. *,
  10043. validate: int = 0, # hash
  10044. cache: bool = False,
  10045. aspage: bool = False,
  10046. ) -> TiffPage | TiffFrame:
  10047. """Return specified page from cache or file."""
  10048. assert self.parent is not None
  10049. key = int(key)
  10050. pages = self._pages
  10051. if key < 0:
  10052. key %= len(self)
  10053. elif self._indexed and key >= len(pages):
  10054. raise IndexError(f'index {key} out of range({len(pages)})')
  10055. tiffpage = TiffPage if aspage else self._tiffpage
  10056. if key < len(pages):
  10057. page = pages[key]
  10058. if self._cache and not aspage:
  10059. if not isinstance(page, (int, numpy.integer)):
  10060. if validate and validate != page.hash:
  10061. raise RuntimeError('page hash mismatch')
  10062. return page
  10063. elif isinstance(page, (TiffPage, tiffpage)):
  10064. # page is not an int
  10065. if (
  10066. validate
  10067. and validate != page.hash # type: ignore[union-attr]
  10068. ):
  10069. raise RuntimeError('page hash mismatch')
  10070. return page # type: ignore[return-value]
  10071. pageindex: int | tuple[int, ...] = (
  10072. key if self._index is None else (*self._index, key)
  10073. )
  10074. self._seek(key)
  10075. page = tiffpage(self.parent, index=pageindex, keyframe=self._keyframe)
  10076. assert isinstance(page, (TiffPage, TiffFrame))
  10077. if validate and validate != page.hash:
  10078. raise RuntimeError('page hash mismatch')
  10079. if self._cache or cache:
  10080. pages[key] = page
  10081. return page
  10082. @overload
  10083. def __getitem__(self, key: int, /) -> TiffPage | TiffFrame: ...
  10084. @overload
  10085. def __getitem__(
  10086. self, key: slice | Iterable[int], /
  10087. ) -> list[TiffPage | TiffFrame]: ...
  10088. def __getitem__(
  10089. self, key: int | slice | Iterable[int], /
  10090. ) -> TiffPage | TiffFrame | list[TiffPage | TiffFrame]:
  10091. pages = self._pages
  10092. getitem = self._getitem
  10093. if isinstance(key, (int, numpy.integer)):
  10094. key = int(key)
  10095. if key == 0:
  10096. return cast(TiffPage, pages[key])
  10097. return getitem(key)
  10098. if isinstance(key, slice):
  10099. start, stop, _ = key.indices(2**31 - 1)
  10100. if not self._indexed and max(stop, start) > len(pages):
  10101. self._seek(-1)
  10102. return [getitem(i) for i in range(*key.indices(len(pages)))]
  10103. if isinstance(key, Iterable):
  10104. return [getitem(k) for k in key]
  10105. raise TypeError('key must be an integer, slice, or iterable')
  10106. def __iter__(self) -> Iterator[TiffPage | TiffFrame]:
  10107. i = 0
  10108. while True:
  10109. try:
  10110. yield self._getitem(i)
  10111. i += 1
  10112. except IndexError:
  10113. break
  10114. if self._cache:
  10115. self._cached = True
  10116. def __bool__(self) -> bool:
  10117. """Return True if file contains any pages."""
  10118. return len(self._pages) > 0
  10119. def __len__(self) -> int:
  10120. """Return number of pages in file."""
  10121. if not self._indexed:
  10122. self._seek(-1)
  10123. return len(self._pages)
  10124. def __repr__(self) -> str:
  10125. return f'<tifffile.TiffPages @{self._offset}>'
  10126. @final
  10127. class TiffTag:
  10128. """TIFF tag structure.
  10129. TiffTag instances are not thread-safe. All attributes are read-only.
  10130. Parameters:
  10131. parent:
  10132. TIFF file tag belongs to.
  10133. offset:
  10134. Position of tag structure in file.
  10135. code:
  10136. Decimal code of tag.
  10137. dtype:
  10138. Data type of tag value item.
  10139. count:
  10140. Number of items in tag value.
  10141. valueoffset:
  10142. Position of tag value in file.
  10143. """
  10144. __slots__ = (
  10145. '_value',
  10146. 'code',
  10147. 'count',
  10148. 'dtype',
  10149. 'offset',
  10150. 'parent',
  10151. 'valueoffset',
  10152. )
  10153. parent: TiffFile | TiffWriter
  10154. """TIFF file tag belongs to."""
  10155. offset: int
  10156. """Position of tag structure in file."""
  10157. code: int
  10158. """Decimal code of tag."""
  10159. dtype: int
  10160. """:py:class:`DATATYPE` of tag value item."""
  10161. count: int
  10162. """Number of items in tag value."""
  10163. valueoffset: int
  10164. """Position of tag value in file."""
  10165. _value: Any
  10166. def __init__(
  10167. self,
  10168. parent: TiffFile | TiffWriter,
  10169. offset: int,
  10170. code: int,
  10171. dtype: DATATYPE | int,
  10172. count: int,
  10173. value: Any,
  10174. valueoffset: int,
  10175. /,
  10176. ) -> None:
  10177. self.parent = parent
  10178. self.offset = int(offset)
  10179. self.code = int(code)
  10180. self.count = int(count)
  10181. self._value = value
  10182. self.valueoffset = valueoffset
  10183. try:
  10184. self.dtype = DATATYPE(dtype)
  10185. except ValueError:
  10186. self.dtype = int(dtype)
  10187. @classmethod
  10188. def fromfile(
  10189. cls,
  10190. parent: TiffFile,
  10191. /,
  10192. *,
  10193. offset: int | None = None,
  10194. header: bytes | None = None,
  10195. validate: bool = True,
  10196. ) -> TiffTag:
  10197. """Return TiffTag instance from file.
  10198. Parameters:
  10199. parent:
  10200. TiffFile instance tag is read from.
  10201. offset:
  10202. Position of tag structure in file.
  10203. The default is the position of the file handle.
  10204. header:
  10205. Tag structure as bytes.
  10206. The default is read from the file.
  10207. validate:
  10208. Raise TiffFileError if data type or value offset are invalid.
  10209. Raises:
  10210. TiffFileError:
  10211. Data type or value offset are invalid and `validate` is *True*.
  10212. """
  10213. tiff = parent.tiff
  10214. fh = parent.filehandle
  10215. if header is None:
  10216. if offset is None:
  10217. offset = fh.tell()
  10218. else:
  10219. fh.seek(offset)
  10220. header = fh.read(tiff.tagsize)
  10221. elif offset is None:
  10222. offset = fh.tell()
  10223. valueoffset = offset + tiff.tagsize - tiff.tagoffsetthreshold
  10224. code, dtype, count, value = struct.unpack(
  10225. tiff.tagformat1 + tiff.tagformat2[1:], header
  10226. )
  10227. try:
  10228. valueformat = TIFF.DATA_FORMATS[dtype]
  10229. except KeyError as exc:
  10230. msg = (
  10231. f'<tifffile.TiffTag {code} @{offset}> '
  10232. f'invalid data type {dtype!r}'
  10233. )
  10234. if validate:
  10235. raise TiffFileError(msg) from exc
  10236. logger().error(msg)
  10237. return cls(parent, offset, code, dtype, count, None, 0)
  10238. valuesize = count * struct.calcsize(valueformat)
  10239. if (
  10240. valuesize > tiff.tagoffsetthreshold
  10241. or code in TIFF.TAG_READERS # TODO: only works with offsets?
  10242. ):
  10243. valueoffset = struct.unpack(tiff.offsetformat, value)[0]
  10244. if validate and code in TIFF.TAG_LOAD:
  10245. value = TiffTag._read_value(
  10246. parent, offset, code, dtype, count, valueoffset
  10247. )
  10248. elif valueoffset < 8 or valueoffset + valuesize > fh.size:
  10249. msg = (
  10250. f'<tifffile.TiffTag {code} @{offset}> '
  10251. f'invalid value offset {valueoffset}'
  10252. )
  10253. if validate:
  10254. raise TiffFileError(msg)
  10255. logger().warning(msg)
  10256. value = None
  10257. elif code in TIFF.TAG_LOAD:
  10258. value = TiffTag._read_value(
  10259. parent, offset, code, dtype, count, valueoffset
  10260. )
  10261. else:
  10262. value = None
  10263. elif dtype in {1, 2, 7}:
  10264. # BYTES, ASCII, UNDEFINED
  10265. value = value[:valuesize]
  10266. elif (
  10267. tiff.is_ndpi
  10268. and count == 1
  10269. and dtype in {4, 9, 13}
  10270. and value[4:] != b'\x00\x00\x00\x00'
  10271. ):
  10272. # NDPI IFD or LONG, for example, in StripOffsets or StripByteCounts
  10273. value = struct.unpack('<Q', value)
  10274. else:
  10275. fmt = (
  10276. f'{tiff.byteorder}'
  10277. f'{count * int(valueformat[0])}'
  10278. f'{valueformat[1]}'
  10279. )
  10280. value = struct.unpack(fmt, value[:valuesize])
  10281. value = TiffTag._process_value(value, code, dtype, offset)
  10282. return cls(parent, offset, code, dtype, count, value, valueoffset)
  10283. @staticmethod
  10284. def _read_value(
  10285. parent: TiffFile | TiffWriter,
  10286. offset: int,
  10287. code: int,
  10288. dtype: int,
  10289. count: int,
  10290. valueoffset: int,
  10291. /,
  10292. ) -> Any:
  10293. """Read tag value from file."""
  10294. try:
  10295. valueformat = TIFF.DATA_FORMATS[dtype]
  10296. except KeyError as exc:
  10297. raise TiffFileError(
  10298. f'<tifffile.TiffTag {code} @{offset}> '
  10299. f'invalid data type {dtype!r}'
  10300. ) from exc
  10301. fh = parent.filehandle
  10302. byteorder = parent.tiff.byteorder
  10303. offsetsize = parent.tiff.offsetsize
  10304. valuesize = count * struct.calcsize(valueformat)
  10305. if valueoffset < 8 or valueoffset + valuesize > fh.size:
  10306. raise TiffFileError(
  10307. f'<tifffile.TiffTag {code} @{offset}> '
  10308. f'invalid value offset {valueoffset}'
  10309. )
  10310. # if valueoffset % 2:
  10311. # logger().warning(
  10312. # f'<tifffile.TiffTag {code} @{offset}> '
  10313. # 'value does not begin on word boundary'
  10314. # )
  10315. fh.seek(valueoffset)
  10316. if code in TIFF.TAG_READERS:
  10317. readfunc = TIFF.TAG_READERS[code]
  10318. try:
  10319. value = readfunc(fh, byteorder, dtype, count, offsetsize)
  10320. except Exception as exc:
  10321. logger().warning(
  10322. f'<tifffile.TiffTag {code} @{offset}> raised {exc!r:.128}'
  10323. )
  10324. else:
  10325. return value
  10326. if dtype in {1, 2, 7}:
  10327. # BYTES, ASCII, UNDEFINED
  10328. value = fh.read(valuesize)
  10329. if len(value) != valuesize:
  10330. logger().warning(
  10331. f'<tifffile.TiffTag {code} @{offset}> '
  10332. 'could not read all values'
  10333. )
  10334. elif code not in TIFF.TAG_TUPLE and count > 1024:
  10335. value = read_numpy(fh, byteorder, dtype, count, offsetsize)
  10336. else:
  10337. value = struct.unpack(
  10338. f'{byteorder}{count * int(valueformat[0])}{valueformat[1]}',
  10339. fh.read(valuesize),
  10340. )
  10341. return value
  10342. @staticmethod
  10343. def _process_value(
  10344. value: Any, code: int, dtype: int, offset: int, /
  10345. ) -> Any:
  10346. """Process tag value."""
  10347. if (
  10348. value is None
  10349. or dtype in {1, 7} # BYTE, UNDEFINED
  10350. or code in TIFF.TAG_READERS
  10351. or not isinstance(value, (bytes, str, tuple))
  10352. ):
  10353. return value
  10354. if dtype == 2:
  10355. # TIFF ASCII fields can contain multiple strings,
  10356. # each terminated with a NUL
  10357. value = value.rstrip(b'\x00')
  10358. try:
  10359. value = value.decode('utf-8').strip()
  10360. except UnicodeDecodeError:
  10361. try:
  10362. value = value.decode('cp1252').strip()
  10363. except UnicodeDecodeError as exc:
  10364. logger().warning(
  10365. f'<tifffile.TiffTag {code} @{offset}> '
  10366. f'coercing invalid ASCII to bytes, due to {exc!r:.128}'
  10367. )
  10368. return value
  10369. if code in TIFF.TAG_ENUM:
  10370. t = TIFF.TAG_ENUM[code]
  10371. try:
  10372. value = tuple(t(v) for v in value)
  10373. except ValueError as exc:
  10374. if code not in {259, 317}: # ignore compression/predictor
  10375. logger().warning(
  10376. f'<tifffile.TiffTag {code} @{offset}> '
  10377. f'raised {exc!r:.128}'
  10378. )
  10379. if len(value) == 1 and code not in TIFF.TAG_TUPLE:
  10380. value = value[0]
  10381. return value
  10382. @property
  10383. def value(self) -> Any:
  10384. """Value of tag, delay-loaded from file if necessary."""
  10385. if self._value is None:
  10386. # print(
  10387. # f'_read_value {self.code} {TIFF.TAGS.get(self.code)} '
  10388. # f'{self.dtype}[{self.count}] @{self.valueoffset} '
  10389. # )
  10390. fh = self.parent.filehandle
  10391. with fh.lock:
  10392. closed = fh.closed
  10393. if closed:
  10394. # this is an inefficient resort in case a user delay loads
  10395. # tag values from a TiffPage with a closed FileHandle.
  10396. warnings.warn(
  10397. f'{self!r} reading value from closed file',
  10398. UserWarning,
  10399. stacklevel=2,
  10400. )
  10401. fh.open()
  10402. try:
  10403. value = TiffTag._read_value(
  10404. self.parent,
  10405. self.offset,
  10406. self.code,
  10407. self.dtype,
  10408. self.count,
  10409. self.valueoffset,
  10410. )
  10411. finally:
  10412. if closed:
  10413. fh.close()
  10414. self._value = TiffTag._process_value(
  10415. value,
  10416. self.code,
  10417. self.dtype,
  10418. self.offset,
  10419. )
  10420. return self._value
  10421. @value.setter
  10422. def value(self, value: Any, /) -> None:
  10423. self._value = value
  10424. @property
  10425. def dtype_name(self) -> str:
  10426. """Name of data type of tag value."""
  10427. try:
  10428. return self.dtype.name # type: ignore[attr-defined]
  10429. except AttributeError:
  10430. return f'TYPE{self.dtype}'
  10431. @property
  10432. def name(self) -> str:
  10433. """Name of tag from :py:attr:`_TIFF.TAGS` registry."""
  10434. return TIFF.TAGS.get(self.code, str(self.code))
  10435. @property
  10436. def dataformat(self) -> str:
  10437. """Data type as `struct.pack` format."""
  10438. return TIFF.DATA_FORMATS[self.dtype]
  10439. @property
  10440. def valuebytecount(self) -> int:
  10441. """Number of bytes of tag value in file."""
  10442. return self.count * struct.calcsize(TIFF.DATA_FORMATS[self.dtype])
  10443. def astuple(self) -> TagTuple:
  10444. """Return tag code, dtype, count, and encoded value.
  10445. The encoded value is read from file if necessary.
  10446. """
  10447. if isinstance(self.value, bytes):
  10448. value = self.value
  10449. else:
  10450. tiff = self.parent.tiff
  10451. dataformat = TIFF.DATA_FORMATS[self.dtype]
  10452. count = self.count * int(dataformat[0])
  10453. fmt = f'{tiff.byteorder}{count}{dataformat[1]}'
  10454. try:
  10455. if self.dtype == 2:
  10456. # ASCII
  10457. value = struct.pack(fmt, self.value.encode('ascii'))
  10458. if len(value) != count:
  10459. raise ValueError
  10460. elif count == 1 and not isinstance(self.value, tuple):
  10461. value = struct.pack(fmt, self.value)
  10462. else:
  10463. value = struct.pack(fmt, *self.value)
  10464. except Exception as exc:
  10465. if tiff.is_ndpi and count == 1:
  10466. raise ValueError(
  10467. 'cannot pack 64-bit NDPI value to 32-bit dtype'
  10468. ) from exc
  10469. fh = self.parent.filehandle
  10470. pos = fh.tell()
  10471. fh.seek(self.valueoffset)
  10472. value = fh.read(struct.calcsize(fmt))
  10473. fh.seek(pos)
  10474. return self.code, int(self.dtype), self.count, value, True
  10475. def overwrite(
  10476. self,
  10477. value: Any,
  10478. /,
  10479. *,
  10480. dtype: DATATYPE | int | str | None = None,
  10481. erase: bool = True,
  10482. ) -> TiffTag:
  10483. """Write new tag value to file and return new TiffTag instance.
  10484. Warning: changing tag values in TIFF files might result in corrupted
  10485. files or have unexpected side effects.
  10486. The packed value is appended to the file if it is longer than the
  10487. old value. The file position is left where it was.
  10488. Overwriting tag values in NDPI files > 4 GB is only supported if
  10489. single integer values and new offsets do not exceed the 32-bit range.
  10490. Parameters:
  10491. value:
  10492. New tag value to write.
  10493. Must be compatible with the `struct.pack` formats corresponding
  10494. to the tag's data type.
  10495. dtype:
  10496. New tag data type. By default, the data type is not changed.
  10497. erase:
  10498. Overwrite previous tag values in file with zeros.
  10499. Raises:
  10500. struct.error:
  10501. Value is not compatible with dtype or new offset exceeds
  10502. TIFF size limit.
  10503. ValueError:
  10504. Invalid value or dtype, or old integer value in NDPI files
  10505. exceeds 32-bit range.
  10506. """
  10507. if self.offset < 8 or self.valueoffset < 8:
  10508. raise ValueError(f'cannot rewrite tag at offset {self.offset} < 8')
  10509. if hasattr(value, 'filehandle'):
  10510. # passing a TiffFile instance is deprecated and no longer required
  10511. # since 2021.7.30
  10512. raise TypeError(
  10513. 'TiffTag.overwrite got an unexpected TiffFile instance '
  10514. 'as first argument'
  10515. )
  10516. fh = self.parent.filehandle
  10517. tiff = self.parent.tiff
  10518. if tiff.is_ndpi:
  10519. # only support files < 4GB
  10520. if self.count == 1 and self.dtype in {4, 13}:
  10521. if isinstance(self.value, tuple):
  10522. v = self.value[0]
  10523. else:
  10524. v = self.value
  10525. if v > 4294967295:
  10526. raise ValueError('cannot patch NDPI > 4 GB files')
  10527. tiff = TIFF.CLASSIC_LE
  10528. if value is None:
  10529. value = b''
  10530. if dtype is None:
  10531. dtype = self.dtype
  10532. elif isinstance(dtype, str):
  10533. if len(dtype) > 1 and dtype[0] in '<>|=':
  10534. dtype = dtype[1:]
  10535. try:
  10536. dtype = TIFF.DATA_DTYPES[dtype]
  10537. except KeyError as exc:
  10538. raise ValueError(f'unknown data type {dtype!r}') from exc
  10539. else:
  10540. dtype = enumarg(DATATYPE, dtype)
  10541. packedvalue: bytes | None = None
  10542. dataformat: str
  10543. try:
  10544. dataformat = TIFF.DATA_FORMATS[dtype]
  10545. except KeyError as exc:
  10546. raise ValueError(f'unknown data type {dtype!r}') from exc
  10547. if dtype == 2:
  10548. # strings
  10549. if isinstance(value, str):
  10550. # enforce 7-bit ASCII on Unicode strings
  10551. try:
  10552. value = value.encode('ascii')
  10553. except UnicodeEncodeError as exc:
  10554. raise ValueError(
  10555. 'TIFF strings must be 7-bit ASCII'
  10556. ) from exc
  10557. elif not isinstance(value, bytes):
  10558. raise ValueError('TIFF strings must be 7-bit ASCII')
  10559. if len(value) == 0 or value[-1:] != b'\x00':
  10560. value += b'\x00'
  10561. count = len(value)
  10562. value = (value,)
  10563. elif isinstance(value, bytes):
  10564. # pre-packed binary data
  10565. dtsize = struct.calcsize(dataformat)
  10566. if len(value) % dtsize:
  10567. raise ValueError('invalid packed binary data')
  10568. count = len(value) // dtsize
  10569. packedvalue = value
  10570. value = (value,)
  10571. else:
  10572. try:
  10573. count = len(value)
  10574. except TypeError:
  10575. value = (value,)
  10576. count = 1
  10577. if dtype in {5, 10}:
  10578. if count < 2 or count % 2:
  10579. raise ValueError('invalid RATIONAL value')
  10580. count //= 2 # rational
  10581. if packedvalue is None:
  10582. packedvalue = struct.pack(
  10583. f'{tiff.byteorder}{count * int(dataformat[0])}{dataformat[1]}',
  10584. *value,
  10585. )
  10586. newsize = len(packedvalue)
  10587. oldsize = self.count * struct.calcsize(TIFF.DATA_FORMATS[self.dtype])
  10588. valueoffset = self.valueoffset
  10589. pos = fh.tell()
  10590. try:
  10591. if dtype != self.dtype:
  10592. # rewrite data type
  10593. fh.seek(self.offset + 2)
  10594. fh.write(struct.pack(tiff.byteorder + 'H', dtype))
  10595. if oldsize <= tiff.tagoffsetthreshold:
  10596. if newsize <= tiff.tagoffsetthreshold:
  10597. # inline -> inline: overwrite
  10598. fh.seek(self.offset + 4)
  10599. fh.write(struct.pack(tiff.tagformat2, count, packedvalue))
  10600. else:
  10601. # inline -> separate: append to file
  10602. fh.seek(0, os.SEEK_END)
  10603. valueoffset = fh.tell()
  10604. if valueoffset % 2:
  10605. # value offset must begin on a word boundary
  10606. fh.write(b'\x00')
  10607. valueoffset += 1
  10608. # write new offset
  10609. fh.seek(self.offset + 4)
  10610. fh.write(
  10611. struct.pack(
  10612. tiff.tagformat2,
  10613. count,
  10614. struct.pack(tiff.offsetformat, valueoffset),
  10615. )
  10616. )
  10617. # write new value
  10618. fh.seek(valueoffset)
  10619. fh.write(packedvalue)
  10620. elif newsize <= tiff.tagoffsetthreshold:
  10621. # separate -> inline: erase old value
  10622. valueoffset = (
  10623. self.offset + 4 + struct.calcsize(tiff.tagformat2[:2])
  10624. )
  10625. fh.seek(self.offset + 4)
  10626. fh.write(struct.pack(tiff.tagformat2, count, packedvalue))
  10627. if erase:
  10628. fh.seek(self.valueoffset)
  10629. fh.write(b'\x00' * oldsize)
  10630. elif newsize <= oldsize or self.valueoffset + oldsize == fh.size:
  10631. # separate -> separate smaller: overwrite, erase remaining
  10632. fh.seek(self.offset + 4)
  10633. fh.write(struct.pack(tiff.tagformat2[:2], count))
  10634. fh.seek(self.valueoffset)
  10635. fh.write(packedvalue)
  10636. if erase and oldsize - newsize > 0:
  10637. fh.write(b'\x00' * (oldsize - newsize))
  10638. else:
  10639. # separate -> separate larger: erase old value, append to file
  10640. fh.seek(0, os.SEEK_END)
  10641. valueoffset = fh.tell()
  10642. if valueoffset % 2:
  10643. # value offset must begin on a word boundary
  10644. fh.write(b'\x00')
  10645. valueoffset += 1
  10646. # write offset
  10647. fh.seek(self.offset + 4)
  10648. fh.write(
  10649. struct.pack(
  10650. tiff.tagformat2,
  10651. count,
  10652. struct.pack(tiff.offsetformat, valueoffset),
  10653. )
  10654. )
  10655. # write value
  10656. fh.seek(valueoffset)
  10657. fh.write(packedvalue)
  10658. if erase:
  10659. fh.seek(self.valueoffset)
  10660. fh.write(b'\x00' * oldsize)
  10661. finally:
  10662. fh.seek(pos) # must restore file position
  10663. return TiffTag(
  10664. self.parent,
  10665. self.offset,
  10666. self.code,
  10667. dtype,
  10668. count,
  10669. value,
  10670. valueoffset,
  10671. )
  10672. def _fix_lsm_bitspersample(self) -> None:
  10673. """Correct LSM bitspersample tag.
  10674. Old LSM writers may use a separate region for two 16-bit values,
  10675. although they fit into the tag value element of the tag.
  10676. """
  10677. if self.code != 258 or self.count != 2:
  10678. return
  10679. # TODO: test this case; need example file
  10680. logger().warning(f'{self!r} correcting LSM bitspersample tag')
  10681. value = struct.pack('<HH', *self.value)
  10682. self.valueoffset = struct.unpack('<I', value)[0]
  10683. self.parent.filehandle.seek(self.valueoffset)
  10684. self.value = struct.unpack('<HH', self.parent.filehandle.read(4))
  10685. def __repr__(self) -> str:
  10686. name = '|'.join(TIFF.TAGS.getall(self.code, []))
  10687. if name:
  10688. name = ' ' + name
  10689. return f'<tifffile.TiffTag {self.code}{name} @{self.offset}>'
  10690. def __str__(self) -> str:
  10691. return self._str()
  10692. def _str(self, detail: int = 0, width: int = 79) -> str:
  10693. """Return string containing information about TiffTag."""
  10694. height = 1 if detail <= 0 else 8 * detail
  10695. dtype = self.dtype_name
  10696. if self.count > 1:
  10697. dtype += f'[{self.count}]'
  10698. name = '|'.join(TIFF.TAGS.getall(self.code, []))
  10699. if name:
  10700. name = f'{self.code} {name} @{self.offset}'
  10701. else:
  10702. name = f'{self.code} @{self.offset}'
  10703. line = f'TiffTag {name} {dtype} @{self.valueoffset} '
  10704. line = line[:width]
  10705. try:
  10706. value = self.value
  10707. except TiffFileError:
  10708. value = 'CORRUPTED'
  10709. else:
  10710. try:
  10711. if self.count == 1:
  10712. value = enumstr(value)
  10713. else:
  10714. value = pformat(tuple(enumstr(v) for v in value))
  10715. except Exception:
  10716. if not isinstance(value, (tuple, list)):
  10717. pass
  10718. elif height == 1:
  10719. value = value[:256]
  10720. elif len(value) > 2048:
  10721. value = (
  10722. value[:1024] + value[-1024:] # type: ignore[operator]
  10723. )
  10724. value = pformat(value, width=width, height=height)
  10725. if detail <= 0:
  10726. line += '= '
  10727. line += value[:width]
  10728. line = line[:width]
  10729. else:
  10730. line += '\n' + value
  10731. return line
  10732. @final
  10733. class TiffTags:
  10734. """Multidict-like interface to TiffTag instances in TiffPage.
  10735. Differences to a regular dict:
  10736. - values are instances of :py:class:`TiffTag`.
  10737. - keys are :py:attr:`TiffTag.code` (int).
  10738. - multiple values can be stored per key.
  10739. - can be indexed by :py:attr:`TiffTag.name` (`str`), slower than by key.
  10740. - `iter()` returns values instead of keys.
  10741. - `values()` and `items()` contain all values sorted by offset.
  10742. - `len()` returns number of all values.
  10743. - `get()` takes optional index argument.
  10744. - some functions are not implemented, such as, `update` and `pop`.
  10745. """
  10746. __slots__ = ('_dict', '_list')
  10747. _dict: dict[int, TiffTag]
  10748. _list: list[dict[int, TiffTag]]
  10749. def __init__(self) -> None:
  10750. self._dict = {}
  10751. self._list = [self._dict]
  10752. def add(self, tag: TiffTag, /) -> None:
  10753. """Add tag."""
  10754. code = tag.code
  10755. for d in self._list:
  10756. if code not in d:
  10757. d[code] = tag
  10758. break
  10759. else:
  10760. self._list.append({code: tag})
  10761. def keys(self) -> list[int]:
  10762. """Return codes of all tags."""
  10763. return list(self._dict.keys())
  10764. def values(self) -> list[TiffTag]:
  10765. """Return all tags in order they are stored in file."""
  10766. tags = (t for d in self._list for t in d.values())
  10767. return sorted(tags, key=lambda t: t.offset)
  10768. def items(self) -> list[tuple[int, TiffTag]]:
  10769. """Return all (code, tag) pairs in order tags are stored in file."""
  10770. items = (i for d in self._list for i in d.items())
  10771. return sorted(items, key=lambda i: i[1].offset)
  10772. def valueof(
  10773. self,
  10774. key: int | str,
  10775. /,
  10776. default: Any = None,
  10777. index: int | None = None,
  10778. ) -> Any:
  10779. """Return value of tag by code or name if exists, else default.
  10780. Parameters:
  10781. key:
  10782. Code or name of tag to return.
  10783. default:
  10784. Another value to return if specified tag is corrupted or
  10785. not found.
  10786. index:
  10787. Specifies tag in case of multiple tags with identical code.
  10788. The default is the first tag.
  10789. """
  10790. tag = self.get(key, default=None, index=index)
  10791. if tag is None:
  10792. return default
  10793. try:
  10794. return tag.value
  10795. except TiffFileError:
  10796. return default # corrupted tag
  10797. def get(
  10798. self,
  10799. key: int | str,
  10800. /,
  10801. default: TiffTag | None = None,
  10802. index: int | None = None,
  10803. ) -> TiffTag | None:
  10804. """Return tag by code or name if exists, else default.
  10805. Parameters:
  10806. key:
  10807. Code or name of tag to return.
  10808. default:
  10809. Another tag to return if specified tag is corrupted or
  10810. not found.
  10811. index:
  10812. Specifies tag in case of multiple tags with identical code.
  10813. The default is the first tag.
  10814. """
  10815. if index is None:
  10816. if key in self._dict:
  10817. return self._dict[cast(int, key)]
  10818. if not isinstance(key, str):
  10819. return default
  10820. index = 0
  10821. try:
  10822. tags = self._list[index]
  10823. except IndexError:
  10824. return default
  10825. if key in tags:
  10826. return tags[cast(int, key)]
  10827. if not isinstance(key, str):
  10828. return default
  10829. for tag in tags.values():
  10830. if tag.name == key:
  10831. return tag
  10832. return default
  10833. def getall(
  10834. self,
  10835. key: int | str,
  10836. /,
  10837. default: Any = None,
  10838. ) -> list[TiffTag] | None:
  10839. """Return list of all tags by code or name if exists, else default.
  10840. Parameters:
  10841. key:
  10842. Code or name of tags to return.
  10843. default:
  10844. Value to return if no tags are found.
  10845. """
  10846. result: list[TiffTag] = []
  10847. for tags in self._list:
  10848. if key in tags:
  10849. result.append(tags[cast(int, key)])
  10850. else:
  10851. break
  10852. if result:
  10853. return result
  10854. if not isinstance(key, str):
  10855. return default
  10856. for tags in self._list:
  10857. for tag in tags.values():
  10858. if tag.name == key:
  10859. result.append(tag)
  10860. break
  10861. if not result:
  10862. break
  10863. return result if result else default
  10864. def __getitem__(self, key: int | str, /) -> TiffTag:
  10865. """Return first tag by code or name. Raise KeyError if not found."""
  10866. if key in self._dict:
  10867. return self._dict[cast(int, key)]
  10868. if not isinstance(key, str):
  10869. raise KeyError(key)
  10870. for tag in self._dict.values():
  10871. if tag.name == key:
  10872. return tag
  10873. raise KeyError(key)
  10874. def __setitem__(self, code: int, tag: TiffTag, /) -> None:
  10875. """Add tag."""
  10876. assert tag.code == code
  10877. self.add(tag)
  10878. def __delitem__(self, key: int | str, /) -> None:
  10879. """Delete all tags by code or name."""
  10880. found = False
  10881. for tags in self._list:
  10882. if key in tags:
  10883. found = True
  10884. del tags[cast(int, key)]
  10885. else:
  10886. break
  10887. if found:
  10888. return
  10889. if not isinstance(key, str):
  10890. raise KeyError(key)
  10891. for tags in self._list:
  10892. for tag in tags.values():
  10893. if tag.name == key:
  10894. del tags[tag.code]
  10895. found = True
  10896. break
  10897. else:
  10898. break
  10899. if not found:
  10900. raise KeyError(key)
  10901. return
  10902. def __contains__(self, item: object, /) -> bool:
  10903. """Return if tag is in map."""
  10904. if item in self._dict:
  10905. return True
  10906. if not isinstance(item, str):
  10907. return False
  10908. return any(tag.name == item for tag in self._dict.values())
  10909. def __iter__(self) -> Iterator[TiffTag]:
  10910. """Return iterator over all tags."""
  10911. return iter(self.values())
  10912. def __len__(self) -> int:
  10913. """Return number of tags."""
  10914. size = 0
  10915. for d in self._list:
  10916. size += len(d)
  10917. return size
  10918. def __repr__(self) -> str:
  10919. return f'<tifffile.TiffTags @0x{id(self):016X}>'
  10920. def __str__(self) -> str:
  10921. return self._str()
  10922. def _str(self, detail: int = 0, width: int = 79) -> str:
  10923. """Return string with information about TiffTags."""
  10924. info = []
  10925. tlines = []
  10926. vlines = []
  10927. for tag in self:
  10928. value = tag._str(width=width + 1)
  10929. tlines.append(value[:width].strip())
  10930. if detail > 0 and len(value) > width:
  10931. try:
  10932. value = tag.value
  10933. except Exception: # noqa: S112
  10934. # delay load failed or closed file
  10935. continue
  10936. if tag.code in {273, 279, 324, 325}:
  10937. if detail < 1:
  10938. value = value[:256]
  10939. elif len(value) > 1024:
  10940. value = value[:512] + value[-512:]
  10941. value = pformat(value, width=width, height=detail * 3)
  10942. else:
  10943. value = pformat(value, width=width, height=detail * 8)
  10944. if tag.count > 1:
  10945. vlines.append(
  10946. f'{tag.name} {tag.dtype_name}[{tag.count}]\n{value}'
  10947. )
  10948. else:
  10949. vlines.append(f'{tag.name}\n{value}')
  10950. info.append('\n'.join(tlines))
  10951. if detail > 0 and vlines:
  10952. info.append('\n')
  10953. info.append('\n\n'.join(vlines))
  10954. return '\n'.join(info)
  10955. @final
  10956. class TiffTagRegistry:
  10957. """Registry of TIFF tag codes and names.
  10958. Map tag codes and names to names and codes respectively.
  10959. One tag code may be registered with several names, for example, 34853 is
  10960. used for GPSTag or OlympusSIS2.
  10961. Different tag codes may be registered with the same name, for example,
  10962. 37387 and 41483 are both named FlashEnergy.
  10963. Parameters:
  10964. arg: Mapping of codes to names.
  10965. Examples:
  10966. >>> tags = TiffTagRegistry([(34853, 'GPSTag'), (34853, 'OlympusSIS2')])
  10967. >>> tags.add(37387, 'FlashEnergy')
  10968. >>> tags.add(41483, 'FlashEnergy')
  10969. >>> tags['GPSTag']
  10970. 34853
  10971. >>> tags[34853]
  10972. 'GPSTag'
  10973. >>> tags.getall(34853)
  10974. ['GPSTag', 'OlympusSIS2']
  10975. >>> tags.getall('FlashEnergy')
  10976. [37387, 41483]
  10977. >>> len(tags)
  10978. 4
  10979. """
  10980. __slots__ = ('_dict', '_list')
  10981. _dict: dict[int | str, str | int]
  10982. _list: list[dict[int | str, str | int]]
  10983. def __init__(
  10984. self,
  10985. arg: TiffTagRegistry | dict[int, str] | Sequence[tuple[int, str]],
  10986. /,
  10987. ) -> None:
  10988. self._dict = {}
  10989. self._list = [self._dict]
  10990. self.update(arg)
  10991. def update(
  10992. self,
  10993. arg: TiffTagRegistry | dict[int, str] | Sequence[tuple[int, str]],
  10994. /,
  10995. ) -> None:
  10996. """Add mapping of codes to names to registry.
  10997. Parameters:
  10998. arg: Mapping of codes to names.
  10999. """
  11000. if isinstance(arg, TiffTagRegistry):
  11001. self._list.extend(arg._list)
  11002. return
  11003. if isinstance(arg, dict):
  11004. arg = list(arg.items())
  11005. for code, name in arg:
  11006. self.add(code, name)
  11007. def add(self, code: int, name: str, /) -> None:
  11008. """Add code and name to registry."""
  11009. for d in self._list:
  11010. if code in d and d[code] == name:
  11011. break
  11012. if code not in d and name not in d:
  11013. d[code] = name
  11014. d[name] = code
  11015. break
  11016. else:
  11017. self._list.append({code: name, name: code})
  11018. def items(self) -> list[tuple[int, str]]:
  11019. """Return all registry items as (code, name)."""
  11020. items = (
  11021. i for d in self._list for i in d.items() if isinstance(i[0], int)
  11022. )
  11023. return sorted(items, key=lambda i: i[0]) # type: ignore[arg-type]
  11024. @overload
  11025. def get(self, key: int, /, default: None) -> str | None: ...
  11026. @overload
  11027. def get(self, key: str, /, default: None) -> int | None: ...
  11028. @overload
  11029. def get(self, key: int, /, default: str) -> str: ...
  11030. def get(
  11031. self, key: int | str, /, default: str | None = None
  11032. ) -> str | int | None:
  11033. """Return first code or name if exists, else default.
  11034. Parameters:
  11035. key: tag code or name to lookup.
  11036. default: value to return if key is not found.
  11037. """
  11038. for d in self._list:
  11039. if key in d:
  11040. return d[key]
  11041. return default
  11042. @overload
  11043. def getall(self, key: int, /, default: None) -> list[str] | None: ...
  11044. @overload
  11045. def getall(self, key: str, /, default: None) -> list[int] | None: ...
  11046. @overload
  11047. def getall(self, key: int, /, default: list[str]) -> list[str]: ...
  11048. def getall(
  11049. self, key: int | str, /, default: list[str] | None = None
  11050. ) -> list[str] | list[int] | None:
  11051. """Return list of all codes or names if exists, else default.
  11052. Parameters:
  11053. key: tag code or name to lookup.
  11054. default: value to return if key is not found.
  11055. """
  11056. result = [d[key] for d in self._list if key in d]
  11057. return result if result else default # type: ignore[return-value]
  11058. @overload
  11059. def __getitem__(self, key: int, /) -> str: ...
  11060. @overload
  11061. def __getitem__(self, key: str, /) -> int: ...
  11062. def __getitem__(self, key: int | str, /) -> int | str:
  11063. """Return first code or name. Raise KeyError if not found."""
  11064. for d in self._list:
  11065. if key in d:
  11066. return d[key]
  11067. raise KeyError(key)
  11068. def __delitem__(self, key: int | str, /) -> None:
  11069. """Delete all tags of code or name."""
  11070. found = False
  11071. for d in self._list:
  11072. if key in d:
  11073. found = True
  11074. value = d[key]
  11075. del d[key]
  11076. del d[value]
  11077. if not found:
  11078. raise KeyError(key)
  11079. def __contains__(self, item: int | str, /) -> bool:
  11080. """Return if code or name is in registry."""
  11081. return any(item in d for d in self._list)
  11082. def __iter__(self) -> Iterator[tuple[int, str]]:
  11083. """Return iterator over all items in registry."""
  11084. return iter(self.items())
  11085. def __len__(self) -> int:
  11086. """Return number of registered tags."""
  11087. size = 0
  11088. for d in self._list:
  11089. size += len(d)
  11090. return size // 2
  11091. def __repr__(self) -> str:
  11092. return f'<tifffile.TiffTagRegistry @0x{id(self):016X}>'
  11093. def __str__(self) -> str:
  11094. return 'TiffTagRegistry(((\n {}\n))'.format(
  11095. ',\n '.join(f'({code}, {name!r})' for code, name in self.items())
  11096. )
  11097. @final
  11098. class TiffPageSeries(Sequence[TiffPage | TiffFrame | None]):
  11099. """Sequence of TIFF pages making up multi-dimensional image.
  11100. Many TIFF based formats, such as OME-TIFF, use series of TIFF pages to
  11101. store chunks of larger, multi-dimensional images.
  11102. The image shape and position of chunks in the multi-dimensional image is
  11103. defined in format-specific metadata.
  11104. All pages in a series must have the same :py:meth:`TiffPage.hash`,
  11105. that is, the same shape, data type, and storage properties.
  11106. Items of a series may be None (missing) or instances of
  11107. :py:class:`TiffPage` or :py:class:`TiffFrame`, possibly belonging to
  11108. different files.
  11109. Parameters:
  11110. pages:
  11111. List of TiffPage, TiffFrame, or None.
  11112. The file handles of TiffPages or TiffFrames may not be open.
  11113. shape:
  11114. Shape of image array in series.
  11115. dtype:
  11116. Data type of image array in series.
  11117. axes:
  11118. Character codes for dimensions in shape.
  11119. Length must match shape.
  11120. attr:
  11121. Arbitrary metadata associated with series.
  11122. index:
  11123. Index of series in multi-series files.
  11124. parent:
  11125. TiffFile instance series belongs to.
  11126. name:
  11127. Name of series.
  11128. kind:
  11129. Nature of series, such as, 'ome' or 'imagej'.
  11130. truncated:
  11131. Series is truncated, for example, ImageJ hyperstack > 4 GB.
  11132. multifile:
  11133. Series contains pages from multiple files.
  11134. squeeze:
  11135. Remove length-1 dimensions (except X and Y) from shape and axes
  11136. by default.
  11137. transform:
  11138. Function to transform image data after decoding.
  11139. """
  11140. levels: list[TiffPageSeries]
  11141. """Multi-resolution, pyramidal levels. ``levels[0] is self``."""
  11142. parent: TiffFile | None
  11143. """TiffFile instance series belongs to."""
  11144. keyframe: TiffPage
  11145. """TiffPage of series."""
  11146. dtype: numpy.dtype[Any]
  11147. """Data type (native byte order) of image array in series."""
  11148. kind: str
  11149. """Nature of series."""
  11150. name: str
  11151. """Name of image series from metadata."""
  11152. transform: Callable[[NDArray[Any]], NDArray[Any]] | None
  11153. """Function to transform image data after decoding."""
  11154. is_multifile: bool
  11155. """Series contains pages from multiple files."""
  11156. is_truncated: bool
  11157. """Series contains single page describing multi-dimensional image."""
  11158. _pages: list[TiffPage | TiffFrame | None]
  11159. # List of pages in series.
  11160. # Might contain only first page of contiguous series
  11161. _index: int # index of series in multi-series files
  11162. _squeeze: bool
  11163. _axes: str
  11164. _axes_squeezed: str
  11165. _shape: tuple[int, ...]
  11166. _shape_squeezed: tuple[int, ...]
  11167. _len: int
  11168. _attr: dict[str, Any]
  11169. def __init__(
  11170. self,
  11171. pages: Sequence[TiffPage | TiffFrame | None],
  11172. /,
  11173. shape: Sequence[int] | None = None,
  11174. dtype: DTypeLike | None = None,
  11175. axes: str | None = None,
  11176. *,
  11177. attr: dict[str, Any] | None = None,
  11178. coords: Mapping[str, NDArray[Any] | None] | None = None,
  11179. index: int | None = None,
  11180. parent: TiffFile | None = None,
  11181. name: str | None = None,
  11182. kind: str | None = None,
  11183. truncated: bool = False,
  11184. multifile: bool = False,
  11185. squeeze: bool = True,
  11186. transform: Callable[[NDArray[Any]], NDArray[Any]] | None = None,
  11187. ) -> None:
  11188. self._shape = ()
  11189. self._shape_squeezed = ()
  11190. self._axes = ''
  11191. self._axes_squeezed = ''
  11192. self._attr = {} if attr is None else dict(attr)
  11193. self._index = int(index) if index else 0
  11194. self._pages = list(pages)
  11195. self.levels = [self]
  11196. npages = len(self._pages)
  11197. try:
  11198. # find open TiffPage
  11199. keyframe = next(
  11200. p.keyframe
  11201. for p in self._pages
  11202. if p is not None
  11203. and p.keyframe is not None
  11204. and not p.keyframe.parent.filehandle.closed
  11205. )
  11206. except StopIteration:
  11207. keyframe = next(
  11208. p.keyframe
  11209. for p in self._pages
  11210. if p is not None and p.keyframe is not None
  11211. )
  11212. if shape is None:
  11213. shape = keyframe.shape
  11214. if axes is None:
  11215. axes = keyframe.axes
  11216. if dtype is None:
  11217. dtype = keyframe.dtype
  11218. self.dtype = numpy.dtype(dtype)
  11219. self.kind = kind if kind else ''
  11220. self.name = name if name else ''
  11221. self.transform = transform
  11222. self.keyframe = keyframe
  11223. self.is_multifile = bool(multifile)
  11224. self.is_truncated = bool(truncated)
  11225. if parent is not None:
  11226. self.parent = parent
  11227. elif self._pages:
  11228. self.parent = self.keyframe.parent
  11229. else:
  11230. self.parent = None
  11231. self._set_dimensions(shape, axes, coords, squeeze)
  11232. if not truncated and npages == 1:
  11233. s = product(keyframe.shape)
  11234. if s > 0:
  11235. self._len = int(product(self.shape) // s)
  11236. else:
  11237. self._len = npages
  11238. else:
  11239. self._len = npages
  11240. def _set_dimensions(
  11241. self,
  11242. shape: Sequence[int],
  11243. axes: str,
  11244. coords: Mapping[str, NDArray[Any] | None] | None = None,
  11245. squeeze: bool = True, # noqa: FBT001, FBT002
  11246. /,
  11247. ) -> None:
  11248. """Set shape, axes, and coords."""
  11249. self._squeeze = bool(squeeze)
  11250. self._shape = tuple(shape)
  11251. self._axes = axes
  11252. self._shape_squeezed, self._axes_squeezed, _ = squeeze_axes(
  11253. shape, axes
  11254. )
  11255. @property
  11256. def shape(self) -> tuple[int, ...]:
  11257. """Shape of image array in series."""
  11258. return self._shape_squeezed if self._squeeze else self._shape
  11259. @property
  11260. def axes(self) -> str:
  11261. """Character codes for dimensions in image array."""
  11262. return self._axes_squeezed if self._squeeze else self._axes
  11263. @property
  11264. def coords(self) -> dict[str, NDArray[Any]]:
  11265. """Ordered map of dimension names to coordinate arrays."""
  11266. raise NotImplementedError
  11267. # return {
  11268. # name: numpy.arange(size)
  11269. # for name, size in zip(self.dims, self.shape)
  11270. # }
  11271. def get_shape(
  11272. self, squeeze: bool | None = None # noqa: FBT001
  11273. ) -> tuple[int, ...]:
  11274. """Return default, squeezed, or expanded shape of series.
  11275. Parameters:
  11276. squeeze: Remove length-1 dimensions from shape.
  11277. """
  11278. if squeeze is None:
  11279. squeeze = self._squeeze
  11280. return self._shape_squeezed if squeeze else self._shape
  11281. def get_axes(self, squeeze: bool | None = None) -> str: # noqa: FBT001
  11282. """Return default, squeezed, or expanded axes of series.
  11283. Parameters:
  11284. squeeze: Remove length-1 dimensions from axes.
  11285. """
  11286. if squeeze is None:
  11287. squeeze = self._squeeze
  11288. return self._axes_squeezed if squeeze else self._axes
  11289. def get_coords(
  11290. self, squeeze: bool | None = None # noqa: FBT001
  11291. ) -> dict[str, NDArray[Any]]:
  11292. """Return default, squeezed, or expanded coords of series.
  11293. Parameters:
  11294. squeeze: Remove length-1 dimensions from coords.
  11295. """
  11296. raise NotImplementedError
  11297. def asarray(
  11298. self, *, level: int | None = None, **kwargs: Any
  11299. ) -> NDArray[Any]:
  11300. """Return images from series of pages as NumPy array.
  11301. Parameters:
  11302. level:
  11303. Pyramid level to return.
  11304. By default, the base layer is returned.
  11305. **kwargs:
  11306. Additional arguments passed to :py:meth:`TiffFile.asarray`.
  11307. """
  11308. if self.parent is None:
  11309. raise ValueError('no parent')
  11310. if level is not None:
  11311. return self.levels[level].asarray(**kwargs)
  11312. result = self.parent.asarray(series=self, **kwargs)
  11313. if self.transform is not None:
  11314. result = self.transform(result)
  11315. return result
  11316. def aszarr(
  11317. self, *, level: int | None = None, **kwargs: Any
  11318. ) -> ZarrTiffStore:
  11319. """Return image array from series of pages as Zarr store.
  11320. Parameters:
  11321. level:
  11322. Pyramid level to return.
  11323. By default, a multi-resolution store is returned.
  11324. **kwargs:
  11325. Additional arguments passed to :py:class:`ZarrTiffStore`.
  11326. """
  11327. if self.parent is None:
  11328. raise ValueError('no parent')
  11329. from .zarr import ZarrTiffStore
  11330. return ZarrTiffStore(self, level=level, **kwargs)
  11331. @cached_property
  11332. def dataoffset(self) -> int | None:
  11333. """Offset to contiguous image data in file."""
  11334. if not self._pages:
  11335. return None
  11336. pos = 0
  11337. for page in self._pages:
  11338. if page is None or len(page.dataoffsets) == 0:
  11339. return None
  11340. if not page.is_final:
  11341. return None
  11342. if not pos:
  11343. pos = page.dataoffsets[0] + page.nbytes
  11344. continue
  11345. if pos != page.dataoffsets[0]:
  11346. return None
  11347. pos += page.nbytes
  11348. page = self._pages[0]
  11349. if page is None or len(page.dataoffsets) == 0:
  11350. return None
  11351. offset = page.dataoffsets[0]
  11352. if (
  11353. len(self._pages) == 1
  11354. and isinstance(page, TiffPage)
  11355. and (page.is_imagej or page.is_shaped or page.is_stk)
  11356. ):
  11357. # truncated files
  11358. return offset
  11359. if pos == offset + product(self.shape) * self.dtype.itemsize:
  11360. return offset
  11361. return None
  11362. @property
  11363. def is_pyramidal(self) -> bool:
  11364. """Series contains multiple resolutions."""
  11365. return len(self.levels) > 1
  11366. @cached_property
  11367. def attr(self) -> dict[str, Any]:
  11368. """Arbitrary metadata associated with series."""
  11369. return self._attr
  11370. @property
  11371. def ndim(self) -> int:
  11372. """Number of array dimensions."""
  11373. return len(self.shape)
  11374. @property
  11375. def dims(self) -> tuple[str, ...]:
  11376. """Names of dimensions in image array."""
  11377. # return tuple(self.coords.keys())
  11378. return tuple(
  11379. unique_strings(TIFF.AXES_NAMES.get(ax, ax) for ax in self.axes)
  11380. )
  11381. @property
  11382. def sizes(self) -> dict[str, int]:
  11383. """Ordered map of dimension names to lengths."""
  11384. # return dict(zip(self.coords.keys(), self.shape))
  11385. return dict(zip(self.dims, self.shape, strict=True))
  11386. @cached_property
  11387. def size(self) -> int:
  11388. """Number of elements in array."""
  11389. return product(self.shape)
  11390. @cached_property
  11391. def nbytes(self) -> int:
  11392. """Number of bytes in array."""
  11393. return self.size * self.dtype.itemsize
  11394. @property
  11395. def pages(self) -> TiffPageSeries:
  11396. # sequence of TiffPages or TiffFrame in series
  11397. # a workaround to keep the old interface working
  11398. return self
  11399. def _getitem(self, key: int, /) -> TiffPage | TiffFrame | None:
  11400. """Return specified page of series from cache or file."""
  11401. key = int(key)
  11402. if key < 0:
  11403. key %= self._len
  11404. if len(self._pages) == 1 and 0 < key < self._len:
  11405. page = self._pages[0]
  11406. assert page is not None
  11407. assert self.parent is not None
  11408. return self.parent.pages._getitem(page.index + key)
  11409. return self._pages[key]
  11410. @overload
  11411. def __getitem__(
  11412. self, key: int | numpy.integer[Any], /
  11413. ) -> TiffPage | TiffFrame | None: ...
  11414. @overload
  11415. def __getitem__(
  11416. self, key: slice | Iterable[int], /
  11417. ) -> list[TiffPage | TiffFrame | None]: ...
  11418. def __getitem__(
  11419. self, key: int | numpy.integer[Any] | slice | Iterable[int], /
  11420. ) -> TiffPage | TiffFrame | list[TiffPage | TiffFrame | None] | None:
  11421. """Return specified page(s)."""
  11422. if isinstance(key, (int, numpy.integer)):
  11423. return self._getitem(int(key))
  11424. if isinstance(key, slice):
  11425. return [self._getitem(i) for i in range(*key.indices(self._len))]
  11426. if isinstance(key, Iterable) and not isinstance(key, str):
  11427. return [self._getitem(k) for k in key]
  11428. raise TypeError('key must be an integer, slice, or iterable')
  11429. def __iter__(self) -> Iterator[TiffPage | TiffFrame | None]:
  11430. """Return iterator over pages in series."""
  11431. if len(self._pages) == self._len:
  11432. yield from self._pages
  11433. else:
  11434. assert self.parent is not None
  11435. assert self._pages[0] is not None
  11436. pages = self.parent.pages
  11437. index = self._pages[0].index
  11438. for i in range(self._len):
  11439. yield pages[index + i]
  11440. def __len__(self) -> int:
  11441. """Return number of pages in series."""
  11442. return self._len
  11443. def __repr__(self) -> str:
  11444. return f'<tifffile.TiffPageSeries {self._index} {self.kind}>'
  11445. def __str__(self) -> str:
  11446. s = ' '.join(
  11447. s
  11448. for s in (
  11449. snipstr(f'{self.name!r}', 20) if self.name else '',
  11450. 'x'.join(str(i) for i in self.shape),
  11451. str(self.dtype),
  11452. self.axes,
  11453. self.kind,
  11454. (f'{len(self.levels)} Levels') if self.is_pyramidal else '',
  11455. f'{len(self)} Pages',
  11456. (f'@{self.dataoffset}') if self.dataoffset else '',
  11457. )
  11458. if s
  11459. )
  11460. return f'TiffPageSeries {self._index} {s}'
  11461. class FileSequence(Sequence[str]):
  11462. r"""Sequence of files containing compatible array data.
  11463. Parameters:
  11464. imread:
  11465. Function to read image array from single file.
  11466. files:
  11467. Glob filename pattern or sequence of file names.
  11468. If *None*, use '\*'.
  11469. All files must contain array data of same shape and dtype.
  11470. Binary streams are not supported.
  11471. container:
  11472. Name or open instance of ZIP file in which files are stored.
  11473. sort:
  11474. Function to sort file names if `files` is a pattern.
  11475. The default is :py:func:`natural_sorted`.
  11476. If *False*, disable sorting.
  11477. parse:
  11478. Function to parse sequence of sorted file names to dims, shape,
  11479. chunk indices, and filtered file names.
  11480. The default is :py:func:`parse_filenames` if `kwargs`
  11481. contains `'pattern'`.
  11482. **kwargs:
  11483. Additional arguments passed to `parse` function.
  11484. Examples:
  11485. >>> filenames = ['temp_C001T002.tif', 'temp_C001T001.tif']
  11486. >>> ims = TiffSequence(filenames, pattern=r'_(C)(\d+)(T)(\d+)')
  11487. >>> ims[0]
  11488. 'temp_C001T002.tif'
  11489. >>> ims.shape
  11490. (1, 2)
  11491. >>> ims.axes
  11492. 'CT'
  11493. """
  11494. imread: Callable[..., NDArray[Any]]
  11495. """Function to read image array from single file."""
  11496. shape: tuple[int, ...]
  11497. """Shape of file series. Excludes shape of chunks in files."""
  11498. axes: str
  11499. """Character codes for dimensions in shape."""
  11500. dims: tuple[str, ...]
  11501. """Names of dimensions in shape."""
  11502. indices: tuple[tuple[int, ...]]
  11503. """Indices of files in shape."""
  11504. _files: list[str] # list of file names
  11505. _container: Any # TODO: container type?
  11506. def __init__(
  11507. self,
  11508. imread: Callable[..., NDArray[Any]],
  11509. files: (
  11510. str | os.PathLike[Any] | Sequence[str | os.PathLike[Any]] | None
  11511. ),
  11512. *,
  11513. container: str | os.PathLike[Any] | None = None,
  11514. sort: Callable[..., Any] | bool | None = None,
  11515. parse: Callable[..., Any] | None = None,
  11516. **kwargs: Any,
  11517. ) -> None:
  11518. sort_func: Callable[..., list[str]] | None = None
  11519. if files is None:
  11520. files = '*'
  11521. if sort is None:
  11522. sort_func = natural_sorted
  11523. elif callable(sort):
  11524. sort_func = sort
  11525. elif sort:
  11526. sort_func = natural_sorted
  11527. # elif not sort:
  11528. # sort_func = None
  11529. self._container = container
  11530. if container is not None:
  11531. import fnmatch
  11532. if isinstance(container, (str, os.PathLike)):
  11533. import zipfile
  11534. self._container = zipfile.ZipFile(container)
  11535. elif not hasattr(self._container, 'open'):
  11536. raise ValueError('invalid container')
  11537. if isinstance(files, str):
  11538. files = fnmatch.filter(self._container.namelist(), files)
  11539. if sort_func is not None:
  11540. files = sort_func(files)
  11541. elif isinstance(files, os.PathLike):
  11542. files = [os.fspath(files)]
  11543. elif isinstance(files, str):
  11544. files = glob.glob(files)
  11545. if sort_func is not None:
  11546. files = sort_func(files)
  11547. elif sort is not None and sort_func is not None:
  11548. # sort sequence if explicitly requested
  11549. files = sort_func(f for f in files)
  11550. files = [os.fspath(f) for f in files] # type: ignore[union-attr]
  11551. if not files:
  11552. raise ValueError('no files found')
  11553. if not callable(imread):
  11554. raise TypeError('invalid imread function')
  11555. if container:
  11556. # redefine imread to read from container
  11557. def imread_(
  11558. fname: str, _imread: Any = imread, **kwargs: Any
  11559. ) -> NDArray[Any]:
  11560. with (
  11561. self._container.open(fname) as handle1,
  11562. io.BytesIO(handle1.read()) as handle2,
  11563. ):
  11564. return _imread(handle2, **kwargs)
  11565. imread = imread_
  11566. if parse is None and kwargs.get('pattern'):
  11567. parse = parse_filenames
  11568. if parse:
  11569. try:
  11570. dims, shape, indices, files = parse(files, **kwargs)
  11571. except ValueError as exc:
  11572. raise ValueError('failed to parse file names') from exc
  11573. else:
  11574. dims = ('sequence',)
  11575. shape = (len(files),)
  11576. indices = tuple((i,) for i in range(len(files)))
  11577. assert isinstance(files, list)
  11578. assert isinstance(files[0], str)
  11579. codes = TIFF.AXES_CODES
  11580. axes = ''.join(codes.get(dim.lower(), dim[0].upper()) for dim in dims)
  11581. self._files = files
  11582. self.imread = imread
  11583. self.axes = axes
  11584. self.dims = tuple(dims)
  11585. self.shape = tuple(shape)
  11586. self.indices = indices
  11587. def asarray(
  11588. self,
  11589. *,
  11590. imreadargs: dict[str, Any] | None = None,
  11591. chunkshape: tuple[int, ...] | None = None,
  11592. chunkdtype: DTypeLike | None = None,
  11593. axestiled: dict[int, int] | Sequence[tuple[int, int]] | None = None,
  11594. ioworkers: int | None = 1,
  11595. out_inplace: bool | None = None,
  11596. out: OutputType = None,
  11597. **kwargs: Any,
  11598. ) -> NDArray[Any]:
  11599. """Return images from files as NumPy array.
  11600. Parameters:
  11601. imreadargs:
  11602. Arguments passed to :py:attr:`FileSequence.imread`.
  11603. chunkshape:
  11604. Shape of chunk in each file.
  11605. Must match ``FileSequence.imread(file, **imreadargs).shape``.
  11606. By default, this is determined by reading the first file.
  11607. chunkdtype:
  11608. Data type of chunk in each file.
  11609. Must match ``FileSequence.imread(file, **imreadargs).dtype``.
  11610. By default, this is determined by reading the first file.
  11611. axestiled:
  11612. Axes to be tiled.
  11613. Map stacked sequence axis to chunk axis.
  11614. ioworkers:
  11615. Maximum number of threads to execute
  11616. :py:attr:`FileSequence.imread` asynchronously.
  11617. If *0*, use up to :py:attr:`_TIFF.MAXIOWORKERS` threads.
  11618. Using threads can significantly improve runtime when reading
  11619. many small files from a network share.
  11620. If enabled, internal threading for the `imread` function
  11621. should be disabled.
  11622. out_inplace:
  11623. :py:attr:`FileSequence.imread` decodes directly to the output
  11624. instead of returning an array, which is copied to the output.
  11625. Not all imread functions support this, especially in
  11626. non-contiguous cases.
  11627. out:
  11628. Specifies how image array is returned.
  11629. By default, create a new array.
  11630. If a *numpy.ndarray*, a writable array to which the images
  11631. are copied.
  11632. If *'memmap'*, create a memory-mapped array in a temporary
  11633. file.
  11634. If a *string* or *open file*, the file used to create a
  11635. memory-mapped array.
  11636. **kwargs:
  11637. Arguments passed to :py:attr:`FileSequence.imread` in
  11638. addition to `imreadargs`.
  11639. Raises:
  11640. IndexError, ValueError: Array shapes do not match.
  11641. """
  11642. # TODO: deprecate kwargs?
  11643. files = self._files
  11644. if imreadargs is not None:
  11645. kwargs |= imreadargs
  11646. if ioworkers is None or ioworkers < 1:
  11647. ioworkers = TIFF.MAXIOWORKERS
  11648. ioworkers = min(len(files), ioworkers)
  11649. assert isinstance(ioworkers, int) # mypy bug?
  11650. if out_inplace is None and self.imread == imread:
  11651. out_inplace = True
  11652. else:
  11653. out_inplace = bool(out_inplace)
  11654. if chunkshape is None or chunkdtype is None:
  11655. im = self.imread(files[0], **kwargs)
  11656. chunkshape = im.shape
  11657. chunkdtype = im.dtype
  11658. del im
  11659. chunkdtype = numpy.dtype(chunkdtype)
  11660. assert chunkshape is not None
  11661. if axestiled:
  11662. tiled = TiledSequence(self.shape, chunkshape, axestiled=axestiled)
  11663. result = create_output(out, tiled.shape, chunkdtype)
  11664. def func(index: tuple[int | slice, ...], fname: str) -> None:
  11665. # read single image from file into result
  11666. # if index is None:
  11667. # return
  11668. if out_inplace:
  11669. self.imread(fname, out=result[index], **kwargs)
  11670. else:
  11671. im = self.imread(fname, **kwargs)
  11672. result[index] = im
  11673. del im # delete memory-mapped file
  11674. if ioworkers < 2:
  11675. for index, fname in zip(
  11676. tiled.slices(self.indices), files, strict=True
  11677. ):
  11678. func(index, fname)
  11679. else:
  11680. with ThreadPoolExecutor(ioworkers) as executor:
  11681. for _ in executor.map(
  11682. func, tiled.slices(self.indices), files
  11683. ):
  11684. pass
  11685. else:
  11686. shape = self.shape + chunkshape
  11687. result = create_output(out, shape, chunkdtype)
  11688. result = result.reshape((-1, *chunkshape))
  11689. def func(index: tuple[int | slice, ...], fname: str) -> None:
  11690. # read single image from file into result
  11691. if index is None:
  11692. return
  11693. index_ = int(
  11694. numpy.ravel_multi_index(
  11695. index, # type: ignore[arg-type]
  11696. self.shape,
  11697. )
  11698. )
  11699. if out_inplace:
  11700. self.imread(fname, out=result[index_], **kwargs)
  11701. else:
  11702. im = self.imread(fname, **kwargs)
  11703. result[index_] = im
  11704. del im # delete memory-mapped file
  11705. if ioworkers < 2:
  11706. for index, fname in zip(self.indices, files, strict=True):
  11707. func(index, fname)
  11708. else:
  11709. with ThreadPoolExecutor(ioworkers) as executor:
  11710. for _ in executor.map(func, self.indices, files):
  11711. pass
  11712. result.shape = shape
  11713. return result
  11714. def aszarr(self, **kwargs: Any) -> ZarrFileSequenceStore:
  11715. """Return images from files as Zarr store.
  11716. Parameters:
  11717. **kwargs: Arguments passed to :py:class:`ZarrFileSequenceStore`.
  11718. """
  11719. from .zarr import ZarrFileSequenceStore
  11720. return ZarrFileSequenceStore(self, **kwargs)
  11721. def close(self) -> None:
  11722. """Close open files."""
  11723. if self._container is not None:
  11724. self._container.close()
  11725. self._container = None
  11726. def commonpath(self) -> str:
  11727. """Return longest common sub-path of each file in sequence."""
  11728. if len(self._files) == 1:
  11729. commonpath = os.path.dirname(self._files[0])
  11730. else:
  11731. commonpath = os.path.commonpath(self._files)
  11732. return commonpath
  11733. @property
  11734. def files(self) -> list[str]:
  11735. """Deprecated. Use the FileSequence sequence interface.
  11736. :meta private:
  11737. """
  11738. warnings.warn(
  11739. '<tifffile.FileSequence.files> is deprecated since 2024.5.22. '
  11740. 'Use the FileSequence sequence interface.',
  11741. DeprecationWarning,
  11742. stacklevel=2,
  11743. )
  11744. return self._files
  11745. @property
  11746. def files_missing(self) -> int:
  11747. """Number of empty chunks."""
  11748. return product(self.shape) - len(self._files)
  11749. def __iter__(self) -> Iterator[str]:
  11750. """Return iterator over all file names."""
  11751. return iter(self._files)
  11752. def __len__(self) -> int:
  11753. return len(self._files)
  11754. @overload
  11755. def __getitem__(self, key: int, /) -> str: ...
  11756. @overload
  11757. def __getitem__(self, key: slice, /) -> list[str]: ...
  11758. def __getitem__(self, key: int | slice, /) -> str | list[str]:
  11759. return self._files[key]
  11760. def __enter__(self) -> Self:
  11761. return self
  11762. def __exit__(
  11763. self,
  11764. exc_type: type[BaseException] | None,
  11765. exc_value: BaseException | None,
  11766. traceback: TracebackType | None,
  11767. ) -> None:
  11768. self.close()
  11769. def __repr__(self) -> str:
  11770. return f'<tifffile.FileSequence @0x{id(self):016X}>'
  11771. def __str__(self) -> str:
  11772. file = str(self._container) if self._container else self._files[0]
  11773. file = os.path.split(file)[-1]
  11774. return '\n '.join(
  11775. (
  11776. self.__class__.__name__,
  11777. file,
  11778. f'files: {len(self._files)} ({self.files_missing} missing)',
  11779. 'shape: {}'.format(', '.join(str(i) for i in self.shape)),
  11780. 'dims: {}'.format(', '.join(s for s in self.dims)),
  11781. # f'axes: {self.axes}',
  11782. )
  11783. )
  11784. @final
  11785. class TiffSequence(FileSequence):
  11786. r"""Sequence of TIFF files containing compatible array data.
  11787. Same as :py:class:`FileSequence` with the :py:func:`imread` function,
  11788. `'\*.tif'` glob pattern, and `out_inplace` enabled by default.
  11789. """
  11790. def __init__(
  11791. self,
  11792. files: (
  11793. str | os.PathLike[Any] | Sequence[str | os.PathLike[Any]] | None
  11794. ) = None,
  11795. *,
  11796. imread: Callable[..., NDArray[Any]] = imread,
  11797. **kwargs: Any,
  11798. ) -> None:
  11799. super().__init__(imread, '*.tif' if files is None else files, **kwargs)
  11800. def __repr__(self) -> str:
  11801. return f'<tifffile.TiffSequence @0x{id(self):016X}>'
  11802. @final
  11803. class TiledSequence:
  11804. """Tiled sequence of chunks.
  11805. Transform a sequence of stacked chunks to tiled chunks.
  11806. Parameters:
  11807. stackshape:
  11808. Shape of stacked sequence excluding chunks.
  11809. chunkshape:
  11810. Shape of chunks.
  11811. axestiled:
  11812. Axes to be tiled. Map stacked sequence axis
  11813. to chunk axis. By default, the sequence is not tiled.
  11814. axes:
  11815. Character codes for dimensions in stackshape and chunkshape.
  11816. Examples:
  11817. >>> ts = TiledSequence((1, 2), (3, 4), axestiled={1: 0}, axes='ABYX')
  11818. >>> ts.shape
  11819. (1, 6, 4)
  11820. >>> ts.chunks
  11821. (1, 3, 4)
  11822. >>> ts.axes
  11823. 'AYX'
  11824. """
  11825. chunks: tuple[int, ...]
  11826. """Shape of chunks in tiled sequence."""
  11827. # with same number of dimensions as shape
  11828. shape: tuple[int, ...]
  11829. """Shape of tiled sequence including chunks."""
  11830. axes: str | tuple[str, ...] | None
  11831. """Dimensions codes of tiled sequence."""
  11832. shape_squeezed: tuple[int, ...]
  11833. """Shape of tiled sequence with length-1 dimensions removed."""
  11834. axes_squeezed: str | tuple[str, ...] | None
  11835. """Dimensions codes of tiled sequence with length-1 dimensions removed."""
  11836. _stackdims: int
  11837. """Number of dimensions in stack excluding chunks."""
  11838. _chunkdims: int
  11839. """Number of dimensions in chunks."""
  11840. _shape_untiled: tuple[int, ...]
  11841. """Shape of untiled sequence (stackshape + chunkshape)."""
  11842. _axestiled: tuple[tuple[int, int], ...]
  11843. """Map axes to tile from stack to chunks."""
  11844. def __init__(
  11845. self,
  11846. stackshape: Sequence[int],
  11847. chunkshape: Sequence[int],
  11848. /,
  11849. *,
  11850. axestiled: dict[int, int] | Sequence[tuple[int, int]] | None = None,
  11851. axes: str | Sequence[str] | None = None,
  11852. ) -> None:
  11853. self._stackdims = len(stackshape)
  11854. self._chunkdims = len(chunkshape)
  11855. self._shape_untiled = tuple(stackshape) + tuple(chunkshape)
  11856. if axes is not None and len(axes) != len(self._shape_untiled):
  11857. raise ValueError(
  11858. 'axes length does not match stackshape + chunkshape'
  11859. )
  11860. if axestiled:
  11861. axestiled = dict(axestiled)
  11862. for ax0, ax1 in axestiled.items():
  11863. axestiled[ax0] = ax1 + self._stackdims
  11864. self._axestiled = tuple(sorted(axestiled.items(), reverse=True))
  11865. axes_list = [] if axes is None else list(axes)
  11866. shape = list(self._shape_untiled)
  11867. chunks = [1] * self._stackdims + list(chunkshape)
  11868. used = set()
  11869. for ax0, ax1 in self._axestiled:
  11870. if ax0 in used or ax1 in used:
  11871. raise ValueError('duplicate axis')
  11872. used.add(ax0)
  11873. used.add(ax1)
  11874. shape[ax1] *= stackshape[ax0]
  11875. for ax0, _ax1 in self._axestiled:
  11876. del shape[ax0]
  11877. del chunks[ax0]
  11878. if axes_list:
  11879. del axes_list[ax0]
  11880. self.shape = tuple(shape)
  11881. self.chunks = tuple(chunks)
  11882. if axes is None:
  11883. self.axes = None
  11884. elif isinstance(axes, str):
  11885. self.axes = ''.join(axes_list)
  11886. else:
  11887. self.axes = tuple(axes_list)
  11888. else:
  11889. self._axestiled = ()
  11890. self.shape = self._shape_untiled
  11891. self.chunks = (1,) * self._stackdims + tuple(chunkshape)
  11892. if axes is None:
  11893. self.axes = None
  11894. elif isinstance(axes, str):
  11895. self.axes = axes
  11896. else:
  11897. self.axes = tuple(axes)
  11898. assert len(self.shape) == len(self.chunks)
  11899. if self.axes is not None:
  11900. assert len(self.shape) == len(self.axes)
  11901. if self.axes is None:
  11902. self.shape_squeezed = tuple(i for i in self.shape if i > 1)
  11903. self.axes_squeezed = None
  11904. else:
  11905. keep = ('X', 'Y', 'width', 'length', 'height')
  11906. self.shape_squeezed = tuple(
  11907. i
  11908. for i, ax in zip(self.shape, self.axes, strict=True)
  11909. if i > 1 or ax in keep
  11910. )
  11911. squeezed = tuple(
  11912. ax
  11913. for i, ax in zip(self.shape, self.axes, strict=True)
  11914. if i > 1 or ax in keep
  11915. )
  11916. self.axes_squeezed = (
  11917. ''.join(squeezed) if isinstance(self.axes, str) else squeezed
  11918. )
  11919. def indices(
  11920. self, indices: Iterable[Sequence[int]], /
  11921. ) -> Iterator[tuple[int, ...]]:
  11922. """Return iterator over chunk indices of tiled sequence.
  11923. Parameters:
  11924. indices: Indices of chunks in stacked sequence.
  11925. """
  11926. chunkindex = [0] * self._chunkdims
  11927. for index in indices:
  11928. if index is None:
  11929. yield None
  11930. else:
  11931. if len(index) != self._stackdims:
  11932. raise ValueError(f'{len(index)} != {self._stackdims}')
  11933. index_list = [*index, *chunkindex]
  11934. for ax0, ax1 in self._axestiled:
  11935. index_list[ax1] = index_list[ax0]
  11936. for ax0, _ax1 in self._axestiled:
  11937. del index_list[ax0]
  11938. yield tuple(index_list)
  11939. def slices(
  11940. self, indices: Iterable[Sequence[int]] | None = None, /
  11941. ) -> Iterator[tuple[int | slice, ...]]:
  11942. """Return iterator over slices of chunks in tiled sequence.
  11943. Parameters:
  11944. indices: Indices of chunks in stacked sequence.
  11945. """
  11946. wholeslice: list[int | slice]
  11947. chunkslice: list[int | slice] = [slice(None)] * self._chunkdims
  11948. if indices is None:
  11949. indices = numpy.ndindex(self._shape_untiled[: self._stackdims])
  11950. for index in indices:
  11951. if index is None:
  11952. yield None
  11953. else:
  11954. assert len(index) == self._stackdims
  11955. wholeslice = [*index, *chunkslice]
  11956. for ax0, ax1 in self._axestiled:
  11957. j = self._shape_untiled[ax1]
  11958. i = cast(int, wholeslice[ax0]) * j
  11959. wholeslice[ax1] = slice(i, i + j)
  11960. for ax0, _ax1 in self._axestiled:
  11961. del wholeslice[ax0]
  11962. yield tuple(wholeslice)
  11963. @property
  11964. def ndim(self) -> int:
  11965. """Number of dimensions of tiled sequence excluding chunks."""
  11966. return len(self.shape)
  11967. @property
  11968. def is_tiled(self) -> bool:
  11969. """Sequence is tiled."""
  11970. return bool(self._axestiled)
  11971. @final
  11972. class FileHandle:
  11973. """Binary file handle.
  11974. A limited, special purpose binary file handle that can:
  11975. - handle embedded files (for example, LSM within LSM files).
  11976. - re-open closed files (for multi-file formats, such as OME-TIFF).
  11977. - read and write NumPy arrays and records from file-like objects.
  11978. When initialized from another file handle, do not use the other handle
  11979. unless this FileHandle is closed.
  11980. FileHandle instances are not thread-safe.
  11981. Parameters:
  11982. file:
  11983. File name or seekable binary stream, such as open file,
  11984. BytesIO, or fsspec OpenFile.
  11985. mode:
  11986. File open mode if `file` is file name.
  11987. The default is 'rb'. Files are always opened in binary mode.
  11988. name:
  11989. Name of file if `file` is binary stream.
  11990. offset:
  11991. Start position of embedded file.
  11992. The default is the current file position.
  11993. size:
  11994. Size of embedded file.
  11995. The default is the number of bytes from `offset` to
  11996. the end of the file.
  11997. """
  11998. # TODO: make FileHandle a subclass of IO[bytes]
  11999. __slots__ = (
  12000. '_close',
  12001. '_dir',
  12002. '_fh',
  12003. '_file',
  12004. '_lock',
  12005. '_mode',
  12006. '_name',
  12007. '_offset',
  12008. '_size',
  12009. )
  12010. _file: str | os.PathLike[Any] | FileHandle | IO[bytes] | None
  12011. _fh: IO[bytes] | None
  12012. _mode: str
  12013. _name: str
  12014. _dir: str
  12015. _offset: int
  12016. _size: int
  12017. _close: bool
  12018. _lock: threading.RLock | NullContext
  12019. def __init__(
  12020. self,
  12021. file: str | os.PathLike[Any] | FileHandle | IO[bytes],
  12022. /,
  12023. mode: (
  12024. Literal['r', 'r+', 'w', 'x', 'rb', 'r+b', 'wb', 'xb'] | None
  12025. ) = None,
  12026. *,
  12027. name: str | None = None,
  12028. offset: int | None = None,
  12029. size: int | None = None,
  12030. ) -> None:
  12031. self._mode = 'rb' if mode is None else mode
  12032. self._fh = None
  12033. self._file = file # reference to original argument for re-opening
  12034. self._name = name if name else ''
  12035. self._dir = ''
  12036. self._offset = -1 if offset is None else offset
  12037. self._size = -1 if size is None else size
  12038. self._close = True
  12039. self._lock = NullContext()
  12040. self.open()
  12041. assert self._fh is not None
  12042. def open(self) -> None:
  12043. """Open or re-open file."""
  12044. if self._fh is not None:
  12045. return # file is open
  12046. if isinstance(self._file, os.PathLike):
  12047. self._file = os.fspath(self._file)
  12048. if isinstance(self._file, str):
  12049. # file name
  12050. if self._mode[-1:] != 'b':
  12051. self._mode += 'b'
  12052. if self._mode not in {'rb', 'r+b', 'wb', 'xb'}:
  12053. raise ValueError(f'invalid mode {self._mode}')
  12054. self._file = os.path.realpath(self._file)
  12055. self._dir, self._name = os.path.split(self._file)
  12056. self._fh = open( # noqa: SIM115
  12057. self._file, self._mode, encoding=None
  12058. )
  12059. self._close = True
  12060. self._offset = max(0, self._offset)
  12061. elif isinstance(self._file, FileHandle):
  12062. # FileHandle
  12063. self._fh = self._file._fh
  12064. self._offset = max(0, self._offset)
  12065. self._offset += self._file._offset
  12066. self._close = False
  12067. if not self._name:
  12068. if self._offset:
  12069. name, ext = os.path.splitext(self._file._name)
  12070. self._name = f'{name}@{self._offset}{ext}'
  12071. else:
  12072. self._name = self._file._name
  12073. self._mode = self._file._mode
  12074. self._dir = self._file._dir
  12075. elif hasattr(self._file, 'seek'):
  12076. # binary stream: open file, BytesIO, fsspec LocalFileOpener
  12077. # cast to IO[bytes] even it might not be
  12078. if isinstance(self._file, io.TextIOBase):
  12079. raise TypeError(f'{self._file!r} is not open in binary mode')
  12080. self._fh = cast(IO[bytes], self._file)
  12081. try:
  12082. self._fh.tell()
  12083. except Exception as exc:
  12084. raise ValueError('binary stream is not seekable') from exc
  12085. if self._offset < 0:
  12086. self._offset = self._fh.tell()
  12087. self._close = False
  12088. if not self._name:
  12089. try:
  12090. self._dir, self._name = os.path.split(self._fh.name)
  12091. except AttributeError:
  12092. try:
  12093. self._dir, self._name = os.path.split(
  12094. self._fh.path # type: ignore[attr-defined]
  12095. )
  12096. except AttributeError:
  12097. self._name = 'Unnamed binary stream'
  12098. try:
  12099. self._mode = self._fh.mode
  12100. except AttributeError:
  12101. pass
  12102. elif hasattr(self._file, 'open'):
  12103. # fsspec OpenFile
  12104. _file: Any = self._file
  12105. self._fh = cast(IO[bytes], _file.open())
  12106. try:
  12107. self._fh.tell()
  12108. except Exception as exc:
  12109. try:
  12110. self._fh.close()
  12111. except Exception: # noqa: S110
  12112. pass
  12113. raise ValueError('OpenFile is not seekable') from exc
  12114. if self._offset < 0:
  12115. self._offset = self._fh.tell()
  12116. self._close = True
  12117. if not self._name:
  12118. try:
  12119. self._dir, self._name = os.path.split(_file.path)
  12120. except AttributeError:
  12121. self._name = 'Unnamed binary stream'
  12122. try:
  12123. self._mode = _file.mode
  12124. except AttributeError:
  12125. pass
  12126. else:
  12127. raise ValueError(
  12128. 'the first parameter must be a file name '
  12129. 'or seekable binary file object, '
  12130. f'not {type(self._file)!r}'
  12131. )
  12132. assert self._fh is not None
  12133. if self._offset:
  12134. self._fh.seek(self._offset)
  12135. if self._size < 0:
  12136. pos = self._fh.tell()
  12137. self._fh.seek(self._offset, os.SEEK_END)
  12138. self._size = self._fh.tell()
  12139. self._fh.seek(pos)
  12140. def close(self) -> None:
  12141. """Close file handle."""
  12142. if self._close and self._fh is not None:
  12143. try:
  12144. self._fh.close()
  12145. except Exception: # noqa: S110
  12146. # PermissionError on MacOS. See issue #184
  12147. pass
  12148. self._fh = None
  12149. def fileno(self) -> int:
  12150. """Return underlying file descriptor if exists, else raise OSError."""
  12151. assert self._fh is not None
  12152. try:
  12153. return self._fh.fileno()
  12154. except (OSError, AttributeError) as exc:
  12155. raise OSError(
  12156. f'{type(self._fh)} does not have a file descriptor'
  12157. ) from exc
  12158. def writable(self) -> bool:
  12159. """Return True if stream supports writing."""
  12160. assert self._fh is not None
  12161. if hasattr(self._fh, 'writable'):
  12162. return self._fh.writable()
  12163. return False
  12164. def seekable(self) -> bool:
  12165. """Return True if stream supports random access."""
  12166. return True
  12167. def tell(self) -> int:
  12168. """Return file's current position."""
  12169. assert self._fh is not None
  12170. return self._fh.tell() - self._offset
  12171. def seek(self, offset: int, /, whence: int = 0) -> int:
  12172. """Set file's current position.
  12173. Parameters:
  12174. offset:
  12175. Position of file handle relative to position indicated
  12176. by `whence`.
  12177. whence:
  12178. Relative position of `offset`.
  12179. 0 (`os.SEEK_SET`) beginning of file (default).
  12180. 1 (`os.SEEK_CUR`) current position.
  12181. 2 (`os.SEEK_END`) end of file.
  12182. """
  12183. assert self._fh is not None
  12184. if self._offset:
  12185. if whence == 0:
  12186. return (
  12187. self._fh.seek(self._offset + offset, whence) - self._offset
  12188. )
  12189. if whence == 2 and self._size > 0:
  12190. return (
  12191. self._fh.seek(self._offset + self._size + offset, 0)
  12192. - self._offset
  12193. )
  12194. return self._fh.seek(offset, whence)
  12195. def read(self, size: int = -1, /) -> bytes:
  12196. """Return bytes read from file.
  12197. Parameters:
  12198. size:
  12199. Number of bytes to read from file.
  12200. By default, read until the end of the file.
  12201. """
  12202. if size < 0 and self._offset:
  12203. size = self._size
  12204. assert self._fh is not None
  12205. return self._fh.read(size)
  12206. def readinto(self, buffer: bytes, /) -> int:
  12207. """Read bytes from file into buffer.
  12208. Parameters:
  12209. buffer: Buffer to read into.
  12210. Returns:
  12211. Number of bytes read from file.
  12212. """
  12213. assert self._fh is not None
  12214. return self._fh.readinto(buffer) # type: ignore[attr-defined]
  12215. def write(self, buffer: bytes | memoryview[Any], /) -> int:
  12216. """Write bytes to file and return number of bytes written.
  12217. Parameters:
  12218. buffer: Bytes to write to file.
  12219. Returns:
  12220. Number of bytes written.
  12221. """
  12222. assert self._fh is not None
  12223. return self._fh.write(buffer)
  12224. def flush(self) -> None:
  12225. """Flush write buffers of stream if applicable."""
  12226. assert self._fh is not None
  12227. if hasattr(self._fh, 'flush'):
  12228. self._fh.flush()
  12229. def memmap_array(
  12230. self,
  12231. dtype: DTypeLike | None,
  12232. shape: tuple[int, ...],
  12233. offset: int = 0,
  12234. *,
  12235. mode: str = 'r',
  12236. order: str = 'C',
  12237. ) -> NDArray[Any]:
  12238. """Return `numpy.memmap` of array data stored in file.
  12239. Parameters:
  12240. dtype:
  12241. Data type of array in file.
  12242. shape:
  12243. Shape of array in file.
  12244. offset:
  12245. Start position of array-data in file.
  12246. mode:
  12247. File is opened in this mode. The default is read-only.
  12248. order:
  12249. Order of ndarray memory layout. The default is 'C'.
  12250. """
  12251. if not self.is_file:
  12252. raise ValueError('cannot memory-map file without fileno')
  12253. assert self._fh is not None
  12254. return numpy.memmap(
  12255. self._fh, # type: ignore[call-overload]
  12256. dtype=dtype,
  12257. mode=mode,
  12258. offset=self._offset + offset,
  12259. shape=shape,
  12260. order=order,
  12261. )
  12262. def read_array(
  12263. self,
  12264. dtype: DTypeLike | None,
  12265. count: int = -1,
  12266. offset: int = 0,
  12267. *,
  12268. out: NDArray[Any] | None = None,
  12269. ) -> NDArray[Any]:
  12270. """Return NumPy array from file in native byte order.
  12271. Parameters:
  12272. dtype:
  12273. Data type of array to read.
  12274. count:
  12275. Number of items to read. By default, all items are read.
  12276. offset:
  12277. Start position of array-data in file.
  12278. out:
  12279. NumPy array to read into. By default, a new array is created.
  12280. """
  12281. dtype = numpy.dtype(dtype)
  12282. if count < 0:
  12283. nbytes = self._size if out is None else out.nbytes
  12284. count = nbytes // dtype.itemsize
  12285. else:
  12286. nbytes = count * dtype.itemsize
  12287. result = numpy.empty(count, dtype) if out is None else out
  12288. if result.nbytes != nbytes:
  12289. raise ValueError('size mismatch')
  12290. assert self._fh is not None
  12291. if offset:
  12292. self._fh.seek(self._offset + offset)
  12293. try:
  12294. n = self._fh.readinto(result) # type: ignore[attr-defined]
  12295. except AttributeError:
  12296. result[:] = numpy.frombuffer(self._fh.read(nbytes), dtype).reshape(
  12297. result.shape
  12298. )
  12299. n = nbytes
  12300. if n != nbytes:
  12301. raise ValueError(f'failed to read {nbytes} bytes, got {n}')
  12302. if not result.dtype.isnative:
  12303. if not dtype.isnative:
  12304. result.byteswap(True)
  12305. result = result.view(result.dtype.newbyteorder())
  12306. elif result.dtype.isnative != dtype.isnative:
  12307. result.byteswap(True)
  12308. if out is not None and hasattr(out, 'flush'):
  12309. out.flush()
  12310. return result
  12311. def read_record(
  12312. self,
  12313. dtype: DTypeLike | None,
  12314. shape: tuple[int, ...] | int | None = 1,
  12315. *,
  12316. byteorder: Literal['S', '<', '>', '=', '|'] | None = None,
  12317. ) -> numpy.recarray[Any, Any]:
  12318. """Return NumPy record from file.
  12319. Parameters:
  12320. dtype:
  12321. Data type of record array to read.
  12322. shape:
  12323. Shape of record array to read.
  12324. byteorder:
  12325. Byte order of record array to read.
  12326. """
  12327. assert self._fh is not None
  12328. dtype = numpy.dtype(dtype)
  12329. if byteorder is not None:
  12330. dtype = dtype.newbyteorder(byteorder)
  12331. try:
  12332. record = numpy.rec.fromfile( # type: ignore[call-overload]
  12333. self._fh, dtype, shape
  12334. )
  12335. except Exception:
  12336. if shape is None:
  12337. shape = self._size // dtype.itemsize
  12338. size = product(sequence(shape)) * dtype.itemsize
  12339. # data = bytearray(size)
  12340. # n = self._fh.readinto(data)
  12341. # data = data[:n]
  12342. # TODO: record is not writable
  12343. data = self._fh.read(size)
  12344. record = numpy.rec.fromstring(
  12345. data,
  12346. dtype,
  12347. shape,
  12348. )
  12349. return record[0] if shape == 1 else record
  12350. def write_empty(self, size: int, /) -> int:
  12351. """Append null-bytes to file.
  12352. The file position must be at the end of the file.
  12353. Parameters:
  12354. size: Number of null-bytes to write to file.
  12355. """
  12356. if size < 1:
  12357. return 0
  12358. assert self._fh is not None
  12359. self._fh.seek(size - 1, os.SEEK_CUR)
  12360. self._fh.write(b'\x00')
  12361. return size
  12362. def write_array(
  12363. self,
  12364. data: NDArray[Any],
  12365. dtype: DTypeLike | None = None,
  12366. /,
  12367. ) -> int:
  12368. """Write NumPy array to file in C contiguous order.
  12369. Parameters:
  12370. data: Array to write to file.
  12371. """
  12372. assert self._fh is not None
  12373. pos = self._fh.tell()
  12374. # writing non-contiguous arrays is very slow
  12375. data = numpy.ascontiguousarray(data, dtype)
  12376. try:
  12377. data.tofile(self._fh)
  12378. except io.UnsupportedOperation:
  12379. # numpy cannot write to BytesIO
  12380. self._fh.write(data.tobytes())
  12381. return self._fh.tell() - pos
  12382. def read_segments(
  12383. self,
  12384. offsets: Sequence[int],
  12385. bytecounts: Sequence[int],
  12386. /,
  12387. indices: Sequence[int] | None = None,
  12388. length: int | None = None,
  12389. *,
  12390. sort: bool = True,
  12391. lock: threading.RLock | NullContext | None = None,
  12392. buffersize: int | None = None,
  12393. flat: bool = True,
  12394. ) -> (
  12395. Iterator[tuple[bytes | None, int]]
  12396. | Iterator[list[tuple[bytes | None, int]]]
  12397. ):
  12398. """Return iterator over segments read from file and their indices.
  12399. The purpose of this function is to
  12400. - reduce small or random reads.
  12401. - reduce acquiring reentrant locks.
  12402. - synchronize seeks and reads.
  12403. - limit size of segments read into memory at once.
  12404. (ThreadPoolExecutor.map is not collecting iterables lazily).
  12405. Parameters:
  12406. offsets:
  12407. Offsets of segments to read from file.
  12408. bytecounts:
  12409. Byte counts of segments to read from file.
  12410. indices:
  12411. Indices of segments in image.
  12412. The default is `range(length)`.
  12413. length:
  12414. Number of segments to read from file.
  12415. By default, `len(offsets)`.
  12416. sort:
  12417. Read segments from file in order of their offsets.
  12418. lock:
  12419. Reentrant lock to synchronize seeks and reads.
  12420. buffersize:
  12421. Approximate number of bytes to read from file in one pass.
  12422. The default is :py:attr:`_TIFF.BUFFERSIZE`.
  12423. flat:
  12424. If *True*, return iterator over individual (segment, index)
  12425. tuples.
  12426. Else, return an iterator over a list of (segment, index)
  12427. tuples that are acquired in one pass.
  12428. Yields:
  12429. Individual or lists of `(segment, index)` tuples.
  12430. """
  12431. # TODO: Cythonize this?
  12432. assert self._fh is not None
  12433. if length is None:
  12434. length = len(offsets)
  12435. if length < 1:
  12436. return
  12437. if length == 1:
  12438. if bytecounts[0] > 0 and offsets[0] > 0:
  12439. if lock is None:
  12440. lock = self._lock
  12441. with lock:
  12442. self.seek(offsets[0])
  12443. data = self._fh.read(bytecounts[0])
  12444. else:
  12445. data = None
  12446. index = 0 if indices is None else indices[0]
  12447. yield (data, index) if flat else [(data, index)]
  12448. return
  12449. if lock is None:
  12450. lock = self._lock
  12451. if buffersize is None:
  12452. buffersize = TIFF.BUFFERSIZE
  12453. if indices is None:
  12454. indices = tuple(range(length))
  12455. # corrupted files may be missing some offsets or bytecounts
  12456. segments = [
  12457. (indices[i], offsets[i], bytecounts[i])
  12458. for i in range(min(length, len(offsets), len(bytecounts)))
  12459. ]
  12460. if len(segments) < length:
  12461. logger().warning(
  12462. 'tifffile.read_segments: '
  12463. f'expected {length} segments, got {len(segments)}'
  12464. )
  12465. segments.extend(
  12466. (indices[i], 0, 0) for i in range(len(segments), length)
  12467. )
  12468. if sort:
  12469. segments = sorted(segments, key=lambda x: x[1])
  12470. iscontig = True
  12471. for i in range(length - 1):
  12472. _, offset, bytecount = segments[i]
  12473. nextoffset = segments[i + 1][1]
  12474. if offset == 0 or bytecount == 0 or nextoffset == 0:
  12475. continue
  12476. if offset + bytecount != nextoffset:
  12477. iscontig = False
  12478. break
  12479. seek = self.seek
  12480. read = self._fh.read
  12481. result: list[tuple[bytes | None, int]]
  12482. if iscontig:
  12483. # consolidate reads
  12484. i = 0
  12485. while i < length:
  12486. j = i
  12487. offset = -1
  12488. bytecount = 0
  12489. while bytecount <= buffersize and i < length:
  12490. _, o, b = segments[i]
  12491. if o > 0 and b > 0:
  12492. if offset < 0:
  12493. offset = o
  12494. bytecount += b
  12495. i += 1
  12496. if offset < 0:
  12497. data = None
  12498. else:
  12499. with lock:
  12500. seek(offset)
  12501. data = read(bytecount)
  12502. start = 0
  12503. stop = 0
  12504. result = []
  12505. while j < i:
  12506. index, offset, bytecount = segments[j]
  12507. if offset > 0 and bytecount > 0:
  12508. stop += bytecount
  12509. result.append(
  12510. (data[start:stop], index) # type: ignore[index]
  12511. )
  12512. start = stop
  12513. else:
  12514. result.append((None, index))
  12515. j += 1
  12516. if flat:
  12517. yield from result
  12518. else:
  12519. yield result
  12520. return
  12521. i = 0
  12522. while i < length:
  12523. result = []
  12524. size = 0
  12525. with lock:
  12526. while size <= buffersize and i < length:
  12527. index, offset, bytecount = segments[i]
  12528. if offset > 0 and bytecount > 0:
  12529. seek(offset)
  12530. result.append((read(bytecount), index))
  12531. # buffer = bytearray(bytecount)
  12532. # n = fh.readinto(buffer)
  12533. # data.append(buffer[:n])
  12534. size += bytecount
  12535. else:
  12536. result.append((None, index))
  12537. i += 1
  12538. if flat:
  12539. yield from result
  12540. else:
  12541. yield result
  12542. def __enter__(self) -> Self:
  12543. return self
  12544. def __exit__(
  12545. self,
  12546. exc_type: type[BaseException] | None,
  12547. exc_value: BaseException | None,
  12548. traceback: TracebackType | None,
  12549. ) -> None:
  12550. self.close()
  12551. self._file = None
  12552. # TODO: this may crash the Python interpreter under certain conditions
  12553. # def __getattr__(self, name: str, /) -> Any:
  12554. # """Return attribute from underlying file object."""
  12555. # if self._offset:
  12556. # warnings.warn(
  12557. # '<tifffile.FileHandle> '
  12558. # f'{name} not implemented for embedded files',
  12559. # UserWarning,
  12560. # )
  12561. # return getattr(self._fh, name)
  12562. def __repr__(self) -> str:
  12563. return f'<tifffile.FileHandle {snipstr(self._name, 32)!r}>'
  12564. def __str__(self) -> str:
  12565. return '\n '.join(
  12566. (
  12567. 'FileHandle',
  12568. self._name,
  12569. self._dir,
  12570. f'{self._size} bytes',
  12571. 'closed' if self._fh is None else 'open',
  12572. )
  12573. )
  12574. @property
  12575. def name(self) -> str:
  12576. """Name of file or stream."""
  12577. return self._name
  12578. @property
  12579. def dirname(self) -> str:
  12580. """Directory in which file is stored."""
  12581. return self._dir
  12582. @property
  12583. def path(self) -> str:
  12584. """Absolute path of file."""
  12585. return os.path.join(self._dir, self._name)
  12586. @property
  12587. def extension(self) -> str:
  12588. """File name extension of file or stream."""
  12589. name, ext = os.path.splitext(self._name.lower())
  12590. if ext and name.endswith('.ome'):
  12591. ext = '.ome' + ext
  12592. return ext
  12593. @property
  12594. def size(self) -> int:
  12595. """Size of file in bytes."""
  12596. return self._size
  12597. @property
  12598. def closed(self) -> bool:
  12599. """File is closed."""
  12600. return self._fh is None
  12601. @property
  12602. def lock(self) -> threading.RLock | NullContext:
  12603. """Reentrant lock to synchronize reads and writes."""
  12604. return self._lock
  12605. @lock.setter
  12606. def lock(self, value: bool, /) -> None:
  12607. self.set_lock(value)
  12608. def set_lock(self, lock: bool) -> None: # noqa: FBT001
  12609. """Set reentrant lock to synchronize reads and writes."""
  12610. if bool(lock) == isinstance(self._lock, NullContext):
  12611. self._lock = threading.RLock() if lock else NullContext()
  12612. @property
  12613. def has_lock(self) -> bool:
  12614. """A reentrant lock is currently used to sync reads and writes."""
  12615. return not isinstance(self._lock, NullContext)
  12616. @property
  12617. def is_file(self) -> bool:
  12618. """File has fileno and can be memory-mapped."""
  12619. try:
  12620. self._fh.fileno() # type: ignore[union-attr]
  12621. except Exception:
  12622. return False
  12623. return True
  12624. @final
  12625. class FileCache:
  12626. """Keep FileHandles open.
  12627. Parameters:
  12628. size: Maximum number of files to keep open. The default is 8.
  12629. lock: Reentrant lock to synchronize reads and writes.
  12630. """
  12631. __slots__ = ('files', 'keep', 'lock', 'past', 'size')
  12632. size: int
  12633. """Maximum number of files to keep open."""
  12634. files: dict[FileHandle, int]
  12635. """Reference counts of opened files."""
  12636. keep: set[FileHandle]
  12637. """Set of files to keep open."""
  12638. past: list[FileHandle]
  12639. """FIFO list of opened files."""
  12640. lock: threading.RLock | NullContext
  12641. """Reentrant lock to synchronize reads and writes."""
  12642. def __init__(
  12643. self,
  12644. size: int | None = None,
  12645. *,
  12646. lock: threading.RLock | NullContext | None = None,
  12647. ) -> None:
  12648. self.past = []
  12649. self.files = {}
  12650. self.keep = set()
  12651. self.size = 8 if size is None else int(size)
  12652. self.lock = NullContext() if lock is None else lock
  12653. def open(self, fh: FileHandle, /) -> None:
  12654. """Open file, re-open if necessary."""
  12655. with self.lock:
  12656. if fh in self.files:
  12657. self.files[fh] += 1
  12658. elif fh.closed:
  12659. fh.open()
  12660. self.files[fh] = 1
  12661. self.past.append(fh)
  12662. else:
  12663. self.files[fh] = 2
  12664. self.keep.add(fh)
  12665. self.past.append(fh)
  12666. def close(self, fh: FileHandle, /) -> None:
  12667. """Close least recently used open files."""
  12668. with self.lock:
  12669. if fh in self.files:
  12670. self.files[fh] -= 1
  12671. self._trim()
  12672. def clear(self) -> None:
  12673. """Close all opened files if not in use when opened first."""
  12674. with self.lock:
  12675. for fh, _refcount in list(self.files.items()):
  12676. if fh not in self.keep:
  12677. fh.close()
  12678. del self.files[fh]
  12679. del self.past[self.past.index(fh)]
  12680. def read(
  12681. self,
  12682. fh: FileHandle,
  12683. /,
  12684. offset: int,
  12685. bytecount: int,
  12686. whence: int = 0,
  12687. ) -> bytes:
  12688. """Return bytes read from binary file.
  12689. Parameters:
  12690. fh:
  12691. File handle to read from.
  12692. offset:
  12693. Position in file to start reading from relative to the
  12694. position indicated by `whence`.
  12695. bytecount:
  12696. Number of bytes to read.
  12697. whence:
  12698. Relative position of offset.
  12699. 0 (`os.SEEK_SET`) beginning of file (default).
  12700. 1 (`os.SEEK_CUR`) current position.
  12701. 2 (`os.SEEK_END`) end of file.
  12702. """
  12703. # this function is more efficient than
  12704. # filecache.open(fh)
  12705. # with lock:
  12706. # fh.seek()
  12707. # data = fh.read()
  12708. # filecache.close(fh)
  12709. with self.lock:
  12710. b = fh not in self.files
  12711. if b:
  12712. if fh.closed:
  12713. fh.open()
  12714. self.files[fh] = 0
  12715. else:
  12716. self.files[fh] = 1
  12717. self.keep.add(fh)
  12718. self.past.append(fh)
  12719. fh.seek(offset, whence)
  12720. data = fh.read(bytecount)
  12721. if b:
  12722. self._trim()
  12723. return data
  12724. def write(
  12725. self,
  12726. fh: FileHandle,
  12727. /,
  12728. offset: int,
  12729. data: bytes,
  12730. whence: int = 0,
  12731. ) -> int:
  12732. """Write bytes to binary file.
  12733. Parameters:
  12734. fh:
  12735. File handle to write to.
  12736. offset:
  12737. Position in file to start writing from relative to the
  12738. position indicated by `whence`.
  12739. value:
  12740. Bytes to write.
  12741. whence:
  12742. Relative position of offset.
  12743. 0 (`os.SEEK_SET`) beginning of file (default).
  12744. 1 (`os.SEEK_CUR`) current position.
  12745. 2 (`os.SEEK_END`) end of file.
  12746. """
  12747. with self.lock:
  12748. b = fh not in self.files
  12749. if b:
  12750. if fh.closed:
  12751. fh.open()
  12752. self.files[fh] = 0
  12753. else:
  12754. self.files[fh] = 1
  12755. self.keep.add(fh)
  12756. self.past.append(fh)
  12757. fh.seek(offset, whence)
  12758. written = fh.write(data)
  12759. if b:
  12760. self._trim()
  12761. return written
  12762. def _trim(self) -> None:
  12763. """Trim file cache."""
  12764. index = 0
  12765. size = len(self.past)
  12766. while index < size > self.size:
  12767. fh = self.past[index]
  12768. if fh not in self.keep and self.files[fh] <= 0:
  12769. fh.close()
  12770. del self.files[fh]
  12771. del self.past[index]
  12772. size -= 1
  12773. else:
  12774. index += 1
  12775. def __len__(self) -> int:
  12776. """Return number of open files."""
  12777. return len(self.files)
  12778. def __repr__(self) -> str:
  12779. return f'<tifffile.FileCache @0x{id(self):016X}>'
  12780. @final
  12781. class StoredShape(Sequence[int]):
  12782. """Normalized shape of image array in TIFF pages.
  12783. Parameters:
  12784. frames:
  12785. Number of TIFF pages.
  12786. separate_samples:
  12787. Number of separate samples.
  12788. depth:
  12789. Image depth.
  12790. length:
  12791. Image length (height).
  12792. width:
  12793. Image width.
  12794. contig_samples:
  12795. Number of contiguous samples.
  12796. extrasamples:
  12797. Number of extra samples.
  12798. """
  12799. __slots__ = (
  12800. 'contig_samples',
  12801. 'depth',
  12802. 'extrasamples',
  12803. 'frames',
  12804. 'length',
  12805. 'separate_samples',
  12806. 'width',
  12807. )
  12808. frames: int
  12809. """Number of TIFF pages."""
  12810. separate_samples: int
  12811. """Number of separate samples."""
  12812. depth: int
  12813. """Image depth. Value of ImageDepth tag or 1."""
  12814. length: int
  12815. """Image length (height). Value of ImageLength tag."""
  12816. width: int
  12817. """Image width. Value of ImageWidth tag."""
  12818. contig_samples: int
  12819. """Number of contiguous samples."""
  12820. extrasamples: int
  12821. """Number of extra samples. Count of ExtraSamples tag or 0."""
  12822. def __init__(
  12823. self,
  12824. frames: int = 1,
  12825. separate_samples: int = 1,
  12826. depth: int = 1,
  12827. length: int = 1,
  12828. width: int = 1,
  12829. contig_samples: int = 1,
  12830. extrasamples: int = 0,
  12831. ) -> None:
  12832. if separate_samples != 1 and contig_samples != 1:
  12833. raise ValueError('invalid samples')
  12834. self.frames = int(frames)
  12835. self.separate_samples = int(separate_samples)
  12836. self.depth = int(depth)
  12837. self.length = int(length)
  12838. self.width = int(width)
  12839. self.contig_samples = int(contig_samples)
  12840. self.extrasamples = int(extrasamples)
  12841. @property
  12842. def size(self) -> int:
  12843. """Product of all dimensions."""
  12844. return (
  12845. abs(self.frames)
  12846. * self.separate_samples
  12847. * self.depth
  12848. * self.length
  12849. * self.width
  12850. * self.contig_samples
  12851. )
  12852. @property
  12853. def samples(self) -> int:
  12854. """Number of samples. Count of SamplesPerPixel tag."""
  12855. assert self.separate_samples == 1 or self.contig_samples == 1
  12856. samples = (
  12857. self.separate_samples
  12858. if self.separate_samples > 1
  12859. else self.contig_samples
  12860. )
  12861. assert self.extrasamples < samples
  12862. return samples
  12863. @property
  12864. def photometric_samples(self) -> int:
  12865. """Number of photometric samples."""
  12866. return self.samples - self.extrasamples
  12867. @property
  12868. def shape(self) -> tuple[int, int, int, int, int, int]:
  12869. """Normalized 6D shape of image array in all pages."""
  12870. return (
  12871. self.frames,
  12872. self.separate_samples,
  12873. self.depth,
  12874. self.length,
  12875. self.width,
  12876. self.contig_samples,
  12877. )
  12878. @property
  12879. def page_shape(self) -> tuple[int, int, int, int, int]:
  12880. """Normalized 5D shape of image array in single page."""
  12881. return (
  12882. self.separate_samples,
  12883. self.depth,
  12884. self.length,
  12885. self.width,
  12886. self.contig_samples,
  12887. )
  12888. @property
  12889. def page_size(self) -> int:
  12890. """Product of dimensions in single page."""
  12891. return (
  12892. self.separate_samples
  12893. * self.depth
  12894. * self.length
  12895. * self.width
  12896. * self.contig_samples
  12897. )
  12898. @property
  12899. def squeezed(self) -> tuple[int, ...]:
  12900. """Shape with length-1 removed, except for length and width."""
  12901. shape = [self.length, self.width]
  12902. if self.separate_samples > 1:
  12903. shape.insert(0, self.separate_samples)
  12904. elif self.contig_samples > 1:
  12905. shape.append(self.contig_samples)
  12906. if self.frames > 1:
  12907. shape.insert(0, self.frames)
  12908. return tuple(shape)
  12909. @property
  12910. def is_valid(self) -> bool:
  12911. """Shape is valid."""
  12912. return (
  12913. self.frames >= 1
  12914. and self.depth >= 1
  12915. and self.length >= 1
  12916. and self.width >= 1
  12917. and (self.separate_samples == 1 or self.contig_samples == 1)
  12918. and (
  12919. self.contig_samples
  12920. if self.contig_samples > 1
  12921. else self.separate_samples
  12922. )
  12923. > self.extrasamples
  12924. )
  12925. @property
  12926. def is_planar(self) -> bool:
  12927. """Shape contains planar samples."""
  12928. return self.separate_samples > 1
  12929. @property
  12930. def planarconfig(self) -> int | None:
  12931. """Value of PlanarConfiguration tag."""
  12932. if self.separate_samples > 1:
  12933. return 2 # PLANARCONFIG.SEPARATE
  12934. if self.contig_samples > 1:
  12935. return 1 # PLANARCONFIG.CONTIG
  12936. return None
  12937. def __len__(self) -> int:
  12938. return 6
  12939. @overload
  12940. def __getitem__(self, key: int, /) -> int: ...
  12941. @overload
  12942. def __getitem__(self, key: slice, /) -> tuple[int, ...]: ...
  12943. def __getitem__(self, key: int | slice, /) -> int | tuple[int, ...]:
  12944. return (
  12945. self.frames,
  12946. self.separate_samples,
  12947. self.depth,
  12948. self.length,
  12949. self.width,
  12950. self.contig_samples,
  12951. )[key]
  12952. def __hash__(self) -> int:
  12953. return hash(
  12954. (
  12955. self.frames,
  12956. self.separate_samples,
  12957. self.depth,
  12958. self.length,
  12959. self.width,
  12960. self.contig_samples,
  12961. )
  12962. )
  12963. def __eq__(self, other: object, /) -> bool:
  12964. return (
  12965. isinstance(other, StoredShape)
  12966. and self.frames == other.frames
  12967. and self.separate_samples == other.separate_samples
  12968. and self.depth == other.depth
  12969. and self.length == other.length
  12970. and self.width == other.width
  12971. and self.contig_samples == other.contig_samples
  12972. )
  12973. def __repr__(self) -> str:
  12974. return (
  12975. '<StoredShape('
  12976. f'frames={self.frames}, '
  12977. f'separate_samples={self.separate_samples}, '
  12978. f'depth={self.depth}, '
  12979. f'length={self.length}, '
  12980. f'width={self.width}, '
  12981. f'contig_samples={self.contig_samples}, '
  12982. f'extrasamples={self.extrasamples}'
  12983. ')>'
  12984. )
  12985. @final
  12986. class NullContext:
  12987. """Null context manager. Can be used as a dummy reentrant lock.
  12988. >>> with NullContext():
  12989. ... pass
  12990. ...
  12991. """
  12992. __slots__ = ()
  12993. def __enter__(self) -> Self:
  12994. return self
  12995. def __exit__(
  12996. self,
  12997. exc_type: type[BaseException] | None,
  12998. exc_value: BaseException | None,
  12999. traceback: TracebackType | None,
  13000. ) -> None:
  13001. pass
  13002. def __repr__(self) -> str:
  13003. return 'NullContext()'
  13004. @final
  13005. class Timer:
  13006. """Stopwatch for timing execution speed.
  13007. Parameters:
  13008. message:
  13009. Message to print.
  13010. end:
  13011. End of print statement.
  13012. started:
  13013. Value of performance counter when started.
  13014. The default is the current performance counter.
  13015. Examples:
  13016. >>> import time
  13017. >>> with Timer('sleep:'):
  13018. ... time.sleep(1.05)
  13019. sleep: 1.0... s
  13020. """
  13021. __slots__ = ('duration', 'started', 'stopped')
  13022. started: float
  13023. """Value of performance counter when started."""
  13024. stopped: float
  13025. """Value of performance counter when stopped."""
  13026. duration: float
  13027. """Duration between `started` and `stopped` in seconds."""
  13028. def __init__(
  13029. self,
  13030. message: str | None = None,
  13031. *,
  13032. end: str = ' ',
  13033. started: float | None = None,
  13034. ) -> None:
  13035. if message is not None:
  13036. print(message, end=end, flush=True)
  13037. self.duration = 0.0
  13038. if started is None:
  13039. started = time.perf_counter()
  13040. self.started = self.stopped = started
  13041. def start(self, message: str | None = None, *, end: str = ' ') -> float:
  13042. """Start timer and return current time."""
  13043. if message is not None:
  13044. print(message, end=end, flush=True)
  13045. self.duration = 0.0
  13046. self.started = self.stopped = time.perf_counter()
  13047. return self.started
  13048. def stop(self, message: str | None = None, *, end: str = ' ') -> float:
  13049. """Return duration of timer till start.
  13050. Parameters:
  13051. message: Message to print.
  13052. end: End of print statement.
  13053. """
  13054. self.stopped = time.perf_counter()
  13055. if message is not None:
  13056. print(message, end=end, flush=True)
  13057. self.duration = self.stopped - self.started
  13058. return self.duration
  13059. def print(
  13060. self, message: str | None = None, *, end: str | None = None
  13061. ) -> None:
  13062. """Print duration from timer start till last stop or now.
  13063. Parameters:
  13064. message: Message to print.
  13065. end: End of print statement.
  13066. """
  13067. msg = str(self)
  13068. if message is not None:
  13069. print(message, end=' ')
  13070. print(msg, end=end, flush=True)
  13071. @staticmethod
  13072. def clock() -> float:
  13073. """Return value of performance counter."""
  13074. return time.perf_counter()
  13075. def __str__(self) -> str:
  13076. """Return duration from timer start till last stop or now."""
  13077. if self.duration <= 0.0:
  13078. # not stopped
  13079. duration = time.perf_counter() - self.started
  13080. else:
  13081. duration = self.duration
  13082. s = str(TimeDelta(seconds=duration))
  13083. i = 0
  13084. while i < len(s) and s[i : i + 2] in '0:0010203040506070809':
  13085. i += 1
  13086. if s[i : i + 1] == ':':
  13087. i += 1
  13088. return f'{s[i:]} s'
  13089. def __repr__(self) -> str:
  13090. return f'Timer(started={self.started})'
  13091. def __enter__(self) -> Self:
  13092. return self
  13093. def __exit__(
  13094. self,
  13095. exc_type: type[BaseException] | None,
  13096. exc_value: BaseException | None,
  13097. traceback: TracebackType | None,
  13098. ) -> None:
  13099. self.print()
  13100. class OmeXmlError(Exception):
  13101. """Exception to indicate invalid OME-XML or unsupported cases."""
  13102. @final
  13103. class OmeXml:
  13104. """Create OME-TIFF XML metadata.
  13105. Parameters:
  13106. **metadata:
  13107. Additional OME-XML attributes or elements to be stored.
  13108. Creator:
  13109. Name of creating application. The default is 'tifffile'.
  13110. UUID:
  13111. Unique identifier.
  13112. Examples:
  13113. >>> omexml = OmeXml()
  13114. >>> omexml.addimage(
  13115. ... dtype='uint16',
  13116. ... shape=(32, 256, 256),
  13117. ... storedshape=(32, 1, 1, 256, 256, 1),
  13118. ... axes='CYX',
  13119. ... Name='First Image',
  13120. ... PhysicalSizeX=2.0,
  13121. ... MapAnnotation={'key': 'value'},
  13122. ... Dataset={'Name': 'FirstDataset'},
  13123. ... )
  13124. >>> xml = omexml.tostring()
  13125. >>> xml
  13126. '<OME ...<Image ID="Image:0" Name="First Image">...</Image>...</OME>'
  13127. >>> OmeXml.validate(xml)
  13128. True
  13129. """
  13130. images: list[str]
  13131. """OME-XML Image elements."""
  13132. annotations: list[str]
  13133. """OME-XML Annotation elements."""
  13134. datasets: list[str]
  13135. """OME-XML Dataset elements."""
  13136. _xml: str
  13137. _ifd: int
  13138. def __init__(self, **metadata: Any) -> None:
  13139. metadata = metadata.get('OME', metadata)
  13140. self._ifd = 0
  13141. self.images = []
  13142. self.annotations = []
  13143. self.datasets = []
  13144. # TODO: parse other OME elements from metadata
  13145. # Project
  13146. # Folder
  13147. # Experiment
  13148. # Plate
  13149. # Screen
  13150. # Experimenter
  13151. # ExperimenterGroup
  13152. # Instrument
  13153. # ROI
  13154. if 'UUID' in metadata:
  13155. uuid = metadata['UUID'].split(':')[-1]
  13156. else:
  13157. from uuid import uuid1
  13158. uuid = str(uuid1())
  13159. creator = OmeXml._attribute(
  13160. metadata, 'Creator', default=f'tifffile.py {__version__}'
  13161. )
  13162. schema = 'http://www.openmicroscopy.org/Schemas/OME/2016-06'
  13163. self._xml = (
  13164. '{declaration}'
  13165. f'<OME xmlns="{schema}" '
  13166. 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
  13167. f'xsi:schemaLocation="{schema} {schema}/ome.xsd" '
  13168. f'UUID="urn:uuid:{uuid}"{creator}>'
  13169. '{datasets}'
  13170. '{images}'
  13171. '{annotations}'
  13172. '</OME>'
  13173. )
  13174. def addimage(
  13175. self,
  13176. dtype: DTypeLike | None,
  13177. shape: Sequence[int],
  13178. storedshape: tuple[int, int, int, int, int, int],
  13179. *,
  13180. axes: str | None = None,
  13181. **metadata: Any,
  13182. ) -> None:
  13183. """Add image to OME-XML.
  13184. The OME model can handle up to 9 dimensional images for selected
  13185. axes orders. Refer to the OME-XML specification for details.
  13186. Non-TZCYXS (modulo) dimensions must be after a TZC dimension or
  13187. require an unused TZC dimension.
  13188. Parameters:
  13189. dtype:
  13190. Data type of image array.
  13191. shape:
  13192. Shape of image array.
  13193. storedshape:
  13194. Normalized shape describing how image array is stored in
  13195. TIFF file as (pages, separate_samples, depth, length, width,
  13196. contig_samples).
  13197. axes:
  13198. Character codes for dimensions in `shape`.
  13199. By default, `axes` is determined from the DimensionOrder
  13200. metadata attribute or matched to the `shape` in reverse order
  13201. of TZC(S)YX(S) based on `storedshape`.
  13202. The following codes are supported: 'S' sample, 'X' width,
  13203. 'Y' length, 'Z' depth, 'C' channel, 'T' time, 'A' angle,
  13204. 'P' phase, 'R' tile, 'H' lifetime, 'E' lambda, 'Q' other.
  13205. **metadata:
  13206. Additional OME-XML attributes or elements to be stored.
  13207. Image/Pixels:
  13208. Name, Description,
  13209. DimensionOrder, TypeDescription,
  13210. PhysicalSizeX, PhysicalSizeXUnit,
  13211. PhysicalSizeY, PhysicalSizeYUnit,
  13212. PhysicalSizeZ, PhysicalSizeZUnit,
  13213. TimeIncrement, TimeIncrementUnit,
  13214. StructuredAnnotations, BooleanAnnotation, DoubleAnnotation,
  13215. LongAnnotation, CommentAnnotation, MapAnnotation,
  13216. Dataset
  13217. Per Plane:
  13218. DeltaT, DeltaTUnit,
  13219. ExposureTime, ExposureTimeUnit,
  13220. PositionX, PositionXUnit,
  13221. PositionY, PositionYUnit,
  13222. PositionZ, PositionZUnit.
  13223. Per Channel:
  13224. Name, AcquisitionMode, Color, ContrastMethod,
  13225. EmissionWavelength, EmissionWavelengthUnit,
  13226. ExcitationWavelength, ExcitationWavelengthUnit,
  13227. Fluor, IlluminationType, NDFilter,
  13228. PinholeSize, PinholeSizeUnit, PockelCellSetting.
  13229. Raises:
  13230. OmeXmlError: Image format not supported.
  13231. """
  13232. index = len(self.images)
  13233. annotation_refs = []
  13234. # get Image and Pixels metadata
  13235. metadata = metadata.get('OME', metadata)
  13236. metadata = metadata.get('Image', metadata)
  13237. if isinstance(metadata, (list, tuple)):
  13238. # multiple images
  13239. metadata = metadata[index]
  13240. if 'Pixels' in metadata:
  13241. # merge with Image
  13242. import copy
  13243. metadata = copy.deepcopy(metadata)
  13244. if 'ID' in metadata['Pixels']:
  13245. del metadata['Pixels']['ID']
  13246. metadata.update(metadata['Pixels'])
  13247. del metadata['Pixels']
  13248. try:
  13249. dtype = numpy.dtype(dtype).name
  13250. dtype = {
  13251. 'int8': 'int8',
  13252. 'int16': 'int16',
  13253. 'int32': 'int32',
  13254. 'uint8': 'uint8',
  13255. 'uint16': 'uint16',
  13256. 'uint32': 'uint32',
  13257. 'float32': 'float',
  13258. 'float64': 'double',
  13259. 'complex64': 'complex',
  13260. 'complex128': 'double-complex',
  13261. 'bool': 'bit',
  13262. }[dtype]
  13263. except KeyError as exc:
  13264. raise OmeXmlError(f'data type {dtype!r} not supported') from exc
  13265. if metadata.get('Type', dtype) != dtype:
  13266. raise OmeXmlError(
  13267. f'metadata Pixels Type {metadata["Type"]!r} '
  13268. f'does not match array dtype {dtype!r}'
  13269. )
  13270. samples = 1
  13271. planecount, separate, depth, length, width, contig = storedshape
  13272. if depth != 1:
  13273. raise OmeXmlError('ImageDepth not supported')
  13274. if not (separate == 1 or contig == 1):
  13275. raise ValueError('invalid stored shape')
  13276. shape = tuple(int(i) for i in shape)
  13277. ndim = len(shape)
  13278. if ndim < 1 or product(shape) <= 0:
  13279. raise OmeXmlError('empty arrays not supported')
  13280. if axes is None:
  13281. # get axes from shape, stored shape, and DimensionOrder
  13282. if contig != 1 or shape[-3:] == (length, width, 1):
  13283. axes = 'YXS'
  13284. samples = contig
  13285. elif separate != 1 or (
  13286. ndim == 6 and shape[-3:] == (1, length, width)
  13287. ):
  13288. axes = 'SYX'
  13289. samples = separate
  13290. else:
  13291. axes = 'YX'
  13292. if not len(axes) <= ndim <= (6 if 'S' in axes else 5):
  13293. raise OmeXmlError(f'{ndim} dimensions not supported')
  13294. hiaxes: str = metadata.get('DimensionOrder', 'XYCZT')[:1:-1]
  13295. axes = hiaxes[(6 if 'S' in axes else 5) - ndim :] + axes
  13296. assert len(axes) == len(shape)
  13297. else:
  13298. # validate axes against shape and stored shape
  13299. axes = axes.upper()
  13300. if len(axes) != len(shape):
  13301. raise ValueError('axes do not match shape')
  13302. if not (
  13303. axes.endswith(('YX', 'YXS'))
  13304. or (axes.endswith('YXC') and 'S' not in axes)
  13305. ):
  13306. raise OmeXmlError('dimensions must end with YX or YXS')
  13307. unique = []
  13308. for ax in axes:
  13309. if ax not in 'TZCYXSAPRHEQ':
  13310. raise OmeXmlError(f'dimension {ax!r} not supported')
  13311. if ax in unique:
  13312. raise OmeXmlError(f'multiple {ax!r} dimensions')
  13313. unique.append(ax)
  13314. if ndim > (9 if 'S' in axes else 8):
  13315. raise OmeXmlError('more than 8 dimensions not supported')
  13316. if contig != 1:
  13317. samples = contig
  13318. if ndim < 3:
  13319. raise ValueError('dimensions do not match stored shape')
  13320. if axes[-1] == 'C':
  13321. # allow C axis instead of S
  13322. if 'S' in axes:
  13323. raise ValueError('invalid axes')
  13324. axes = axes.replace('C', 'S')
  13325. elif axes[-1] != 'S':
  13326. raise ValueError('axes do not match stored shape')
  13327. if shape[-1] != contig or shape[-2] != width:
  13328. raise ValueError('shape does not match stored shape')
  13329. elif separate != 1:
  13330. samples = separate
  13331. if ndim < 3:
  13332. raise ValueError('dimensions do not match stored shape')
  13333. if axes[-3] == 'C':
  13334. # allow C axis instead of S
  13335. if 'S' in axes:
  13336. raise ValueError('invalid axes')
  13337. axes = axes.replace('C', 'S')
  13338. elif axes[-3] != 'S':
  13339. raise ValueError('axes do not match stored shape')
  13340. if shape[-3] != separate or shape[-1] != width:
  13341. raise ValueError('shape does not match stored shape')
  13342. if shape[axes.index('X')] != width or shape[axes.index('Y')] != length:
  13343. raise ValueError('shape does not match stored shape')
  13344. if 'S' in axes:
  13345. hiaxes = axes[: min(axes.index('S'), axes.index('Y'))]
  13346. else:
  13347. hiaxes = axes[: axes.index('Y')]
  13348. if any(ax in 'APRHEQ' for ax in hiaxes):
  13349. # modulo axes
  13350. modulo = {}
  13351. dimorder = ''
  13352. axestype = {
  13353. 'A': 'angle',
  13354. 'P': 'phase',
  13355. 'R': 'tile',
  13356. 'H': 'lifetime',
  13357. 'E': 'lambda',
  13358. 'Q': 'other',
  13359. }
  13360. axestypedescr = metadata.get('TypeDescription', {})
  13361. for i, ax in enumerate(hiaxes):
  13362. if ax in 'APRHEQ':
  13363. if ax in axestypedescr:
  13364. typedescr = f'TypeDescription="{axestypedescr[ax]}" '
  13365. else:
  13366. typedescr = ''
  13367. x = hiaxes[i - 1 : i]
  13368. if x and x in 'TZC':
  13369. # use previous axis
  13370. modulo[x] = axestype[ax], shape[i], typedescr
  13371. else:
  13372. # use next unused axis
  13373. for x in 'TZC':
  13374. if (
  13375. x not in dimorder
  13376. and x not in hiaxes
  13377. and x not in modulo
  13378. ):
  13379. modulo[x] = axestype[ax], shape[i], typedescr
  13380. dimorder += x
  13381. break
  13382. else:
  13383. # TODO: support any order of axes, such as, APRTZC
  13384. raise OmeXmlError('more than 3 modulo dimensions')
  13385. else:
  13386. dimorder += ax
  13387. hiaxes = dimorder
  13388. # TODO: use user-specified start, stop, step, or labels
  13389. moduloalong = ''.join(
  13390. f'<ModuloAlong{ax} Type="{axtype}" {typedescr}'
  13391. f'Start="0" End="{size - 1}"/>'
  13392. for ax, (axtype, size, typedescr) in modulo.items()
  13393. )
  13394. annotation_refs.append(
  13395. f'<AnnotationRef ID="Annotation:{len(self.annotations)}"/>'
  13396. )
  13397. self.annotations.append(
  13398. f'<XMLAnnotation ID="Annotation:{len(self.annotations)}" '
  13399. 'Namespace="openmicroscopy.org/omero/dimension/modulo">'
  13400. '<Value>'
  13401. '<Modulo namespace='
  13402. '"http://www.openmicroscopy.org/Schemas/Additions/2011-09">'
  13403. f'{moduloalong}'
  13404. '</Modulo>'
  13405. '</Value>'
  13406. '</XMLAnnotation>'
  13407. )
  13408. else:
  13409. modulo = {}
  13410. annotationref = ''
  13411. hiaxes = hiaxes[::-1]
  13412. for dimorder in (
  13413. metadata.get('DimensionOrder', 'XYCZT'),
  13414. 'XYCZT',
  13415. 'XYZCT',
  13416. 'XYZTC',
  13417. 'XYCTZ',
  13418. 'XYTCZ',
  13419. 'XYTZC',
  13420. ):
  13421. if hiaxes in dimorder:
  13422. break
  13423. else:
  13424. raise OmeXmlError(
  13425. f'dimension order {axes!r} not supported ({hiaxes=})'
  13426. )
  13427. dimsizes = []
  13428. for ax in dimorder:
  13429. if ax == 'S':
  13430. continue
  13431. size = shape[axes.index(ax)] if ax in axes else 1
  13432. if ax == 'C':
  13433. sizec = size
  13434. size *= samples
  13435. if ax in modulo:
  13436. size *= modulo[ax][1]
  13437. dimsizes.append(size)
  13438. sizes = ''.join(
  13439. f' Size{ax}="{size}"'
  13440. for ax, size in zip(dimorder, dimsizes, strict=True)
  13441. )
  13442. # verify DimensionOrder in metadata is compatible
  13443. if 'DimensionOrder' in metadata:
  13444. omedimorder = metadata['DimensionOrder']
  13445. omedimorder = ''.join(
  13446. ax for ax in omedimorder if dimsizes[dimorder.index(ax)] > 1
  13447. )
  13448. if hiaxes not in omedimorder:
  13449. raise OmeXmlError(
  13450. f'metadata DimensionOrder does not match {axes!r}'
  13451. )
  13452. # verify metadata Size values match shape
  13453. for ax, size in zip(dimorder, dimsizes, strict=True):
  13454. if metadata.get(f'Size{ax}', size) != size:
  13455. raise OmeXmlError(
  13456. f'metadata Size{ax} does not match {shape!r}'
  13457. )
  13458. dimsizes[dimorder.index('C')] //= samples
  13459. if planecount != product(dimsizes[2:]):
  13460. raise ValueError('shape does not match stored shape')
  13461. plane_list = []
  13462. planeattributes = metadata.get('Plane', '')
  13463. if planeattributes:
  13464. cztorder = tuple(dimorder[2:].index(ax) for ax in 'CZT')
  13465. for p in range(planecount):
  13466. attributes = OmeXml._attributes(
  13467. planeattributes,
  13468. p,
  13469. 'DeltaT',
  13470. 'DeltaTUnit',
  13471. 'ExposureTime',
  13472. 'ExposureTimeUnit',
  13473. 'PositionX',
  13474. 'PositionXUnit',
  13475. 'PositionY',
  13476. 'PositionYUnit',
  13477. 'PositionZ',
  13478. 'PositionZUnit',
  13479. )
  13480. unraveled = numpy.unravel_index(p, dimsizes[2:], order='F')
  13481. c, z, t = (int(unraveled[i]) for i in cztorder)
  13482. plane_list.append(
  13483. f'<Plane TheC="{c}" TheZ="{z}" TheT="{t}"{attributes}/>'
  13484. )
  13485. # TODO: if possible, verify c, z, t match planeattributes
  13486. planes = ''.join(plane_list)
  13487. channel_list = []
  13488. for c in range(sizec):
  13489. lightpath = '<LightPath/>'
  13490. # TODO: use LightPath elements from metadata
  13491. # 'AnnotationRef',
  13492. # 'DichroicRef',
  13493. # 'EmissionFilterRef',
  13494. # 'ExcitationFilterRef'
  13495. attributes = OmeXml._attributes(
  13496. metadata.get('Channel', ''),
  13497. c,
  13498. 'Name',
  13499. 'AcquisitionMode',
  13500. 'Color',
  13501. 'ContrastMethod',
  13502. 'EmissionWavelength',
  13503. 'EmissionWavelengthUnit',
  13504. 'ExcitationWavelength',
  13505. 'ExcitationWavelengthUnit',
  13506. 'Fluor',
  13507. 'IlluminationType',
  13508. 'NDFilter',
  13509. 'PinholeSize',
  13510. 'PinholeSizeUnit',
  13511. 'PockelCellSetting',
  13512. )
  13513. channel_list.append(
  13514. f'<Channel ID="Channel:{index}:{c}" '
  13515. f'SamplesPerPixel="{samples}"'
  13516. f'{attributes}>'
  13517. f'{lightpath}'
  13518. '</Channel>'
  13519. )
  13520. channels = ''.join(channel_list)
  13521. # TODO: support more Image elements
  13522. elements = OmeXml._elements(metadata, 'AcquisitionDate', 'Description')
  13523. name = OmeXml._attribute(metadata, 'Name', default=f'Image{index}')
  13524. attributes = OmeXml._attributes(
  13525. metadata,
  13526. None,
  13527. 'SignificantBits',
  13528. 'PhysicalSizeX',
  13529. 'PhysicalSizeXUnit',
  13530. 'PhysicalSizeY',
  13531. 'PhysicalSizeYUnit',
  13532. 'PhysicalSizeZ',
  13533. 'PhysicalSizeZUnit',
  13534. 'TimeIncrement',
  13535. 'TimeIncrementUnit',
  13536. )
  13537. if separate > 1 or contig > 1:
  13538. interleaved = 'false' if separate > 1 else 'true'
  13539. interleaved = f' Interleaved="{interleaved}"'
  13540. else:
  13541. interleaved = ''
  13542. self._dataset(
  13543. metadata.get('Dataset', {}), f'<ImageRef ID="Image:{index}"/>'
  13544. )
  13545. self._annotations(
  13546. metadata.get('StructuredAnnotations', metadata), annotation_refs
  13547. )
  13548. annotationref = ''.join(annotation_refs)
  13549. self.images.append(
  13550. f'<Image ID="Image:{index}"{name}>'
  13551. f'{elements}'
  13552. f'<Pixels ID="Pixels:{index}" '
  13553. f'DimensionOrder="{dimorder}" '
  13554. f'Type="{dtype}"'
  13555. f'{sizes}'
  13556. f'{interleaved}'
  13557. f'{attributes}>'
  13558. f'{channels}'
  13559. f'<TiffData IFD="{self._ifd}" PlaneCount="{planecount}"/>'
  13560. f'{planes}'
  13561. '</Pixels>'
  13562. f'{annotationref}'
  13563. '</Image>'
  13564. )
  13565. self._ifd += planecount
  13566. def tostring(self, *, declaration: bool = False) -> str:
  13567. """Return OME-XML string.
  13568. Parameters:
  13569. declaration: Include XML declaration.
  13570. """
  13571. # TODO: support other top-level elements
  13572. datasets = ''.join(self.datasets)
  13573. images = ''.join(self.images)
  13574. annotations = ''.join(self.annotations)
  13575. if annotations:
  13576. annotations = (
  13577. f'<StructuredAnnotations>{annotations}</StructuredAnnotations>'
  13578. )
  13579. if declaration:
  13580. declaration_str = '<?xml version="1.0" encoding="UTF-8"?>'
  13581. else:
  13582. declaration_str = ''
  13583. return self._xml.format(
  13584. declaration=declaration_str,
  13585. images=images,
  13586. annotations=annotations,
  13587. datasets=datasets,
  13588. )
  13589. def __repr__(self) -> str:
  13590. return f'<tifffile.OmeXml @0x{id(self):016X}>'
  13591. def __str__(self) -> str:
  13592. """Return OME-XML string."""
  13593. xml = self.tostring()
  13594. try:
  13595. from lxml import etree
  13596. parser = etree.XMLParser(remove_blank_text=True)
  13597. tree = etree.fromstring(xml, parser)
  13598. xml = etree.tostring(
  13599. tree, encoding='utf-8', pretty_print=True, xml_declaration=True
  13600. ).decode()
  13601. except ImportError:
  13602. pass
  13603. except Exception as exc:
  13604. warnings.warn(
  13605. f'<tifffile.OmeXml.__str__> {exc.__class__.__name__}: {exc}',
  13606. UserWarning,
  13607. stacklevel=2,
  13608. )
  13609. return xml
  13610. @staticmethod
  13611. def _escape(value: object, /) -> str:
  13612. """Return escaped string of value."""
  13613. if not isinstance(value, str):
  13614. value = str(value)
  13615. elif '&amp;' in value or '&gt;' in value or '&lt;' in value:
  13616. return value
  13617. value = value.replace('&', '&amp;')
  13618. value = value.replace('>', '&gt;')
  13619. return value.replace('<', '&lt;')
  13620. @staticmethod
  13621. def _element(
  13622. metadata: dict[str, Any], name: str, default: str | None = None
  13623. ) -> str:
  13624. """Return XML formatted element if name in metadata."""
  13625. value = metadata.get(name, default)
  13626. if value is None:
  13627. return ''
  13628. return f'<{name}>{OmeXml._escape(value)}</{name}>'
  13629. @staticmethod
  13630. def _elements(metadata: dict[str, Any], /, *names: str) -> str:
  13631. """Return XML formatted elements."""
  13632. if not metadata:
  13633. return ''
  13634. elements = (OmeXml._element(metadata, name) for name in names)
  13635. return ''.join(e for e in elements if e)
  13636. @staticmethod
  13637. def _attribute(
  13638. metadata: dict[str, Any],
  13639. name: str,
  13640. /,
  13641. index: int | None = None,
  13642. default: Any = None,
  13643. ) -> str:
  13644. """Return XML formatted attribute if name in metadata."""
  13645. value = metadata.get(name, default)
  13646. if value is None:
  13647. return ''
  13648. if index is not None:
  13649. if isinstance(value, (list, tuple)):
  13650. try:
  13651. value = value[index]
  13652. except IndexError as exc:
  13653. raise IndexError(
  13654. f'list index out of range for attribute {name!r}'
  13655. ) from exc
  13656. elif index > 0:
  13657. raise TypeError(
  13658. f'{type(value).__name__!r} is not a list or tuple'
  13659. )
  13660. return f' {name}="{OmeXml._escape(value)}"'
  13661. @staticmethod
  13662. def _attributes(
  13663. metadata: dict[str, Any],
  13664. index_: int | None,
  13665. /,
  13666. *names: str,
  13667. ) -> str:
  13668. """Return XML formatted attributes."""
  13669. if not metadata:
  13670. return ''
  13671. if index_ is None:
  13672. attributes = (OmeXml._attribute(metadata, name) for name in names)
  13673. elif isinstance(metadata, (list, tuple)):
  13674. metadata = metadata[index_]
  13675. attributes = (OmeXml._attribute(metadata, name) for name in names)
  13676. elif isinstance(metadata, dict):
  13677. attributes = (
  13678. OmeXml._attribute(metadata, name, index_) for name in names
  13679. )
  13680. return ''.join(a for a in attributes if a)
  13681. def _dataset(self, metadata: dict[str, Any] | None, imageref: str) -> None:
  13682. """Add Dataset element to self.datasets."""
  13683. index = len(self.datasets)
  13684. if metadata is None:
  13685. # dataset explicitly disabled
  13686. return
  13687. if not metadata and index == 0:
  13688. # no dataset provided yet
  13689. return
  13690. if not metadata:
  13691. # use previous dataset
  13692. index -= 1
  13693. if '<AnnotationRef' in self.datasets[index]:
  13694. self.datasets[index] = self.datasets[index].replace(
  13695. '<AnnotationRef', f'{imageref}<AnnotationRef'
  13696. )
  13697. else:
  13698. self.datasets[index] = self.datasets[index].replace(
  13699. '</Dataset>', f'{imageref}</Dataset>'
  13700. )
  13701. return
  13702. # new dataset
  13703. name = metadata.get('Name', '')
  13704. if name:
  13705. name = f' Name="{OmeXml._escape(name)}"'
  13706. description = metadata.get('Description', '')
  13707. if description:
  13708. description = (
  13709. f'<Description>{OmeXml._escape(description)}</Description>'
  13710. )
  13711. annotation_refs: list[str] = []
  13712. self._annotations(metadata, annotation_refs)
  13713. annotationref = ''.join(annotation_refs)
  13714. self.datasets.append(
  13715. f'<Dataset ID="Dataset:{index}"{name}>'
  13716. f'{description}'
  13717. f'{imageref}'
  13718. f'{annotationref}'
  13719. '</Dataset>'
  13720. )
  13721. return # f'<DatasetRef ID="Dataset:{index}"/>'
  13722. def _annotations(
  13723. self, metadata: dict[str, Any], annotation_refs: list[str]
  13724. ) -> None:
  13725. """Add annotations to self.annotations and annotation_refs."""
  13726. values: Any
  13727. for item in metadata.items():
  13728. name, values = item
  13729. if not values:
  13730. continue
  13731. if name not in {
  13732. 'BooleanAnnotation',
  13733. 'DoubleAnnotation',
  13734. 'LongAnnotation',
  13735. 'CommentAnnotation',
  13736. 'MapAnnotation',
  13737. # 'FileAnnotation',
  13738. # 'ListAnnotation',
  13739. # 'TimestampAnnotation,
  13740. # 'XmlAnnotation',
  13741. }:
  13742. continue
  13743. if not isinstance(values, (list, tuple)):
  13744. values = [values]
  13745. for value in values:
  13746. namespace = ''
  13747. description = ''
  13748. if isinstance(value, dict):
  13749. value = value.copy() # noqa: PLW2901
  13750. description = value.pop('Description', '')
  13751. if description:
  13752. description = (
  13753. '<Description>'
  13754. f'{OmeXml._escape(description)}'
  13755. '</Description>'
  13756. )
  13757. namespace = value.pop('Namespace', '')
  13758. if namespace:
  13759. namespace = f' Namespace="{OmeXml._escape(namespace)}"'
  13760. value = value.pop('Value', value) # noqa: PLW2901
  13761. if name == 'MapAnnotation':
  13762. if not isinstance(value, dict):
  13763. raise ValueError('MapAnnotation is not a dict')
  13764. values = [
  13765. f'<M K="{OmeXml._escape(k)}">{OmeXml._escape(v)}</M>'
  13766. for k, v in value.items()
  13767. ]
  13768. elif name == 'BooleanAnnotation':
  13769. values = [f'{bool(value)}'.lower()]
  13770. else:
  13771. values = [OmeXml._escape(str(value))]
  13772. annotation_refs.append(
  13773. f'<AnnotationRef ID="Annotation:{len(self.annotations)}"/>'
  13774. )
  13775. self.annotations.append(
  13776. ''.join(
  13777. (
  13778. f'<{name} '
  13779. f'ID="Annotation:{len(self.annotations)}"'
  13780. f'{namespace}>',
  13781. description,
  13782. '<Value>',
  13783. ''.join(values),
  13784. '</Value>',
  13785. f'</{name}>',
  13786. )
  13787. )
  13788. )
  13789. @staticmethod
  13790. def validate(
  13791. omexml: str,
  13792. /,
  13793. omexsd: bytes | None = None,
  13794. *,
  13795. assert_: bool = True,
  13796. _schema: list[Any] = [], # noqa: B006 (etree.XMLSchema)
  13797. ) -> bool | None:
  13798. r"""Return if OME-XML is valid according to XMLSchema.
  13799. Parameters:
  13800. omexml:
  13801. OME-XML string to validate.
  13802. omexsd:
  13803. Content of OME-XSD schema to validate against.
  13804. By default, the 2016-06 OME XMLSchema is downloaded on first
  13805. run.
  13806. assert\_:
  13807. Raise AssertionError if validation fails.
  13808. _schema:
  13809. Internal use.
  13810. Raises:
  13811. AssertionError:
  13812. Validation failed and `assert\_` is *True*.
  13813. """
  13814. from lxml import etree
  13815. if not _schema:
  13816. if omexsd is None:
  13817. omexsd_path = os.path.join(
  13818. os.path.dirname(__file__), 'ome.xsd'
  13819. )
  13820. if os.path.exists(omexsd_path):
  13821. with open(omexsd_path, 'rb') as fh:
  13822. omexsd = fh.read()
  13823. else:
  13824. import urllib.request
  13825. with urllib.request.urlopen(
  13826. 'https://www.openmicroscopy.org/'
  13827. 'Schemas/OME/2016-06/ome.xsd'
  13828. ) as fh:
  13829. omexsd = fh.read()
  13830. if omexsd.startswith(b'<?xml'):
  13831. omexsd = omexsd.split(b'>', 1)[-1]
  13832. try:
  13833. _schema.append(
  13834. etree.XMLSchema(etree.fromstring(omexsd.decode()))
  13835. )
  13836. except Exception:
  13837. # raise
  13838. _schema.append(None)
  13839. if _schema and _schema[0] is not None:
  13840. if omexml.startswith('<?xml'):
  13841. omexml = omexml.split('>', 1)[-1]
  13842. tree = etree.fromstring(omexml)
  13843. if assert_:
  13844. _schema[0].assert_(tree)
  13845. return True
  13846. return bool(_schema[0].validate(tree))
  13847. return None
  13848. @final
  13849. class CompressionCodec(Mapping[int, Callable[..., object]]):
  13850. """Map :py:class:`COMPRESSION` value to encode or decode function.
  13851. Parameters:
  13852. encode: If *True*, return encode functions, else decode functions.
  13853. """
  13854. _codecs: dict[int, Callable[..., Any]]
  13855. _encode: bool
  13856. def __init__(self, /, *, encode: bool) -> None:
  13857. self._codecs = {1: identityfunc}
  13858. self._encode = bool(encode)
  13859. def __getitem__(self, key: int, /) -> Callable[..., Any]:
  13860. if key in self._codecs:
  13861. return self._codecs[key]
  13862. codec: Callable[..., Any]
  13863. try:
  13864. # TODO: enable CCITTRLE decoder for future imagecodecs
  13865. # if key == 2:
  13866. # if self._encode:
  13867. # codec = imagecodecs.ccittrle_encode
  13868. # else:
  13869. # codec = imagecodecs.ccittrle_decode
  13870. if key == 5:
  13871. if self._encode:
  13872. codec = imagecodecs.lzw_encode
  13873. else:
  13874. codec = imagecodecs.lzw_decode
  13875. elif key in {6, 7, 33007}:
  13876. if self._encode:
  13877. if key in {6, 33007}:
  13878. raise NotImplementedError
  13879. codec = imagecodecs.jpeg_encode
  13880. else:
  13881. codec = imagecodecs.jpeg_decode
  13882. elif key in {8, 32946, 50013}:
  13883. if (
  13884. hasattr(imagecodecs, 'DEFLATE')
  13885. and imagecodecs.DEFLATE.available
  13886. ):
  13887. # imagecodecs built with deflate
  13888. if self._encode:
  13889. codec = imagecodecs.deflate_encode
  13890. else:
  13891. codec = imagecodecs.deflate_decode
  13892. elif (
  13893. hasattr(imagecodecs, 'ZLIB') and imagecodecs.ZLIB.available
  13894. ):
  13895. if self._encode:
  13896. codec = imagecodecs.zlib_encode
  13897. else:
  13898. codec = imagecodecs.zlib_decode
  13899. else:
  13900. # imagecodecs built without zlib
  13901. try:
  13902. from . import _imagecodecs
  13903. except ImportError:
  13904. import _imagecodecs # type: ignore[no-redef]
  13905. if self._encode:
  13906. codec = _imagecodecs.zlib_encode
  13907. else:
  13908. codec = _imagecodecs.zlib_decode
  13909. elif key == 32773:
  13910. if self._encode:
  13911. codec = imagecodecs.packbits_encode
  13912. else:
  13913. codec = imagecodecs.packbits_decode
  13914. elif key in {33003, 33004, 33005, 34712}:
  13915. if self._encode:
  13916. codec = imagecodecs.jpeg2k_encode
  13917. else:
  13918. codec = imagecodecs.jpeg2k_decode
  13919. elif key == 34887:
  13920. if self._encode:
  13921. codec = imagecodecs.lerc_encode
  13922. else:
  13923. codec = imagecodecs.lerc_decode
  13924. elif key == 34892:
  13925. # DNG lossy
  13926. if self._encode:
  13927. codec = imagecodecs.jpeg8_encode
  13928. else:
  13929. codec = imagecodecs.jpeg8_decode
  13930. elif key == 34925:
  13931. if hasattr(imagecodecs, 'LZMA') and imagecodecs.LZMA.available:
  13932. if self._encode:
  13933. codec = imagecodecs.lzma_encode
  13934. else:
  13935. codec = imagecodecs.lzma_decode
  13936. else:
  13937. # imagecodecs built without lzma
  13938. try:
  13939. from . import _imagecodecs
  13940. except ImportError:
  13941. import _imagecodecs # type: ignore[no-redef]
  13942. if self._encode:
  13943. codec = _imagecodecs.lzma_encode
  13944. else:
  13945. codec = _imagecodecs.lzma_decode
  13946. elif key == 34933:
  13947. if self._encode:
  13948. codec = imagecodecs.png_encode
  13949. else:
  13950. codec = imagecodecs.png_decode
  13951. elif key in {34934, 22610}:
  13952. if self._encode:
  13953. codec = imagecodecs.jpegxr_encode
  13954. else:
  13955. codec = imagecodecs.jpegxr_decode
  13956. elif key == 48124:
  13957. if self._encode:
  13958. codec = imagecodecs.jetraw_encode
  13959. else:
  13960. codec = imagecodecs.jetraw_decode
  13961. elif key in {50000, 34926}: # 34926 deprecated
  13962. if hasattr(imagecodecs, 'ZSTD') and imagecodecs.ZSTD.available:
  13963. if self._encode:
  13964. codec = imagecodecs.zstd_encode
  13965. else:
  13966. codec = imagecodecs.zstd_decode
  13967. else:
  13968. # imagecodecs built without zstd
  13969. try:
  13970. from . import _imagecodecs
  13971. except ImportError:
  13972. import _imagecodecs # type: ignore[no-redef]
  13973. if self._encode:
  13974. codec = _imagecodecs.zstd_encode
  13975. else:
  13976. codec = _imagecodecs.zstd_decode
  13977. elif key in {50001, 34927}: # 34927 deprecated
  13978. if self._encode:
  13979. codec = imagecodecs.webp_encode
  13980. else:
  13981. codec = imagecodecs.webp_decode
  13982. elif key in {65000, 65001, 65002} and not self._encode:
  13983. codec = imagecodecs.eer_decode
  13984. elif key in {50002, 52546}:
  13985. if self._encode:
  13986. codec = imagecodecs.jpegxl_encode
  13987. else:
  13988. codec = imagecodecs.jpegxl_decode
  13989. else:
  13990. try:
  13991. msg = f'{COMPRESSION(key)!r} not supported'
  13992. except ValueError:
  13993. msg = f'{key} is not a known COMPRESSION'
  13994. raise KeyError(msg)
  13995. except (AttributeError, ImportError) as exc:
  13996. raise KeyError(
  13997. f"{COMPRESSION(key)!r} requires the 'imagecodecs' package"
  13998. ) from exc
  13999. except NotImplementedError as exc:
  14000. raise KeyError(f'{COMPRESSION(key)!r} not implemented') from exc
  14001. self._codecs[key] = codec
  14002. return codec
  14003. def __contains__(self, key: Any, /) -> bool:
  14004. try:
  14005. self[key]
  14006. except KeyError:
  14007. return False
  14008. return True
  14009. def __iter__(self) -> Iterator[int]:
  14010. yield 1 # dummy
  14011. def __len__(self) -> int:
  14012. return 1 # dummy
  14013. @final
  14014. class PredictorCodec(Mapping[int, Callable[..., object]]):
  14015. """Map :py:class:`PREDICTOR` value to encode or decode function.
  14016. Parameters:
  14017. encode: If *True*, return encode functions, else decode functions.
  14018. """
  14019. _codecs: dict[int, Callable[..., Any]]
  14020. _encode: bool
  14021. def __init__(self, /, *, encode: bool) -> None:
  14022. self._codecs = {1: identityfunc}
  14023. self._encode = bool(encode)
  14024. def __getitem__(self, key: int, /) -> Callable[..., Any]:
  14025. if key in self._codecs:
  14026. return self._codecs[key]
  14027. codec: Callable[..., Any]
  14028. try:
  14029. if key == 2:
  14030. if self._encode:
  14031. codec = imagecodecs.delta_encode
  14032. else:
  14033. codec = imagecodecs.delta_decode
  14034. elif key == 3:
  14035. if self._encode:
  14036. codec = imagecodecs.floatpred_encode
  14037. else:
  14038. codec = imagecodecs.floatpred_decode
  14039. elif key == 34892:
  14040. if self._encode:
  14041. def codec(data, axis=-1, out=None):
  14042. return imagecodecs.delta_encode(
  14043. data, axis=axis, out=out, dist=2
  14044. )
  14045. else:
  14046. def codec(data, axis=-1, out=None):
  14047. return imagecodecs.delta_decode(
  14048. data, axis=axis, out=out, dist=2
  14049. )
  14050. elif key == 34893:
  14051. if self._encode:
  14052. def codec(data, axis=-1, out=None):
  14053. return imagecodecs.delta_encode(
  14054. data, axis=axis, out=out, dist=4
  14055. )
  14056. else:
  14057. def codec(data, axis=-1, out=None):
  14058. return imagecodecs.delta_decode(
  14059. data, axis=axis, out=out, dist=4
  14060. )
  14061. elif key == 34894:
  14062. if self._encode:
  14063. def codec(data, axis=-1, out=None):
  14064. return imagecodecs.floatpred_encode(
  14065. data, axis=axis, out=out, dist=2
  14066. )
  14067. else:
  14068. def codec(data, axis=-1, out=None):
  14069. return imagecodecs.floatpred_decode(
  14070. data, axis=axis, out=out, dist=2
  14071. )
  14072. elif key == 34895:
  14073. if self._encode:
  14074. def codec(data, axis=-1, out=None):
  14075. return imagecodecs.floatpred_encode(
  14076. data, axis=axis, out=out, dist=4
  14077. )
  14078. else:
  14079. def codec(data, axis=-1, out=None):
  14080. return imagecodecs.floatpred_decode(
  14081. data, axis=axis, out=out, dist=4
  14082. )
  14083. else:
  14084. raise KeyError(f'{key} is not a known PREDICTOR')
  14085. except AttributeError as exc:
  14086. raise KeyError(
  14087. f"{PREDICTOR(key)!r} requires the 'imagecodecs' package"
  14088. ) from exc
  14089. except NotImplementedError as exc:
  14090. raise KeyError(f'{PREDICTOR(key)!r} not implemented') from exc
  14091. self._codecs[key] = codec
  14092. return codec
  14093. def __contains__(self, key: Any, /) -> bool:
  14094. try:
  14095. self[key]
  14096. except KeyError:
  14097. return False
  14098. return True
  14099. def __iter__(self) -> Iterator[int]:
  14100. yield 1 # dummy
  14101. def __len__(self) -> int:
  14102. return 1 # dummy
  14103. class DATATYPE(enum.IntEnum):
  14104. """TIFF tag data types."""
  14105. BYTE = 1
  14106. """8-bit unsigned integer."""
  14107. ASCII = 2
  14108. """8-bit byte with last byte null, containing 7-bit ASCII code."""
  14109. SHORT = 3
  14110. """16-bit unsigned integer."""
  14111. LONG = 4
  14112. """32-bit unsigned integer."""
  14113. RATIONAL = 5
  14114. """Two 32-bit unsigned integers, numerator and denominator of fraction."""
  14115. SBYTE = 6
  14116. """8-bit signed integer."""
  14117. UNDEFINED = 7
  14118. """8-bit byte that may contain anything."""
  14119. SSHORT = 8
  14120. """16-bit signed integer."""
  14121. SLONG = 9
  14122. """32-bit signed integer."""
  14123. SRATIONAL = 10
  14124. """Two 32-bit signed integers, numerator and denominator of fraction."""
  14125. FLOAT = 11
  14126. """Single precision (4-byte) IEEE format."""
  14127. DOUBLE = 12
  14128. """Double precision (8-byte) IEEE format."""
  14129. IFD = 13
  14130. """Unsigned 4 byte IFD offset."""
  14131. UNICODE = 14
  14132. """UTF-16 (2-byte) unicode string."""
  14133. COMPLEX = 15
  14134. """Single precision (8-byte) complex number."""
  14135. LONG8 = 16
  14136. """Unsigned 8 byte integer (BigTIFF)."""
  14137. SLONG8 = 17
  14138. """Signed 8 byte integer (BigTIFF)."""
  14139. IFD8 = 18
  14140. """Unsigned 8 byte IFD offset (BigTIFF)."""
  14141. class COMPRESSION(enum.IntEnum):
  14142. """Values of Compression tag.
  14143. Compression scheme used on image data.
  14144. """
  14145. NONE = 1
  14146. """No compression (default)."""
  14147. CCITTRLE = 2 # CCITT 1D
  14148. CCITT_T4 = 3 # T4/Group 3 Fax
  14149. CCITT_T6 = 4 # T6/Group 4 Fax
  14150. LZW = 5
  14151. """Lempel-Ziv-Welch."""
  14152. OJPEG = 6 # old-style JPEG
  14153. JPEG = 7
  14154. """New style JPEG."""
  14155. ADOBE_DEFLATE = 8
  14156. """Deflate, aka ZLIB."""
  14157. JBIG_BW = 9 # VC5
  14158. JBIG_COLOR = 10
  14159. JPEG_99 = 99 # Leaf MOS lossless JPEG
  14160. IMPACJ = 103 # Pegasus Imaging Corporation DCT
  14161. KODAK_262 = 262
  14162. JPEGXR_NDPI = 22610
  14163. """JPEG XR (Hammatsu NDPI)."""
  14164. NEXT = 32766
  14165. SONY_ARW = 32767
  14166. PACKED_RAW = 32769
  14167. SAMSUNG_SRW = 32770
  14168. CCIRLEW = 32771 # Word-aligned 1D Huffman compression
  14169. SAMSUNG_SRW2 = 32772
  14170. PACKBITS = 32773
  14171. """PackBits, aka Macintosh RLE."""
  14172. THUNDERSCAN = 32809
  14173. IT8CTPAD = 32895 # TIFF/IT
  14174. IT8LW = 32896 # TIFF/IT
  14175. IT8MP = 32897 # TIFF/IT
  14176. IT8BL = 32898 # TIFF/IT
  14177. PIXARFILM = 32908
  14178. PIXARLOG = 32909
  14179. DEFLATE = 32946
  14180. DCS = 32947
  14181. APERIO_JP2000_YCBC = 33003 # Matrox libraries
  14182. """JPEG 2000 YCbCr (Leica Aperio)."""
  14183. JPEG_2000_LOSSY = 33004
  14184. """Lossy JPEG 2000 (Bio-Formats)."""
  14185. APERIO_JP2000_RGB = 33005 # Kakadu libraries
  14186. """JPEG 2000 RGB (Leica Aperio)."""
  14187. ALT_JPEG = 33007
  14188. """JPEG (Bio-Formats)."""
  14189. # PANASONIC_RAW1 = 34316
  14190. # PANASONIC_RAW2 = 34826
  14191. # PANASONIC_RAW3 = 34828
  14192. # PANASONIC_RAW4 = 34830
  14193. JBIG = 34661
  14194. SGILOG = 34676 # LogLuv32
  14195. SGILOG24 = 34677
  14196. LURADOC = 34692 # LuraWave
  14197. JPEG2000 = 34712
  14198. """JPEG 2000."""
  14199. NIKON_NEF = 34713
  14200. JBIG2 = 34715
  14201. MDI_BINARY = 34718 # Microsoft Document Imaging
  14202. MDI_PROGRESSIVE = 34719 # Microsoft Document Imaging
  14203. MDI_VECTOR = 34720 # Microsoft Document Imaging
  14204. LERC = 34887
  14205. """ESRI Limited Error Raster Compression."""
  14206. JPEG_LOSSY = 34892 # DNG
  14207. LZMA = 34925
  14208. """Lempel-Ziv-Markov chain Algorithm."""
  14209. ZSTD_DEPRECATED = 34926
  14210. WEBP_DEPRECATED = 34927
  14211. PNG = 34933 # Objective Pathology Services
  14212. """Portable Network Graphics (Zoomable Image File format)."""
  14213. JPEGXR = 34934
  14214. """JPEG XR (Zoomable Image File format)."""
  14215. JETRAW = 48124
  14216. """Jetraw by Dotphoton."""
  14217. ZSTD = 50000
  14218. """Zstandard."""
  14219. WEBP = 50001
  14220. """WebP."""
  14221. JPEGXL = 50002 # GDAL
  14222. """JPEG XL."""
  14223. PIXTIFF = 50013
  14224. """ZLIB (Atalasoft)."""
  14225. JPEGXL_DNG = 52546
  14226. """JPEG XL (DNG)."""
  14227. EER_V0 = 65000 # FIXED82 Thermo Fisher Scientific
  14228. EER_V1 = 65001 # FIXED72 Thermo Fisher Scientific
  14229. EER_V2 = 65002 # VARIABLE Thermo Fisher Scientific
  14230. # KODAK_DCR = 65000
  14231. # PENTAX_PEF = 65535
  14232. def __bool__(self) -> bool:
  14233. return self > 1
  14234. class PREDICTOR(enum.IntEnum):
  14235. """Values of Predictor tag.
  14236. A mathematical operator that is applied to the image data before
  14237. compression.
  14238. """
  14239. NONE = 1
  14240. """No prediction scheme used (default)."""
  14241. HORIZONTAL = 2
  14242. """Horizontal differencing."""
  14243. FLOATINGPOINT = 3
  14244. """Floating-point horizontal differencing."""
  14245. HORIZONTALX2 = 34892 # DNG
  14246. HORIZONTALX4 = 34893
  14247. FLOATINGPOINTX2 = 34894
  14248. FLOATINGPOINTX4 = 34895
  14249. def __bool__(self) -> bool:
  14250. return self > 1
  14251. class PHOTOMETRIC(enum.IntEnum):
  14252. """Values of PhotometricInterpretation tag.
  14253. The color space of the image.
  14254. """
  14255. MINISWHITE = 0
  14256. """For bilevel and grayscale images, 0 is imaged as white."""
  14257. MINISBLACK = 1
  14258. """For bilevel and grayscale images, 0 is imaged as black."""
  14259. RGB = 2
  14260. """Chroma components are Red, Green, Blue."""
  14261. PALETTE = 3
  14262. """Single chroma component is index into colormap."""
  14263. MASK = 4
  14264. SEPARATED = 5
  14265. """Chroma components are Cyan, Magenta, Yellow, and Key (black)."""
  14266. YCBCR = 6
  14267. """Chroma components are Luma, blue-difference, and red-difference."""
  14268. CIELAB = 8
  14269. ICCLAB = 9
  14270. ITULAB = 10
  14271. CFA = 32803
  14272. """Color Filter Array."""
  14273. LOGL = 32844
  14274. LOGLUV = 32845
  14275. LINEAR_RAW = 34892
  14276. DEPTH_MAP = 51177 # DNG 1.5
  14277. SEMANTIC_MASK = 52527 # DNG 1.6
  14278. class FILETYPE(enum.IntFlag):
  14279. """Values of NewSubfileType tag.
  14280. A general indication of the kind of the image.
  14281. """
  14282. UNDEFINED = 0
  14283. """Image is full-resolution (default)."""
  14284. REDUCEDIMAGE = 1
  14285. """Image is reduced-resolution version of another image."""
  14286. PAGE = 2
  14287. """Image is single page of multi-page image."""
  14288. MASK = 4
  14289. """Image is transparency mask for another image."""
  14290. MACRO = 8 # Aperio SVS, or DNG Depth map
  14291. """Image is MACRO image (SVS) or depth map for another image (DNG)."""
  14292. ENHANCED = 16 # DNG
  14293. """Image contains enhanced image (DNG)."""
  14294. DNG = 65536 # 65537: Alternative, 65540: Semantic mask
  14295. class OFILETYPE(enum.IntEnum):
  14296. """Values of deprecated SubfileType tag."""
  14297. UNDEFINED = 0
  14298. IMAGE = 1 # full-resolution image
  14299. REDUCEDIMAGE = 2 # reduced-resolution image
  14300. PAGE = 3 # single page of multi-page image
  14301. class FILLORDER(enum.IntEnum):
  14302. """Values of FillOrder tag.
  14303. The logical order of bits within a byte.
  14304. """
  14305. MSB2LSB = 1
  14306. """Pixel values are stored in higher-order bits of byte (default)."""
  14307. LSB2MSB = 2
  14308. """Pixels values are stored in lower-order bits of byte."""
  14309. class ORIENTATION(enum.IntEnum):
  14310. """Values of Orientation tag.
  14311. The orientation of the image with respect to the rows and columns.
  14312. """
  14313. TOPLEFT = 1 # default
  14314. TOPRIGHT = 2
  14315. BOTRIGHT = 3
  14316. BOTLEFT = 4
  14317. LEFTTOP = 5
  14318. RIGHTTOP = 6
  14319. RIGHTBOT = 7
  14320. LEFTBOT = 8
  14321. class PLANARCONFIG(enum.IntEnum):
  14322. """Values of PlanarConfiguration tag.
  14323. Specifies how components of each pixel are stored.
  14324. """
  14325. CONTIG = 1
  14326. """Chunky, component values are stored contiguously (default)."""
  14327. SEPARATE = 2
  14328. """Planar, component values are stored in separate planes."""
  14329. class RESUNIT(enum.IntEnum):
  14330. """Values of ResolutionUnit tag.
  14331. The unit of measurement for XResolution and YResolution.
  14332. """
  14333. NONE = 1
  14334. """No absolute unit of measurement."""
  14335. INCH = 2
  14336. """Inch (default)."""
  14337. CENTIMETER = 3
  14338. """Centimeter."""
  14339. MILLIMETER = 4
  14340. """Millimeter (DNG)."""
  14341. MICROMETER = 5
  14342. """Micrometer (DNG)."""
  14343. def __bool__(self) -> bool:
  14344. return self > 1
  14345. class EXTRASAMPLE(enum.IntEnum):
  14346. """Values of ExtraSamples tag.
  14347. Interpretation of extra components in a pixel.
  14348. """
  14349. UNSPECIFIED = 0
  14350. """Unspecified data."""
  14351. ASSOCALPHA = 1
  14352. """Associated alpha data with premultiplied color."""
  14353. UNASSALPHA = 2
  14354. """Unassociated alpha data."""
  14355. class SAMPLEFORMAT(enum.IntEnum):
  14356. """Values of SampleFormat tag.
  14357. Data type of samples in a pixel.
  14358. """
  14359. UINT = 1
  14360. """Unsigned integer."""
  14361. INT = 2
  14362. """Signed integer."""
  14363. IEEEFP = 3
  14364. """IEEE floating-point"""
  14365. VOID = 4
  14366. """Undefined."""
  14367. COMPLEXINT = 5
  14368. """Complex integer."""
  14369. COMPLEXIEEEFP = 6
  14370. """Complex floating-point."""
  14371. class CHUNKMODE(enum.IntEnum):
  14372. """ZarrStore chunk modes.
  14373. Specifies how to chunk data in Zarr stores.
  14374. """
  14375. STRILE = 0
  14376. """Chunk is strip or tile."""
  14377. PLANE = 1
  14378. """Chunk is image plane."""
  14379. PAGE = 2
  14380. """Chunk is image in page."""
  14381. FILE = 3
  14382. """Chunk is image in file."""
  14383. # class THRESHOLD(enum.IntEnum):
  14384. # BILEVEL = 1
  14385. # HALFTONE = 2
  14386. # ERRORDIFFUSE = 3
  14387. #
  14388. # class GRAYRESPONSEUNIT(enum.IntEnum):
  14389. # _10S = 1
  14390. # _100S = 2
  14391. # _1000S = 3
  14392. # _10000S = 4
  14393. # _100000S = 5
  14394. #
  14395. # class COLORRESPONSEUNIT(enum.IntEnum):
  14396. # _10S = 1
  14397. # _100S = 2
  14398. # _1000S = 3
  14399. # _10000S = 4
  14400. # _100000S = 5
  14401. #
  14402. # class GROUP4OPT(enum.IntEnum):
  14403. # UNCOMPRESSED = 2
  14404. class _TIFF:
  14405. """Delay-loaded constants, accessible via :py:attr:`TIFF` instance."""
  14406. @cached_property
  14407. def CLASSIC_LE(self) -> TiffFormat:
  14408. """32-bit little-endian TIFF format."""
  14409. return TiffFormat(
  14410. version=42,
  14411. byteorder='<',
  14412. offsetsize=4,
  14413. offsetformat='<I',
  14414. tagnosize=2,
  14415. tagnoformat='<H',
  14416. tagsize=12,
  14417. tagformat1='<HH',
  14418. tagformat2='<I4s',
  14419. tagoffsetthreshold=4,
  14420. )
  14421. @cached_property
  14422. def CLASSIC_BE(self) -> TiffFormat:
  14423. """32-bit big-endian TIFF format."""
  14424. return TiffFormat(
  14425. version=42,
  14426. byteorder='>',
  14427. offsetsize=4,
  14428. offsetformat='>I',
  14429. tagnosize=2,
  14430. tagnoformat='>H',
  14431. tagsize=12,
  14432. tagformat1='>HH',
  14433. tagformat2='>I4s',
  14434. tagoffsetthreshold=4,
  14435. )
  14436. @cached_property
  14437. def BIG_LE(self) -> TiffFormat:
  14438. """64-bit little-endian TIFF format."""
  14439. return TiffFormat(
  14440. version=43,
  14441. byteorder='<',
  14442. offsetsize=8,
  14443. offsetformat='<Q',
  14444. tagnosize=8,
  14445. tagnoformat='<Q',
  14446. tagsize=20,
  14447. tagformat1='<HH',
  14448. tagformat2='<Q8s',
  14449. tagoffsetthreshold=8,
  14450. )
  14451. @cached_property
  14452. def BIG_BE(self) -> TiffFormat:
  14453. """64-bit big-endian TIFF format."""
  14454. return TiffFormat(
  14455. version=43,
  14456. byteorder='>',
  14457. offsetsize=8,
  14458. offsetformat='>Q',
  14459. tagnosize=8,
  14460. tagnoformat='>Q',
  14461. tagsize=20,
  14462. tagformat1='>HH',
  14463. tagformat2='>Q8s',
  14464. tagoffsetthreshold=8,
  14465. )
  14466. @cached_property
  14467. def NDPI_LE(self) -> TiffFormat:
  14468. """32-bit little-endian TIFF format with 64-bit offsets."""
  14469. return TiffFormat(
  14470. version=42,
  14471. byteorder='<',
  14472. offsetsize=8, # NDPI uses 8 bytes IFD and tag offsets
  14473. offsetformat='<Q',
  14474. tagnosize=2,
  14475. tagnoformat='<H',
  14476. tagsize=12, # 16 after patching
  14477. tagformat1='<HH',
  14478. tagformat2='<I8s', # after patching
  14479. tagoffsetthreshold=4,
  14480. )
  14481. @cached_property
  14482. def TAGS(self) -> TiffTagRegistry:
  14483. """Registry of TIFF tag codes and names from TIFF6, TIFF/EP, EXIF."""
  14484. # TODO: divide into baseline, exif, private, ... tags
  14485. return TiffTagRegistry(
  14486. (
  14487. (11, 'ProcessingSoftware'),
  14488. (254, 'NewSubfileType'),
  14489. (255, 'SubfileType'),
  14490. (256, 'ImageWidth'),
  14491. (257, 'ImageLength'),
  14492. (258, 'BitsPerSample'),
  14493. (259, 'Compression'),
  14494. (262, 'PhotometricInterpretation'),
  14495. (263, 'Thresholding'),
  14496. (264, 'CellWidth'),
  14497. (265, 'CellLength'),
  14498. (266, 'FillOrder'),
  14499. (269, 'DocumentName'),
  14500. (270, 'ImageDescription'),
  14501. (271, 'Make'),
  14502. (272, 'Model'),
  14503. (273, 'StripOffsets'),
  14504. (274, 'Orientation'),
  14505. (277, 'SamplesPerPixel'),
  14506. (278, 'RowsPerStrip'),
  14507. (279, 'StripByteCounts'),
  14508. (280, 'MinSampleValue'),
  14509. (281, 'MaxSampleValue'),
  14510. (282, 'XResolution'),
  14511. (283, 'YResolution'),
  14512. (284, 'PlanarConfiguration'),
  14513. (285, 'PageName'),
  14514. (286, 'XPosition'),
  14515. (287, 'YPosition'),
  14516. (288, 'FreeOffsets'),
  14517. (289, 'FreeByteCounts'),
  14518. (290, 'GrayResponseUnit'),
  14519. (291, 'GrayResponseCurve'),
  14520. (292, 'T4Options'),
  14521. (293, 'T6Options'),
  14522. (296, 'ResolutionUnit'),
  14523. (297, 'PageNumber'),
  14524. (300, 'ColorResponseUnit'),
  14525. (301, 'TransferFunction'),
  14526. (305, 'Software'),
  14527. (306, 'DateTime'),
  14528. (315, 'Artist'),
  14529. (316, 'HostComputer'),
  14530. (317, 'Predictor'),
  14531. (318, 'WhitePoint'),
  14532. (319, 'PrimaryChromaticities'),
  14533. (320, 'ColorMap'),
  14534. (321, 'HalftoneHints'),
  14535. (322, 'TileWidth'),
  14536. (323, 'TileLength'),
  14537. (324, 'TileOffsets'),
  14538. (325, 'TileByteCounts'),
  14539. (326, 'BadFaxLines'),
  14540. (327, 'CleanFaxData'),
  14541. (328, 'ConsecutiveBadFaxLines'),
  14542. (330, 'SubIFDs'),
  14543. (332, 'InkSet'),
  14544. (333, 'InkNames'),
  14545. (334, 'NumberOfInks'),
  14546. (336, 'DotRange'),
  14547. (337, 'TargetPrinter'),
  14548. (338, 'ExtraSamples'),
  14549. (339, 'SampleFormat'),
  14550. (340, 'SMinSampleValue'),
  14551. (341, 'SMaxSampleValue'),
  14552. (342, 'TransferRange'),
  14553. (343, 'ClipPath'),
  14554. (344, 'XClipPathUnits'),
  14555. (345, 'YClipPathUnits'),
  14556. (346, 'Indexed'),
  14557. (347, 'JPEGTables'),
  14558. (351, 'OPIProxy'),
  14559. (400, 'GlobalParametersIFD'),
  14560. (401, 'ProfileType'),
  14561. (402, 'FaxProfile'),
  14562. (403, 'CodingMethods'),
  14563. (404, 'VersionYear'),
  14564. (405, 'ModeNumber'),
  14565. (433, 'Decode'),
  14566. (434, 'DefaultImageColor'),
  14567. (435, 'T82Options'),
  14568. (437, 'JPEGTables'), # 347
  14569. (512, 'JPEGProc'),
  14570. (513, 'JPEGInterchangeFormat'),
  14571. (514, 'JPEGInterchangeFormatLength'),
  14572. (515, 'JPEGRestartInterval'),
  14573. (517, 'JPEGLosslessPredictors'),
  14574. (518, 'JPEGPointTransforms'),
  14575. (519, 'JPEGQTables'),
  14576. (520, 'JPEGDCTables'),
  14577. (521, 'JPEGACTables'),
  14578. (529, 'YCbCrCoefficients'),
  14579. (530, 'YCbCrSubSampling'),
  14580. (531, 'YCbCrPositioning'),
  14581. (532, 'ReferenceBlackWhite'),
  14582. (559, 'StripRowCounts'),
  14583. (700, 'XMP'), # XMLPacket
  14584. (769, 'GDIGamma'), # GDI+
  14585. (770, 'ICCProfileDescriptor'), # GDI+
  14586. (771, 'SRGBRenderingIntent'), # GDI+
  14587. (800, 'ImageTitle'), # GDI+
  14588. (907, 'SiffCompress'), # https://github.com/MaimonLab/SiffPy
  14589. (999, 'USPTO_Miscellaneous'),
  14590. (4864, 'AndorId'), # TODO, Andor Technology 4864 - 5030
  14591. (4869, 'AndorTemperature'),
  14592. (4876, 'AndorExposureTime'),
  14593. (4878, 'AndorKineticCycleTime'),
  14594. (4879, 'AndorAccumulations'),
  14595. (4881, 'AndorAcquisitionCycleTime'),
  14596. (4882, 'AndorReadoutTime'),
  14597. (4884, 'AndorPhotonCounting'),
  14598. (4885, 'AndorEmDacLevel'),
  14599. (4890, 'AndorFrames'),
  14600. (4896, 'AndorHorizontalFlip'),
  14601. (4897, 'AndorVerticalFlip'),
  14602. (4898, 'AndorClockwise'),
  14603. (4899, 'AndorCounterClockwise'),
  14604. (4904, 'AndorVerticalClockVoltage'),
  14605. (4905, 'AndorVerticalShiftSpeed'),
  14606. (4907, 'AndorPreAmpSetting'),
  14607. (4908, 'AndorCameraSerial'),
  14608. (4911, 'AndorActualTemperature'),
  14609. (4912, 'AndorBaselineClamp'),
  14610. (4913, 'AndorPrescans'),
  14611. (4914, 'AndorModel'),
  14612. (4915, 'AndorChipSizeX'),
  14613. (4916, 'AndorChipSizeY'),
  14614. (4944, 'AndorBaselineOffset'),
  14615. (4966, 'AndorSoftwareVersion'),
  14616. (18246, 'Rating'),
  14617. (18247, 'XP_DIP_XML'),
  14618. (18248, 'StitchInfo'),
  14619. (18249, 'RatingPercent'),
  14620. (20481, 'ResolutionXUnit'), # GDI+
  14621. (20482, 'ResolutionYUnit'), # GDI+
  14622. (20483, 'ResolutionXLengthUnit'), # GDI+
  14623. (20484, 'ResolutionYLengthUnit'), # GDI+
  14624. (20485, 'PrintFlags'), # GDI+
  14625. (20486, 'PrintFlagsVersion'), # GDI+
  14626. (20487, 'PrintFlagsCrop'), # GDI+
  14627. (20488, 'PrintFlagsBleedWidth'), # GDI+
  14628. (20489, 'PrintFlagsBleedWidthScale'), # GDI+
  14629. (20490, 'HalftoneLPI'), # GDI+
  14630. (20491, 'HalftoneLPIUnit'), # GDI+
  14631. (20492, 'HalftoneDegree'), # GDI+
  14632. (20493, 'HalftoneShape'), # GDI+
  14633. (20494, 'HalftoneMisc'), # GDI+
  14634. (20495, 'HalftoneScreen'), # GDI+
  14635. (20496, 'JPEGQuality'), # GDI+
  14636. (20497, 'GridSize'), # GDI+
  14637. (20498, 'ThumbnailFormat'), # GDI+
  14638. (20499, 'ThumbnailWidth'), # GDI+
  14639. (20500, 'ThumbnailHeight'), # GDI+
  14640. (20501, 'ThumbnailColorDepth'), # GDI+
  14641. (20502, 'ThumbnailPlanes'), # GDI+
  14642. (20503, 'ThumbnailRawBytes'), # GDI+
  14643. (20504, 'ThumbnailSize'), # GDI+
  14644. (20505, 'ThumbnailCompressedSize'), # GDI+
  14645. (20506, 'ColorTransferFunction'), # GDI+
  14646. (20507, 'ThumbnailData'),
  14647. (20512, 'ThumbnailImageWidth'), # GDI+
  14648. (20513, 'ThumbnailImageHeight'), # GDI+
  14649. (20514, 'ThumbnailBitsPerSample'), # GDI+
  14650. (20515, 'ThumbnailCompression'),
  14651. (20516, 'ThumbnailPhotometricInterp'), # GDI+
  14652. (20517, 'ThumbnailImageDescription'), # GDI+
  14653. (20518, 'ThumbnailEquipMake'), # GDI+
  14654. (20519, 'ThumbnailEquipModel'), # GDI+
  14655. (20520, 'ThumbnailStripOffsets'), # GDI+
  14656. (20521, 'ThumbnailOrientation'), # GDI+
  14657. (20522, 'ThumbnailSamplesPerPixel'), # GDI+
  14658. (20523, 'ThumbnailRowsPerStrip'), # GDI+
  14659. (20524, 'ThumbnailStripBytesCount'), # GDI+
  14660. (20525, 'ThumbnailResolutionX'),
  14661. (20526, 'ThumbnailResolutionY'),
  14662. (20527, 'ThumbnailPlanarConfig'), # GDI+
  14663. (20528, 'ThumbnailResolutionUnit'),
  14664. (20529, 'ThumbnailTransferFunction'),
  14665. (20530, 'ThumbnailSoftwareUsed'), # GDI+
  14666. (20531, 'ThumbnailDateTime'), # GDI+
  14667. (20532, 'ThumbnailArtist'), # GDI+
  14668. (20533, 'ThumbnailWhitePoint'), # GDI+
  14669. (20534, 'ThumbnailPrimaryChromaticities'), # GDI+
  14670. (20535, 'ThumbnailYCbCrCoefficients'), # GDI+
  14671. (20536, 'ThumbnailYCbCrSubsampling'), # GDI+
  14672. (20537, 'ThumbnailYCbCrPositioning'),
  14673. (20538, 'ThumbnailRefBlackWhite'), # GDI+
  14674. (20539, 'ThumbnailCopyRight'), # GDI+
  14675. (20545, 'InteroperabilityIndex'),
  14676. (20546, 'InteroperabilityVersion'),
  14677. (20624, 'LuminanceTable'),
  14678. (20625, 'ChrominanceTable'),
  14679. (20736, 'FrameDelay'), # GDI+
  14680. (20737, 'LoopCount'), # GDI+
  14681. (20738, 'GlobalPalette'), # GDI+
  14682. (20739, 'IndexBackground'), # GDI+
  14683. (20740, 'IndexTransparent'), # GDI+
  14684. (20752, 'PixelUnit'), # GDI+
  14685. (20753, 'PixelPerUnitX'), # GDI+
  14686. (20754, 'PixelPerUnitY'), # GDI+
  14687. (20755, 'PaletteHistogram'), # GDI+
  14688. (28672, 'SonyRawFileType'), # Sony ARW
  14689. (28722, 'VignettingCorrParams'), # Sony ARW
  14690. (28725, 'ChromaticAberrationCorrParams'), # Sony ARW
  14691. (28727, 'DistortionCorrParams'), # Sony ARW
  14692. # Private tags >= 32768
  14693. (32781, 'ImageID'),
  14694. (32931, 'WangTag1'),
  14695. (32932, 'WangAnnotation'),
  14696. (32933, 'WangTag3'),
  14697. (32934, 'WangTag4'),
  14698. (32953, 'ImageReferencePoints'),
  14699. (32954, 'RegionXformTackPoint'),
  14700. (32955, 'WarpQuadrilateral'),
  14701. (32956, 'AffineTransformMat'),
  14702. (32995, 'Matteing'),
  14703. (32996, 'DataType'), # use SampleFormat
  14704. (32997, 'ImageDepth'),
  14705. (32998, 'TileDepth'),
  14706. (33300, 'ImageFullWidth'),
  14707. (33301, 'ImageFullLength'),
  14708. (33302, 'TextureFormat'),
  14709. (33303, 'TextureWrapModes'),
  14710. (33304, 'FieldOfViewCotangent'),
  14711. (33305, 'MatrixWorldToScreen'),
  14712. (33306, 'MatrixWorldToCamera'),
  14713. (33405, 'Model2'),
  14714. (33421, 'CFARepeatPatternDim'),
  14715. (33422, 'CFAPattern'),
  14716. (33423, 'BatteryLevel'),
  14717. (33424, 'KodakIFD'),
  14718. (33434, 'ExposureTime'),
  14719. (33437, 'FNumber'),
  14720. (33432, 'Copyright'),
  14721. (33445, 'MDFileTag'),
  14722. (33446, 'MDScalePixel'),
  14723. (33447, 'MDColorTable'),
  14724. (33448, 'MDLabName'),
  14725. (33449, 'MDSampleInfo'),
  14726. (33450, 'MDPrepDate'),
  14727. (33451, 'MDPrepTime'),
  14728. (33452, 'MDFileUnits'),
  14729. (33465, 'NiffRotation'), # NIFF
  14730. (33466, 'NiffNavyCompression'), # NIFF
  14731. (33467, 'NiffTileIndex'), # NIFF
  14732. (33471, 'OlympusINI'),
  14733. (33550, 'ModelPixelScaleTag'),
  14734. (33560, 'OlympusSIS'), # see also 33471 and 34853
  14735. (33589, 'AdventScale'),
  14736. (33590, 'AdventRevision'),
  14737. (33628, 'UIC1tag'), # Metamorph Universal Imaging Corp STK
  14738. (33629, 'UIC2tag'),
  14739. (33630, 'UIC3tag'),
  14740. (33631, 'UIC4tag'),
  14741. (33723, 'IPTCNAA'),
  14742. (33858, 'ExtendedTagsOffset'), # DEFF points IFD with tags
  14743. (33918, 'IntergraphPacketData'), # INGRPacketDataTag
  14744. (33919, 'IntergraphFlagRegisters'), # INGRFlagRegisters
  14745. (33920, 'IntergraphMatrixTag'), # IrasBTransformationMatrix
  14746. (33921, 'INGRReserved'),
  14747. (33922, 'ModelTiepointTag'),
  14748. (33923, 'LeicaMagic'),
  14749. (34016, 'Site'), # 34016..34032 ANSI IT8 TIFF/IT
  14750. (34017, 'ColorSequence'),
  14751. (34018, 'IT8Header'),
  14752. (34019, 'RasterPadding'),
  14753. (34020, 'BitsPerRunLength'),
  14754. (34021, 'BitsPerExtendedRunLength'),
  14755. (34022, 'ColorTable'),
  14756. (34023, 'ImageColorIndicator'),
  14757. (34024, 'BackgroundColorIndicator'),
  14758. (34025, 'ImageColorValue'),
  14759. (34026, 'BackgroundColorValue'),
  14760. (34027, 'PixelIntensityRange'),
  14761. (34028, 'TransparencyIndicator'),
  14762. (34029, 'ColorCharacterization'),
  14763. (34030, 'HCUsage'),
  14764. (34031, 'TrapIndicator'),
  14765. (34032, 'CMYKEquivalent'),
  14766. (34118, 'CZ_SEM'), # Zeiss SEM
  14767. (34152, 'AFCP_IPTC'),
  14768. (34232, 'PixelMagicJBIGOptions'), # EXIF, also TI FrameCount
  14769. (34263, 'JPLCartoIFD'),
  14770. (34122, 'IPLAB'), # number of images
  14771. (34264, 'ModelTransformationTag'),
  14772. (34306, 'WB_GRGBLevels'), # Leaf MOS
  14773. (34310, 'LeafData'),
  14774. (34361, 'MM_Header'),
  14775. (34362, 'MM_Stamp'),
  14776. (34363, 'MM_Unknown'),
  14777. (34377, 'ImageResources'), # Photoshop
  14778. (34386, 'MM_UserBlock'),
  14779. (34412, 'CZ_LSMINFO'),
  14780. (34665, 'ExifTag'),
  14781. (34675, 'InterColorProfile'), # ICCProfile
  14782. (34680, 'FEI_SFEG'),
  14783. (34682, 'FEI_HELIOS'),
  14784. (34683, 'FEI_TITAN'),
  14785. (34687, 'FXExtensions'),
  14786. (34688, 'MultiProfiles'),
  14787. (34689, 'SharedData'),
  14788. (34690, 'T88Options'),
  14789. (34710, 'MarCCD'), # offset to MarCCD header
  14790. (34732, 'ImageLayer'),
  14791. (34735, 'GeoKeyDirectoryTag'),
  14792. (34736, 'GeoDoubleParamsTag'),
  14793. (34737, 'GeoAsciiParamsTag'),
  14794. (34750, 'JBIGOptions'),
  14795. (34821, 'PIXTIFF'), # ? Pixel Translations Inc
  14796. (34850, 'ExposureProgram'),
  14797. (34852, 'SpectralSensitivity'),
  14798. (34853, 'GPSTag'), # GPSIFD also OlympusSIS2
  14799. (34853, 'OlympusSIS2'),
  14800. (34855, 'ISOSpeedRatings'),
  14801. (34855, 'PhotographicSensitivity'),
  14802. (34856, 'OECF'), # optoelectric conversion factor
  14803. (34857, 'Interlace'), # TIFF/EP
  14804. (34858, 'TimeZoneOffset'), # TIFF/EP
  14805. (34859, 'SelfTimerMode'), # TIFF/EP
  14806. (34864, 'SensitivityType'),
  14807. (34865, 'StandardOutputSensitivity'),
  14808. (34866, 'RecommendedExposureIndex'),
  14809. (34867, 'ISOSpeed'),
  14810. (34868, 'ISOSpeedLatitudeyyy'),
  14811. (34869, 'ISOSpeedLatitudezzz'),
  14812. (34908, 'HylaFAXFaxRecvParams'),
  14813. (34909, 'HylaFAXFaxSubAddress'),
  14814. (34910, 'HylaFAXFaxRecvTime'),
  14815. (34911, 'FaxDcs'),
  14816. (34929, 'FedexEDR'),
  14817. (34954, 'LeafSubIFD'),
  14818. (34959, 'Aphelion1'),
  14819. (34960, 'Aphelion2'),
  14820. (34961, 'AphelionInternal'), # ADCIS
  14821. (36864, 'ExifVersion'),
  14822. (36867, 'DateTimeOriginal'),
  14823. (36868, 'DateTimeDigitized'),
  14824. (36873, 'GooglePlusUploadCode'),
  14825. (36880, 'OffsetTime'),
  14826. (36881, 'OffsetTimeOriginal'),
  14827. (36882, 'OffsetTimeDigitized'),
  14828. # TODO, Pilatus/CHESS/TV6 36864..37120 conflicting with Exif
  14829. (36864, 'TVX_Unknown'),
  14830. (36865, 'TVX_NumExposure'),
  14831. (36866, 'TVX_NumBackground'),
  14832. (36867, 'TVX_ExposureTime'),
  14833. (36868, 'TVX_BackgroundTime'),
  14834. (36870, 'TVX_Unknown'),
  14835. (36873, 'TVX_SubBpp'),
  14836. (36874, 'TVX_SubWide'),
  14837. (36875, 'TVX_SubHigh'),
  14838. (36876, 'TVX_BlackLevel'),
  14839. (36877, 'TVX_DarkCurrent'),
  14840. (36878, 'TVX_ReadNoise'),
  14841. (36879, 'TVX_DarkCurrentNoise'),
  14842. (36880, 'TVX_BeamMonitor'),
  14843. (37120, 'TVX_UserVariables'), # A/D values
  14844. (37121, 'ComponentsConfiguration'),
  14845. (37122, 'CompressedBitsPerPixel'),
  14846. (37377, 'ShutterSpeedValue'),
  14847. (37378, 'ApertureValue'),
  14848. (37379, 'BrightnessValue'),
  14849. (37380, 'ExposureBiasValue'),
  14850. (37381, 'MaxApertureValue'),
  14851. (37382, 'SubjectDistance'),
  14852. (37383, 'MeteringMode'),
  14853. (37384, 'LightSource'),
  14854. (37385, 'Flash'),
  14855. (37386, 'FocalLength'),
  14856. (37387, 'FlashEnergy'), # TIFF/EP
  14857. (37388, 'SpatialFrequencyResponse'), # TIFF/EP
  14858. (37389, 'Noise'), # TIFF/EP
  14859. (37390, 'FocalPlaneXResolution'), # TIFF/EP
  14860. (37391, 'FocalPlaneYResolution'), # TIFF/EP
  14861. (37392, 'FocalPlaneResolutionUnit'), # TIFF/EP
  14862. (37393, 'ImageNumber'), # TIFF/EP
  14863. (37394, 'SecurityClassification'), # TIFF/EP
  14864. (37395, 'ImageHistory'), # TIFF/EP
  14865. (37396, 'SubjectLocation'), # TIFF/EP
  14866. (37397, 'ExposureIndex'), # TIFF/EP
  14867. (37398, 'TIFFEPStandardID'), # TIFF/EP
  14868. (37399, 'SensingMethod'), # TIFF/EP
  14869. (37434, 'CIP3DataFile'),
  14870. (37435, 'CIP3Sheet'),
  14871. (37436, 'CIP3Side'),
  14872. (37439, 'StoNits'),
  14873. (37500, 'MakerNote'),
  14874. (37510, 'UserComment'),
  14875. (37520, 'SubsecTime'),
  14876. (37521, 'SubsecTimeOriginal'),
  14877. (37522, 'SubsecTimeDigitized'),
  14878. (37679, 'MODIText'), # Microsoft Office Document Imaging
  14879. (37680, 'MODIOLEPropertySetStorage'),
  14880. (37681, 'MODIPositioning'),
  14881. (37701, 'AgilentBinary'), # private structure
  14882. (37702, 'AgilentString'), # file description
  14883. (37706, 'TVIPS'), # offset to TemData structure
  14884. (37707, 'TVIPS1'),
  14885. (37708, 'TVIPS2'), # same TemData structure as undefined
  14886. (37724, 'ImageSourceData'), # Photoshop
  14887. (37888, 'Temperature'),
  14888. (37889, 'Humidity'),
  14889. (37890, 'Pressure'),
  14890. (37891, 'WaterDepth'),
  14891. (37892, 'Acceleration'),
  14892. (37893, 'CameraElevationAngle'),
  14893. (40000, 'XPos'), # Janelia
  14894. (40001, 'YPos'),
  14895. (40002, 'ZPos'),
  14896. (40001, 'MC_IpWinScal'), # Media Cybernetics
  14897. (40001, 'RecipName'), # MS FAX
  14898. (40002, 'RecipNumber'),
  14899. (40003, 'SenderName'),
  14900. (40004, 'Routing'),
  14901. (40005, 'CallerId'),
  14902. (40006, 'TSID'),
  14903. (40007, 'CSID'),
  14904. (40008, 'FaxTime'),
  14905. (40100, 'MC_IdOld'),
  14906. (40106, 'MC_Unknown'),
  14907. (40965, 'InteroperabilityTag'), # InteropOffset
  14908. (40091, 'XPTitle'),
  14909. (40092, 'XPComment'),
  14910. (40093, 'XPAuthor'),
  14911. (40094, 'XPKeywords'),
  14912. (40095, 'XPSubject'),
  14913. (40960, 'FlashpixVersion'),
  14914. (40961, 'ColorSpace'),
  14915. (40962, 'PixelXDimension'),
  14916. (40963, 'PixelYDimension'),
  14917. (40964, 'RelatedSoundFile'),
  14918. (40976, 'SamsungRawPointersOffset'),
  14919. (40977, 'SamsungRawPointersLength'),
  14920. (41217, 'SamsungRawByteOrder'),
  14921. (41218, 'SamsungRawUnknown'),
  14922. (41483, 'FlashEnergy'),
  14923. (41484, 'SpatialFrequencyResponse'),
  14924. (41485, 'Noise'), # 37389
  14925. (41486, 'FocalPlaneXResolution'), # 37390
  14926. (41487, 'FocalPlaneYResolution'), # 37391
  14927. (41488, 'FocalPlaneResolutionUnit'), # 37392
  14928. (41489, 'ImageNumber'), # 37393
  14929. (41490, 'SecurityClassification'), # 37394
  14930. (41491, 'ImageHistory'), # 37395
  14931. (41492, 'SubjectLocation'), # 37395
  14932. (41493, 'ExposureIndex '), # 37397
  14933. (41494, 'TIFF-EPStandardID'),
  14934. (41495, 'SensingMethod'), # 37399
  14935. (41728, 'FileSource'),
  14936. (41729, 'SceneType'),
  14937. (41730, 'CFAPattern'), # 33422
  14938. (41985, 'CustomRendered'),
  14939. (41986, 'ExposureMode'),
  14940. (41987, 'WhiteBalance'),
  14941. (41988, 'DigitalZoomRatio'),
  14942. (41989, 'FocalLengthIn35mmFilm'),
  14943. (41990, 'SceneCaptureType'),
  14944. (41991, 'GainControl'),
  14945. (41992, 'Contrast'),
  14946. (41993, 'Saturation'),
  14947. (41994, 'Sharpness'),
  14948. (41995, 'DeviceSettingDescription'),
  14949. (41996, 'SubjectDistanceRange'),
  14950. (42016, 'ImageUniqueID'),
  14951. (42032, 'CameraOwnerName'),
  14952. (42033, 'BodySerialNumber'),
  14953. (42034, 'LensSpecification'),
  14954. (42035, 'LensMake'),
  14955. (42036, 'LensModel'),
  14956. (42037, 'LensSerialNumber'),
  14957. (42080, 'CompositeImage'),
  14958. (42081, 'SourceImageNumberCompositeImage'),
  14959. (42082, 'SourceExposureTimesCompositeImage'),
  14960. (42112, 'GDAL_METADATA'),
  14961. (42113, 'GDAL_NODATA'),
  14962. (42240, 'Gamma'),
  14963. (43314, 'NIHImageHeader'),
  14964. (44992, 'ExpandSoftware'),
  14965. (44993, 'ExpandLens'),
  14966. (44994, 'ExpandFilm'),
  14967. (44995, 'ExpandFilterLens'),
  14968. (44996, 'ExpandScanner'),
  14969. (44997, 'ExpandFlashLamp'),
  14970. (48129, 'PixelFormat'), # HDP and WDP
  14971. (48130, 'Transformation'),
  14972. (48131, 'Uncompressed'),
  14973. (48132, 'ImageType'),
  14974. (48256, 'ImageWidth'), # 256
  14975. (48257, 'ImageHeight'),
  14976. (48258, 'WidthResolution'),
  14977. (48259, 'HeightResolution'),
  14978. (48320, 'ImageOffset'),
  14979. (48321, 'ImageByteCount'),
  14980. (48322, 'AlphaOffset'),
  14981. (48323, 'AlphaByteCount'),
  14982. (48324, 'ImageDataDiscard'),
  14983. (48325, 'AlphaDataDiscard'),
  14984. (50003, 'KodakAPP3'),
  14985. (50215, 'OceScanjobDescription'),
  14986. (50216, 'OceApplicationSelector'),
  14987. (50217, 'OceIdentificationNumber'),
  14988. (50218, 'OceImageLogicCharacteristics'),
  14989. (50255, 'Annotations'),
  14990. (50288, 'MC_Id'), # Media Cybernetics
  14991. (50289, 'MC_XYPosition'),
  14992. (50290, 'MC_ZPosition'),
  14993. (50291, 'MC_XYCalibration'),
  14994. (50292, 'MC_LensCharacteristics'),
  14995. (50293, 'MC_ChannelName'),
  14996. (50294, 'MC_ExcitationWavelength'),
  14997. (50295, 'MC_TimeStamp'),
  14998. (50296, 'MC_FrameProperties'),
  14999. (50341, 'PrintImageMatching'),
  15000. (50495, 'PCO_RAW'), # TODO, PCO CamWare
  15001. (50547, 'OriginalFileName'),
  15002. (50560, 'USPTO_OriginalContentType'), # US Patent Office
  15003. (50561, 'USPTO_RotationCode'),
  15004. (50648, 'CR2Unknown1'),
  15005. (50649, 'CR2Unknown2'),
  15006. (50656, 'CR2CFAPattern'),
  15007. (50674, 'LercParameters'), # ESGI 50674 .. 50677
  15008. (50706, 'DNGVersion'), # DNG 50706 .. 51114
  15009. (50707, 'DNGBackwardVersion'),
  15010. (50708, 'UniqueCameraModel'),
  15011. (50709, 'LocalizedCameraModel'),
  15012. (50710, 'CFAPlaneColor'),
  15013. (50711, 'CFALayout'),
  15014. (50712, 'LinearizationTable'),
  15015. (50713, 'BlackLevelRepeatDim'),
  15016. (50714, 'BlackLevel'),
  15017. (50715, 'BlackLevelDeltaH'),
  15018. (50716, 'BlackLevelDeltaV'),
  15019. (50717, 'WhiteLevel'),
  15020. (50718, 'DefaultScale'),
  15021. (50719, 'DefaultCropOrigin'),
  15022. (50720, 'DefaultCropSize'),
  15023. (50721, 'ColorMatrix1'),
  15024. (50722, 'ColorMatrix2'),
  15025. (50723, 'CameraCalibration1'),
  15026. (50724, 'CameraCalibration2'),
  15027. (50725, 'ReductionMatrix1'),
  15028. (50726, 'ReductionMatrix2'),
  15029. (50727, 'AnalogBalance'),
  15030. (50728, 'AsShotNeutral'),
  15031. (50729, 'AsShotWhiteXY'),
  15032. (50730, 'BaselineExposure'),
  15033. (50731, 'BaselineNoise'),
  15034. (50732, 'BaselineSharpness'),
  15035. (50733, 'BayerGreenSplit'),
  15036. (50734, 'LinearResponseLimit'),
  15037. (50735, 'CameraSerialNumber'),
  15038. (50736, 'LensInfo'),
  15039. (50737, 'ChromaBlurRadius'),
  15040. (50738, 'AntiAliasStrength'),
  15041. (50739, 'ShadowScale'),
  15042. (50740, 'DNGPrivateData'),
  15043. (50741, 'MakerNoteSafety'),
  15044. (50752, 'RawImageSegmentation'),
  15045. (50778, 'CalibrationIlluminant1'),
  15046. (50779, 'CalibrationIlluminant2'),
  15047. (50780, 'BestQualityScale'),
  15048. (50781, 'RawDataUniqueID'),
  15049. (50784, 'AliasLayerMetadata'),
  15050. (50827, 'OriginalRawFileName'),
  15051. (50828, 'OriginalRawFileData'),
  15052. (50829, 'ActiveArea'),
  15053. (50830, 'MaskedAreas'),
  15054. (50831, 'AsShotICCProfile'),
  15055. (50832, 'AsShotPreProfileMatrix'),
  15056. (50833, 'CurrentICCProfile'),
  15057. (50834, 'CurrentPreProfileMatrix'),
  15058. (50838, 'IJMetadataByteCounts'),
  15059. (50839, 'IJMetadata'),
  15060. (50844, 'RPCCoefficientTag'),
  15061. (50879, 'ColorimetricReference'),
  15062. (50885, 'SRawType'),
  15063. (50898, 'PanasonicTitle'),
  15064. (50899, 'PanasonicTitle2'),
  15065. (50908, 'RSID'), # DGIWG
  15066. (50909, 'GEO_METADATA'), # DGIWG XML
  15067. (50931, 'CameraCalibrationSignature'),
  15068. (50932, 'ProfileCalibrationSignature'),
  15069. (50933, 'ProfileIFD'), # EXTRACAMERAPROFILES
  15070. (50934, 'AsShotProfileName'),
  15071. (50935, 'NoiseReductionApplied'),
  15072. (50936, 'ProfileName'),
  15073. (50937, 'ProfileHueSatMapDims'),
  15074. (50938, 'ProfileHueSatMapData1'),
  15075. (50939, 'ProfileHueSatMapData2'),
  15076. (50940, 'ProfileToneCurve'),
  15077. (50941, 'ProfileEmbedPolicy'),
  15078. (50942, 'ProfileCopyright'),
  15079. (50964, 'ForwardMatrix1'),
  15080. (50965, 'ForwardMatrix2'),
  15081. (50966, 'PreviewApplicationName'),
  15082. (50967, 'PreviewApplicationVersion'),
  15083. (50968, 'PreviewSettingsName'),
  15084. (50969, 'PreviewSettingsDigest'),
  15085. (50970, 'PreviewColorSpace'),
  15086. (50971, 'PreviewDateTime'),
  15087. (50972, 'RawImageDigest'),
  15088. (50973, 'OriginalRawFileDigest'),
  15089. (50974, 'SubTileBlockSize'),
  15090. (50975, 'RowInterleaveFactor'),
  15091. (50981, 'ProfileLookTableDims'),
  15092. (50982, 'ProfileLookTableData'),
  15093. (51008, 'OpcodeList1'),
  15094. (51009, 'OpcodeList2'),
  15095. (51022, 'OpcodeList3'),
  15096. (51023, 'FibicsXML'),
  15097. (51041, 'NoiseProfile'),
  15098. (51043, 'TimeCodes'),
  15099. (51044, 'FrameRate'),
  15100. (51058, 'TStop'),
  15101. (51081, 'ReelName'),
  15102. (51089, 'OriginalDefaultFinalSize'),
  15103. (51090, 'OriginalBestQualitySize'),
  15104. (51091, 'OriginalDefaultCropSize'),
  15105. (51105, 'CameraLabel'),
  15106. (51107, 'ProfileHueSatMapEncoding'),
  15107. (51108, 'ProfileLookTableEncoding'),
  15108. (51109, 'BaselineExposureOffset'),
  15109. (51110, 'DefaultBlackRender'),
  15110. (51111, 'NewRawImageDigest'),
  15111. (51112, 'RawToPreviewGain'),
  15112. (51113, 'CacheBlob'),
  15113. (51114, 'CacheVersion'),
  15114. (51123, 'MicroManagerMetadata'),
  15115. (51125, 'DefaultUserCrop'),
  15116. (51159, 'ZIFmetadata'), # Objective Pathology Services
  15117. (51160, 'ZIFannotations'), # Objective Pathology Services
  15118. (51177, 'DepthFormat'),
  15119. (51178, 'DepthNear'),
  15120. (51179, 'DepthFar'),
  15121. (51180, 'DepthUnits'),
  15122. (51181, 'DepthMeasureType'),
  15123. (51182, 'EnhanceParams'),
  15124. (52525, 'ProfileGainTableMap'), # DNG 1.6
  15125. (52526, 'SemanticName'), # DNG 1.6
  15126. (52528, 'SemanticInstanceID'), # DNG 1.6
  15127. (52536, 'MaskSubArea'), # DNG 1.6
  15128. (52543, 'RGBTables'), # DNG 1.6
  15129. (52529, 'CalibrationIlluminant3'), # DNG 1.6
  15130. (52531, 'ColorMatrix3'), # DNG 1.6
  15131. (52530, 'CameraCalibration3'), # DNG 1.6
  15132. (52538, 'ReductionMatrix3'), # DNG 1.6
  15133. (52537, 'ProfileHueSatMapData3'), # DNG 1.6
  15134. (52532, 'ForwardMatrix3'), # DNG 1.6
  15135. (52533, 'IlluminantData1'), # DNG 1.6
  15136. (52534, 'IlluminantData2'), # DNG 1.6
  15137. (53535, 'IlluminantData3'), # DNG 1.6
  15138. (52544, 'ProfileGainTableMap2'), # DNG 1.7
  15139. (52547, 'ColumnInterleaveFactor'), # DNG 1.7
  15140. (52548, 'ImageSequenceInfo'), # DNG 1.7
  15141. (52550, 'ImageStats'), # DNG 1.7
  15142. (52551, 'ProfileDynamicRange'), # DNG 1.7
  15143. (52552, 'ProfileGroupName'), # DNG 1.7
  15144. (52553, 'JXLDistance'), # DNG 1.7
  15145. (52554, 'JXLEffort'), # DNG 1.7
  15146. (52555, 'JXLDecodeSpeed'), # DNG 1.7
  15147. (55000, 'AperioUnknown55000'),
  15148. (55001, 'AperioMagnification'),
  15149. (55002, 'AperioMPP'),
  15150. (55003, 'AperioScanScopeID'),
  15151. (55004, 'AperioDate'),
  15152. (59932, 'Padding'),
  15153. (59933, 'OffsetSchema'),
  15154. # Reusable Tags 65000-65535
  15155. # (65000, 'DimapDocumentXML'),
  15156. # EER metadata:
  15157. # (65001, 'AcquisitionMetadata'),
  15158. # (65002, 'FrameMetadata'),
  15159. # (65005, 'ImageMetadata'), # ?
  15160. # (65006, 'ImageMetadata'),
  15161. # (65007, 'PosSkipBits'),
  15162. # (65008, 'HorzSubBits'),
  15163. # (65009, 'VertSubBits'),
  15164. # Photoshop Camera RAW EXIF tags:
  15165. # (65000, 'OwnerName'),
  15166. # (65001, 'SerialNumber'),
  15167. # (65002, 'Lens'),
  15168. # (65024, 'KodakKDCPrivateIFD'),
  15169. # (65100, 'RawFile'),
  15170. # (65101, 'Converter'),
  15171. # (65102, 'WhiteBalance'),
  15172. # (65105, 'Exposure'),
  15173. # (65106, 'Shadows'),
  15174. # (65107, 'Brightness'),
  15175. # (65108, 'Contrast'),
  15176. # (65109, 'Saturation'),
  15177. # (65110, 'Sharpness'),
  15178. # (65111, 'Smoothness'),
  15179. # (65112, 'MoireFilter'),
  15180. # JEOL TEM metadata
  15181. # (65006, 'JEOL_DOUBLE1'),
  15182. # (65007, 'JEOL_DOUBLE2'),
  15183. # (65009, 'JEOL_DOUBLE3'),
  15184. # (65010, 'JEOL_DOUBLE4'),
  15185. # (65015, 'JEOL_SLONG1'),
  15186. # (65016, 'JEOL_SLONG2'),
  15187. # (65024, 'JEOL_DOUBLE5'),
  15188. # (65025, 'JEOL_DOUBLE6'),
  15189. # (65026, 'JEOL_SLONG3'),
  15190. (65027, 'JEOL_Header'),
  15191. (65200, 'FlexXML'),
  15192. )
  15193. )
  15194. @cached_property
  15195. def TAG_READERS(
  15196. self,
  15197. ) -> dict[int, Callable[[FileHandle, ByteOrder, int, int, int], Any]]:
  15198. # map tag codes to import functions
  15199. return {
  15200. 301: read_colormap,
  15201. 320: read_colormap,
  15202. # 700: read_bytes, # read_utf8,
  15203. # 34377: read_bytes,
  15204. 33723: read_bytes,
  15205. # 34675: read_bytes,
  15206. 33628: read_uic1tag, # Universal Imaging Corp STK
  15207. 33629: read_uic2tag,
  15208. 33630: read_uic3tag,
  15209. 33631: read_uic4tag,
  15210. 34118: read_cz_sem, # Carl Zeiss SEM
  15211. 34361: read_mm_header, # Olympus FluoView
  15212. 34362: read_mm_stamp,
  15213. 34363: read_numpy, # MM_Unknown
  15214. 34386: read_numpy, # MM_UserBlock
  15215. 34412: read_cz_lsminfo, # Carl Zeiss LSM
  15216. 34680: read_fei_metadata, # S-FEG
  15217. 34682: read_fei_metadata, # Helios NanoLab
  15218. 37706: read_tvips_header, # TVIPS EMMENU
  15219. 37724: read_bytes, # ImageSourceData
  15220. 33923: read_bytes, # read_leica_magic
  15221. 43314: read_nih_image_header,
  15222. # 40001: read_bytes,
  15223. 40100: read_bytes,
  15224. 50288: read_bytes,
  15225. 50296: read_bytes,
  15226. 50839: read_bytes,
  15227. 51123: read_json,
  15228. 33471: read_sis_ini,
  15229. 33560: read_sis,
  15230. 34665: read_exif_ifd,
  15231. 34853: read_gps_ifd, # conflicts with OlympusSIS
  15232. 40965: read_interoperability_ifd,
  15233. 65426: read_numpy, # NDPI McuStarts
  15234. 65432: read_numpy, # NDPI McuStartsHighBytes
  15235. 65439: read_numpy, # NDPI unknown
  15236. 65459: read_bytes, # NDPI bytes, not string
  15237. }
  15238. @cached_property
  15239. def TAG_LOAD(self) -> frozenset[int]:
  15240. # tags whose values are not delay loaded
  15241. return frozenset(
  15242. (
  15243. 258, # BitsPerSample
  15244. 270, # ImageDescription
  15245. 273, # StripOffsets
  15246. 277, # SamplesPerPixel
  15247. 279, # StripByteCounts
  15248. 282, # XResolution
  15249. 283, # YResolution
  15250. # 301, # TransferFunction
  15251. 305, # Software
  15252. # 306, # DateTime
  15253. # 320, # ColorMap
  15254. 324, # TileOffsets
  15255. 325, # TileByteCounts
  15256. 330, # SubIFDs
  15257. 338, # ExtraSamples
  15258. 339, # SampleFormat
  15259. 347, # JPEGTables
  15260. 513, # JPEGInterchangeFormat
  15261. 514, # JPEGInterchangeFormatLength
  15262. 530, # YCbCrSubSampling
  15263. 33628, # UIC1tag
  15264. 42113, # GDAL_NODATA
  15265. 50838, # IJMetadataByteCounts
  15266. 50839, # IJMetadata
  15267. )
  15268. )
  15269. @cached_property
  15270. def TAG_FILTERED(self) -> frozenset[int]:
  15271. # tags filtered from extratags in :py:meth:`TiffWriter.write`
  15272. return frozenset(
  15273. (
  15274. 256, # ImageWidth
  15275. 257, # ImageLength
  15276. 258, # BitsPerSample
  15277. 259, # Compression
  15278. 262, # PhotometricInterpretation
  15279. 266, # FillOrder
  15280. 273, # StripOffsets
  15281. 277, # SamplesPerPixel
  15282. 278, # RowsPerStrip
  15283. 279, # StripByteCounts
  15284. 284, # PlanarConfiguration
  15285. 317, # Predictor
  15286. 322, # TileWidth
  15287. 323, # TileLength
  15288. 324, # TileOffsets
  15289. 325, # TileByteCounts
  15290. 330, # SubIFDs,
  15291. 338, # ExtraSamples
  15292. 339, # SampleFormat
  15293. 400, # GlobalParametersIFD
  15294. 32997, # ImageDepth
  15295. 32998, # TileDepth
  15296. 34665, # ExifTag
  15297. 34853, # GPSTag
  15298. 40965, # InteroperabilityTag
  15299. )
  15300. )
  15301. @cached_property
  15302. def TAG_TUPLE(self) -> frozenset[int]:
  15303. # tags whose values must be stored as tuples
  15304. return frozenset(
  15305. (
  15306. 273,
  15307. 279,
  15308. 282,
  15309. 283,
  15310. 324,
  15311. 325,
  15312. 330,
  15313. 338,
  15314. 513,
  15315. 514,
  15316. 530,
  15317. 531,
  15318. 34736,
  15319. 50838,
  15320. )
  15321. )
  15322. @cached_property
  15323. def TAG_ATTRIBUTES(self) -> dict[int, str]:
  15324. # map tag codes to TiffPage attribute names
  15325. return {
  15326. 254: 'subfiletype',
  15327. 256: 'imagewidth',
  15328. 257: 'imagelength',
  15329. # 258: 'bitspersample', # set manually
  15330. 259: 'compression',
  15331. 262: 'photometric',
  15332. 266: 'fillorder',
  15333. 270: 'description',
  15334. 277: 'samplesperpixel',
  15335. 278: 'rowsperstrip',
  15336. 284: 'planarconfig',
  15337. # 301: 'transferfunction', # delay load
  15338. 305: 'software',
  15339. # 320: 'colormap', # delay load
  15340. 317: 'predictor',
  15341. 322: 'tilewidth',
  15342. 323: 'tilelength',
  15343. 330: 'subifds',
  15344. 338: 'extrasamples',
  15345. # 339: 'sampleformat', # set manually
  15346. 347: 'jpegtables',
  15347. 530: 'subsampling',
  15348. 32997: 'imagedepth',
  15349. 32998: 'tiledepth',
  15350. }
  15351. @cached_property
  15352. def TAG_ENUM(self) -> dict[int, type[enum.Enum]]:
  15353. # map tag codes to Enums
  15354. return {
  15355. 254: FILETYPE,
  15356. 255: OFILETYPE,
  15357. 259: COMPRESSION,
  15358. 262: PHOTOMETRIC,
  15359. # 263: THRESHOLD,
  15360. 266: FILLORDER,
  15361. 274: ORIENTATION,
  15362. 284: PLANARCONFIG,
  15363. # 290: GRAYRESPONSEUNIT,
  15364. # 292: GROUP3OPT
  15365. # 293: GROUP4OPT
  15366. 296: RESUNIT,
  15367. # 300: COLORRESPONSEUNIT,
  15368. 317: PREDICTOR,
  15369. 338: EXTRASAMPLE,
  15370. 339: SAMPLEFORMAT,
  15371. # 512: JPEGPROC
  15372. # 531: YCBCRPOSITION
  15373. }
  15374. @cached_property
  15375. def EXIF_TAGS(self) -> TiffTagRegistry:
  15376. """Registry of EXIF tags, including private Photoshop Camera RAW."""
  15377. # 65000 - 65112 Photoshop Camera RAW EXIF tags
  15378. tags = TiffTagRegistry(
  15379. (
  15380. (65000, 'OwnerName'),
  15381. (65001, 'SerialNumber'),
  15382. (65002, 'Lens'),
  15383. (65100, 'RawFile'),
  15384. (65101, 'Converter'),
  15385. (65102, 'WhiteBalance'),
  15386. (65105, 'Exposure'),
  15387. (65106, 'Shadows'),
  15388. (65107, 'Brightness'),
  15389. (65108, 'Contrast'),
  15390. (65109, 'Saturation'),
  15391. (65110, 'Sharpness'),
  15392. (65111, 'Smoothness'),
  15393. (65112, 'MoireFilter'),
  15394. )
  15395. )
  15396. tags.update(TIFF.TAGS)
  15397. return tags
  15398. @cached_property
  15399. def NDPI_TAGS(self) -> TiffTagRegistry:
  15400. """Registry of private TIFF tags for Hamamatsu NDPI (65420-65458)."""
  15401. # TODO: obtain specification
  15402. return TiffTagRegistry(
  15403. (
  15404. (65324, 'OffsetHighBytes'),
  15405. (65325, 'ByteCountHighBytes'),
  15406. (65420, 'FileFormat'),
  15407. (65421, 'Magnification'), # SourceLens
  15408. (65422, 'XOffsetFromSlideCenter'),
  15409. (65423, 'YOffsetFromSlideCenter'),
  15410. (65424, 'ZOffsetFromSlideCenter'), # FocalPlane
  15411. (65425, 'TissueIndex'),
  15412. (65426, 'McuStarts'),
  15413. (65427, 'SlideLabel'),
  15414. (65428, 'AuthCode'), # ?
  15415. (65429, '65429'),
  15416. (65430, '65430'),
  15417. (65431, '65431'),
  15418. (65432, 'McuStartsHighBytes'),
  15419. (65433, '65433'),
  15420. (65434, 'Fluorescence'), # FilterSetName, Channel
  15421. (65435, 'ExposureRatio'),
  15422. (65436, 'RedMultiplier'),
  15423. (65437, 'GreenMultiplier'),
  15424. (65438, 'BlueMultiplier'),
  15425. (65439, 'FocusPoints'),
  15426. (65440, 'FocusPointRegions'),
  15427. (65441, 'CaptureMode'),
  15428. (65442, 'ScannerSerialNumber'),
  15429. (65443, '65443'),
  15430. (65444, 'JpegQuality'),
  15431. (65445, 'RefocusInterval'),
  15432. (65446, 'FocusOffset'),
  15433. (65447, 'BlankLines'),
  15434. (65448, 'FirmwareVersion'),
  15435. (65449, 'Comments'), # PropertyMap, CalibrationInfo
  15436. (65450, 'LabelObscured'),
  15437. (65451, 'Wavelength'),
  15438. (65452, '65452'),
  15439. (65453, 'LampAge'),
  15440. (65454, 'ExposureTime'),
  15441. (65455, 'FocusTime'),
  15442. (65456, 'ScanTime'),
  15443. (65457, 'WriteTime'),
  15444. (65458, 'FullyAutoFocus'),
  15445. (65500, 'DefaultGamma'),
  15446. )
  15447. )
  15448. @cached_property
  15449. def GPS_TAGS(self) -> TiffTagRegistry:
  15450. """Registry of GPS IFD tags."""
  15451. return TiffTagRegistry(
  15452. (
  15453. (0, 'GPSVersionID'),
  15454. (1, 'GPSLatitudeRef'),
  15455. (2, 'GPSLatitude'),
  15456. (3, 'GPSLongitudeRef'),
  15457. (4, 'GPSLongitude'),
  15458. (5, 'GPSAltitudeRef'),
  15459. (6, 'GPSAltitude'),
  15460. (7, 'GPSTimeStamp'),
  15461. (8, 'GPSSatellites'),
  15462. (9, 'GPSStatus'),
  15463. (10, 'GPSMeasureMode'),
  15464. (11, 'GPSDOP'),
  15465. (12, 'GPSSpeedRef'),
  15466. (13, 'GPSSpeed'),
  15467. (14, 'GPSTrackRef'),
  15468. (15, 'GPSTrack'),
  15469. (16, 'GPSImgDirectionRef'),
  15470. (17, 'GPSImgDirection'),
  15471. (18, 'GPSMapDatum'),
  15472. (19, 'GPSDestLatitudeRef'),
  15473. (20, 'GPSDestLatitude'),
  15474. (21, 'GPSDestLongitudeRef'),
  15475. (22, 'GPSDestLongitude'),
  15476. (23, 'GPSDestBearingRef'),
  15477. (24, 'GPSDestBearing'),
  15478. (25, 'GPSDestDistanceRef'),
  15479. (26, 'GPSDestDistance'),
  15480. (27, 'GPSProcessingMethod'),
  15481. (28, 'GPSAreaInformation'),
  15482. (29, 'GPSDateStamp'),
  15483. (30, 'GPSDifferential'),
  15484. (31, 'GPSHPositioningError'),
  15485. )
  15486. )
  15487. @cached_property
  15488. def IOP_TAGS(self) -> TiffTagRegistry:
  15489. """Registry of Interoperability IFD tags."""
  15490. return TiffTagRegistry(
  15491. (
  15492. (1, 'InteroperabilityIndex'),
  15493. (2, 'InteroperabilityVersion'),
  15494. (4096, 'RelatedImageFileFormat'),
  15495. (4097, 'RelatedImageWidth'),
  15496. (4098, 'RelatedImageLength'),
  15497. )
  15498. )
  15499. @cached_property
  15500. def PHOTOMETRIC_SAMPLES(self) -> dict[int, int]:
  15501. """Map :py:class:`PHOTOMETRIC` to number of photometric samples."""
  15502. return {
  15503. 0: 1, # MINISWHITE
  15504. 1: 1, # MINISBLACK
  15505. 2: 3, # RGB
  15506. 3: 1, # PALETTE
  15507. 4: 1, # MASK
  15508. 5: 4, # SEPARATED
  15509. 6: 3, # YCBCR
  15510. 8: 3, # CIELAB
  15511. 9: 3, # ICCLAB
  15512. 10: 3, # ITULAB
  15513. 32803: 1, # CFA
  15514. 32844: 1, # LOGL ?
  15515. 32845: 3, # LOGLUV
  15516. 34892: 3, # LINEAR_RAW ?
  15517. 51177: 1, # DEPTH_MAP ?
  15518. 52527: 1, # SEMANTIC_MASK ?
  15519. }
  15520. @cached_property
  15521. def DATA_FORMATS(self) -> dict[int, str]:
  15522. """Map :py:class:`DATATYPE` to Python struct formats."""
  15523. return {
  15524. 1: '1B',
  15525. 2: '1s',
  15526. 3: '1H',
  15527. 4: '1I',
  15528. 5: '2I',
  15529. 6: '1b',
  15530. 7: '1B',
  15531. 8: '1h',
  15532. 9: '1i',
  15533. 10: '2i',
  15534. 11: '1f',
  15535. 12: '1d',
  15536. 13: '1I',
  15537. # 14: '',
  15538. # 15: '',
  15539. 16: '1Q',
  15540. 17: '1q',
  15541. 18: '1Q',
  15542. }
  15543. @cached_property
  15544. def DATA_DTYPES(self) -> dict[str, int]:
  15545. """Map NumPy dtype to :py:class:`DATATYPE`."""
  15546. return {
  15547. 'B': 1,
  15548. 's': 2,
  15549. 'H': 3,
  15550. 'I': 4,
  15551. '2I': 5,
  15552. 'b': 6,
  15553. 'h': 8,
  15554. 'i': 9,
  15555. '2i': 10,
  15556. 'f': 11,
  15557. 'd': 12,
  15558. 'Q': 16,
  15559. 'q': 17,
  15560. }
  15561. @cached_property
  15562. def SAMPLE_DTYPES(self) -> dict[tuple[int, int | tuple[int, ...]], str]:
  15563. """Map :py:class:`SAMPLEFORMAT` and BitsPerSample to NumPy dtype."""
  15564. return {
  15565. # UINT
  15566. (1, 1): '?', # bitmap
  15567. (1, 2): 'B',
  15568. (1, 3): 'B',
  15569. (1, 4): 'B',
  15570. (1, 5): 'B',
  15571. (1, 6): 'B',
  15572. (1, 7): 'B',
  15573. (1, 8): 'B',
  15574. (1, 9): 'H',
  15575. (1, 10): 'H',
  15576. (1, 11): 'H',
  15577. (1, 12): 'H',
  15578. (1, 13): 'H',
  15579. (1, 14): 'H',
  15580. (1, 15): 'H',
  15581. (1, 16): 'H',
  15582. (1, 17): 'I',
  15583. (1, 18): 'I',
  15584. (1, 19): 'I',
  15585. (1, 20): 'I',
  15586. (1, 21): 'I',
  15587. (1, 22): 'I',
  15588. (1, 23): 'I',
  15589. (1, 24): 'I',
  15590. (1, 25): 'I',
  15591. (1, 26): 'I',
  15592. (1, 27): 'I',
  15593. (1, 28): 'I',
  15594. (1, 29): 'I',
  15595. (1, 30): 'I',
  15596. (1, 31): 'I',
  15597. (1, 32): 'I',
  15598. (1, 64): 'Q',
  15599. # VOID : treat as UINT
  15600. (4, 1): '?', # bitmap
  15601. (4, 2): 'B',
  15602. (4, 3): 'B',
  15603. (4, 4): 'B',
  15604. (4, 5): 'B',
  15605. (4, 6): 'B',
  15606. (4, 7): 'B',
  15607. (4, 8): 'B',
  15608. (4, 9): 'H',
  15609. (4, 10): 'H',
  15610. (4, 11): 'H',
  15611. (4, 12): 'H',
  15612. (4, 13): 'H',
  15613. (4, 14): 'H',
  15614. (4, 15): 'H',
  15615. (4, 16): 'H',
  15616. (4, 17): 'I',
  15617. (4, 18): 'I',
  15618. (4, 19): 'I',
  15619. (4, 20): 'I',
  15620. (4, 21): 'I',
  15621. (4, 22): 'I',
  15622. (4, 23): 'I',
  15623. (4, 24): 'I',
  15624. (4, 25): 'I',
  15625. (4, 26): 'I',
  15626. (4, 27): 'I',
  15627. (4, 28): 'I',
  15628. (4, 29): 'I',
  15629. (4, 30): 'I',
  15630. (4, 31): 'I',
  15631. (4, 32): 'I',
  15632. (4, 64): 'Q',
  15633. # INT
  15634. (2, 8): 'b',
  15635. (2, 16): 'h',
  15636. (2, 32): 'i',
  15637. (2, 64): 'q',
  15638. # IEEEFP
  15639. (3, 16): 'e',
  15640. (3, 24): 'f', # float24 bit not supported by numpy
  15641. (3, 32): 'f',
  15642. (3, 64): 'd',
  15643. # COMPLEXIEEEFP
  15644. (6, 64): 'F',
  15645. (6, 128): 'D',
  15646. # RGB565
  15647. (1, (5, 6, 5)): 'B',
  15648. # COMPLEXINT : not supported by numpy
  15649. (5, 16): 'E',
  15650. (5, 32): 'F',
  15651. (5, 64): 'D',
  15652. }
  15653. @cached_property
  15654. def PREDICTORS(self) -> Mapping[int, Callable[..., Any]]:
  15655. """Map :py:class:`PREDICTOR` value to encode function."""
  15656. return PredictorCodec(encode=True)
  15657. @cached_property
  15658. def UNPREDICTORS(self) -> Mapping[int, Callable[..., Any]]:
  15659. """Map :py:class:`PREDICTOR` value to decode function."""
  15660. return PredictorCodec(encode=False)
  15661. @cached_property
  15662. def COMPRESSORS(self) -> Mapping[int, Callable[..., Any]]:
  15663. """Map :py:class:`COMPRESSION` value to compress function."""
  15664. return CompressionCodec(encode=True)
  15665. @cached_property
  15666. def DECOMPRESSORS(self) -> Mapping[int, Callable[..., Any]]:
  15667. """Map :py:class:`COMPRESSION` value to decompress function."""
  15668. return CompressionCodec(encode=False)
  15669. @cached_property
  15670. def IMAGE_COMPRESSIONS(self) -> set[int]:
  15671. # set of compression to encode/decode images
  15672. # encode/decode preserves shape and dtype
  15673. # cannot be used with predictors or fillorder
  15674. return {
  15675. 6, # jpeg
  15676. 7, # jpeg
  15677. 22610, # jpegxr
  15678. 33003, # jpeg2k
  15679. 33004, # jpeg2k
  15680. 33005, # jpeg2k
  15681. 33007, # alt_jpeg
  15682. 34712, # jpeg2k
  15683. 34892, # jpeg
  15684. 34933, # png
  15685. 34934, # jpegxr ZIF
  15686. 48124, # jetraw
  15687. 50001, # webp
  15688. 50002, # jpegxl
  15689. 52546, # jpegxl DNG
  15690. 65000, # EER
  15691. 65001, # EER
  15692. 65002, # EER
  15693. }
  15694. @cached_property
  15695. def AXES_NAMES(self) -> dict[str, str]:
  15696. """Map axes character codes to dimension names.
  15697. - **X : width** (image width)
  15698. - **Y : height** (image length)
  15699. - **Z : depth** (image depth)
  15700. - **S : sample** (color space and extra samples)
  15701. - **I : sequence** (generic sequence of images, frames, planes, pages)
  15702. - **T : time** (time series)
  15703. - **C : channel** (acquisition path or emission wavelength)
  15704. - **A : angle** (OME)
  15705. - **P : phase** (OME. In LSM, **P** maps to **position**)
  15706. - **R : tile** (OME. Region, position, or mosaic)
  15707. - **H : lifetime** (OME. Histogram)
  15708. - **E : lambda** (OME. Excitation wavelength)
  15709. - **Q : other** (OME)
  15710. - **L : exposure** (FluoView)
  15711. - **V : event** (FluoView)
  15712. - **M : mosaic** (LSM 6)
  15713. - **J : column** (NDTiff)
  15714. - **K : row** (NDTiff)
  15715. There is no universal standard for dimension codes or names.
  15716. This mapping mainly follows TIFF, OME-TIFF, ImageJ, LSM, and FluoView
  15717. conventions.
  15718. """
  15719. return {
  15720. 'X': 'width',
  15721. 'Y': 'height',
  15722. 'Z': 'depth',
  15723. 'S': 'sample',
  15724. 'I': 'sequence',
  15725. # 'F': 'file',
  15726. 'T': 'time',
  15727. 'C': 'channel',
  15728. 'A': 'angle',
  15729. 'P': 'phase',
  15730. 'R': 'tile',
  15731. 'H': 'lifetime',
  15732. 'E': 'lambda',
  15733. 'L': 'exposure',
  15734. 'V': 'event',
  15735. 'M': 'mosaic',
  15736. 'Q': 'other',
  15737. 'J': 'column',
  15738. 'K': 'row',
  15739. }
  15740. @cached_property
  15741. def AXES_CODES(self) -> dict[str, str]:
  15742. """Map dimension names to axes character codes.
  15743. Reverse mapping of :py:attr:`AXES_NAMES`.
  15744. """
  15745. codes = {name: code for code, name in TIFF.AXES_NAMES.items()}
  15746. codes['z'] = 'Z' # NDTiff
  15747. codes['position'] = 'R' # NDTiff
  15748. return codes
  15749. @cached_property
  15750. def GEO_KEYS(self) -> type[enum.IntEnum]:
  15751. """:py:class:`geodb.GeoKeys`."""
  15752. try:
  15753. from .geodb import GeoKeys
  15754. except ImportError:
  15755. class GeoKeys(enum.IntEnum): # type: ignore[no-redef]
  15756. pass
  15757. return GeoKeys
  15758. @cached_property
  15759. def GEO_CODES(self) -> dict[int, type[enum.IntEnum]]:
  15760. """Map :py:class:`geodb.GeoKeys` to GeoTIFF codes."""
  15761. try:
  15762. from .geodb import GEO_CODES
  15763. except ImportError:
  15764. GEO_CODES = {}
  15765. return GEO_CODES
  15766. @cached_property
  15767. def PAGE_FLAGS(self) -> set[str]:
  15768. # TiffFile and TiffPage 'is_\*' attributes
  15769. exclude = {
  15770. 'reduced',
  15771. 'mask',
  15772. 'final',
  15773. 'memmappable',
  15774. 'contiguous',
  15775. 'tiled',
  15776. 'subsampled',
  15777. 'jfif',
  15778. }
  15779. return {
  15780. a[3:]
  15781. for a in dir(TiffPage)
  15782. if a[:3] == 'is_' and a[3:] not in exclude
  15783. }
  15784. @cached_property
  15785. def FILE_FLAGS(self) -> set[str]:
  15786. # TiffFile 'is_\*' attributes
  15787. exclude = {'bigtiff', 'appendable'}
  15788. return {
  15789. a[3:]
  15790. for a in dir(TiffFile)
  15791. if a[:3] == 'is_' and a[3:] not in exclude
  15792. }.union(TIFF.PAGE_FLAGS)
  15793. @property
  15794. def FILE_PATTERNS(self) -> dict[str, str]:
  15795. # predefined FileSequence patterns
  15796. return {
  15797. 'axes': r"""(?ix)
  15798. # matches Olympus OIF and Leica TIFF series
  15799. _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))
  15800. _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
  15801. _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
  15802. _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
  15803. _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
  15804. _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
  15805. _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
  15806. """
  15807. }
  15808. @property
  15809. def FILE_EXTENSIONS(self) -> tuple[str, ...]:
  15810. """Known TIFF file extensions."""
  15811. return (
  15812. 'tif',
  15813. 'tiff',
  15814. 'ome.tif',
  15815. 'lsm',
  15816. 'stk',
  15817. 'qpi',
  15818. 'pcoraw',
  15819. 'qptiff',
  15820. 'ptiff',
  15821. 'ptif',
  15822. 'gel',
  15823. 'seq',
  15824. 'svs',
  15825. 'avs',
  15826. 'scn',
  15827. 'zif',
  15828. 'ndpi',
  15829. 'bif',
  15830. 'tf8',
  15831. 'tf2',
  15832. 'btf',
  15833. 'eer',
  15834. )
  15835. @property
  15836. def FILEOPEN_FILTER(self) -> list[tuple[str, str]]:
  15837. # string for use in Windows File Open box
  15838. return [
  15839. (f'{ext.upper()} files', f'*.{ext}')
  15840. for ext in TIFF.FILE_EXTENSIONS
  15841. ] + [('All files', '*')]
  15842. @property
  15843. def CZ_LSMINFO(self) -> list[tuple[str, str]]:
  15844. # numpy data type of LSMINFO structure
  15845. return [
  15846. ('MagicNumber', 'u4'),
  15847. ('StructureSize', 'i4'),
  15848. ('DimensionX', 'i4'),
  15849. ('DimensionY', 'i4'),
  15850. ('DimensionZ', 'i4'),
  15851. ('DimensionChannels', 'i4'),
  15852. ('DimensionTime', 'i4'),
  15853. ('DataType', 'i4'), # DATATYPES
  15854. ('ThumbnailX', 'i4'),
  15855. ('ThumbnailY', 'i4'),
  15856. ('VoxelSizeX', 'f8'),
  15857. ('VoxelSizeY', 'f8'),
  15858. ('VoxelSizeZ', 'f8'),
  15859. ('OriginX', 'f8'),
  15860. ('OriginY', 'f8'),
  15861. ('OriginZ', 'f8'),
  15862. ('ScanType', 'u2'),
  15863. ('SpectralScan', 'u2'),
  15864. ('TypeOfData', 'u4'), # TYPEOFDATA
  15865. ('OffsetVectorOverlay', 'u4'),
  15866. ('OffsetInputLut', 'u4'),
  15867. ('OffsetOutputLut', 'u4'),
  15868. ('OffsetChannelColors', 'u4'),
  15869. ('TimeIntervall', 'f8'), # typo in LSM spec
  15870. ('OffsetChannelDataTypes', 'u4'),
  15871. ('OffsetScanInformation', 'u4'), # SCANINFO
  15872. ('OffsetKsData', 'u4'),
  15873. ('OffsetTimeStamps', 'u4'),
  15874. ('OffsetEventList', 'u4'),
  15875. ('OffsetRoi', 'u4'),
  15876. ('OffsetBleachRoi', 'u4'),
  15877. ('OffsetNextRecording', 'u4'),
  15878. # LSM 2.0 ends here
  15879. ('DisplayAspectX', 'f8'),
  15880. ('DisplayAspectY', 'f8'),
  15881. ('DisplayAspectZ', 'f8'),
  15882. ('DisplayAspectTime', 'f8'),
  15883. ('OffsetMeanOfRoisOverlay', 'u4'),
  15884. ('OffsetTopoIsolineOverlay', 'u4'),
  15885. ('OffsetTopoProfileOverlay', 'u4'),
  15886. ('OffsetLinescanOverlay', 'u4'),
  15887. ('ToolbarFlags', 'u4'),
  15888. ('OffsetChannelWavelength', 'u4'),
  15889. ('OffsetChannelFactors', 'u4'),
  15890. ('ObjectiveSphereCorrection', 'f8'),
  15891. ('OffsetUnmixParameters', 'u4'),
  15892. # LSM 3.2, 4.0 end here
  15893. ('OffsetAcquisitionParameters', 'u4'),
  15894. ('OffsetCharacteristics', 'u4'),
  15895. ('OffsetPalette', 'u4'),
  15896. ('TimeDifferenceX', 'f8'),
  15897. ('TimeDifferenceY', 'f8'),
  15898. ('TimeDifferenceZ', 'f8'),
  15899. ('InternalUse1', 'u4'),
  15900. ('DimensionP', 'i4'),
  15901. ('DimensionM', 'i4'),
  15902. ('DimensionsReserved', '16i4'),
  15903. ('OffsetTilePositions', 'u4'),
  15904. ('', '9u4'), # Reserved
  15905. ('OffsetPositions', 'u4'),
  15906. # ('', '21u4'), # must be 0
  15907. ]
  15908. @property
  15909. def CZ_LSMINFO_READERS(
  15910. self,
  15911. ) -> dict[str, Callable[[FileHandle], Any] | None]:
  15912. # import functions for CZ_LSMINFO sub-records
  15913. # TODO: read more CZ_LSMINFO sub-records
  15914. return {
  15915. 'ScanInformation': read_lsm_scaninfo,
  15916. 'TimeStamps': read_lsm_timestamps,
  15917. 'EventList': read_lsm_eventlist,
  15918. 'ChannelColors': read_lsm_channelcolors,
  15919. 'Positions': read_lsm_positions,
  15920. 'TilePositions': read_lsm_positions,
  15921. 'VectorOverlay': None,
  15922. 'InputLut': read_lsm_lookuptable,
  15923. 'OutputLut': read_lsm_lookuptable,
  15924. 'TimeIntervall': None, # typo in LSM spec
  15925. 'ChannelDataTypes': read_lsm_channeldatatypes,
  15926. 'KsData': None,
  15927. 'Roi': None,
  15928. 'BleachRoi': None,
  15929. 'NextRecording': None, # read with TiffFile(fh, offset=)
  15930. 'MeanOfRoisOverlay': None,
  15931. 'TopoIsolineOverlay': None,
  15932. 'TopoProfileOverlay': None,
  15933. 'ChannelWavelength': read_lsm_channelwavelength,
  15934. 'SphereCorrection': None,
  15935. 'ChannelFactors': None,
  15936. 'UnmixParameters': None,
  15937. 'AcquisitionParameters': None,
  15938. 'Characteristics': None,
  15939. }
  15940. @property
  15941. def CZ_LSMINFO_SCANTYPE(self) -> dict[int, str]:
  15942. # map CZ_LSMINFO.ScanType to dimension order
  15943. return {
  15944. 0: 'ZCYX', # Stack, normal x-y-z-scan
  15945. 1: 'CZX', # Z-Scan, x-z-plane
  15946. 2: 'CTX', # Line or Time Series Line
  15947. 3: 'TCYX', # Time Series Plane, x-y
  15948. 4: 'TCZX', # Time Series z-Scan, x-z
  15949. 5: 'CTX', # Time Series Mean-of-ROIs
  15950. 6: 'TZCYX', # Time Series Stack, x-y-z
  15951. 7: 'TZCYX', # TODO: Spline Scan
  15952. 8: 'CZX', # Spline Plane, x-z
  15953. 9: 'TCZX', # Time Series Spline Plane, x-z
  15954. 10: 'CTX', # Point or Time Series Point
  15955. }
  15956. @property
  15957. def CZ_LSMINFO_DIMENSIONS(self) -> dict[str, str]:
  15958. # map dimension codes to CZ_LSMINFO attribute
  15959. return {
  15960. 'X': 'DimensionX',
  15961. 'Y': 'DimensionY',
  15962. 'Z': 'DimensionZ',
  15963. 'C': 'DimensionChannels',
  15964. 'T': 'DimensionTime',
  15965. 'P': 'DimensionP',
  15966. 'M': 'DimensionM',
  15967. }
  15968. @property
  15969. def CZ_LSMINFO_DATATYPES(self) -> dict[int, str]:
  15970. # description of CZ_LSMINFO.DataType
  15971. return {
  15972. 0: 'varying data types',
  15973. 1: '8 bit unsigned integer',
  15974. 2: '12 bit unsigned integer',
  15975. 5: '32 bit float',
  15976. }
  15977. @property
  15978. def CZ_LSMINFO_TYPEOFDATA(self) -> dict[int, str]:
  15979. # description of CZ_LSMINFO.TypeOfData
  15980. return {
  15981. 0: 'Original scan data',
  15982. 1: 'Calculated data',
  15983. 2: '3D reconstruction',
  15984. 3: 'Topography height map',
  15985. }
  15986. @property
  15987. def CZ_LSMINFO_SCANINFO_ARRAYS(self) -> dict[int, str]:
  15988. return {
  15989. 0x20000000: 'Tracks',
  15990. 0x30000000: 'Lasers',
  15991. 0x60000000: 'DetectionChannels',
  15992. 0x80000000: 'IlluminationChannels',
  15993. 0xA0000000: 'BeamSplitters',
  15994. 0xC0000000: 'DataChannels',
  15995. 0x11000000: 'Timers',
  15996. 0x13000000: 'Markers',
  15997. }
  15998. @property
  15999. def CZ_LSMINFO_SCANINFO_STRUCTS(self) -> dict[int, str]:
  16000. return {
  16001. # 0x10000000: 'Recording',
  16002. 0x40000000: 'Track',
  16003. 0x50000000: 'Laser',
  16004. 0x70000000: 'DetectionChannel',
  16005. 0x90000000: 'IlluminationChannel',
  16006. 0xB0000000: 'BeamSplitter',
  16007. 0xD0000000: 'DataChannel',
  16008. 0x12000000: 'Timer',
  16009. 0x14000000: 'Marker',
  16010. }
  16011. @property
  16012. def CZ_LSMINFO_SCANINFO_ATTRIBUTES(self) -> dict[int, str]:
  16013. return {
  16014. # Recording
  16015. 0x10000001: 'Name',
  16016. 0x10000002: 'Description',
  16017. 0x10000003: 'Notes',
  16018. 0x10000004: 'Objective',
  16019. 0x10000005: 'ProcessingSummary',
  16020. 0x10000006: 'SpecialScanMode',
  16021. 0x10000007: 'ScanType',
  16022. 0x10000008: 'ScanMode',
  16023. 0x10000009: 'NumberOfStacks',
  16024. 0x1000000A: 'LinesPerPlane',
  16025. 0x1000000B: 'SamplesPerLine',
  16026. 0x1000000C: 'PlanesPerVolume',
  16027. 0x1000000D: 'ImagesWidth',
  16028. 0x1000000E: 'ImagesHeight',
  16029. 0x1000000F: 'ImagesNumberPlanes',
  16030. 0x10000010: 'ImagesNumberStacks',
  16031. 0x10000011: 'ImagesNumberChannels',
  16032. 0x10000012: 'LinscanXySize',
  16033. 0x10000013: 'ScanDirection',
  16034. 0x10000014: 'TimeSeries',
  16035. 0x10000015: 'OriginalScanData',
  16036. 0x10000016: 'ZoomX',
  16037. 0x10000017: 'ZoomY',
  16038. 0x10000018: 'ZoomZ',
  16039. 0x10000019: 'Sample0X',
  16040. 0x1000001A: 'Sample0Y',
  16041. 0x1000001B: 'Sample0Z',
  16042. 0x1000001C: 'SampleSpacing',
  16043. 0x1000001D: 'LineSpacing',
  16044. 0x1000001E: 'PlaneSpacing',
  16045. 0x1000001F: 'PlaneWidth',
  16046. 0x10000020: 'PlaneHeight',
  16047. 0x10000021: 'VolumeDepth',
  16048. 0x10000023: 'Nutation',
  16049. 0x10000034: 'Rotation',
  16050. 0x10000035: 'Precession',
  16051. 0x10000036: 'Sample0time',
  16052. 0x10000037: 'StartScanTriggerIn',
  16053. 0x10000038: 'StartScanTriggerOut',
  16054. 0x10000039: 'StartScanEvent',
  16055. 0x10000040: 'StartScanTime',
  16056. 0x10000041: 'StopScanTriggerIn',
  16057. 0x10000042: 'StopScanTriggerOut',
  16058. 0x10000043: 'StopScanEvent',
  16059. 0x10000044: 'StopScanTime',
  16060. 0x10000045: 'UseRois',
  16061. 0x10000046: 'UseReducedMemoryRois',
  16062. 0x10000047: 'User',
  16063. 0x10000048: 'UseBcCorrection',
  16064. 0x10000049: 'PositionBcCorrection1',
  16065. 0x10000050: 'PositionBcCorrection2',
  16066. 0x10000051: 'InterpolationY',
  16067. 0x10000052: 'CameraBinning',
  16068. 0x10000053: 'CameraSupersampling',
  16069. 0x10000054: 'CameraFrameWidth',
  16070. 0x10000055: 'CameraFrameHeight',
  16071. 0x10000056: 'CameraOffsetX',
  16072. 0x10000057: 'CameraOffsetY',
  16073. 0x10000059: 'RtBinning',
  16074. 0x1000005A: 'RtFrameWidth',
  16075. 0x1000005B: 'RtFrameHeight',
  16076. 0x1000005C: 'RtRegionWidth',
  16077. 0x1000005D: 'RtRegionHeight',
  16078. 0x1000005E: 'RtOffsetX',
  16079. 0x1000005F: 'RtOffsetY',
  16080. 0x10000060: 'RtZoom',
  16081. 0x10000061: 'RtLinePeriod',
  16082. 0x10000062: 'Prescan',
  16083. 0x10000063: 'ScanDirectionZ',
  16084. # Track
  16085. 0x40000001: 'MultiplexType', # 0 After Line; 1 After Frame
  16086. 0x40000002: 'MultiplexOrder',
  16087. 0x40000003: 'SamplingMode', # 0 Sample; 1 Line Avg; 2 Frame Avg
  16088. 0x40000004: 'SamplingMethod', # 1 Mean; 2 Sum
  16089. 0x40000005: 'SamplingNumber',
  16090. 0x40000006: 'Acquire',
  16091. 0x40000007: 'SampleObservationTime',
  16092. 0x4000000B: 'TimeBetweenStacks',
  16093. 0x4000000C: 'Name',
  16094. 0x4000000D: 'Collimator1Name',
  16095. 0x4000000E: 'Collimator1Position',
  16096. 0x4000000F: 'Collimator2Name',
  16097. 0x40000010: 'Collimator2Position',
  16098. 0x40000011: 'IsBleachTrack',
  16099. 0x40000012: 'IsBleachAfterScanNumber',
  16100. 0x40000013: 'BleachScanNumber',
  16101. 0x40000014: 'TriggerIn',
  16102. 0x40000015: 'TriggerOut',
  16103. 0x40000016: 'IsRatioTrack',
  16104. 0x40000017: 'BleachCount',
  16105. 0x40000018: 'SpiCenterWavelength',
  16106. 0x40000019: 'PixelTime',
  16107. 0x40000021: 'CondensorFrontlens',
  16108. 0x40000023: 'FieldStopValue',
  16109. 0x40000024: 'IdCondensorAperture',
  16110. 0x40000025: 'CondensorAperture',
  16111. 0x40000026: 'IdCondensorRevolver',
  16112. 0x40000027: 'CondensorFilter',
  16113. 0x40000028: 'IdTransmissionFilter1',
  16114. 0x40000029: 'IdTransmission1',
  16115. 0x40000030: 'IdTransmissionFilter2',
  16116. 0x40000031: 'IdTransmission2',
  16117. 0x40000032: 'RepeatBleach',
  16118. 0x40000033: 'EnableSpotBleachPos',
  16119. 0x40000034: 'SpotBleachPosx',
  16120. 0x40000035: 'SpotBleachPosy',
  16121. 0x40000036: 'SpotBleachPosz',
  16122. 0x40000037: 'IdTubelens',
  16123. 0x40000038: 'IdTubelensPosition',
  16124. 0x40000039: 'TransmittedLight',
  16125. 0x4000003A: 'ReflectedLight',
  16126. 0x4000003B: 'SimultanGrabAndBleach',
  16127. 0x4000003C: 'BleachPixelTime',
  16128. # Laser
  16129. 0x50000001: 'Name',
  16130. 0x50000002: 'Acquire',
  16131. 0x50000003: 'Power',
  16132. # DetectionChannel
  16133. 0x70000001: 'IntegrationMode',
  16134. 0x70000002: 'SpecialMode',
  16135. 0x70000003: 'DetectorGainFirst',
  16136. 0x70000004: 'DetectorGainLast',
  16137. 0x70000005: 'AmplifierGainFirst',
  16138. 0x70000006: 'AmplifierGainLast',
  16139. 0x70000007: 'AmplifierOffsFirst',
  16140. 0x70000008: 'AmplifierOffsLast',
  16141. 0x70000009: 'PinholeDiameter',
  16142. 0x7000000A: 'CountingTrigger',
  16143. 0x7000000B: 'Acquire',
  16144. 0x7000000C: 'PointDetectorName',
  16145. 0x7000000D: 'AmplifierName',
  16146. 0x7000000E: 'PinholeName',
  16147. 0x7000000F: 'FilterSetName',
  16148. 0x70000010: 'FilterName',
  16149. 0x70000013: 'IntegratorName',
  16150. 0x70000014: 'ChannelName',
  16151. 0x70000015: 'DetectorGainBc1',
  16152. 0x70000016: 'DetectorGainBc2',
  16153. 0x70000017: 'AmplifierGainBc1',
  16154. 0x70000018: 'AmplifierGainBc2',
  16155. 0x70000019: 'AmplifierOffsetBc1',
  16156. 0x70000020: 'AmplifierOffsetBc2',
  16157. 0x70000021: 'SpectralScanChannels',
  16158. 0x70000022: 'SpiWavelengthStart',
  16159. 0x70000023: 'SpiWavelengthStop',
  16160. 0x70000026: 'DyeName',
  16161. 0x70000027: 'DyeFolder',
  16162. # IlluminationChannel
  16163. 0x90000001: 'Name',
  16164. 0x90000002: 'Power',
  16165. 0x90000003: 'Wavelength',
  16166. 0x90000004: 'Aquire', # typo in LSM spec
  16167. 0x90000005: 'DetchannelName',
  16168. 0x90000006: 'PowerBc1',
  16169. 0x90000007: 'PowerBc2',
  16170. # BeamSplitter
  16171. 0xB0000001: 'FilterSet',
  16172. 0xB0000002: 'Filter',
  16173. 0xB0000003: 'Name',
  16174. # DataChannel
  16175. 0xD0000001: 'Name',
  16176. 0xD0000003: 'Acquire',
  16177. 0xD0000004: 'Color',
  16178. 0xD0000005: 'SampleType',
  16179. 0xD0000006: 'BitsPerSample',
  16180. 0xD0000007: 'RatioType',
  16181. 0xD0000008: 'RatioTrack1',
  16182. 0xD0000009: 'RatioTrack2',
  16183. 0xD000000A: 'RatioChannel1',
  16184. 0xD000000B: 'RatioChannel2',
  16185. 0xD000000C: 'RatioConst1',
  16186. 0xD000000D: 'RatioConst2',
  16187. 0xD000000E: 'RatioConst3',
  16188. 0xD000000F: 'RatioConst4',
  16189. 0xD0000010: 'RatioConst5',
  16190. 0xD0000011: 'RatioConst6',
  16191. 0xD0000012: 'RatioFirstImages1',
  16192. 0xD0000013: 'RatioFirstImages2',
  16193. 0xD0000014: 'DyeName',
  16194. 0xD0000015: 'DyeFolder',
  16195. 0xD0000016: 'Spectrum',
  16196. 0xD0000017: 'Acquire',
  16197. # Timer
  16198. 0x12000001: 'Name',
  16199. 0x12000002: 'Description',
  16200. 0x12000003: 'Interval',
  16201. 0x12000004: 'TriggerIn',
  16202. 0x12000005: 'TriggerOut',
  16203. 0x12000006: 'ActivationTime',
  16204. 0x12000007: 'ActivationNumber',
  16205. # Marker
  16206. 0x14000001: 'Name',
  16207. 0x14000002: 'Description',
  16208. 0x14000003: 'TriggerIn',
  16209. 0x14000004: 'TriggerOut',
  16210. }
  16211. @cached_property
  16212. def CZ_LSM_LUTTYPE(self): # TODO: type this
  16213. class CZ_LSM_LUTTYPE(enum.IntEnum):
  16214. NORMAL = 0
  16215. ORIGINAL = 1
  16216. RAMP = 2
  16217. POLYLINE = 3
  16218. SPLINE = 4
  16219. GAMMA = 5
  16220. return CZ_LSM_LUTTYPE
  16221. @cached_property
  16222. def CZ_LSM_SUBBLOCK_TYPE(self): # TODO: type this
  16223. class CZ_LSM_SUBBLOCK_TYPE(enum.IntEnum):
  16224. END = 0
  16225. GAMMA = 1
  16226. BRIGHTNESS = 2
  16227. CONTRAST = 3
  16228. RAMP = 4
  16229. KNOTS = 5
  16230. PALETTE_12_TO_12 = 6
  16231. return CZ_LSM_SUBBLOCK_TYPE
  16232. @property
  16233. def NIH_IMAGE_HEADER(self): # TODO: type this
  16234. return [
  16235. ('FileID', 'S8'),
  16236. ('nLines', 'i2'),
  16237. ('PixelsPerLine', 'i2'),
  16238. ('Version', 'i2'),
  16239. ('OldLutMode', 'i2'),
  16240. ('OldnColors', 'i2'),
  16241. ('Colors', 'u1', (3, 32)),
  16242. ('OldColorStart', 'i2'),
  16243. ('ColorWidth', 'i2'),
  16244. ('ExtraColors', 'u2', (6, 3)),
  16245. ('nExtraColors', 'i2'),
  16246. ('ForegroundIndex', 'i2'),
  16247. ('BackgroundIndex', 'i2'),
  16248. ('XScale', 'f8'),
  16249. ('Unused2', 'i2'),
  16250. ('Unused3', 'i2'),
  16251. ('UnitsID', 'i2'), # NIH_UNITS_TYPE
  16252. ('p1', [('x', 'i2'), ('y', 'i2')]),
  16253. ('p2', [('x', 'i2'), ('y', 'i2')]),
  16254. ('CurveFitType', 'i2'), # NIH_CURVEFIT_TYPE
  16255. ('nCoefficients', 'i2'),
  16256. ('Coeff', 'f8', 6),
  16257. ('UMsize', 'u1'),
  16258. ('UM', 'S15'),
  16259. ('UnusedBoolean', 'u1'),
  16260. ('BinaryPic', 'b1'),
  16261. ('SliceStart', 'i2'),
  16262. ('SliceEnd', 'i2'),
  16263. ('ScaleMagnification', 'f4'),
  16264. ('nSlices', 'i2'),
  16265. ('SliceSpacing', 'f4'),
  16266. ('CurrentSlice', 'i2'),
  16267. ('FrameInterval', 'f4'),
  16268. ('PixelAspectRatio', 'f4'),
  16269. ('ColorStart', 'i2'),
  16270. ('ColorEnd', 'i2'),
  16271. ('nColors', 'i2'),
  16272. ('Fill1', '3u2'),
  16273. ('Fill2', '3u2'),
  16274. ('Table', 'u1'), # NIH_COLORTABLE_TYPE
  16275. ('LutMode', 'u1'), # NIH_LUTMODE_TYPE
  16276. ('InvertedTable', 'b1'),
  16277. ('ZeroClip', 'b1'),
  16278. ('XUnitSize', 'u1'),
  16279. ('XUnit', 'S11'),
  16280. ('StackType', 'i2'), # NIH_STACKTYPE_TYPE
  16281. # ('UnusedBytes', 'u1', 200)
  16282. ]
  16283. @property
  16284. def NIH_COLORTABLE_TYPE(self) -> tuple[str, ...]:
  16285. return (
  16286. 'CustomTable',
  16287. 'AppleDefault',
  16288. 'Pseudo20',
  16289. 'Pseudo32',
  16290. 'Rainbow',
  16291. 'Fire1',
  16292. 'Fire2',
  16293. 'Ice',
  16294. 'Grays',
  16295. 'Spectrum',
  16296. )
  16297. @property
  16298. def NIH_LUTMODE_TYPE(self) -> tuple[str, ...]:
  16299. return (
  16300. 'PseudoColor',
  16301. 'OldAppleDefault',
  16302. 'OldSpectrum',
  16303. 'GrayScale',
  16304. 'ColorLut',
  16305. 'CustomGrayscale',
  16306. )
  16307. @property
  16308. def NIH_CURVEFIT_TYPE(self) -> tuple[str, ...]:
  16309. return (
  16310. 'StraightLine',
  16311. 'Poly2',
  16312. 'Poly3',
  16313. 'Poly4',
  16314. 'Poly5',
  16315. 'ExpoFit',
  16316. 'PowerFit',
  16317. 'LogFit',
  16318. 'RodbardFit',
  16319. 'SpareFit1',
  16320. 'Uncalibrated',
  16321. 'UncalibratedOD',
  16322. )
  16323. @property
  16324. def NIH_UNITS_TYPE(self) -> tuple[str, ...]:
  16325. return (
  16326. 'Nanometers',
  16327. 'Micrometers',
  16328. 'Millimeters',
  16329. 'Centimeters',
  16330. 'Meters',
  16331. 'Kilometers',
  16332. 'Inches',
  16333. 'Feet',
  16334. 'Miles',
  16335. 'Pixels',
  16336. 'OtherUnits',
  16337. )
  16338. @property
  16339. def TVIPS_HEADER_V1(self) -> list[tuple[str, str]]:
  16340. # TVIPS TemData structure from EMMENU Help file
  16341. return [
  16342. ('Version', 'i4'),
  16343. ('CommentV1', 'S80'),
  16344. ('HighTension', 'i4'),
  16345. ('SphericalAberration', 'i4'),
  16346. ('IlluminationAperture', 'i4'),
  16347. ('Magnification', 'i4'),
  16348. ('PostMagnification', 'i4'),
  16349. ('FocalLength', 'i4'),
  16350. ('Defocus', 'i4'),
  16351. ('Astigmatism', 'i4'),
  16352. ('AstigmatismDirection', 'i4'),
  16353. ('BiprismVoltage', 'i4'),
  16354. ('SpecimenTiltAngle', 'i4'),
  16355. ('SpecimenTiltDirection', 'i4'),
  16356. ('IlluminationTiltDirection', 'i4'),
  16357. ('IlluminationTiltAngle', 'i4'),
  16358. ('ImageMode', 'i4'),
  16359. ('EnergySpread', 'i4'),
  16360. ('ChromaticAberration', 'i4'),
  16361. ('ShutterType', 'i4'),
  16362. ('DefocusSpread', 'i4'),
  16363. ('CcdNumber', 'i4'),
  16364. ('CcdSize', 'i4'),
  16365. ('OffsetXV1', 'i4'),
  16366. ('OffsetYV1', 'i4'),
  16367. ('PhysicalPixelSize', 'i4'),
  16368. ('Binning', 'i4'),
  16369. ('ReadoutSpeed', 'i4'),
  16370. ('GainV1', 'i4'),
  16371. ('SensitivityV1', 'i4'),
  16372. ('ExposureTimeV1', 'i4'),
  16373. ('FlatCorrected', 'i4'),
  16374. ('DeadPxCorrected', 'i4'),
  16375. ('ImageMean', 'i4'),
  16376. ('ImageStd', 'i4'),
  16377. ('DisplacementX', 'i4'),
  16378. ('DisplacementY', 'i4'),
  16379. ('DateV1', 'i4'),
  16380. ('TimeV1', 'i4'),
  16381. ('ImageMin', 'i4'),
  16382. ('ImageMax', 'i4'),
  16383. ('ImageStatisticsQuality', 'i4'),
  16384. ]
  16385. @property
  16386. def TVIPS_HEADER_V2(self) -> list[tuple[str, str]]:
  16387. return [
  16388. ('ImageName', 'V160'), # utf16
  16389. ('ImageFolder', 'V160'),
  16390. ('ImageSizeX', 'i4'),
  16391. ('ImageSizeY', 'i4'),
  16392. ('ImageSizeZ', 'i4'),
  16393. ('ImageSizeE', 'i4'),
  16394. ('ImageDataType', 'i4'),
  16395. ('Date', 'i4'),
  16396. ('Time', 'i4'),
  16397. ('Comment', 'V1024'),
  16398. ('ImageHistory', 'V1024'),
  16399. ('Scaling', '16f4'),
  16400. ('ImageStatistics', '16c16'),
  16401. ('ImageType', 'i4'),
  16402. ('ImageDisplayType', 'i4'),
  16403. ('PixelSizeX', 'f4'), # distance between two px in x, [nm]
  16404. ('PixelSizeY', 'f4'), # distance between two px in y, [nm]
  16405. ('ImageDistanceZ', 'f4'),
  16406. ('ImageDistanceE', 'f4'),
  16407. ('ImageMisc', '32f4'),
  16408. ('TemType', 'V160'),
  16409. ('TemHighTension', 'f4'),
  16410. ('TemAberrations', '32f4'),
  16411. ('TemEnergy', '32f4'),
  16412. ('TemMode', 'i4'),
  16413. ('TemMagnification', 'f4'),
  16414. ('TemMagnificationCorrection', 'f4'),
  16415. ('PostMagnification', 'f4'),
  16416. ('TemStageType', 'i4'),
  16417. ('TemStagePosition', '5f4'), # x, y, z, a, b
  16418. ('TemImageShift', '2f4'),
  16419. ('TemBeamShift', '2f4'),
  16420. ('TemBeamTilt', '2f4'),
  16421. ('TilingParameters', '7f4'), # 0: tiling? 1:x 2:y 3: max x
  16422. # 4: max y 5: overlap x 6: overlap y
  16423. ('TemIllumination', '3f4'), # 0: spotsize 1: intensity
  16424. ('TemShutter', 'i4'),
  16425. ('TemMisc', '32f4'),
  16426. ('CameraType', 'V160'),
  16427. ('PhysicalPixelSizeX', 'f4'),
  16428. ('PhysicalPixelSizeY', 'f4'),
  16429. ('OffsetX', 'i4'),
  16430. ('OffsetY', 'i4'),
  16431. ('BinningX', 'i4'),
  16432. ('BinningY', 'i4'),
  16433. ('ExposureTime', 'f4'),
  16434. ('Gain', 'f4'),
  16435. ('ReadoutRate', 'f4'),
  16436. ('FlatfieldDescription', 'V160'),
  16437. ('Sensitivity', 'f4'),
  16438. ('Dose', 'f4'),
  16439. ('CamMisc', '32f4'),
  16440. ('FeiMicroscopeInformation', 'V1024'),
  16441. ('FeiSpecimenInformation', 'V1024'),
  16442. ('Magic', 'u4'),
  16443. ]
  16444. @property
  16445. def MM_HEADER(self) -> list[tuple[Any, ...]]:
  16446. # Olympus FluoView MM_Header
  16447. MM_DIMENSION = [
  16448. ('Name', 'S16'),
  16449. ('Size', 'i4'),
  16450. ('Origin', 'f8'),
  16451. ('Resolution', 'f8'),
  16452. ('Unit', 'S64'),
  16453. ]
  16454. return [
  16455. ('HeaderFlag', 'i2'),
  16456. ('ImageType', 'u1'),
  16457. ('ImageName', 'S257'),
  16458. ('OffsetData', 'u4'),
  16459. ('PaletteSize', 'i4'),
  16460. ('OffsetPalette0', 'u4'),
  16461. ('OffsetPalette1', 'u4'),
  16462. ('CommentSize', 'i4'),
  16463. ('OffsetComment', 'u4'),
  16464. ('Dimensions', MM_DIMENSION, 10),
  16465. ('OffsetPosition', 'u4'),
  16466. ('MapType', 'i2'),
  16467. ('MapMin', 'f8'),
  16468. ('MapMax', 'f8'),
  16469. ('MinValue', 'f8'),
  16470. ('MaxValue', 'f8'),
  16471. ('OffsetMap', 'u4'),
  16472. ('Gamma', 'f8'),
  16473. ('Offset', 'f8'),
  16474. ('GrayChannel', MM_DIMENSION),
  16475. ('OffsetThumbnail', 'u4'),
  16476. ('VoiceField', 'i4'),
  16477. ('OffsetVoiceField', 'u4'),
  16478. ]
  16479. @property
  16480. def MM_DIMENSIONS(self) -> dict[str, str]:
  16481. # map FluoView MM_Header.Dimensions to axes characters
  16482. return {
  16483. 'X': 'X',
  16484. 'Y': 'Y',
  16485. 'Z': 'Z',
  16486. 'T': 'T',
  16487. 'CH': 'C',
  16488. 'WAVELENGTH': 'C',
  16489. 'TIME': 'T',
  16490. 'XY': 'R',
  16491. 'EVENT': 'V',
  16492. 'EXPOSURE': 'L',
  16493. }
  16494. @property
  16495. def UIC_TAGS(self) -> list[tuple[str, Any]]:
  16496. # map Universal Imaging Corporation MetaMorph internal tag ids to
  16497. # name and type
  16498. from fractions import Fraction
  16499. return [
  16500. ('AutoScale', int),
  16501. ('MinScale', int),
  16502. ('MaxScale', int),
  16503. ('SpatialCalibration', int),
  16504. ('XCalibration', Fraction),
  16505. ('YCalibration', Fraction),
  16506. ('CalibrationUnits', str),
  16507. ('Name', str),
  16508. ('ThreshState', int),
  16509. ('ThreshStateRed', int),
  16510. ('tagid_10', None), # undefined
  16511. ('ThreshStateGreen', int),
  16512. ('ThreshStateBlue', int),
  16513. ('ThreshStateLo', int),
  16514. ('ThreshStateHi', int),
  16515. ('Zoom', int),
  16516. ('CreateTime', julian_datetime),
  16517. ('LastSavedTime', julian_datetime),
  16518. ('currentBuffer', int),
  16519. ('grayFit', None),
  16520. ('grayPointCount', None),
  16521. ('grayX', Fraction),
  16522. ('grayY', Fraction),
  16523. ('grayMin', Fraction),
  16524. ('grayMax', Fraction),
  16525. ('grayUnitName', str),
  16526. ('StandardLUT', int),
  16527. ('wavelength', int),
  16528. ('StagePosition', '(%i,2,2)u4'), # N xy positions as fract
  16529. ('CameraChipOffset', '(%i,2,2)u4'), # N xy offsets as fract
  16530. ('OverlayMask', None),
  16531. ('OverlayCompress', None),
  16532. ('Overlay', None),
  16533. ('SpecialOverlayMask', None),
  16534. ('SpecialOverlayCompress', None),
  16535. ('SpecialOverlay', None),
  16536. ('ImageProperty', read_uic_property),
  16537. ('StageLabel', '%ip'), # N str
  16538. ('AutoScaleLoInfo', Fraction),
  16539. ('AutoScaleHiInfo', Fraction),
  16540. ('AbsoluteZ', '(%i,2)u4'), # N fractions
  16541. ('AbsoluteZValid', '(%i,)u4'), # N long
  16542. ('Gamma', 'I'), # 'I' uses offset
  16543. ('GammaRed', 'I'),
  16544. ('GammaGreen', 'I'),
  16545. ('GammaBlue', 'I'),
  16546. ('CameraBin', '2I'),
  16547. ('NewLUT', int),
  16548. ('ImagePropertyEx', None),
  16549. ('PlaneProperty', int),
  16550. ('UserLutTable', '(256,3)u1'),
  16551. ('RedAutoScaleInfo', int),
  16552. ('RedAutoScaleLoInfo', Fraction),
  16553. ('RedAutoScaleHiInfo', Fraction),
  16554. ('RedMinScaleInfo', int),
  16555. ('RedMaxScaleInfo', int),
  16556. ('GreenAutoScaleInfo', int),
  16557. ('GreenAutoScaleLoInfo', Fraction),
  16558. ('GreenAutoScaleHiInfo', Fraction),
  16559. ('GreenMinScaleInfo', int),
  16560. ('GreenMaxScaleInfo', int),
  16561. ('BlueAutoScaleInfo', int),
  16562. ('BlueAutoScaleLoInfo', Fraction),
  16563. ('BlueAutoScaleHiInfo', Fraction),
  16564. ('BlueMinScaleInfo', int),
  16565. ('BlueMaxScaleInfo', int),
  16566. # ('OverlayPlaneColor', read_uic_overlay_plane_color),
  16567. ]
  16568. @property
  16569. def PILATUS_HEADER(self) -> dict[str, Any]:
  16570. # PILATUS CBF Header Specification, Version 1.4
  16571. # map key to [value_indices], type
  16572. return {
  16573. 'Detector': ([slice(1, None)], str),
  16574. 'Pixel_size': ([1, 4], float),
  16575. 'Silicon': ([3], float),
  16576. 'Exposure_time': ([1], float),
  16577. 'Exposure_period': ([1], float),
  16578. 'Tau': ([1], float),
  16579. 'Count_cutoff': ([1], int),
  16580. 'Threshold_setting': ([1], float),
  16581. 'Gain_setting': ([1, 2], str),
  16582. 'N_excluded_pixels': ([1], int),
  16583. 'Excluded_pixels': ([1], str),
  16584. 'Flat_field': ([1], str),
  16585. 'Trim_file': ([1], str),
  16586. 'Image_path': ([1], str),
  16587. # optional
  16588. 'Wavelength': ([1], float),
  16589. 'Energy_range': ([1, 2], float),
  16590. 'Detector_distance': ([1], float),
  16591. 'Detector_Voffset': ([1], float),
  16592. 'Beam_xy': ([1, 2], float),
  16593. 'Flux': ([1], str),
  16594. 'Filter_transmission': ([1], float),
  16595. 'Start_angle': ([1], float),
  16596. 'Angle_increment': ([1], float),
  16597. 'Detector_2theta': ([1], float),
  16598. 'Polarization': ([1], float),
  16599. 'Alpha': ([1], float),
  16600. 'Kappa': ([1], float),
  16601. 'Phi': ([1], float),
  16602. 'Phi_increment': ([1], float),
  16603. 'Chi': ([1], float),
  16604. 'Chi_increment': ([1], float),
  16605. 'Oscillation_axis': ([slice(1, None)], str),
  16606. 'N_oscillations': ([1], int),
  16607. 'Start_position': ([1], float),
  16608. 'Position_increment': ([1], float),
  16609. 'Shutter_time': ([1], float),
  16610. 'Omega': ([1], float),
  16611. 'Omega_increment': ([1], float),
  16612. }
  16613. @cached_property
  16614. def ALLOCATIONGRANULARITY(self) -> int:
  16615. # alignment for writing contiguous data to TIFF
  16616. import mmap
  16617. return mmap.ALLOCATIONGRANULARITY
  16618. @cached_property
  16619. def MAXWORKERS(self) -> int:
  16620. """Default maximum number of threads for de/compressing segments.
  16621. The value of the ``TIFFFILE_NUM_THREADS`` environment variable if set,
  16622. else half the CPU cores up to 32.
  16623. """
  16624. if 'TIFFFILE_NUM_THREADS' in os.environ:
  16625. return max(1, int(os.environ['TIFFFILE_NUM_THREADS']))
  16626. cpu_count: int | None
  16627. try:
  16628. cpu_count = len(
  16629. os.sched_getaffinity(0) # type: ignore[attr-defined]
  16630. )
  16631. except AttributeError:
  16632. cpu_count = os.cpu_count()
  16633. if cpu_count is None:
  16634. return 1
  16635. return min(32, max(1, cpu_count // 2))
  16636. @cached_property
  16637. def MAXIOWORKERS(self) -> int:
  16638. """Default maximum number of I/O threads for reading file sequences.
  16639. The value of the ``TIFFFILE_NUM_IOTHREADS`` environment variable if
  16640. set, else 4 more than the number of CPU cores up to 32.
  16641. """
  16642. if 'TIFFFILE_NUM_IOTHREADS' in os.environ:
  16643. return max(1, int(os.environ['TIFFFILE_NUM_IOTHREADS']))
  16644. cpu_count: int | None
  16645. try:
  16646. cpu_count = len(
  16647. os.sched_getaffinity(0) # type: ignore[attr-defined]
  16648. )
  16649. except AttributeError:
  16650. cpu_count = os.cpu_count()
  16651. if cpu_count is None:
  16652. return 5
  16653. return min(32, cpu_count + 4)
  16654. BUFFERSIZE: int = 268435456
  16655. """Default number of bytes to read or encode in one pass (256 MB)."""
  16656. TIFF = _TIFF()
  16657. def read_tags(
  16658. fh: FileHandle,
  16659. /,
  16660. byteorder: ByteOrder,
  16661. offsetsize: int,
  16662. tagnames: TiffTagRegistry,
  16663. *,
  16664. maxifds: int | None = None,
  16665. customtags: (
  16666. dict[int, Callable[[FileHandle, ByteOrder, int, int, int], Any]] | None
  16667. ) = None,
  16668. ) -> list[dict[str, Any]]:
  16669. """Read tag values from chain of IFDs.
  16670. Parameters:
  16671. fh:
  16672. Binary file handle to read from.
  16673. The file handle position must be at a valid IFD header.
  16674. byteorder:
  16675. Byte order of TIFF file.
  16676. offsetsize:
  16677. Size of offsets in TIFF file (8 for BigTIFF, else 4).
  16678. tagnames:
  16679. Map of tag codes to names.
  16680. For example, :py:class:`_TIFF.GPS_TAGS` or
  16681. :py:class:`_TIFF.IOP_TAGS`.
  16682. maxifds:
  16683. Maximum number of IFDs to read.
  16684. By default, read the whole IFD chain.
  16685. customtags:
  16686. Mapping of tag codes to functions reading special tag value from
  16687. file.
  16688. Raises:
  16689. TiffFileError: Invalid TIFF structure.
  16690. Notes:
  16691. This implementation does not support 64-bit NDPI files.
  16692. """
  16693. code: int
  16694. dtype: int
  16695. count: int
  16696. valuebytes: bytes
  16697. valueoffset: int
  16698. if offsetsize == 4:
  16699. offsetformat = byteorder + 'I'
  16700. tagnosize = 2
  16701. tagnoformat = byteorder + 'H'
  16702. tagsize = 12
  16703. tagformat1 = byteorder + 'HH'
  16704. tagformat2 = byteorder + 'I4s'
  16705. elif offsetsize == 8:
  16706. offsetformat = byteorder + 'Q'
  16707. tagnosize = 8
  16708. tagnoformat = byteorder + 'Q'
  16709. tagsize = 20
  16710. tagformat1 = byteorder + 'HH'
  16711. tagformat2 = byteorder + 'Q8s'
  16712. else:
  16713. raise ValueError('invalid offset size')
  16714. if customtags is None:
  16715. customtags = {}
  16716. if maxifds is None:
  16717. maxifds = 2**32
  16718. result: list[dict[str, Any]] = []
  16719. unpack = struct.unpack
  16720. offset = fh.tell()
  16721. while len(result) < maxifds:
  16722. # loop over IFDs
  16723. try:
  16724. tagno = unpack(tagnoformat, fh.read(tagnosize))[0]
  16725. if tagno > 4096:
  16726. raise TiffFileError(f'suspicious number of tags {tagno}')
  16727. except Exception as exc:
  16728. logger().error(
  16729. f'<tifffile.read_tags> corrupted tag list @{offset} '
  16730. f'raised {exc!r:.128}'
  16731. )
  16732. break
  16733. tags = {}
  16734. data = fh.read(tagsize * tagno)
  16735. pos = fh.tell()
  16736. index = 0
  16737. for _ in range(tagno):
  16738. code, dtype = unpack(tagformat1, data[index : index + 4])
  16739. count, valuebytes = unpack(
  16740. tagformat2, data[index + 4 : index + tagsize]
  16741. )
  16742. index += tagsize
  16743. name = tagnames.get(code, str(code))
  16744. try:
  16745. valueformat = TIFF.DATA_FORMATS[dtype]
  16746. except KeyError:
  16747. logger().error(f'invalid data type {dtype!r} for tag #{code}')
  16748. continue
  16749. valuesize = count * struct.calcsize(valueformat)
  16750. if valuesize > offsetsize or code in customtags:
  16751. valueoffset = unpack(offsetformat, valuebytes)[0]
  16752. if valueoffset < 8 or valueoffset + valuesize > fh.size:
  16753. logger().error(
  16754. f'invalid value offset {valueoffset} for tag #{code}'
  16755. )
  16756. continue
  16757. fh.seek(valueoffset)
  16758. if code in customtags:
  16759. readfunc = customtags[code]
  16760. value = readfunc(fh, byteorder, dtype, count, offsetsize)
  16761. elif dtype in {1, 2, 7}:
  16762. # BYTES, ASCII, UNDEFINED
  16763. value = fh.read(valuesize)
  16764. if len(value) != valuesize:
  16765. logger().warning(
  16766. '<tifffile.read_tags> '
  16767. f'could not read all values for tag #{code}'
  16768. )
  16769. elif code in tagnames:
  16770. fmt = (
  16771. f'{byteorder}'
  16772. f'{count * int(valueformat[0])}'
  16773. f'{valueformat[1]}'
  16774. )
  16775. value = unpack(fmt, fh.read(valuesize))
  16776. else:
  16777. value = read_numpy(fh, byteorder, dtype, count, offsetsize)
  16778. elif dtype in {1, 2, 7}:
  16779. # BYTES, ASCII, UNDEFINED
  16780. value = valuebytes[:valuesize]
  16781. else:
  16782. fmt = (
  16783. f'{byteorder}'
  16784. f'{count * int(valueformat[0])}'
  16785. f'{valueformat[1]}'
  16786. )
  16787. value = unpack(fmt, valuebytes[:valuesize])
  16788. process = (
  16789. code not in customtags
  16790. and code not in TIFF.TAG_TUPLE
  16791. and dtype != 7 # UNDEFINED
  16792. )
  16793. if process and dtype == 2:
  16794. # TIFF ASCII fields can contain multiple strings,
  16795. # each terminated with a NUL
  16796. value = value.rstrip(b'\x00')
  16797. try:
  16798. value = value.decode('utf-8').strip()
  16799. except UnicodeDecodeError:
  16800. try:
  16801. value = value.decode('cp1252').strip()
  16802. except UnicodeDecodeError as exc:
  16803. logger().warning(
  16804. '<tifffile.read_tags> coercing invalid ASCII to '
  16805. f'bytes for tag #{code}, due to {exc!r:.128}'
  16806. )
  16807. else:
  16808. if code in TIFF.TAG_ENUM:
  16809. t = TIFF.TAG_ENUM[code]
  16810. try:
  16811. value = tuple(t(v) for v in value)
  16812. except ValueError as exc:
  16813. if code not in {259, 317}:
  16814. # ignore compression/predictor
  16815. logger().warning(
  16816. f'<tifffile.read_tags> tag #{code} '
  16817. f'raised {exc!r:.128}'
  16818. )
  16819. if process and len(value) == 1:
  16820. value = value[0]
  16821. tags[name] = value
  16822. result.append(tags)
  16823. # read offset to next page
  16824. fh.seek(pos)
  16825. offset = unpack(offsetformat, fh.read(offsetsize))[0]
  16826. if offset == 0:
  16827. break
  16828. if offset >= fh.size:
  16829. logger().error(f'<tifffile.read_tags> invalid next page {offset=}')
  16830. break
  16831. fh.seek(offset)
  16832. return result
  16833. def read_exif_ifd(
  16834. fh: FileHandle,
  16835. byteorder: ByteOrder,
  16836. dtype: int,
  16837. count: int,
  16838. offsetsize: int,
  16839. /,
  16840. ) -> dict[str, Any]:
  16841. """Read EXIF tags from file."""
  16842. exif = read_tags(fh, byteorder, offsetsize, TIFF.EXIF_TAGS, maxifds=1)[0]
  16843. for name in ('ExifVersion', 'FlashpixVersion'):
  16844. try:
  16845. exif[name] = bytes2str(exif[name])
  16846. except Exception: # noqa: S110
  16847. pass
  16848. if 'UserComment' in exif:
  16849. idcode = exif['UserComment'][:8]
  16850. try:
  16851. if idcode == b'ASCII\x00\x00\x00':
  16852. exif['UserComment'] = bytes2str(exif['UserComment'][8:])
  16853. elif idcode == b'UNICODE\x00':
  16854. exif['UserComment'] = exif['UserComment'][8:].decode('utf-16')
  16855. except Exception: # noqa: S110
  16856. pass
  16857. return exif
  16858. def read_gps_ifd(
  16859. fh: FileHandle,
  16860. byteorder: ByteOrder,
  16861. dtype: int,
  16862. count: int,
  16863. offsetsize: int,
  16864. /,
  16865. ) -> dict[str, Any]:
  16866. """Read GPS tags from file."""
  16867. return read_tags(fh, byteorder, offsetsize, TIFF.GPS_TAGS, maxifds=1)[0]
  16868. def read_interoperability_ifd(
  16869. fh: FileHandle,
  16870. byteorder: ByteOrder,
  16871. dtype: int,
  16872. count: int,
  16873. offsetsize: int,
  16874. /,
  16875. ) -> dict[str, Any]:
  16876. """Read Interoperability tags from file."""
  16877. return read_tags(fh, byteorder, offsetsize, TIFF.IOP_TAGS, maxifds=1)[0]
  16878. def read_bytes(
  16879. fh: FileHandle,
  16880. byteorder: ByteOrder,
  16881. dtype: int,
  16882. count: int,
  16883. offsetsize: int,
  16884. /,
  16885. ) -> bytes:
  16886. """Read tag data from file."""
  16887. count *= numpy.dtype(
  16888. 'B' if dtype == 2 else byteorder + TIFF.DATA_FORMATS[dtype][-1]
  16889. ).itemsize
  16890. data = fh.read(count)
  16891. if len(data) != count:
  16892. logger().warning(
  16893. '<tifffile.read_bytes> '
  16894. f'failed to read {count} bytes, got {len(data)})'
  16895. )
  16896. return data
  16897. def read_utf8(
  16898. fh: FileHandle,
  16899. byteorder: ByteOrder,
  16900. dtype: int,
  16901. count: int,
  16902. offsetsize: int,
  16903. /,
  16904. ) -> str:
  16905. """Read unicode tag value from file."""
  16906. return fh.read(count).decode()
  16907. def read_numpy(
  16908. fh: FileHandle,
  16909. byteorder: ByteOrder,
  16910. dtype: int,
  16911. count: int,
  16912. offsetsize: int,
  16913. /,
  16914. ) -> NDArray[Any]:
  16915. """Read NumPy array tag value from file."""
  16916. return fh.read_array(
  16917. 'b' if dtype == 2 else byteorder + TIFF.DATA_FORMATS[dtype][-1], count
  16918. )
  16919. def read_colormap(
  16920. fh: FileHandle,
  16921. byteorder: ByteOrder,
  16922. dtype: int,
  16923. count: int,
  16924. offsetsize: int,
  16925. /,
  16926. ) -> NDArray[Any]:
  16927. """Read ColorMap or TransferFunction tag value from file."""
  16928. cmap = fh.read_array(byteorder + TIFF.DATA_FORMATS[dtype][-1], count)
  16929. if count % 3 == 0:
  16930. cmap = cmap.reshape((3, -1))
  16931. return cmap
  16932. def read_json(
  16933. fh: FileHandle,
  16934. byteorder: ByteOrder,
  16935. dtype: int,
  16936. count: int,
  16937. offsetsize: int,
  16938. /,
  16939. ) -> Any:
  16940. """Read JSON tag value from file."""
  16941. data = fh.read(count)
  16942. try:
  16943. return json.loads(bytes2str(data, 'utf-8'))
  16944. except ValueError as exc:
  16945. logger().warning(f'<tifffile.read_json> raised {exc!r:.128}')
  16946. return None
  16947. def read_mm_header(
  16948. fh: FileHandle,
  16949. byteorder: ByteOrder,
  16950. dtype: int,
  16951. count: int,
  16952. offsetsize: int,
  16953. /,
  16954. ) -> dict[str, Any]:
  16955. """Read FluoView mm_header tag value from file."""
  16956. meta = recarray2dict(
  16957. fh.read_record(numpy.dtype(TIFF.MM_HEADER), byteorder=byteorder)
  16958. )
  16959. meta['Dimensions'] = [
  16960. (bytes2str(d[0]).strip(), d[1], d[2], d[3], bytes2str(d[4]).strip())
  16961. for d in meta['Dimensions']
  16962. ]
  16963. d = meta['GrayChannel']
  16964. meta['GrayChannel'] = (
  16965. bytes2str(d[0]).strip(),
  16966. d[1],
  16967. d[2],
  16968. d[3],
  16969. bytes2str(d[4]).strip(),
  16970. )
  16971. return meta
  16972. def read_mm_stamp(
  16973. fh: FileHandle,
  16974. byteorder: ByteOrder,
  16975. dtype: int,
  16976. count: int,
  16977. offsetsize: int,
  16978. /,
  16979. ) -> NDArray[Any]:
  16980. """Read FluoView mm_stamp tag value from file."""
  16981. return fh.read_array(byteorder + 'f8', 8)
  16982. def read_uic1tag(
  16983. fh: FileHandle,
  16984. byteorder: ByteOrder,
  16985. dtype: int,
  16986. count: int,
  16987. offsetsize: int,
  16988. /,
  16989. planecount: int = 0,
  16990. ) -> dict[str, Any]:
  16991. """Read MetaMorph STK UIC1Tag value from file.
  16992. Return empty dictionary if planecount is unknown.
  16993. """
  16994. if dtype not in {4, 5} or byteorder != '<':
  16995. raise ValueError(f'invalid UIC1Tag {byteorder}{dtype}')
  16996. result = {}
  16997. if dtype == 5:
  16998. # pre MetaMorph 2.5 (not tested)
  16999. values = fh.read_array('<u4', 2 * count).reshape((count, 2))
  17000. result = {'ZDistance': values[:, 0] / values[:, 1]}
  17001. else:
  17002. for _ in range(count):
  17003. tagid = struct.unpack('<I', fh.read(4))[0]
  17004. if planecount > 1 and tagid in {28, 29, 37, 40, 41}:
  17005. # silently skip unexpected tags
  17006. fh.read(4)
  17007. continue
  17008. name, value = read_uic_tag(fh, tagid, planecount, True)
  17009. if name == 'PlaneProperty':
  17010. pos = fh.tell()
  17011. fh.seek(value + 4)
  17012. result.setdefault(name, []).append(read_uic_property(fh))
  17013. fh.seek(pos)
  17014. else:
  17015. result[name] = value
  17016. return result
  17017. def read_uic2tag(
  17018. fh: FileHandle,
  17019. byteorder: ByteOrder,
  17020. dtype: int,
  17021. count: int,
  17022. offsetsize: int,
  17023. /,
  17024. ) -> dict[str, NDArray[Any]]:
  17025. """Read MetaMorph STK UIC2Tag value from file."""
  17026. if dtype != 5 or byteorder != '<':
  17027. raise ValueError('invalid UIC2Tag')
  17028. values = fh.read_array('<u4', 6 * count).reshape((count, 6))
  17029. return {
  17030. 'ZDistance': values[:, 0] / values[:, 1],
  17031. 'DateCreated': values[:, 2], # julian days
  17032. 'TimeCreated': values[:, 3], # milliseconds
  17033. 'DateModified': values[:, 4], # julian days
  17034. 'TimeModified': values[:, 5], # milliseconds
  17035. }
  17036. def read_uic3tag(
  17037. fh: FileHandle,
  17038. byteorder: ByteOrder,
  17039. dtype: int,
  17040. count: int,
  17041. offsetsize: int,
  17042. /,
  17043. ) -> dict[str, NDArray[Any]]:
  17044. """Read MetaMorph STK UIC3Tag value from file."""
  17045. if dtype != 5 or byteorder != '<':
  17046. raise ValueError('invalid UIC3Tag')
  17047. values = fh.read_array('<u4', 2 * count).reshape((count, 2))
  17048. return {'Wavelengths': values[:, 0] / values[:, 1]}
  17049. def read_uic4tag(
  17050. fh: FileHandle,
  17051. byteorder: ByteOrder,
  17052. dtype: int,
  17053. count: int,
  17054. offsetsize: int,
  17055. /,
  17056. ) -> dict[str, NDArray[Any]]:
  17057. """Read MetaMorph STK UIC4Tag value from file."""
  17058. if dtype != 4 or byteorder != '<':
  17059. raise ValueError('invalid UIC4Tag')
  17060. result = {}
  17061. while True:
  17062. tagid: int = struct.unpack('<H', fh.read(2))[0]
  17063. if tagid == 0:
  17064. break
  17065. name, value = read_uic_tag(fh, tagid, count, False)
  17066. result[name] = value
  17067. return result
  17068. def read_uic_tag(
  17069. fh: FileHandle,
  17070. tagid: int,
  17071. planecount: int,
  17072. offset: bool, # noqa: FBT001
  17073. ) -> tuple[str, Any]:
  17074. """Read single UIC tag value from file and return tag name and value.
  17075. UIC1Tags use an offset.
  17076. """
  17077. def read_int() -> int:
  17078. return int(struct.unpack('<I', fh.read(4))[0])
  17079. def read_int2() -> tuple[int, int]:
  17080. value = struct.unpack('<2I', fh.read(8))
  17081. return int(value[0]), (value[1])
  17082. try:
  17083. name, dtype = TIFF.UIC_TAGS[tagid]
  17084. except IndexError:
  17085. # unknown tag
  17086. return f'_TagId{tagid}', read_int()
  17087. Fraction = TIFF.UIC_TAGS[4][1]
  17088. if offset:
  17089. pos = fh.tell()
  17090. if dtype not in {int, None}:
  17091. off = read_int()
  17092. if off < 8:
  17093. # undocumented cases, or invalid offset
  17094. if dtype is str:
  17095. return name, ''
  17096. if tagid == 41: # AbsoluteZValid
  17097. return name, off
  17098. logger().warning(
  17099. '<tifffile.read_uic_tag> '
  17100. f'invalid offset for tag {name!r} @{off}'
  17101. )
  17102. return name, off
  17103. fh.seek(off)
  17104. value: Any
  17105. if dtype is None:
  17106. # skip
  17107. name = '_' + name
  17108. value = read_int()
  17109. elif dtype is int:
  17110. # int
  17111. value = read_int()
  17112. elif dtype is Fraction:
  17113. # fraction
  17114. value = read_int2()
  17115. value = value[0] / value[1]
  17116. elif dtype is julian_datetime:
  17117. # datetime
  17118. value = read_int2()
  17119. try:
  17120. value = julian_datetime(*value)
  17121. except Exception as exc:
  17122. value = None
  17123. logger().warning(
  17124. f'<tifffile.read_uic_tag> reading {name} raised {exc!r:.128}'
  17125. )
  17126. elif dtype is read_uic_property:
  17127. # ImagePropertyEx
  17128. value = read_uic_property(fh)
  17129. elif dtype is str:
  17130. # pascal string
  17131. size = read_int()
  17132. if 0 <= size < 2**10:
  17133. value = struct.unpack(f'{size}s', fh.read(size))[0][:-1]
  17134. value = bytes2str(value)
  17135. elif offset:
  17136. value = ''
  17137. logger().warning(
  17138. f'<tifffile.read_uic_tag> invalid string in tag {name!r}'
  17139. )
  17140. else:
  17141. raise ValueError(f'invalid string size {size}')
  17142. elif planecount == 0:
  17143. value = None
  17144. elif dtype == '%ip':
  17145. # sequence of pascal strings
  17146. value = []
  17147. for _ in range(planecount):
  17148. size = read_int()
  17149. if 0 <= size < 2**10:
  17150. string = struct.unpack(f'{size}s', fh.read(size))[0][:-1]
  17151. value.append(bytes2str(string))
  17152. elif offset:
  17153. logger().warning(
  17154. f'<tifffile.read_uic_tag> invalid string in tag {name!r}'
  17155. )
  17156. else:
  17157. raise ValueError(f'invalid string size: {size}')
  17158. else:
  17159. # struct or numpy type
  17160. dtype = '<' + dtype
  17161. if '%i' in dtype:
  17162. dtype = dtype % planecount
  17163. if '(' in dtype:
  17164. # numpy type
  17165. value = fh.read_array(dtype, 1)[0]
  17166. if value.shape[-1] == 2:
  17167. # assume fractions
  17168. value = value[..., 0] / value[..., 1]
  17169. else:
  17170. # struct format
  17171. value = struct.unpack(dtype, fh.read(struct.calcsize(dtype)))
  17172. if len(value) == 1:
  17173. value = value[0]
  17174. if offset:
  17175. fh.seek(pos + 4)
  17176. return name, value
  17177. def read_uic_property(fh: FileHandle, /) -> dict[str, Any]:
  17178. """Read UIC ImagePropertyEx or PlaneProperty tag from file."""
  17179. size = struct.unpack('B', fh.read(1))[0]
  17180. name = bytes2str(struct.unpack(f'{size}s', fh.read(size))[0])
  17181. flags, prop = struct.unpack('<IB', fh.read(5))
  17182. if prop == 1:
  17183. value = struct.unpack('II', fh.read(8))
  17184. value = value[0] / value[1]
  17185. else:
  17186. size = struct.unpack('B', fh.read(1))[0]
  17187. value = bytes2str(
  17188. struct.unpack(f'{size}s', fh.read(size))[0]
  17189. ) # type: ignore[assignment]
  17190. return {'name': name, 'flags': flags, 'value': value}
  17191. def read_cz_lsminfo(
  17192. fh: FileHandle,
  17193. byteorder: ByteOrder,
  17194. dtype: int,
  17195. count: int,
  17196. offsetsize: int,
  17197. /,
  17198. ) -> dict[str, Any]:
  17199. """Read CZ_LSMINFO tag value from file."""
  17200. if byteorder != '<':
  17201. raise ValueError('invalid CZ_LSMINFO structure')
  17202. magic_number, structure_size = struct.unpack('<II', fh.read(8))
  17203. if magic_number not in {50350412, 67127628}:
  17204. raise ValueError('invalid CZ_LSMINFO structure')
  17205. fh.seek(-8, os.SEEK_CUR)
  17206. CZ_LSMINFO = TIFF.CZ_LSMINFO
  17207. if structure_size < numpy.dtype(CZ_LSMINFO).itemsize:
  17208. # adjust structure according to structure_size
  17209. lsminfo: list[tuple[str, str]] = []
  17210. size = 0
  17211. for name, typestr in CZ_LSMINFO:
  17212. size += numpy.dtype(typestr).itemsize
  17213. if size > structure_size:
  17214. break
  17215. lsminfo.append((name, typestr))
  17216. else:
  17217. lsminfo = CZ_LSMINFO
  17218. result = recarray2dict(
  17219. fh.read_record(numpy.dtype(lsminfo), byteorder=byteorder)
  17220. )
  17221. # read LSM info subrecords at offsets
  17222. for name, reader in TIFF.CZ_LSMINFO_READERS.items():
  17223. if reader is None:
  17224. continue
  17225. offset = result.get('Offset' + name, 0)
  17226. if offset < 8:
  17227. continue
  17228. fh.seek(offset)
  17229. try:
  17230. result[name] = reader(fh)
  17231. except ValueError:
  17232. pass
  17233. return result
  17234. def read_lsm_channeldatatypes(fh: FileHandle, /) -> NDArray[Any]:
  17235. """Read LSM channel data type from file."""
  17236. size = struct.unpack('<I', fh.read(4))[0]
  17237. return fh.read_array('<u4', count=size)
  17238. def read_lsm_channelwavelength(fh: FileHandle, /) -> NDArray[Any]:
  17239. """Read LSM channel wavelength ranges from file."""
  17240. size = struct.unpack('<i', fh.read(4))[0]
  17241. return fh.read_array('<2f8', count=size)
  17242. def read_lsm_positions(fh: FileHandle, /) -> NDArray[Any]:
  17243. """Read LSM positions from file."""
  17244. size = struct.unpack('<I', fh.read(4))[0]
  17245. return fh.read_array('<3f8', count=size)
  17246. def read_lsm_timestamps(fh: FileHandle, /) -> NDArray[Any]:
  17247. """Read LSM time stamps from file."""
  17248. size, count = struct.unpack('<ii', fh.read(8))
  17249. if size != (8 + 8 * count):
  17250. logger().warning(
  17251. '<tifffile.read_lsm_timestamps> invalid LSM TimeStamps block'
  17252. )
  17253. return numpy.empty((0,), '<f8')
  17254. # return struct.unpack(f'<{count}d', fh.read(8 * count))
  17255. return fh.read_array('<f8', count=count)
  17256. def read_lsm_eventlist(fh: FileHandle, /) -> list[tuple[float, int, str]]:
  17257. """Read LSM events from file and return as list of (time, type, text)."""
  17258. count = struct.unpack('<II', fh.read(8))[1]
  17259. events = []
  17260. while count > 0:
  17261. esize, etime, etype = struct.unpack('<IdI', fh.read(16))
  17262. etext = bytes2str(fh.read(esize - 16))
  17263. events.append((etime, etype, etext))
  17264. count -= 1
  17265. return events
  17266. def read_lsm_channelcolors(fh: FileHandle, /) -> dict[str, Any]:
  17267. """Read LSM ChannelColors structure from file."""
  17268. result = {'Mono': False, 'Colors': [], 'ColorNames': []}
  17269. pos = fh.tell()
  17270. (size, ncolors, nnames, coffset, noffset, mono) = struct.unpack(
  17271. '<IIIIII', fh.read(24)
  17272. )
  17273. if ncolors != nnames:
  17274. logger().warning(
  17275. '<tifffile.read_lsm_channelcolors> '
  17276. 'invalid LSM ChannelColors structure'
  17277. )
  17278. return result
  17279. result['Mono'] = bool(mono)
  17280. # Colors
  17281. fh.seek(pos + coffset)
  17282. colors = fh.read_array(numpy.uint8, count=ncolors * 4)
  17283. colors = colors.reshape((ncolors, 4))
  17284. result['Colors'] = colors.tolist()
  17285. # ColorNames
  17286. fh.seek(pos + noffset)
  17287. buffer = fh.read(size - noffset)
  17288. names = []
  17289. while len(buffer) > 4:
  17290. size = struct.unpack('<I', buffer[:4])[0]
  17291. names.append(bytes2str(buffer[4 : 3 + size]))
  17292. buffer = buffer[4 + size :]
  17293. result['ColorNames'] = names
  17294. return result
  17295. def read_lsm_lookuptable(fh: FileHandle, /) -> dict[str, Any]:
  17296. """Read LSM lookup tables from file."""
  17297. result: dict[str, Any] = {}
  17298. (
  17299. size,
  17300. nsubblocks,
  17301. nchannels,
  17302. luttype,
  17303. advanced,
  17304. currentchannel,
  17305. ) = struct.unpack('<iiiiii', fh.read(24))
  17306. if size < 60:
  17307. logger().warning(
  17308. '<tifffile.read_lsm_lookuptable> '
  17309. 'invalid LSM LookupTables structure'
  17310. )
  17311. return result
  17312. fh.read(9 * 4) # reserved
  17313. result['LutType'] = TIFF.CZ_LSM_LUTTYPE(luttype)
  17314. result['Advanced'] = advanced
  17315. result['NumberChannels'] = nchannels
  17316. result['CurrentChannel'] = currentchannel
  17317. result['SubBlocks'] = subblocks = []
  17318. for _ in range(nsubblocks):
  17319. sbtype = struct.unpack('<i', fh.read(4))[0]
  17320. if sbtype <= 0:
  17321. break
  17322. size = struct.unpack('<i', fh.read(4))[0] - 8
  17323. if 0 < sbtype < 4:
  17324. data = fh.read_array('<f8', count=nchannels)
  17325. elif sbtype == 4:
  17326. # the data type is wrongly documented as f8
  17327. data = fh.read_array('<i4', count=nchannels * 4)
  17328. data = data.reshape((-1, 2, 2))
  17329. elif sbtype == 5:
  17330. # the data type is wrongly documented as f8
  17331. nknots = struct.unpack('<i', fh.read(4))[0] # undocumented
  17332. data = fh.read_array('<i4', count=nchannels * nknots * 2)
  17333. data = data.reshape((nchannels, nknots, 2))
  17334. elif sbtype == 6:
  17335. data = fh.read_array('<i2', count=nchannels * 4096)
  17336. data = data.reshape((-1, 4096))
  17337. else:
  17338. logger().warning(
  17339. '<tifffile.read_lsm_lookuptable> '
  17340. f'invalid LSM SubBlock type {sbtype}'
  17341. )
  17342. break
  17343. subblocks.append(
  17344. {'Type': TIFF.CZ_LSM_SUBBLOCK_TYPE(sbtype), 'Data': data}
  17345. )
  17346. return result
  17347. def read_lsm_scaninfo(fh: FileHandle, /) -> dict[str, Any]:
  17348. """Read LSM ScanInfo structure from file."""
  17349. value: Any
  17350. block: dict[str, Any] = {}
  17351. blocks = [block]
  17352. unpack = struct.unpack
  17353. if struct.unpack('<I', fh.read(4))[0] != 0x10000000:
  17354. # not a Recording sub block
  17355. logger().warning(
  17356. '<tifffile.read_lsm_scaninfo> invalid LSM ScanInfo structure'
  17357. )
  17358. return block
  17359. fh.read(8)
  17360. while True:
  17361. entry, dtype, size = unpack('<III', fh.read(12))
  17362. if dtype == 2:
  17363. # ascii
  17364. value = bytes2str(fh.read(size))
  17365. elif dtype == 4:
  17366. # long
  17367. value = unpack('<i', fh.read(4))[0]
  17368. elif dtype == 5:
  17369. # rational
  17370. value = unpack('<d', fh.read(8))[0]
  17371. else:
  17372. value = 0
  17373. if entry in TIFF.CZ_LSMINFO_SCANINFO_ARRAYS:
  17374. blocks.append(block)
  17375. name = TIFF.CZ_LSMINFO_SCANINFO_ARRAYS[entry]
  17376. newlist: list[dict[str, Any]] = []
  17377. block[name] = newlist
  17378. # TODO: fix types
  17379. block = newlist # type: ignore[assignment]
  17380. elif entry in TIFF.CZ_LSMINFO_SCANINFO_STRUCTS:
  17381. blocks.append(block)
  17382. newdict: dict[str, Any] = {}
  17383. # TODO: fix types
  17384. block.append(newdict) # type: ignore[attr-defined]
  17385. block = newdict
  17386. elif entry in TIFF.CZ_LSMINFO_SCANINFO_ATTRIBUTES:
  17387. block[TIFF.CZ_LSMINFO_SCANINFO_ATTRIBUTES[entry]] = value
  17388. elif entry == 0xFFFFFFFF:
  17389. # end sub block
  17390. block = blocks.pop()
  17391. else:
  17392. # unknown entry
  17393. block[f'Entry0x{entry:x}'] = value
  17394. if not blocks:
  17395. break
  17396. return block
  17397. def read_sis(
  17398. fh: FileHandle,
  17399. byteorder: ByteOrder,
  17400. dtype: int,
  17401. count: int,
  17402. offsetsize: int,
  17403. /,
  17404. ) -> dict[str, Any]:
  17405. """Read OlympusSIS structure from file.
  17406. No specification is available. Only few fields are known.
  17407. """
  17408. result: dict[str, Any] = {}
  17409. (magic, minute, hour, day, month, year, name, tagcount) = struct.unpack(
  17410. '<4s6xhhhhh6x32sh', fh.read(60)
  17411. )
  17412. if magic != b'SIS0':
  17413. raise ValueError('invalid OlympusSIS structure')
  17414. result['name'] = bytes2str(name)
  17415. try:
  17416. result['datetime'] = DateTime(
  17417. 1900 + year, month + 1, day, hour, minute
  17418. )
  17419. except ValueError:
  17420. pass
  17421. data = fh.read(8 * tagcount)
  17422. for i in range(0, tagcount * 8, 8):
  17423. tagtype, _count, offset = struct.unpack('<hhI', data[i : i + 8])
  17424. fh.seek(offset)
  17425. if tagtype == 1:
  17426. # general data
  17427. (lenexp, xcal, ycal, mag, camname, pictype) = struct.unpack(
  17428. '<10xhdd8xd2x34s32s', fh.read(112) # 220
  17429. )
  17430. m = math.pow(10, lenexp)
  17431. result['pixelsizex'] = xcal * m
  17432. result['pixelsizey'] = ycal * m
  17433. result['magnification'] = mag
  17434. result['cameraname'] = bytes2str(camname)
  17435. result['picturetype'] = bytes2str(pictype)
  17436. elif tagtype == 10:
  17437. # channel data
  17438. continue
  17439. # TODO: does not seem to work?
  17440. # (length, _, exptime, emv, _, camname, _, mictype,
  17441. # ) = struct.unpack('<h22sId4s32s48s32s', fh.read(152)) # 720
  17442. # result['exposuretime'] = exptime
  17443. # result['emvoltage'] = emv
  17444. # result['cameraname2'] = bytes2str(camname)
  17445. # result['microscopename'] = bytes2str(mictype)
  17446. return result
  17447. def read_sis_ini(
  17448. fh: FileHandle,
  17449. byteorder: ByteOrder,
  17450. dtype: int,
  17451. count: int,
  17452. offsetsize: int,
  17453. /,
  17454. ) -> dict[str, Any]:
  17455. """Read OlympusSIS INI string from file."""
  17456. try:
  17457. return olympus_ini_metadata(bytes2str(fh.read(count)))
  17458. except Exception as exc:
  17459. logger().warning(
  17460. f'<tifffile.olympus_ini_metadata> raised {exc!r:.128}'
  17461. )
  17462. return {}
  17463. def read_tvips_header(
  17464. fh: FileHandle,
  17465. byteorder: ByteOrder,
  17466. dtype: int,
  17467. count: int,
  17468. offsetsize: int,
  17469. /,
  17470. ) -> dict[str, Any]:
  17471. """Read TVIPS EM-MENU headers from file."""
  17472. result: dict[str, Any] = {}
  17473. header_v1 = TIFF.TVIPS_HEADER_V1
  17474. header = fh.read_record(numpy.dtype(header_v1), byteorder=byteorder)
  17475. for name, _typestr in header_v1:
  17476. result[name] = header[name].tolist()
  17477. if header['Version'] == 2:
  17478. header_v2 = TIFF.TVIPS_HEADER_V2
  17479. header = fh.read_record(numpy.dtype(header_v2), byteorder=byteorder)
  17480. if header['Magic'] != 0xAAAAAAAA:
  17481. logger().warning(
  17482. '<tifffile.read_tvips_header> invalid TVIPS v2 magic number'
  17483. )
  17484. return {}
  17485. # decode utf16 strings
  17486. for name, typestr in header_v2:
  17487. if typestr.startswith('V'):
  17488. result[name] = bytes2str(
  17489. header[name].tobytes(), 'utf-16', 'ignore'
  17490. )
  17491. else:
  17492. result[name] = header[name].tolist()
  17493. # convert nm to m
  17494. for axis in 'XY':
  17495. header['PhysicalPixelSize' + axis] /= 1e9
  17496. header['PixelSize' + axis] /= 1e9
  17497. elif header.version != 1:
  17498. logger().warning(
  17499. '<tifffile.read_tvips_header> unknown TVIPS header version'
  17500. )
  17501. return {}
  17502. return result
  17503. def read_fei_metadata(
  17504. fh: FileHandle,
  17505. byteorder: ByteOrder,
  17506. dtype: int,
  17507. count: int,
  17508. offsetsize: int,
  17509. /,
  17510. ) -> dict[str, Any]:
  17511. """Read FEI SFEG/HELIOS headers from file."""
  17512. result: dict[str, Any] = {}
  17513. section: dict[str, Any] = {}
  17514. data = bytes2str(fh.read(count))
  17515. for line in data.splitlines():
  17516. line = line.strip() # noqa: PLW2901
  17517. if line.startswith('['):
  17518. section = {}
  17519. result[line[1:-1]] = section
  17520. continue
  17521. try:
  17522. key, value = line.split('=')
  17523. except ValueError:
  17524. continue
  17525. section[key] = astype(value)
  17526. return result
  17527. def read_cz_sem(
  17528. fh: FileHandle,
  17529. byteorder: ByteOrder,
  17530. dtype: int,
  17531. count: int,
  17532. offsetsize: int,
  17533. /,
  17534. ) -> dict[str, Any]:
  17535. """Read Zeiss SEM tag from file.
  17536. See https://sourceforge.net/p/gwyddion/mailman/message/29275000/ for
  17537. unnamed values.
  17538. """
  17539. result: dict[str, Any] = {'': ()}
  17540. value: Any
  17541. key = None
  17542. data = bytes2str(fh.read(count))
  17543. for line in data.splitlines():
  17544. if line.isupper():
  17545. key = line.lower()
  17546. elif key:
  17547. try:
  17548. name, value = line.split('=')
  17549. except ValueError:
  17550. try:
  17551. name, value = line.split(':', 1)
  17552. except ValueError:
  17553. continue
  17554. value = value.strip()
  17555. unit = ''
  17556. try:
  17557. v, u = value.split()
  17558. number = astype(v, (int, float))
  17559. if number != v:
  17560. value = number
  17561. unit = u
  17562. except Exception:
  17563. number = astype(value, (int, float))
  17564. if number != value:
  17565. value = number
  17566. if value in {'No', 'Off'}:
  17567. value = False
  17568. elif value in {'Yes', 'On'}:
  17569. value = True
  17570. result[key] = (name.strip(), value)
  17571. if unit:
  17572. result[key] += (unit,)
  17573. key = None
  17574. else:
  17575. result[''] += (astype(line, (int, float)),)
  17576. return result
  17577. def read_nih_image_header(
  17578. fh: FileHandle,
  17579. byteorder: ByteOrder,
  17580. dtype: int,
  17581. count: int,
  17582. offsetsize: int,
  17583. /,
  17584. ) -> dict[str, Any]:
  17585. """Read NIH_IMAGE_HEADER tag value from file."""
  17586. arr = fh.read_record(TIFF.NIH_IMAGE_HEADER, byteorder=byteorder)
  17587. arr = arr.view(arr.dtype.newbyteorder(byteorder))
  17588. result = recarray2dict(arr)
  17589. result['XUnit'] = result['XUnit'][: result['XUnitSize']]
  17590. result['UM'] = result['UM'][: result['UMsize']]
  17591. return result
  17592. def read_scanimage_metadata(
  17593. fh: FileHandle, /
  17594. ) -> tuple[dict[str, Any], dict[str, Any], int]:
  17595. """Read ScanImage BigTIFF v3 or v4 static and ROI metadata from file.
  17596. The settings can be used to read image and metadata without parsing
  17597. the TIFF file.
  17598. Frame data and ROI groups can alternatively be obtained from the Software
  17599. and Artist tags of any TIFF page.
  17600. Parameters:
  17601. fh: Binary file handle to read from.
  17602. Returns:
  17603. - Non-varying frame data, parsed with :py:func:`matlabstr2py`.
  17604. - ROI group data, parsed from JSON.
  17605. - Version of metadata (3 or 4).
  17606. Raises:
  17607. ValueError: File does not contain valid ScanImage metadata.
  17608. """
  17609. fh.seek(0)
  17610. try:
  17611. byteorder, version = struct.unpack('<2sH', fh.read(4))
  17612. if byteorder != b'II' or version != 43:
  17613. raise ValueError('not a BigTIFF file')
  17614. fh.seek(16)
  17615. magic, version, size0, size1 = struct.unpack('<IIII', fh.read(16))
  17616. if magic != 117637889 or version not in {3, 4}:
  17617. raise ValueError(
  17618. f'invalid magic {magic} or version {version} number'
  17619. )
  17620. except UnicodeDecodeError as exc:
  17621. raise ValueError('file must be opened in binary mode') from exc
  17622. except Exception as exc:
  17623. raise ValueError('not a ScanImage BigTIFF v3 or v4 file') from exc
  17624. frame_data = matlabstr2py(bytes2str(fh.read(size0)[:-1]))
  17625. roi_data = read_json(fh, '<', 0, size1, 0) if size1 > 1 else {}
  17626. return frame_data, roi_data, version
  17627. def read_micromanager_metadata(
  17628. fh: FileHandle | IO[bytes], /, keys: Container[str] | None = None
  17629. ) -> dict[str, Any]:
  17630. """Return Micro-Manager non-TIFF settings from file.
  17631. The settings can be used to read image data without parsing any TIFF
  17632. structures.
  17633. Parameters:
  17634. fh: Open file handle to Micro-Manager TIFF file.
  17635. keys: Name of keys to return in result.
  17636. Returns:
  17637. Micro-Manager non-TIFF settings, which may contain the following keys:
  17638. - 'MajorVersion' (str)
  17639. - 'MinorVersion' (str)
  17640. - 'Summary' (dict):
  17641. Specifies the dataset, such as shape, dimensions, and coordinates.
  17642. - 'IndexMap' (numpy.ndarray):
  17643. (channel, slice, frame, position, ifd_offset) indices of all frames.
  17644. - 'DisplaySettings' (list[dict]):
  17645. Image display settings such as channel contrast and colors.
  17646. - 'Comments' (dict):
  17647. User comments.
  17648. Notes:
  17649. Summary metadata are the same for all files in a dataset.
  17650. DisplaySettings metadata are frequently corrupted, and Comments are
  17651. often empty.
  17652. The Summary and IndexMap metadata are stored at the beginning of
  17653. the file, while DisplaySettings and Comments are towards the end.
  17654. Excluding DisplaySettings and Comments from the results may
  17655. significantly speed up reading metadata of interest.
  17656. References:
  17657. - https://micro-manager.org/Micro-Manager_File_Formats
  17658. - https://github.com/micro-manager/NDTiffStorage
  17659. """
  17660. if keys is None:
  17661. keys = {'Summary', 'IndexMap', 'DisplaySettings', 'Comments'}
  17662. fh.seek(0)
  17663. try:
  17664. byteorder = {b'II': '<', b'MM': '>'}[fh.read(2)]
  17665. fh.seek(8)
  17666. (
  17667. index_header,
  17668. index_offset,
  17669. ) = struct.unpack(byteorder + 'II', fh.read(8))
  17670. except Exception as exc:
  17671. raise ValueError('not a Micro-Manager TIFF file') from exc
  17672. result = {}
  17673. if index_header == 483729:
  17674. # NDTiff >= v2
  17675. result['MajorVersion'] = index_offset
  17676. try:
  17677. (
  17678. summary_header,
  17679. summary_length,
  17680. ) = struct.unpack(byteorder + 'II', fh.read(8))
  17681. if summary_header != 2355492:
  17682. # NDTiff v3
  17683. result['MinorVersion'] = summary_header
  17684. if summary_length != 2355492:
  17685. raise ValueError(
  17686. f'invalid summary_length {summary_length}'
  17687. )
  17688. summary_length = struct.unpack(byteorder + 'I', fh.read(4))[0]
  17689. if 'Summary' in keys:
  17690. data = fh.read(summary_length)
  17691. if len(data) != summary_length:
  17692. raise ValueError('not enough data')
  17693. result['Summary'] = json.loads(bytes2str(data, 'utf-8'))
  17694. except Exception as exc:
  17695. logger().warning(
  17696. '<tifffile.read_micromanager_metadata> '
  17697. f'failed to read NDTiffv{index_offset} summary settings, '
  17698. f'raised {exc!r:.128}'
  17699. )
  17700. return result
  17701. # Micro-Manager multipage TIFF or NDTiff v1
  17702. try:
  17703. (
  17704. display_header,
  17705. display_offset,
  17706. comments_header,
  17707. comments_offset,
  17708. summary_header,
  17709. summary_length,
  17710. ) = struct.unpack(byteorder + 'IIIIII', fh.read(24))
  17711. except Exception as exc:
  17712. logger().warning(
  17713. '<tifffile.read_micromanager_metadata> failed to read header, '
  17714. f'raised {exc!r:.128}'
  17715. )
  17716. if 'Summary' in keys:
  17717. try:
  17718. if summary_header != 2355492:
  17719. raise ValueError(f'invalid summary_header {summary_header}')
  17720. data = fh.read(summary_length)
  17721. if len(data) != summary_length:
  17722. raise ValueError('not enough data')
  17723. result['Summary'] = json.loads(bytes2str(data, 'utf-8'))
  17724. except Exception as exc:
  17725. logger().warning(
  17726. '<tifffile.read_micromanager_metadata> '
  17727. f'failed to read summary settings, raised {exc!r:.128}'
  17728. )
  17729. if 'IndexMap' in keys:
  17730. try:
  17731. if index_header != 54773648:
  17732. raise ValueError(f'invalid index_header {index_header}')
  17733. fh.seek(index_offset)
  17734. header, count = struct.unpack(byteorder + 'II', fh.read(8))
  17735. if header != 3453623:
  17736. raise ValueError('invalid header')
  17737. data = fh.read(count * 20)
  17738. result['IndexMap'] = numpy.frombuffer(
  17739. data, byteorder + 'u4', count * 5
  17740. ).reshape((-1, 5))
  17741. except Exception as exc:
  17742. logger().warning(
  17743. '<tifffile.read_micromanager_metadata> '
  17744. f'failed to read index map, raised {exc!r:.128}'
  17745. )
  17746. if 'DisplaySettings' in keys:
  17747. try:
  17748. if display_header != 483765892:
  17749. raise ValueError(f'invalid display_header {display_header}')
  17750. fh.seek(display_offset)
  17751. header, count = struct.unpack(byteorder + 'II', fh.read(8))
  17752. if header != 347834724:
  17753. # display_offset might be wrapped at 4 GB
  17754. fh.seek(display_offset + 2**32)
  17755. header, count = struct.unpack(byteorder + 'II', fh.read(8))
  17756. if header != 347834724:
  17757. raise ValueError('invalid display header')
  17758. data = fh.read(count)
  17759. if len(data) != count:
  17760. raise ValueError('not enough data')
  17761. result['DisplaySettings'] = json.loads(bytes2str(data, 'utf-8'))
  17762. except json.decoder.JSONDecodeError:
  17763. pass # ignore frequent truncated JSON data
  17764. except Exception as exc:
  17765. logger().warning(
  17766. '<tifffile.read_micromanager_metadata> '
  17767. f'failed to read display settings, raised {exc!r:.128}'
  17768. )
  17769. result['MajorVersion'] = 0
  17770. try:
  17771. if comments_header == 99384722:
  17772. # Micro-Manager multipage TIFF
  17773. if 'Comments' in keys:
  17774. fh.seek(comments_offset)
  17775. header, count = struct.unpack(byteorder + 'II', fh.read(8))
  17776. if header != 84720485:
  17777. # comments_offset might be wrapped at 4 GB
  17778. fh.seek(comments_offset + 2**32)
  17779. header, count = struct.unpack(byteorder + 'II', fh.read(8))
  17780. if header != 84720485:
  17781. raise ValueError('invalid comments header')
  17782. data = fh.read(count)
  17783. if len(data) != count:
  17784. raise ValueError('not enough data')
  17785. result['Comments'] = json.loads(bytes2str(data, 'utf-8'))
  17786. elif comments_header == 483729:
  17787. # NDTiff v1
  17788. result['MajorVersion'] = comments_offset
  17789. elif comments_header == 0 and comments_offset == 0:
  17790. pass
  17791. elif 'Comments' in keys:
  17792. raise ValueError(f'invalid comments_header {comments_header}')
  17793. except Exception as exc:
  17794. logger().warning(
  17795. '<tifffile.read_micromanager_metadata> failed to read comments, '
  17796. f'raised {exc!r:.128}'
  17797. )
  17798. return result
  17799. def read_ndtiff_index(
  17800. file: str | os.PathLike[Any], /
  17801. ) -> Iterator[
  17802. tuple[dict[str, int | str], str, int, int, int, int, int, int, int, int]
  17803. ]:
  17804. """Return iterator over fields in Micro-Manager NDTiff.index file.
  17805. Parameters:
  17806. file: Path of NDTiff.index file.
  17807. Yields:
  17808. Fields in NDTiff.index file:
  17809. - axes_dict: Axes indices of frame in image.
  17810. - filename: Name of file containing frame and metadata.
  17811. - dataoffset: Offset of frame data in file.
  17812. - width: Width of frame.
  17813. - height: Height of frame.
  17814. - pixeltype: Pixel type.
  17815. 0: 8-bit monochrome;
  17816. 1: 16-bit monochrome;
  17817. 2: 8-bit RGB;
  17818. 3: 10-bit monochrome;
  17819. 4: 12-bit monochrome;
  17820. 5: 14-bit monochrome;
  17821. 6: 11-bit monochrome.
  17822. - compression: Pixel compression. 0: Uncompressed.
  17823. - metaoffset: Offset of JSON metadata in file.
  17824. - metabytecount: Length of metadata.
  17825. - metacompression: Metadata compression. 0: Uncompressed.
  17826. """
  17827. with open(file, 'rb') as fh:
  17828. while True:
  17829. b = fh.read(4)
  17830. if len(b) != 4:
  17831. break
  17832. k = struct.unpack('<i', b)[0]
  17833. axes_dict = json.loads(fh.read(k))
  17834. n = struct.unpack('<i', fh.read(4))[0]
  17835. filename = fh.read(n).decode()
  17836. (
  17837. dataoffset,
  17838. width,
  17839. height,
  17840. pixeltype,
  17841. compression,
  17842. metaoffset,
  17843. metabytecount,
  17844. metacompression,
  17845. ) = struct.unpack('<IiiiiIii', fh.read(32))
  17846. yield (
  17847. axes_dict,
  17848. filename,
  17849. dataoffset,
  17850. width,
  17851. height,
  17852. pixeltype,
  17853. compression,
  17854. metaoffset,
  17855. metabytecount,
  17856. metacompression,
  17857. )
  17858. def read_gdal_structural_metadata(
  17859. fh: FileHandle | IO[bytes], /
  17860. ) -> dict[str, str] | None:
  17861. """Read non-TIFF GDAL structural metadata from file.
  17862. Return None if the file does not contain valid GDAL structural metadata.
  17863. The metadata can be used to optimize reading image data from a COG file.
  17864. """
  17865. fh.seek(0)
  17866. try:
  17867. if fh.read(2) not in {b'II', b'MM'}:
  17868. raise ValueError('not a TIFF file')
  17869. fh.seek({b'*': 8, b'+': 16}[fh.read(1)])
  17870. header = fh.read(43).decode()
  17871. if header[:30] != 'GDAL_STRUCTURAL_METADATA_SIZE=':
  17872. return None
  17873. size = int(header[30:36])
  17874. lines = fh.read(size).decode()
  17875. except Exception:
  17876. return None
  17877. result: dict[str, Any] = {}
  17878. try:
  17879. for line in lines.splitlines():
  17880. if '=' in line:
  17881. key, value = line.split('=', 1)
  17882. result[key.strip()] = value.strip()
  17883. except Exception as exc:
  17884. logger().warning(
  17885. f'<tifffile.read_gdal_structural_metadata> raised {exc!r:.128}'
  17886. )
  17887. return None
  17888. return result
  17889. def read_metaseries_catalog(fh: FileHandle | IO[bytes], /) -> None:
  17890. """Read MetaSeries non-TIFF hint catalog from file.
  17891. Raise ValueError if the file does not contain a valid hint catalog.
  17892. """
  17893. # TODO: implement read_metaseries_catalog
  17894. raise NotImplementedError
  17895. def imagej_metadata_tag(
  17896. metadata: dict[str, Any], byteorder: ByteOrder, /
  17897. ) -> tuple[
  17898. tuple[int, int, int, bytes, bool], tuple[int, int, int, bytes, bool]
  17899. ]:
  17900. """Return IJMetadata and IJMetadataByteCounts tags from metadata dict.
  17901. Parameters:
  17902. metadata:
  17903. May contain the following keys and values:
  17904. 'Info' (str):
  17905. Human-readable information as string.
  17906. 'Labels' (Sequence[str]):
  17907. Human-readable label for each image.
  17908. 'Ranges' (Sequence[float]):
  17909. Lower and upper values for each channel.
  17910. 'LUTs' (list[numpy.ndarray[(3, 256), 'uint8']]):
  17911. Color palettes for each channel.
  17912. 'Plot' (bytes):
  17913. Undocumented ImageJ internal format.
  17914. 'ROI', 'Overlays' (bytes):
  17915. Undocumented ImageJ internal region of interest and overlay
  17916. format. Can be created with the
  17917. `roifile <https://pypi.org/project/roifile/>`_ package.
  17918. 'Properties' (dict[str, str]):
  17919. Map of key, value items.
  17920. byteorder:
  17921. Byte order of TIFF file.
  17922. Returns:
  17923. IJMetadata and IJMetadataByteCounts tags in :py:meth:`TiffWriter.write`
  17924. `extratags` format.
  17925. """
  17926. if not metadata:
  17927. return () # type: ignore[return-value]
  17928. header_list = [{'>': b'IJIJ', '<': b'JIJI'}[byteorder]]
  17929. bytecount_list = [0]
  17930. body_list = []
  17931. def _string(data: str, byteorder: ByteOrder, /) -> bytes:
  17932. return data.encode('utf-16' + {'>': 'be', '<': 'le'}[byteorder])
  17933. def _doubles(data: Sequence[float], byteorder: ByteOrder, /) -> bytes:
  17934. return struct.pack(f'{byteorder}{len(data)}d', *data)
  17935. def _ndarray(data: NDArray[Any], byteorder: ByteOrder, /) -> bytes:
  17936. return data.tobytes()
  17937. def _bytes(data: bytes, byteorder: ByteOrder, /) -> bytes:
  17938. return data
  17939. metadata_types: tuple[
  17940. tuple[str, bytes, Callable[[Any, ByteOrder], bytes]], ...
  17941. ] = (
  17942. ('Info', b'info', _string),
  17943. ('Labels', b'labl', _string),
  17944. ('Ranges', b'rang', _doubles),
  17945. ('LUTs', b'luts', _ndarray),
  17946. ('Plot', b'plot', _bytes),
  17947. ('ROI', b'roi ', _bytes),
  17948. ('Overlays', b'over', _bytes),
  17949. ('Properties', b'prop', _string),
  17950. )
  17951. for item in metadata_types:
  17952. key, mtype, func = item
  17953. if key.lower() in metadata:
  17954. key = key.lower()
  17955. elif key not in metadata:
  17956. continue
  17957. if byteorder == '<':
  17958. mtype = mtype[::-1]
  17959. values = metadata[key]
  17960. if isinstance(values, dict):
  17961. values = [str(i) for item in values.items() for i in item]
  17962. count = len(values)
  17963. elif func is _doubles:
  17964. values = [values]
  17965. count = 1
  17966. elif isinstance(values, (list, tuple)):
  17967. count = len(values)
  17968. else:
  17969. values = [values]
  17970. count = 1
  17971. header_list.append(mtype + struct.pack(byteorder + 'I', count))
  17972. for value in values:
  17973. data = func(value, byteorder)
  17974. body_list.append(data)
  17975. bytecount_list.append(len(data))
  17976. if not body_list:
  17977. return () # type: ignore[return-value]
  17978. body = b''.join(body_list)
  17979. header = b''.join(header_list)
  17980. data = header + body
  17981. bytecount_list[0] = len(header)
  17982. bytecounts = struct.pack(
  17983. byteorder + ('I' * len(bytecount_list)), *bytecount_list
  17984. )
  17985. return (
  17986. (50839, 1, len(data), data, True),
  17987. (50838, 4, len(bytecounts) // 4, bytecounts, True),
  17988. )
  17989. def imagej_metadata(
  17990. data: bytes, bytecounts: Sequence[int], byteorder: ByteOrder, /
  17991. ) -> dict[str, Any]:
  17992. """Return IJMetadata tag value.
  17993. Parameters:
  17994. bytes:
  17995. Encoded value of IJMetadata tag.
  17996. bytecounts:
  17997. Value of IJMetadataByteCounts tag.
  17998. byteorder:
  17999. Byte order of TIFF file.
  18000. Returns:
  18001. Metadata dict with optional items:
  18002. 'Info' (str):
  18003. Human-readable information as string.
  18004. Some formats, such as OIF or ScanImage, can be parsed into
  18005. dicts with :py:func:`matlabstr2py` or the
  18006. `oiffile.SettingsFile()` function of the
  18007. `oiffile <https://pypi.org/project/oiffile/>`_ package.
  18008. 'Labels' (Sequence[str]):
  18009. Human-readable labels for each channel.
  18010. 'Ranges' (Sequence[float]):
  18011. Lower and upper values for each channel.
  18012. 'LUTs' (list[numpy.ndarray[(3, 256), 'uint8']]):
  18013. Color palettes for each channel.
  18014. 'Plot' (bytes):
  18015. Undocumented ImageJ internal format.
  18016. 'ROI', 'Overlays' (bytes):
  18017. Undocumented ImageJ internal region of interest and overlay
  18018. format. Can be parsed with the
  18019. `roifile <https://pypi.org/project/roifile/>`_ package.
  18020. 'Properties' (dict[str, str]):
  18021. Map of key, value items.
  18022. """
  18023. def _string(data: bytes, byteorder: ByteOrder, /) -> str:
  18024. return data.decode('utf-16' + {'>': 'be', '<': 'le'}[byteorder])
  18025. def _doubles(data: bytes, byteorder: ByteOrder, /) -> tuple[float, ...]:
  18026. return struct.unpack(byteorder + ('d' * (len(data) // 8)), data)
  18027. def _lut(data: bytes, byteorder: ByteOrder, /) -> NDArray[numpy.uint8]:
  18028. return numpy.frombuffer(data, numpy.uint8).reshape((-1, 256))
  18029. def _bytes(data: bytes, byteorder: ByteOrder, /) -> bytes:
  18030. return data
  18031. # big-endian
  18032. metadata_types: dict[
  18033. bytes, tuple[str, Callable[[bytes, ByteOrder], Any]]
  18034. ] = {
  18035. b'info': ('Info', _string),
  18036. b'labl': ('Labels', _string),
  18037. b'rang': ('Ranges', _doubles),
  18038. b'luts': ('LUTs', _lut),
  18039. b'plot': ('Plot', _bytes),
  18040. b'roi ': ('ROI', _bytes),
  18041. b'over': ('Overlays', _bytes),
  18042. b'prop': ('Properties', _string),
  18043. }
  18044. # little-endian
  18045. metadata_types.update({k[::-1]: v for k, v in metadata_types.items()})
  18046. if len(bytecounts) == 0:
  18047. raise ValueError('no ImageJ metadata')
  18048. if data[:4] not in {b'IJIJ', b'JIJI'}:
  18049. raise ValueError('invalid ImageJ metadata')
  18050. header_size = bytecounts[0]
  18051. if header_size < 12 or header_size > 804:
  18052. raise ValueError('invalid ImageJ metadata header size')
  18053. ntypes = (header_size - 4) // 8
  18054. header = struct.unpack(
  18055. byteorder + '4sI' * ntypes, data[4 : 4 + ntypes * 8]
  18056. )
  18057. pos = 4 + ntypes * 8
  18058. counter = 0
  18059. result = {}
  18060. for mtype, count in zip(header[::2], header[1::2], strict=True):
  18061. values = []
  18062. name, func = metadata_types.get(mtype, (bytes2str(mtype), _bytes))
  18063. for _ in range(count):
  18064. counter += 1
  18065. pos1 = pos + bytecounts[counter]
  18066. values.append(func(data[pos:pos1], byteorder))
  18067. pos = pos1
  18068. result[name.strip()] = values[0] if count == 1 else values
  18069. prop = result.get('Properties')
  18070. if prop and len(prop) % 2 == 0:
  18071. result['Properties'] = dict(
  18072. prop[i : i + 2] for i in range(0, len(prop), 2)
  18073. )
  18074. return result
  18075. def imagej_description_metadata(description: str, /) -> dict[str, Any]:
  18076. r"""Return metadata from ImageJ image description.
  18077. Raise ValueError if not a valid ImageJ description.
  18078. >>> description = 'ImageJ=1.11a\nimages=510\nhyperstack=true\n'
  18079. >>> imagej_description_metadata(description) # doctest: +SKIP
  18080. {'ImageJ': '1.11a', 'images': 510, 'hyperstack': True}
  18081. """
  18082. def _bool(val: str, /) -> bool:
  18083. return {'true': True, 'false': False}[val.lower()]
  18084. result: dict[str, Any] = {}
  18085. for line in description.splitlines():
  18086. try:
  18087. key, val = line.split('=')
  18088. except Exception: # noqa: S112
  18089. continue
  18090. key = key.strip()
  18091. val = val.strip()
  18092. for dtype in (int, float, _bool):
  18093. try:
  18094. val = dtype(val) # type: ignore[assignment]
  18095. break
  18096. except Exception: # noqa: S110
  18097. pass
  18098. result[key] = val
  18099. if 'ImageJ' not in result and 'SCIFIO' not in result:
  18100. raise ValueError(f'not an ImageJ image description {result!r}')
  18101. return result
  18102. def imagej_description(
  18103. shape: Sequence[int],
  18104. /,
  18105. axes: str | None = None,
  18106. *,
  18107. rgb: bool | None = None,
  18108. colormaped: bool = False,
  18109. **metadata: Any, # TODO: use TypedDict
  18110. ) -> str:
  18111. """Return ImageJ image description from data shape and metadata.
  18112. Parameters:
  18113. shape:
  18114. Shape of image array.
  18115. axes:
  18116. Character codes for dimensions in `shape`.
  18117. ImageJ can handle up to 6 dimensions in order TZCYXS.
  18118. `Axes` and `shape` are used to determine the images, channels,
  18119. slices, and frames entries of the image description.
  18120. rgb:
  18121. Image is RGB type.
  18122. colormaped:
  18123. Image is indexed color.
  18124. **metadata:
  18125. Additional items to be included in image description:
  18126. hyperstack (bool):
  18127. Image is a hyperstack.
  18128. The default is True unless `colormapped` is true.
  18129. mode (str):
  18130. Display mode: 'grayscale', 'composite', or 'color'.
  18131. The default is 'grayscale' unless `rgb` or `colormaped` are
  18132. true. Ignored if `hyperstack` is false.
  18133. loop (bool):
  18134. Loop frames back and forth. The default is False.
  18135. finterval (float):
  18136. Frame interval in seconds.
  18137. fps (float):
  18138. Frames per seconds. The inverse of `finterval`.
  18139. spacing (float):
  18140. Voxel spacing in `unit` units.
  18141. unit (str):
  18142. Unit for `spacing` and X/YResolution tags.
  18143. Usually 'um' (micrometer) or 'pixel'.
  18144. xorigin, yorigin, zorigin (float):
  18145. X, Y, and Z origins in pixel units.
  18146. version (str):
  18147. ImageJ version string. The default is '1.11a'.
  18148. images, channels, slices, frames (int):
  18149. Ignored.
  18150. Examples:
  18151. >>> imagej_description((51, 5, 2, 196, 171)) # doctest: +SKIP
  18152. ImageJ=1.11a
  18153. images=510
  18154. channels=2
  18155. slices=5
  18156. frames=51
  18157. hyperstack=true
  18158. mode=grayscale
  18159. loop=false
  18160. """
  18161. mode = metadata.pop('mode', None)
  18162. hyperstack = metadata.pop('hyperstack', None)
  18163. loop = metadata.pop('loop', None)
  18164. version = metadata.pop('ImageJ', '1.11a')
  18165. if colormaped:
  18166. hyperstack = False
  18167. rgb = False
  18168. shape = imagej_shape(shape, rgb=rgb, axes=axes)
  18169. rgb = shape[-1] in {3, 4}
  18170. append = []
  18171. result = [f'ImageJ={version}']
  18172. result.append(f'images={product(shape[:-3])}')
  18173. if hyperstack is None:
  18174. hyperstack = True
  18175. append.append('hyperstack=true')
  18176. else:
  18177. append.append(f'hyperstack={bool(hyperstack)}'.lower())
  18178. if shape[2] > 1:
  18179. result.append(f'channels={shape[2]}')
  18180. if mode is None and not rgb and not colormaped:
  18181. mode = 'grayscale'
  18182. if hyperstack and mode:
  18183. append.append(f'mode={mode}')
  18184. if shape[1] > 1:
  18185. result.append(f'slices={shape[1]}')
  18186. if shape[0] > 1:
  18187. result.append(f'frames={shape[0]}')
  18188. if loop is None:
  18189. append.append('loop=false')
  18190. if loop is not None:
  18191. append.append(f'loop={bool(loop)}'.lower())
  18192. for key, value in metadata.items():
  18193. if key not in {'images', 'channels', 'slices', 'frames', 'SCIFIO'}:
  18194. val = str(value).lower() if isinstance(value, bool) else value
  18195. append.append(f'{key.lower()}={val}')
  18196. return '\n'.join(result + append + [''])
  18197. def imagej_shape(
  18198. shape: Sequence[int],
  18199. /,
  18200. *,
  18201. rgb: bool | None = None,
  18202. axes: str | None = None,
  18203. ) -> tuple[int, ...]:
  18204. """Return shape normalized to 6D ImageJ hyperstack TZCYXS.
  18205. Raise ValueError if not a valid ImageJ hyperstack shape or axes order.
  18206. >>> imagej_shape((2, 3, 4, 5, 3), rgb=False)
  18207. (2, 3, 4, 5, 3, 1)
  18208. """
  18209. shape = tuple(int(i) for i in shape)
  18210. ndim = len(shape)
  18211. if 1 > ndim > 6:
  18212. raise ValueError('ImageJ hyperstack must be 2-6 dimensional')
  18213. if axes:
  18214. if len(axes) != ndim:
  18215. raise ValueError('ImageJ hyperstack shape and axes do not match')
  18216. i = 0
  18217. axes = axes.upper()
  18218. for ax in axes:
  18219. j = 'TZCYXS'.find(ax)
  18220. if j < i:
  18221. raise ValueError(
  18222. 'ImageJ hyperstack axes must be in TZCYXS order'
  18223. )
  18224. i = j
  18225. ndims = len(axes)
  18226. newshape = []
  18227. i = 0
  18228. for ax in 'TZCYXS':
  18229. if i < ndims and ax == axes[i]:
  18230. newshape.append(shape[i])
  18231. i += 1
  18232. else:
  18233. newshape.append(1)
  18234. if newshape[-1] not in {1, 3, 4}:
  18235. raise ValueError(
  18236. 'ImageJ hyperstack must contain 1, 3, or 4 samples'
  18237. )
  18238. return tuple(newshape)
  18239. if rgb is None:
  18240. rgb = shape[-1] in {3, 4} and ndim > 2
  18241. if rgb and shape[-1] not in {3, 4}:
  18242. raise ValueError('ImageJ hyperstack is not a RGB image')
  18243. if not rgb and ndim == 6 and shape[-1] != 1:
  18244. raise ValueError('ImageJ hyperstack is not a grayscale image')
  18245. if rgb or shape[-1] == 1:
  18246. return (1,) * (6 - ndim) + shape
  18247. return (1,) * (5 - ndim) + shape + (1,)
  18248. def jpeg_decode_colorspace(
  18249. photometric: int,
  18250. planarconfig: int,
  18251. extrasamples: tuple[int, ...],
  18252. jfif: bool, # noqa: FBT001
  18253. /,
  18254. ) -> tuple[int | None, int | str | None]:
  18255. """Return JPEG and output color space for `jpeg_decode` function."""
  18256. colorspace: int | None = None
  18257. outcolorspace: int | str | None = None
  18258. if extrasamples:
  18259. pass
  18260. elif photometric == 6:
  18261. # YCBCR -> RGB
  18262. outcolorspace = 2 # RGB
  18263. elif photometric == 2:
  18264. # RGB -> RGB
  18265. if not jfif:
  18266. # found in Aperio SVS
  18267. colorspace = 2
  18268. outcolorspace = 2
  18269. elif photometric == 5:
  18270. # CMYK
  18271. outcolorspace = 4
  18272. elif photometric > 3:
  18273. outcolorspace = PHOTOMETRIC(photometric).name
  18274. if planarconfig != 1:
  18275. outcolorspace = 1 # decode separate planes to grayscale
  18276. return colorspace, outcolorspace
  18277. def jpeg_shape(jpeg: bytes, /) -> tuple[int, int, int, int]:
  18278. """Return bitdepth and shape of JPEG image."""
  18279. i = 0
  18280. while i < len(jpeg):
  18281. marker = struct.unpack('>H', jpeg[i : i + 2])[0]
  18282. i += 2
  18283. if marker == 0xFFD8:
  18284. # start of image
  18285. continue
  18286. if marker == 0xFFD9:
  18287. # end of image
  18288. break
  18289. if 0xFFD0 <= marker <= 0xFFD7:
  18290. # restart marker
  18291. continue
  18292. if marker == 0xFF01:
  18293. # private marker
  18294. continue
  18295. length = struct.unpack('>H', jpeg[i : i + 2])[0]
  18296. i += 2
  18297. if 0xFFC0 <= marker <= 0xFFC3:
  18298. # start of frame
  18299. return struct.unpack('>BHHB', jpeg[i : i + 6])
  18300. if marker == 0xFFDA:
  18301. # start of scan
  18302. break
  18303. # skip to next marker
  18304. i += length - 2
  18305. raise ValueError('no SOF marker found')
  18306. def ndpi_jpeg_tile(jpeg: bytes, /) -> tuple[int, int, bytes]:
  18307. """Return tile shape and JPEG header from JPEG with restart markers."""
  18308. marker: int
  18309. length: int
  18310. factor: int
  18311. ncomponents: int
  18312. restartinterval: int = 0
  18313. sofoffset: int = 0
  18314. sosoffset: int = 0
  18315. i: int = 0
  18316. while i < len(jpeg):
  18317. marker = struct.unpack('>H', jpeg[i : i + 2])[0]
  18318. i += 2
  18319. if marker == 0xFFD8:
  18320. # start of image
  18321. continue
  18322. if marker == 0xFFD9:
  18323. # end of image
  18324. break
  18325. if 0xFFD0 <= marker <= 0xFFD7:
  18326. # restart marker
  18327. continue
  18328. if marker == 0xFF01:
  18329. # private marker
  18330. continue
  18331. length = struct.unpack('>H', jpeg[i : i + 2])[0]
  18332. i += 2
  18333. if marker == 0xFFDD:
  18334. # define restart interval
  18335. restartinterval = struct.unpack('>H', jpeg[i : i + 2])[0]
  18336. elif marker == 0xFFC0:
  18337. # start of frame
  18338. sofoffset = i + 1
  18339. _precision, _imlength, _imwidth, ncomponents = struct.unpack(
  18340. '>BHHB', jpeg[i : i + 6]
  18341. )
  18342. i += 6
  18343. mcuwidth = 1
  18344. mcuheight = 1
  18345. for _ in range(ncomponents):
  18346. _cid, factor, _table = struct.unpack('>BBB', jpeg[i : i + 3])
  18347. i += 3
  18348. mcuwidth = max(mcuwidth, factor >> 4)
  18349. mcuheight = max(mcuheight, factor & 0b00001111)
  18350. mcuwidth *= 8
  18351. mcuheight *= 8
  18352. i = sofoffset - 1
  18353. elif marker == 0xFFDA:
  18354. # start of scan
  18355. sosoffset = i + length - 2
  18356. break
  18357. # skip to next marker
  18358. i += length - 2
  18359. if restartinterval == 0 or sofoffset == 0 or sosoffset == 0:
  18360. raise ValueError('missing required JPEG markers')
  18361. # patch jpeg header for tile size
  18362. tilelength = mcuheight
  18363. tilewidth = restartinterval * mcuwidth
  18364. jpegheader = (
  18365. jpeg[:sofoffset]
  18366. + struct.pack('>HH', tilelength, tilewidth)
  18367. + jpeg[sofoffset + 4 : sosoffset]
  18368. )
  18369. return tilelength, tilewidth, jpegheader
  18370. def shaped_description(shape: Sequence[int], /, **metadata: Any) -> str:
  18371. """Return JSON image description from data shape and other metadata.
  18372. Return UTF-8 encoded JSON.
  18373. >>> shaped_description((256, 256, 3), axes='YXS') # doctest: +SKIP
  18374. '{"shape": [256, 256, 3], "axes": "YXS"}'
  18375. """
  18376. metadata.update(shape=shape)
  18377. return json.dumps(metadata) # .encode()
  18378. def shaped_description_metadata(description: str, /) -> dict[str, Any]:
  18379. """Return metadata from JSON formatted image description.
  18380. Raise ValueError if `description` is of unknown format.
  18381. >>> description = '{"shape": [256, 256, 3], "axes": "YXS"}'
  18382. >>> shaped_description_metadata(description) # doctest: +SKIP
  18383. {'shape': [256, 256, 3], 'axes': 'YXS'}
  18384. >>> shaped_description_metadata('shape=(256, 256, 3)')
  18385. {'shape': (256, 256, 3)}
  18386. """
  18387. if description[:6] == 'shape=':
  18388. # old-style 'shaped' description; not JSON
  18389. shape = tuple(int(i) for i in description[7:-1].split(','))
  18390. return {'shape': shape}
  18391. if description[:1] == '{' and description[-1:] == '}':
  18392. # JSON description
  18393. return json.loads(description)
  18394. raise ValueError('invalid JSON image description', description)
  18395. def fluoview_description_metadata(
  18396. description: str,
  18397. /,
  18398. ignoresections: Container[str] | None = None,
  18399. ) -> dict[str, Any]:
  18400. r"""Return metadata from FluoView image description.
  18401. The FluoView image description format is unspecified. Expect failures.
  18402. >>> descr = (
  18403. ... '[Intensity Mapping]\nMap Ch0: Range=00000 to 02047\n'
  18404. ... '[Intensity Mapping End]'
  18405. ... )
  18406. >>> fluoview_description_metadata(descr)
  18407. {'Intensity Mapping': {'Map Ch0: Range': '00000 to 02047'}}
  18408. """
  18409. if not description.startswith('['):
  18410. raise ValueError('invalid FluoView image description')
  18411. if ignoresections is None:
  18412. ignoresections = {'Region Info (Fields)', 'Protocol Description'}
  18413. section: Any
  18414. result: dict[str, Any] = {}
  18415. sections = [result]
  18416. comment = False
  18417. for line in description.splitlines():
  18418. if not comment:
  18419. line = line.strip() # noqa: PLW2901
  18420. if not line:
  18421. continue
  18422. if line[0] == '[':
  18423. if line[-5:] == ' End]':
  18424. # close section
  18425. del sections[-1]
  18426. section = sections[-1]
  18427. name = line[1:-5]
  18428. if comment:
  18429. section[name] = '\n'.join(section[name])
  18430. if name[:4] == 'LUT ':
  18431. a = numpy.array(section[name], dtype=numpy.uint8)
  18432. section[name] = a.reshape((-1, 3))
  18433. continue
  18434. # new section
  18435. comment = False
  18436. name = line[1:-1]
  18437. if name[:4] == 'LUT ':
  18438. section = []
  18439. elif name in ignoresections:
  18440. section = []
  18441. comment = True
  18442. else:
  18443. section = {}
  18444. sections.append(section)
  18445. result[name] = section
  18446. continue
  18447. # add entry
  18448. if comment:
  18449. section.append(line)
  18450. continue
  18451. lines = line.split('=', 1)
  18452. if len(line) == 1:
  18453. section[lines[0].strip()] = None
  18454. continue
  18455. key, value = lines
  18456. if key[:4] == 'RGB ':
  18457. section.extend(int(rgb) for rgb in value.split())
  18458. else:
  18459. section[key.strip()] = astype(value.strip())
  18460. return result
  18461. def pilatus_description_metadata(description: str, /) -> dict[str, Any]:
  18462. """Return metadata from Pilatus image description.
  18463. Return metadata from Pilatus pixel array detectors by Dectris, created
  18464. by camserver or TVX software.
  18465. >>> pilatus_description_metadata('# Pixel_size 172e-6 m x 172e-6 m')
  18466. {'Pixel_size': (0.000172, 0.000172)}
  18467. """
  18468. result: dict[str, Any] = {}
  18469. values: Any
  18470. if not description.startswith('# '):
  18471. return result
  18472. for c in '#:=,()':
  18473. description = description.replace(c, ' ')
  18474. for lines in description.split('\n'):
  18475. if lines[:2] != ' ':
  18476. continue
  18477. line = lines.split()
  18478. name = line[0]
  18479. if line[0] not in TIFF.PILATUS_HEADER:
  18480. try:
  18481. result['DateTime'] = strptime(
  18482. ' '.join(line), '%Y-%m-%dT%H %M %S.%f'
  18483. )
  18484. except Exception:
  18485. result[name] = ' '.join(line[1:])
  18486. continue
  18487. indices, dtype = TIFF.PILATUS_HEADER[line[0]]
  18488. if isinstance(indices[0], slice):
  18489. # assumes one slice
  18490. values = line[indices[0]]
  18491. else:
  18492. values = [line[i] for i in indices]
  18493. if dtype is float and values[0] == 'not':
  18494. values = ['NaN']
  18495. values = tuple(dtype(v) for v in values)
  18496. if dtype is str:
  18497. values = ' '.join(values)
  18498. elif len(values) == 1:
  18499. values = values[0]
  18500. result[name] = values
  18501. return result
  18502. def svs_description_metadata(description: str, /) -> dict[str, Any]:
  18503. """Return metadata from Aperio image description.
  18504. The Aperio image description format is unspecified. Expect failures.
  18505. >>> svs_description_metadata('Aperio Image Library v1.0')
  18506. {'Header': 'Aperio Image Library v1.0'}
  18507. """
  18508. if not description.startswith('Aperio '):
  18509. raise ValueError('invalid Aperio image description')
  18510. result = {}
  18511. items = description.split('|')
  18512. result['Header'] = items[0]
  18513. for item in items[1:]:
  18514. try:
  18515. key, value = item.split('=', maxsplit=1)
  18516. except ValueError:
  18517. # empty item or missing '='
  18518. continue
  18519. result[key.strip()] = astype(value.strip())
  18520. return result
  18521. def stk_description_metadata(description: str, /) -> list[dict[str, Any]]:
  18522. """Return metadata from MetaMorph image description.
  18523. The MetaMorph image description format is unspecified. Expect failures.
  18524. """
  18525. description = description.strip()
  18526. if not description:
  18527. return []
  18528. # try:
  18529. # description = bytes2str(description)
  18530. # except UnicodeDecodeError as exc:
  18531. # logger().warning(
  18532. # '<tifffile.stk_description_metadata> raised {exc!r:.128}'
  18533. # )
  18534. # return []
  18535. result = []
  18536. for plane in description.split('\x00'):
  18537. d = {}
  18538. for line in plane.split('\r\n'):
  18539. lines = line.split(':', 1)
  18540. if len(lines) > 1:
  18541. name, value = lines
  18542. d[name.strip()] = astype(value.strip())
  18543. else:
  18544. value = lines[0].strip()
  18545. if value:
  18546. if '' in d:
  18547. d[''].append(value)
  18548. else:
  18549. d[''] = [value]
  18550. result.append(d)
  18551. return result
  18552. def metaseries_description_metadata(description: str, /) -> dict[str, Any]:
  18553. """Return metadata from MetaSeries image description."""
  18554. if not description.startswith('<MetaData>'):
  18555. raise ValueError('invalid MetaSeries image description')
  18556. import uuid
  18557. from xml.etree import ElementTree
  18558. root = ElementTree.fromstring(description)
  18559. types: dict[str, Callable[..., Any]] = {
  18560. 'float': float,
  18561. 'int': int,
  18562. 'bool': lambda x: asbool(x, 'on', 'off'),
  18563. 'time': lambda x: strptime(x, '%Y%m%d %H:%M:%S.%f'),
  18564. 'guid': uuid.UUID,
  18565. # 'float-array':
  18566. # 'colorref':
  18567. }
  18568. def parse(
  18569. root: ElementTree.Element, result: dict[str, Any], /
  18570. ) -> dict[str, Any]:
  18571. # recursive
  18572. for child in root:
  18573. attrib = child.attrib
  18574. if not attrib:
  18575. result[child.tag] = parse(child, {})
  18576. continue
  18577. if 'id' in attrib:
  18578. i = attrib['id']
  18579. t = attrib['type']
  18580. v = attrib['value']
  18581. if t in types:
  18582. try:
  18583. result[i] = types[t](v)
  18584. except Exception:
  18585. result[i] = v
  18586. else:
  18587. result[i] = v
  18588. return result
  18589. adict = parse(root, {})
  18590. if 'Description' in adict:
  18591. adict['Description'] = adict['Description'].replace('&#13;&#10;', '\n')
  18592. return adict
  18593. def scanimage_description_metadata(description: str, /) -> Any:
  18594. """Return metadata from ScanImage image description."""
  18595. return matlabstr2py(description)
  18596. def scanimage_artist_metadata(artist: str, /) -> dict[str, Any] | None:
  18597. """Return metadata from ScanImage artist tag."""
  18598. try:
  18599. return json.loads(artist)
  18600. except ValueError as exc:
  18601. logger().warning(
  18602. f'<tifffile.scanimage_artist_metadata> raised {exc!r:.128}'
  18603. )
  18604. return None
  18605. def olympus_ini_metadata(inistr: str, /) -> dict[str, Any]:
  18606. """Return OlympusSIS metadata from INI string.
  18607. No specification is available.
  18608. """
  18609. def keyindex(key: str, /) -> tuple[str, int]:
  18610. # split key into name and index
  18611. index = 0
  18612. i = len(key.rstrip('0123456789'))
  18613. if i < len(key):
  18614. index = int(key[i:]) - 1
  18615. key = key[:i]
  18616. return key, index
  18617. value: Any
  18618. result: dict[str, Any] = {}
  18619. bands: list[dict[str, Any]] = []
  18620. zpos: list[Any] | None = None
  18621. tpos: list[Any] | None = None
  18622. for line in inistr.splitlines():
  18623. line = line.strip() # noqa: PLW2901
  18624. if line == '' or line[0] == ';':
  18625. continue
  18626. if line[0] == '[' and line[-1] == ']':
  18627. section_name = line[1:-1]
  18628. result[section_name] = section = {}
  18629. if section_name == 'Dimension':
  18630. result['axes'] = axes = []
  18631. result['shape'] = shape = []
  18632. elif section_name == 'ASD':
  18633. result[section_name] = []
  18634. elif section_name == 'Z':
  18635. if 'Dimension' in result:
  18636. result[section_name]['ZPos'] = zpos = []
  18637. elif section_name == 'Time':
  18638. if 'Dimension' in result:
  18639. result[section_name]['TimePos'] = tpos = []
  18640. elif section_name == 'Band':
  18641. nbands = result['Dimension']['Band']
  18642. bands = [{'LUT': []} for _ in range(nbands)]
  18643. result[section_name] = bands
  18644. iband = 0
  18645. else:
  18646. key, value = line.split('=')
  18647. if value.strip() == '':
  18648. value = None
  18649. elif ',' in value:
  18650. value = tuple(astype(v) for v in value.split(','))
  18651. else:
  18652. value = astype(value)
  18653. if section_name == 'Dimension':
  18654. section[key] = value
  18655. axes.append(key)
  18656. shape.append(value)
  18657. elif section_name == 'ASD':
  18658. if key == 'Count':
  18659. result['ASD'] = [{}] * value
  18660. else:
  18661. key, index = keyindex(key)
  18662. result['ASD'][index][key] = value
  18663. elif section_name == 'Band':
  18664. if key[:3] == 'LUT':
  18665. lut = bands[iband]['LUT']
  18666. value = struct.pack('<I', value)
  18667. lut.append(
  18668. [ord(value[0:1]), ord(value[1:2]), ord(value[2:3])]
  18669. )
  18670. else:
  18671. key, iband = keyindex(key)
  18672. bands[iband][key] = value
  18673. elif key[:4] == 'ZPos' and zpos is not None:
  18674. zpos.append(value)
  18675. elif key[:7] == 'TimePos' and tpos is not None:
  18676. tpos.append(value)
  18677. else:
  18678. section[key] = value
  18679. if 'axes' in result:
  18680. sisaxes = {'Band': 'C'}
  18681. axes = []
  18682. shape = []
  18683. for i, x in zip(result['shape'], result['axes'], strict=True):
  18684. if i > 1:
  18685. axes.append(sisaxes.get(x, x[0].upper()))
  18686. shape.append(i)
  18687. result['axes'] = ''.join(axes)
  18688. result['shape'] = tuple(shape)
  18689. try:
  18690. result['Z']['ZPos'] = numpy.array(
  18691. result['Z']['ZPos'][: result['Dimension']['Z']], numpy.float64
  18692. )
  18693. except (TypeError, ValueError):
  18694. pass
  18695. try:
  18696. result['Time']['TimePos'] = numpy.array(
  18697. result['Time']['TimePos'][: result['Dimension']['Time']],
  18698. numpy.int32,
  18699. )
  18700. except (TypeError, ValueError):
  18701. pass
  18702. for band in bands:
  18703. band['LUT'] = numpy.array(band['LUT'], numpy.uint8)
  18704. return result
  18705. def astrotiff_description_metadata(
  18706. description: str, /, sep: str = ':'
  18707. ) -> dict[str, Any]:
  18708. """Return metadata from AstroTIFF image description."""
  18709. logmsg = '<tifffile.astrotiff_description_metadata> '
  18710. counts: dict[str, int] = {}
  18711. result: dict[str, Any] = {}
  18712. value: Any
  18713. for line in description.splitlines():
  18714. line = line.strip() # noqa: PLW2901
  18715. if not line:
  18716. continue
  18717. key = line[:8].strip()
  18718. value = line[8:]
  18719. if not value.startswith('='):
  18720. # for example, COMMENT or HISTORY
  18721. if key + f'{sep}0' not in result:
  18722. result[key + f'{sep}0'] = value
  18723. counts[key] = 1
  18724. else:
  18725. result[key + f'{sep}{counts[key]}'] = value
  18726. counts[key] += 1
  18727. continue
  18728. value = value[1:]
  18729. if '/' in value:
  18730. value, comment = value.split('/', 1)
  18731. comment = comment.strip()
  18732. else:
  18733. comment = ''
  18734. value = value.strip()
  18735. if not value:
  18736. # undefined
  18737. value = None
  18738. elif value[0] == "'":
  18739. # string
  18740. if len(value) < 2:
  18741. logger().warning(logmsg + f'{key}: invalid string {value!r}')
  18742. continue
  18743. if value[-1] == "'":
  18744. value = value[1:-1]
  18745. else:
  18746. # string containing '/'
  18747. if not ("'" in comment and '/' in comment):
  18748. logger().warning(
  18749. logmsg + f'{key}: invalid string {value!r}'
  18750. )
  18751. continue
  18752. value, comment = line[9:].strip()[1:].split("'", 1)
  18753. comment = comment.split('/', 1)[-1].strip()
  18754. # TODO: string containing single quote '
  18755. elif value[0] == '(' and value[-1] == ')':
  18756. # complex number
  18757. value = value[1:-1]
  18758. dtype = float if '.' in value else int
  18759. value = tuple(dtype(v.strip()) for v in value.split(','))
  18760. elif value == 'T':
  18761. value = True
  18762. elif value == 'F':
  18763. value = False
  18764. elif '.' in value:
  18765. value = float(value)
  18766. else:
  18767. try:
  18768. value = int(value)
  18769. except Exception:
  18770. logger().warning(logmsg + f'{key}: invalid value {value!r}')
  18771. continue
  18772. if key in result:
  18773. logger().warning(logmsg + f'{key}: duplicate key')
  18774. result[key] = value
  18775. if comment:
  18776. result[key + f'{sep}COMMENT'] = comment
  18777. if comment[0] == '[' and ']' in comment:
  18778. result[key + f'{sep}UNIT'] = comment[1:].split(']', 1)[0]
  18779. return result
  18780. def streak_description_metadata(
  18781. description: str, fh: FileHandle, /
  18782. ) -> dict[str, Any]:
  18783. """Return metadata from Hamamatsu streak image description."""
  18784. section_pattern = re.compile(
  18785. r'\[([a-zA-Z0-9 _\-\.]+)\],([^\[]*)', re.DOTALL
  18786. )
  18787. properties_pattern = re.compile(
  18788. r'([a-zA-Z0-9 _\-\.]+)=(\"[^\"]*\"|[\+\-0-9\.]+|[^,]*)'
  18789. )
  18790. result: dict[str, Any] = {}
  18791. for section, values in section_pattern.findall(description.strip()):
  18792. properties = {}
  18793. for item in properties_pattern.findall(values):
  18794. key, value = item
  18795. value = value.strip()
  18796. if not value or value == '"':
  18797. value = None
  18798. elif value[0] == '"' and value[-1] == '"':
  18799. value = value[1:-1]
  18800. if ',' in value:
  18801. try:
  18802. value = tuple(
  18803. (
  18804. float(v)
  18805. if '.' in value
  18806. else int(v[1:] if v[0] == '#' else v)
  18807. )
  18808. for v in value.split(',')
  18809. )
  18810. except ValueError:
  18811. pass
  18812. elif '.' in value:
  18813. try:
  18814. value = float(value)
  18815. except ValueError:
  18816. pass
  18817. else:
  18818. try:
  18819. value = int(value)
  18820. except ValueError:
  18821. pass
  18822. properties[key] = value
  18823. result[section] = properties
  18824. if not fh.closed:
  18825. pos = fh.tell()
  18826. for scaling in ('ScalingXScaling', 'ScalingYScaling'):
  18827. try:
  18828. offset, count = result['Scaling'][scaling + 'File']
  18829. fh.seek(offset)
  18830. result['Scaling'][scaling] = fh.read_array(
  18831. dtype='<f4', count=count
  18832. )
  18833. except Exception: # noqa: S110
  18834. pass
  18835. fh.seek(pos)
  18836. return result
  18837. def eer_xml_metadata(xmlstr: str, /) -> dict[str, Any]:
  18838. """Return metadata from EER XML tag values."""
  18839. from xml.etree import ElementTree
  18840. value: Any
  18841. root = ElementTree.fromstring(xmlstr)
  18842. result = {}
  18843. for item in root.findall('./item'):
  18844. key = item.attrib['name']
  18845. value = item.text
  18846. if value is None:
  18847. continue
  18848. if value == 'Yes':
  18849. value = True
  18850. elif value == 'No':
  18851. value = False
  18852. elif key == 'timestamp':
  18853. try:
  18854. # ISO 8601
  18855. value = DateTime.fromisoformat(value)
  18856. except (TypeError, ValueError):
  18857. pass
  18858. else:
  18859. value = astype(value)
  18860. result[key] = value
  18861. if 'unit' in item.attrib:
  18862. result[key + '.unit'] = item.attrib['unit']
  18863. return result
  18864. def unpack_rgb(
  18865. data: bytes,
  18866. /,
  18867. dtype: DTypeLike | None = None,
  18868. bitspersample: tuple[int, ...] | None = None,
  18869. *,
  18870. rescale: bool = True,
  18871. ) -> NDArray[Any]:
  18872. """Return array from bytes containing packed samples.
  18873. Use to unpack RGB565 or RGB555 to RGB888 format.
  18874. Works on little-endian platforms only.
  18875. Parameters:
  18876. data:
  18877. Bytes to be decoded.
  18878. Samples in each pixel are stored consecutively.
  18879. Pixels are aligned to 8, 16, or 32 bit boundaries.
  18880. dtype:
  18881. Data type of samples.
  18882. The byte order applies also to the data stream.
  18883. bitspersample:
  18884. Number of bits for each sample in pixel.
  18885. rescale:
  18886. Upscale samples to number of bits in dtype.
  18887. Returns:
  18888. Flattened array of unpacked samples of native dtype.
  18889. Examples:
  18890. >>> data = struct.pack('BBBB', 0x21, 0x08, 0xFF, 0xFF)
  18891. >>> print(unpack_rgb(data, '<B', (5, 6, 5), rescale=False))
  18892. [ 1 1 1 31 63 31]
  18893. >>> print(unpack_rgb(data, '<B', (5, 6, 5)))
  18894. [ 8 4 8 255 255 255]
  18895. >>> print(unpack_rgb(data, '<B', (5, 5, 5)))
  18896. [ 16 8 8 255 255 255]
  18897. """
  18898. if bitspersample is None:
  18899. bitspersample = (5, 6, 5)
  18900. if dtype is None:
  18901. dtype = '<B'
  18902. dtype = numpy.dtype(dtype)
  18903. bits = int(numpy.sum(bitspersample))
  18904. if not (
  18905. bits <= 32 and all(i <= dtype.itemsize * 8 for i in bitspersample)
  18906. ):
  18907. raise ValueError(f'sample size not supported: {bitspersample}')
  18908. dt = next(i for i in 'BHI' if numpy.dtype(i).itemsize * 8 >= bits)
  18909. data_array = numpy.frombuffer(data, dtype.byteorder + dt)
  18910. result = numpy.empty((data_array.size, len(bitspersample)), dtype.char)
  18911. for i, bps in enumerate(bitspersample):
  18912. t = data_array >> int(numpy.sum(bitspersample[i + 1 :]))
  18913. t &= int('0b' + '1' * bps, 2)
  18914. if rescale:
  18915. o = ((dtype.itemsize * 8) // bps + 1) * bps
  18916. if o > data_array.dtype.itemsize * 8:
  18917. t = t.astype('I')
  18918. t *= (2**o - 1) // (2**bps - 1)
  18919. t //= 2 ** (o - (dtype.itemsize * 8))
  18920. result[:, i] = t
  18921. return result.reshape(-1)
  18922. def apply_colormap(
  18923. image: NDArray[Any], colormap: NDArray[Any], /, *, contig: bool = True
  18924. ) -> NDArray[Any]:
  18925. """Return palette-colored image.
  18926. The image array values are used to index the colormap on axis 1.
  18927. The returned image array is of shape `image.shape+colormap.shape[0]`
  18928. and dtype `colormap.dtype`.
  18929. Parameters:
  18930. image:
  18931. Array of indices into colormap.
  18932. colormap:
  18933. RGB lookup table aka palette of shape `(3, 2**bitspersample)`.
  18934. contig:
  18935. Return contiguous array.
  18936. Examples:
  18937. >>> import numpy
  18938. >>> im = numpy.arange(256, dtype='uint8')
  18939. >>> colormap = numpy.vstack([im, im, im]).astype('uint16') * 256
  18940. >>> apply_colormap(im, colormap)[-1]
  18941. array([65280, 65280, 65280], dtype=uint16)
  18942. """
  18943. image = numpy.take(colormap, image, axis=1)
  18944. image = numpy.rollaxis(image, 0, image.ndim)
  18945. if contig:
  18946. image = numpy.ascontiguousarray(image)
  18947. return image
  18948. def parse_filenames(
  18949. files: Sequence[str],
  18950. /,
  18951. pattern: str | None = None,
  18952. axesorder: Sequence[int] | None = None,
  18953. categories: dict[str, dict[str, int]] | None = None,
  18954. *,
  18955. _shape: Sequence[int] | None = None,
  18956. ) -> tuple[
  18957. tuple[str, ...], tuple[int, ...], list[tuple[int, ...]], Sequence[str]
  18958. ]:
  18959. r"""Return shape and axes from sequence of file names matching pattern.
  18960. Parameters:
  18961. files:
  18962. Sequence of file names to parse.
  18963. pattern:
  18964. Regular expression pattern matching axes names and chunk indices
  18965. in file names.
  18966. By default, no pattern matching is performed.
  18967. Axes names can be specified by matching groups preceding the index
  18968. groups in the file name, be provided as group names for the index
  18969. groups, or be omitted.
  18970. The predefined 'axes' pattern matches Olympus OIF and Leica TIFF
  18971. series.
  18972. axesorder:
  18973. Indices of axes in pattern. By default, axes are returned in the
  18974. order they appear in pattern.
  18975. categories:
  18976. Map of index group matches to integer indices.
  18977. `{'axislabel': {'category': index}}`
  18978. _shape:
  18979. Shape of file sequence. The default is
  18980. `maximum - minimum + 1` of the parsed indices for each dimension.
  18981. Returns:
  18982. - Axes names for each dimension.
  18983. - Shape of file series.
  18984. - Index of each file in shape.
  18985. - Filtered sequence of file names.
  18986. Examples:
  18987. >>> parse_filenames(
  18988. ... ['c1001.ext', 'c2002.ext'], r'([^\d])(\d)(?P<t>\d+)\.ext'
  18989. ... )
  18990. (('c', 't'), (2, 2), [(0, 0), (1, 1)], ['c1001.ext', 'c2002.ext'])
  18991. """
  18992. # TODO: add option to filter files that do not match pattern
  18993. shape = None if _shape is None else tuple(_shape)
  18994. if pattern is None:
  18995. if shape is not None and (len(shape) != 1 or shape[0] < len(files)):
  18996. raise ValueError(
  18997. f'shape {(len(files),)} does not fit provided shape {shape}'
  18998. )
  18999. return (
  19000. ('I',),
  19001. (len(files),),
  19002. [(i,) for i in range(len(files))],
  19003. files,
  19004. )
  19005. pattern = TIFF.FILE_PATTERNS.get(pattern, pattern)
  19006. if not pattern:
  19007. raise ValueError('invalid pattern')
  19008. pattern_compiled: Any
  19009. if isinstance(pattern, str):
  19010. pattern_compiled = re.compile(pattern)
  19011. elif hasattr(pattern, 'groupindex'):
  19012. pattern_compiled = pattern
  19013. else:
  19014. raise ValueError('invalid pattern')
  19015. if categories is None:
  19016. categories = {}
  19017. def parse(fname: str, /) -> tuple[tuple[str, ...], tuple[int, ...]]:
  19018. # return axes names and indices from file name
  19019. assert categories is not None
  19020. dims: list[str] = []
  19021. indices: list[int] = []
  19022. groupindex = {v: k for k, v in pattern_compiled.groupindex.items()}
  19023. matches = pattern_compiled.search(fname)
  19024. if matches is None:
  19025. raise ValueError(f'pattern does not match file name {fname!r}')
  19026. ax = None
  19027. for i, match in enumerate(matches.groups()):
  19028. m = match
  19029. if m is None:
  19030. continue
  19031. if i + 1 in groupindex:
  19032. ax = groupindex[i + 1]
  19033. elif m[0].isalpha():
  19034. ax = m # axis label for next index
  19035. continue
  19036. if ax is None:
  19037. ax = 'Q' # no preceding axis letter
  19038. try:
  19039. m = int(categories[ax][m] if ax in categories else m)
  19040. except Exception as exc:
  19041. raise ValueError(f'invalid index {m!r}') from exc
  19042. indices.append(m)
  19043. dims.append(ax)
  19044. ax = None
  19045. return tuple(dims), tuple(indices)
  19046. normpaths = [os.path.normpath(f) for f in files]
  19047. if len(normpaths) == 1:
  19048. prefix_str = os.path.dirname(normpaths[0])
  19049. else:
  19050. prefix_str = os.path.commonpath(normpaths)
  19051. prefix = len(prefix_str)
  19052. dims: tuple[str, ...] | None = None
  19053. indices: list[tuple[int, ...]] = []
  19054. for fname in normpaths:
  19055. lbl, idx = parse(fname[prefix:])
  19056. if dims is None:
  19057. dims = lbl
  19058. if axesorder is not None and (
  19059. len(axesorder) != len(dims)
  19060. or any(i not in axesorder for i in range(len(dims)))
  19061. ):
  19062. raise ValueError(
  19063. f'invalid axesorder {axesorder!r} for {dims!r}'
  19064. )
  19065. elif dims != lbl:
  19066. raise ValueError('dims do not match within image sequence')
  19067. if axesorder is not None:
  19068. idx = tuple(idx[i] for i in axesorder)
  19069. indices.append(idx)
  19070. assert dims is not None
  19071. if axesorder is not None:
  19072. dims = tuple(dims[i] for i in axesorder)
  19073. # determine shape
  19074. indices_array = numpy.array(indices, dtype=numpy.intp)
  19075. parsedshape = numpy.max(indices, axis=0)
  19076. if shape is None:
  19077. startindex = numpy.min(indices_array, axis=0)
  19078. indices_array -= startindex
  19079. parsedshape -= startindex
  19080. parsedshape += 1
  19081. shape = tuple(int(i) for i in parsedshape.tolist())
  19082. elif len(parsedshape) != len(shape) or any(
  19083. i > j for i, j in zip(shape, parsedshape, strict=True)
  19084. ):
  19085. raise ValueError(
  19086. f'parsed shape {parsedshape} does not fit provided shape {shape}'
  19087. )
  19088. indices_list: list[list[int]] = indices_array.tolist()
  19089. indices = [tuple(index) for index in indices_list]
  19090. return dims, shape, indices, files
  19091. def iter_images(data: NDArray[Any], /) -> Iterator[NDArray[Any]]:
  19092. """Return iterator over pages in data array of normalized shape."""
  19093. yield from data
  19094. def iter_strips(
  19095. pageiter: Iterator[NDArray[Any] | None],
  19096. shape: tuple[int, ...],
  19097. dtype: numpy.dtype[Any],
  19098. rowsperstrip: int,
  19099. /,
  19100. ) -> Iterator[NDArray[Any]]:
  19101. """Return iterator over strips in pages."""
  19102. numstrips = (shape[-3] + rowsperstrip - 1) // rowsperstrip
  19103. for iteritem in pageiter:
  19104. if iteritem is None:
  19105. # for _ in range(numstrips):
  19106. # yield None
  19107. # continue
  19108. pagedata = numpy.zeros(shape, dtype)
  19109. else:
  19110. pagedata = iteritem.reshape(shape)
  19111. for plane in pagedata:
  19112. for depth in plane:
  19113. for i in range(numstrips):
  19114. yield depth[i * rowsperstrip : (i + 1) * rowsperstrip]
  19115. def iter_tiles(
  19116. data: NDArray[Any],
  19117. tile: tuple[int, ...],
  19118. tiles: tuple[int, ...],
  19119. /,
  19120. ) -> Iterator[NDArray[Any]]:
  19121. """Return iterator over full tiles in data array of normalized shape.
  19122. Tiles are zero-padded if necessary.
  19123. """
  19124. if not 1 < len(tile) < 4 or len(tile) != len(tiles):
  19125. raise ValueError('invalid tile or tiles shape')
  19126. chunkshape = (*tile, data.shape[-1])
  19127. chunksize = product(chunkshape)
  19128. dtype = data.dtype
  19129. sz, sy, sx = data.shape[2:5]
  19130. if len(tile) == 2:
  19131. y, x = tile
  19132. for page in data:
  19133. for plane in page:
  19134. for iy in range(tiles[0]):
  19135. ty = iy * y
  19136. cy = min(y, sy - ty)
  19137. for ix in range(tiles[1]):
  19138. tx = ix * x
  19139. cx = min(x, sx - tx)
  19140. chunk = plane[0, ty : ty + cy, tx : tx + cx]
  19141. if chunk.size != chunksize:
  19142. chunk_ = numpy.zeros(chunkshape, dtype)
  19143. chunk_[:cy, :cx] = chunk
  19144. chunk = chunk_
  19145. yield chunk
  19146. else:
  19147. z, y, x = tile
  19148. for page in data:
  19149. for plane in page:
  19150. for iz in range(tiles[0]):
  19151. tz = iz * z
  19152. cz = min(z, sz - tz)
  19153. for iy in range(tiles[1]):
  19154. ty = iy * y
  19155. cy = min(y, sy - ty)
  19156. for ix in range(tiles[2]):
  19157. tx = ix * x
  19158. cx = min(x, sx - tx)
  19159. chunk = plane[
  19160. tz : tz + cz, ty : ty + cy, tx : tx + cx
  19161. ]
  19162. if chunk.size != chunksize:
  19163. chunk_ = numpy.zeros(chunkshape, dtype)
  19164. chunk_[:cz, :cy, :cx] = chunk
  19165. chunk = chunk_
  19166. yield chunk[0] if z == 1 else chunk
  19167. def encode_chunks(
  19168. numchunks: int,
  19169. chunkiter: Iterator[NDArray[Any] | None],
  19170. encode: Callable[[NDArray[Any]], bytes],
  19171. shape: Sequence[int],
  19172. dtype: numpy.dtype[Any],
  19173. maxworkers: int | None,
  19174. buffersize: int | None,
  19175. tiled: bool, # noqa: FBT001
  19176. /,
  19177. ) -> Iterator[bytes]:
  19178. """Return iterator over encoded chunks."""
  19179. if numchunks <= 0:
  19180. return
  19181. chunksize = product(shape) * dtype.itemsize
  19182. if tiled:
  19183. # pad tiles
  19184. def func(chunk: NDArray[Any] | None, /) -> bytes:
  19185. if chunk is None:
  19186. return b''
  19187. chunk = numpy.ascontiguousarray(chunk, dtype)
  19188. if chunk.nbytes != chunksize:
  19189. # if chunk.dtype != dtype:
  19190. # raise ValueError('dtype of chunk does not match data')
  19191. pad = tuple(
  19192. (0, i - j)
  19193. for i, j in zip(shape, chunk.shape, strict=False)
  19194. )
  19195. chunk = numpy.pad(chunk, pad)
  19196. return encode(chunk)
  19197. else:
  19198. # strips
  19199. def func(chunk: NDArray[Any] | None, /) -> bytes:
  19200. if chunk is None:
  19201. return b''
  19202. chunk = numpy.ascontiguousarray(chunk, dtype)
  19203. return encode(chunk)
  19204. if maxworkers is None or maxworkers < 2 or numchunks < 2:
  19205. for _ in range(numchunks):
  19206. chunk = next(chunkiter)
  19207. # assert chunk is None or isinstance(chunk, numpy.ndarray)
  19208. yield func(chunk)
  19209. del chunk
  19210. return
  19211. # because ThreadPoolExecutor.map is not collecting items lazily, reduce
  19212. # memory overhead by processing chunks iterator maxchunks items at a time
  19213. if buffersize is None:
  19214. buffersize = TIFF.BUFFERSIZE * 2
  19215. maxchunks = max(maxworkers, buffersize // chunksize)
  19216. if numchunks <= maxchunks:
  19217. def chunks() -> Iterator[NDArray[Any] | None]:
  19218. for _ in range(numchunks):
  19219. chunk = next(chunkiter)
  19220. # assert chunk is None or isinstance(chunk, numpy.ndarray)
  19221. yield chunk
  19222. del chunk
  19223. with ThreadPoolExecutor(maxworkers) as executor:
  19224. yield from executor.map(func, chunks())
  19225. return
  19226. with ThreadPoolExecutor(maxworkers) as executor:
  19227. count = 1
  19228. chunk_list = []
  19229. for _ in range(numchunks):
  19230. chunk = next(chunkiter)
  19231. if chunk is not None:
  19232. count += 1
  19233. # assert chunk is None or isinstance(chunk, numpy.ndarray)
  19234. chunk_list.append(chunk)
  19235. if count == maxchunks:
  19236. yield from executor.map(func, chunk_list)
  19237. chunk_list.clear()
  19238. count = 0
  19239. if chunk_list:
  19240. yield from executor.map(func, chunk_list)
  19241. def reorient(
  19242. image: NDArray[Any], orientation: ORIENTATION | int | str, /
  19243. ) -> NDArray[Any]:
  19244. """Return reoriented view of image array.
  19245. Parameters:
  19246. image:
  19247. Non-squeezed output of `asarray` functions.
  19248. Axes -3 and -2 must be image length and width respectively.
  19249. orientation:
  19250. Value of Orientation tag.
  19251. """
  19252. orientation = cast(ORIENTATION, enumarg(ORIENTATION, orientation))
  19253. if orientation == ORIENTATION.TOPLEFT:
  19254. return image
  19255. if orientation == ORIENTATION.TOPRIGHT:
  19256. return image[..., ::-1, :]
  19257. if orientation == ORIENTATION.BOTLEFT:
  19258. return image[..., ::-1, :, :]
  19259. if orientation == ORIENTATION.BOTRIGHT:
  19260. return image[..., ::-1, ::-1, :]
  19261. if orientation == ORIENTATION.LEFTTOP:
  19262. return numpy.swapaxes(image, -3, -2)
  19263. if orientation == ORIENTATION.RIGHTTOP:
  19264. return numpy.swapaxes(image, -3, -2)[..., ::-1, :]
  19265. if orientation == ORIENTATION.RIGHTBOT:
  19266. return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :]
  19267. if orientation == ORIENTATION.LEFTBOT:
  19268. return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :]
  19269. return image
  19270. def repeat_nd(a: ArrayLike, repeats: Sequence[int], /) -> NDArray[Any]:
  19271. """Return read-only view into input array with elements repeated.
  19272. Zoom image array by integer factors using nearest neighbor interpolation
  19273. (box filter).
  19274. Parameters:
  19275. a: Input array.
  19276. repeats: Number of repetitions to apply along each dimension of input.
  19277. Examples:
  19278. >>> repeat_nd([[1, 2], [3, 4]], (2, 2))
  19279. array([[1, 1, 2, 2],
  19280. [1, 1, 2, 2],
  19281. [3, 3, 4, 4],
  19282. [3, 3, 4, 4]])
  19283. """
  19284. reshape: list[int] = []
  19285. shape: list[int] = []
  19286. strides: list[int] = []
  19287. a = numpy.asarray(a)
  19288. for i, j, k in zip(a.strides, a.shape, repeats, strict=True):
  19289. shape.extend((j, k))
  19290. strides.extend((i, 0))
  19291. reshape.append(j * k)
  19292. return numpy.lib.stride_tricks.as_strided(
  19293. a, shape, strides, writeable=False
  19294. ).reshape(reshape)
  19295. @overload
  19296. def reshape_nd(
  19297. data_or_shape: tuple[int, ...], ndim: int, /
  19298. ) -> tuple[int, ...]: ...
  19299. @overload
  19300. def reshape_nd(data_or_shape: NDArray[Any], ndim: int, /) -> NDArray[Any]: ...
  19301. def reshape_nd(
  19302. data_or_shape: tuple[int, ...] | NDArray[Any], ndim: int, /
  19303. ) -> tuple[int, ...] | NDArray[Any]:
  19304. """Return image array or shape with at least `ndim` dimensions.
  19305. Prepend 1s to image shape as necessary.
  19306. >>> import numpy
  19307. >>> reshape_nd(numpy.empty(0), 1).shape
  19308. (0,)
  19309. >>> reshape_nd(numpy.empty(1), 2).shape
  19310. (1, 1)
  19311. >>> reshape_nd(numpy.empty((2, 3)), 3).shape
  19312. (1, 2, 3)
  19313. >>> reshape_nd(numpy.empty((3, 4, 5)), 3).shape
  19314. (3, 4, 5)
  19315. >>> reshape_nd((2, 3), 3)
  19316. (1, 2, 3)
  19317. """
  19318. if isinstance(data_or_shape, tuple):
  19319. shape = data_or_shape
  19320. else:
  19321. shape = data_or_shape.shape
  19322. if len(shape) >= ndim:
  19323. return data_or_shape
  19324. shape = (1,) * (ndim - len(shape)) + shape
  19325. if isinstance(data_or_shape, tuple):
  19326. return shape
  19327. return data_or_shape.reshape(shape)
  19328. @overload
  19329. def squeeze_axes(
  19330. shape: Sequence[int],
  19331. axes: str,
  19332. /,
  19333. skip: str | None = None,
  19334. ) -> tuple[tuple[int, ...], str, tuple[bool, ...]]: ...
  19335. @overload
  19336. def squeeze_axes(
  19337. shape: Sequence[int],
  19338. axes: Sequence[str],
  19339. /,
  19340. skip: Sequence[str] | None = None,
  19341. ) -> tuple[tuple[int, ...], Sequence[str], tuple[bool, ...]]: ...
  19342. def squeeze_axes(
  19343. shape: Sequence[int],
  19344. axes: str | Sequence[str],
  19345. /,
  19346. skip: str | Sequence[str] | None = None,
  19347. ) -> tuple[tuple[int, ...], str | Sequence[str], tuple[bool, ...]]:
  19348. """Return shape and axes with length-1 dimensions removed.
  19349. Remove unused dimensions unless their axes are listed in `skip`.
  19350. Parameters:
  19351. shape:
  19352. Sequence of dimension sizes.
  19353. axes:
  19354. Character codes for dimensions in `shape`.
  19355. skip:
  19356. Character codes for dimensions whose length-1 dimensions are
  19357. not removed. The default is 'XY'.
  19358. Returns:
  19359. shape:
  19360. Sequence of dimension sizes with length-1 dimensions removed.
  19361. axes:
  19362. Character codes for dimensions in output `shape`.
  19363. squeezed:
  19364. Dimensions were kept (True) or removed (False).
  19365. Examples:
  19366. >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC')
  19367. ((5, 2, 1), 'TYX', (True, False, True, True, False))
  19368. >>> squeeze_axes((1,), 'Q')
  19369. ((1,), 'Q', (True,))
  19370. """
  19371. if len(shape) != len(axes):
  19372. raise ValueError('dimensions of axes and shape do not match')
  19373. if not axes:
  19374. return tuple(shape), axes, ()
  19375. if skip is None:
  19376. skip = 'X', 'Y', 'width', 'height', 'length'
  19377. squeezed: list[bool] = []
  19378. shape_squeezed: list[int] = []
  19379. axes_squeezed: list[str] = []
  19380. for size, ax in zip(shape, axes, strict=True):
  19381. if size > 1 or ax in skip:
  19382. squeezed.append(True)
  19383. shape_squeezed.append(size)
  19384. axes_squeezed.append(ax)
  19385. else:
  19386. squeezed.append(False)
  19387. if len(shape_squeezed) == 0:
  19388. squeezed[-1] = True
  19389. shape_squeezed.append(shape[-1])
  19390. axes_squeezed.append(axes[-1])
  19391. if isinstance(axes, str):
  19392. axes = ''.join(axes_squeezed)
  19393. else:
  19394. axes = tuple(axes_squeezed)
  19395. return (tuple(shape_squeezed), axes, tuple(squeezed))
  19396. def transpose_axes(
  19397. image: NDArray[Any],
  19398. axes: str,
  19399. /,
  19400. asaxes: Sequence[str] | None = None,
  19401. ) -> NDArray[Any]:
  19402. """Return image array with its axes permuted to match specified axes.
  19403. Parameters:
  19404. image:
  19405. Image array to permute.
  19406. axes:
  19407. Character codes for dimensions in image array.
  19408. asaxes:
  19409. Character codes for dimensions in output image array.
  19410. The default is 'CTZYX'.
  19411. Returns:
  19412. Transposed image array.
  19413. A length-1 dimension is added for added dimensions.
  19414. A view of the input array is returned if possible.
  19415. Examples:
  19416. >>> import numpy
  19417. >>> transpose_axes(
  19418. ... numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX'
  19419. ... ).shape
  19420. (5, 2, 1, 3, 4)
  19421. """
  19422. if asaxes is None:
  19423. asaxes = 'CTZYX'
  19424. for ax in axes:
  19425. if ax not in asaxes:
  19426. raise ValueError(f'unknown axis {ax}')
  19427. # add missing axes to image
  19428. shape = image.shape
  19429. for ax in reversed(asaxes):
  19430. if ax not in axes:
  19431. axes = ax + axes
  19432. shape = (1, *shape)
  19433. image = image.reshape(shape)
  19434. # transpose axes
  19435. return image.transpose([axes.index(ax) for ax in asaxes])
  19436. @overload
  19437. def reshape_axes(
  19438. axes: str,
  19439. shape: Sequence[int],
  19440. newshape: Sequence[int],
  19441. /,
  19442. unknown: str | None = None,
  19443. ) -> str: ...
  19444. @overload
  19445. def reshape_axes(
  19446. axes: Sequence[str],
  19447. shape: Sequence[int],
  19448. newshape: Sequence[int],
  19449. /,
  19450. unknown: str | None = None,
  19451. ) -> Sequence[str]: ...
  19452. def reshape_axes(
  19453. axes: str | Sequence[str],
  19454. shape: Sequence[int],
  19455. newshape: Sequence[int],
  19456. /,
  19457. unknown: str | None = None,
  19458. ) -> str | Sequence[str]:
  19459. """Return axes matching new shape.
  19460. Parameters:
  19461. axes:
  19462. Character codes for dimensions in `shape`.
  19463. shape:
  19464. Input shape matching `axes`.
  19465. newshape:
  19466. Output shape matching output axes.
  19467. Size must match size of `shape`.
  19468. unknown:
  19469. Character used for new axes in output. The default is 'Q'.
  19470. Returns:
  19471. Character codes for dimensions in `newshape`.
  19472. Examples:
  19473. >>> reshape_axes('YXS', (219, 301, 1), (219, 301))
  19474. 'YX'
  19475. >>> reshape_axes('IYX', (12, 219, 301), (3, 4, 219, 1, 301, 1))
  19476. 'QQYQXQ'
  19477. """
  19478. shape = tuple(shape)
  19479. newshape = tuple(newshape)
  19480. if len(axes) != len(shape):
  19481. raise ValueError('axes do not match shape')
  19482. size = product(shape)
  19483. newsize = product(newshape)
  19484. if size != newsize:
  19485. raise ValueError(f'cannot reshape {shape} to {newshape}')
  19486. if not axes or not newshape:
  19487. return '' if isinstance(axes, str) else ()
  19488. lendiff = max(0, len(shape) - len(newshape))
  19489. if lendiff:
  19490. newshape = newshape + (1,) * lendiff
  19491. i = len(shape) - 1
  19492. prodns = 1
  19493. prods = 1
  19494. result = []
  19495. for ns in newshape[::-1]:
  19496. prodns *= ns
  19497. while i > 0 and shape[i] == 1 and ns != 1:
  19498. i -= 1
  19499. if ns == shape[i] and prodns == prods * shape[i]:
  19500. prods *= shape[i]
  19501. result.append(axes[i])
  19502. i -= 1
  19503. elif unknown:
  19504. result.append(unknown)
  19505. else:
  19506. unknown = 'Q'
  19507. result.append(unknown)
  19508. if isinstance(axes, str):
  19509. axes = ''.join(reversed(result[lendiff:]))
  19510. else:
  19511. axes = tuple(reversed(result[lendiff:]))
  19512. return axes
  19513. def order_axes(
  19514. indices: ArrayLike,
  19515. /,
  19516. *,
  19517. squeeze: bool = False,
  19518. ) -> tuple[int, ...]:
  19519. """Return order of axes sorted by variations in indices.
  19520. Parameters:
  19521. indices:
  19522. Multi-dimensional indices of chunks in array.
  19523. squeeze:
  19524. Remove length-1 dimensions of nonvarying axes.
  19525. Returns:
  19526. Order of axes sorted by variations in indices.
  19527. The axis with the least variations in indices is returned first,
  19528. the axis varying fastest is last.
  19529. Examples:
  19530. First axis varies fastest, second axis is squeezed:
  19531. >>> order_axes(
  19532. ... [(0, 2, 0), (1, 2, 0), (0, 2, 1), (1, 2, 1)], squeeze=True
  19533. ... )
  19534. (2, 0)
  19535. """
  19536. diff = numpy.sum(numpy.abs(numpy.diff(indices, axis=0)), axis=0).tolist()
  19537. order = tuple(sorted(range(len(diff)), key=diff.__getitem__))
  19538. if squeeze:
  19539. order = tuple(i for i in order if diff[i] != 0)
  19540. return order
  19541. def check_shape(
  19542. page_shape: Sequence[int], series_shape: Sequence[int]
  19543. ) -> bool:
  19544. """Return if page and series shapes are compatible."""
  19545. pi = product(page_shape)
  19546. pj = product(series_shape)
  19547. if pi == 0 and pj == 0:
  19548. return True
  19549. if pi == 0 or pj == 0:
  19550. return False
  19551. if pj % pi:
  19552. return False
  19553. series_shape = tuple(reversed(series_shape))
  19554. a = 0
  19555. pi = pj = 1
  19556. for i in reversed(page_shape):
  19557. pi *= i
  19558. # if a == len(series_shape):
  19559. # return not pj % pi
  19560. for j in series_shape[a:]:
  19561. a += 1
  19562. pj *= j
  19563. if i == j or pi == pj:
  19564. break
  19565. if j == 1:
  19566. continue
  19567. if pj != pi:
  19568. return False
  19569. return True
  19570. @overload
  19571. def subresolution(
  19572. a: TiffPage, b: TiffPage, /, p: int = 2, n: int = 16
  19573. ) -> int | None: ...
  19574. @overload
  19575. def subresolution(
  19576. a: TiffPageSeries, b: TiffPageSeries, /, p: int = 2, n: int = 16
  19577. ) -> int | None: ...
  19578. def subresolution(
  19579. a: TiffPage | TiffPageSeries,
  19580. b: TiffPage | TiffPageSeries,
  19581. /,
  19582. p: int = 2,
  19583. n: int = 16,
  19584. ) -> int | None:
  19585. """Return level of subresolution of series or page b vs a."""
  19586. if a.axes != b.axes or a.dtype != b.dtype:
  19587. return None
  19588. level = None
  19589. for ax, i, j in zip(a.axes.lower(), a.shape, b.shape, strict=True):
  19590. if ax in 'xyz':
  19591. if level is None:
  19592. for r in range(n):
  19593. d = p**r
  19594. if d > i:
  19595. return None
  19596. if abs((i / d) - j) < 1.0:
  19597. level = r
  19598. break
  19599. else:
  19600. return None
  19601. else:
  19602. d = p**level
  19603. if d > i:
  19604. return None
  19605. if abs((i / d) - j) >= 1.0:
  19606. return None
  19607. elif i != j:
  19608. return None
  19609. return level
  19610. def pyramidize_series(
  19611. series: list[TiffPageSeries], /, *, reduced: bool = False
  19612. ) -> None:
  19613. """Pyramidize list of TiffPageSeries in-place.
  19614. TiffPageSeries that are a subresolution of another TiffPageSeries are
  19615. appended to the other's TiffPageSeries levels and removed from the list.
  19616. Levels are to be ordered by size using the same downsampling factor.
  19617. TiffPageSeries of subifds cannot be pyramid top levels.
  19618. """
  19619. samplingfactors = (2, 3, 4)
  19620. i = 0
  19621. while i < len(series):
  19622. a = series[i]
  19623. p = None
  19624. j = i + 1
  19625. if a.keyframe.is_subifd:
  19626. # subifds cannot be pyramid top levels
  19627. i += 1
  19628. continue
  19629. while j < len(series):
  19630. b = series[j]
  19631. if reduced and not b.keyframe.is_reduced:
  19632. # pyramid levels must be reduced
  19633. j += 1
  19634. continue # not a pyramid level
  19635. if p is None:
  19636. for f in samplingfactors:
  19637. if subresolution(a.levels[-1], b, p=f) == 1:
  19638. p = f
  19639. break # not a pyramid level
  19640. else:
  19641. j += 1
  19642. continue # not a pyramid level
  19643. elif subresolution(a.levels[-1], b, p=p) != 1:
  19644. j += 1
  19645. continue
  19646. a.levels.append(b)
  19647. del series[j]
  19648. i += 1
  19649. def stack_pages(
  19650. pages: Sequence[TiffPage | TiffFrame | None],
  19651. /,
  19652. *,
  19653. tiled: TiledSequence | None = None,
  19654. lock: threading.RLock | NullContext | None = None,
  19655. maxworkers: int | None = None,
  19656. out: OutputType = None,
  19657. **kwargs: Any,
  19658. ) -> NDArray[Any]:
  19659. """Return vertically stacked image arrays from sequence of TIFF pages.
  19660. Parameters:
  19661. pages:
  19662. TIFF pages or frames to stack.
  19663. tiled:
  19664. Organize pages in non-overlapping grid.
  19665. lock:
  19666. Reentrant lock to synchronize seeks and reads from file.
  19667. maxworkers:
  19668. Maximum number of threads to concurrently decode pages or segments.
  19669. By default, use up to :py:attr:`_TIFF.MAXWORKERS` threads.
  19670. out:
  19671. Specifies how image array is returned.
  19672. By default, a new NumPy array is created.
  19673. If a *numpy.ndarray*, a writable array to which the images
  19674. are copied.
  19675. If a string or open file, the file used to create a memory-mapped
  19676. array.
  19677. **kwargs:
  19678. Additional arguments passed to :py:meth:`TiffPage.asarray`.
  19679. """
  19680. npages = len(pages)
  19681. if npages == 0:
  19682. raise ValueError('no pages')
  19683. if npages == 1:
  19684. kwargs['maxworkers'] = maxworkers
  19685. assert pages[0] is not None
  19686. return pages[0].asarray(out=out, **kwargs)
  19687. page0 = next(p.keyframe for p in pages if p is not None)
  19688. assert page0 is not None
  19689. shape = (npages, *page0.shape) if tiled is None else tiled.shape
  19690. dtype = page0.dtype
  19691. assert dtype is not None
  19692. out = create_output(out, shape, dtype)
  19693. # TODO: benchmark and optimize this
  19694. if maxworkers is None or maxworkers < 1:
  19695. # auto-detect
  19696. page_maxworkers = page0.maxworkers
  19697. maxworkers = min(npages, TIFF.MAXWORKERS)
  19698. if maxworkers == 1 or page_maxworkers < 1:
  19699. maxworkers = page_maxworkers = 1
  19700. elif npages < 3 or (
  19701. page_maxworkers <= 2
  19702. and page0.compression == 1
  19703. and page0.fillorder == 1
  19704. and page0.predictor == 1
  19705. ):
  19706. maxworkers = 1
  19707. else:
  19708. page_maxworkers = 1
  19709. elif maxworkers == 1:
  19710. maxworkers = page_maxworkers = 1
  19711. elif npages > maxworkers or page0.maxworkers < 2:
  19712. page_maxworkers = 1
  19713. else:
  19714. page_maxworkers = maxworkers
  19715. maxworkers = 1
  19716. kwargs['maxworkers'] = page_maxworkers
  19717. fh = page0.parent.filehandle
  19718. if lock is None:
  19719. haslock = fh.has_lock
  19720. if (not haslock and maxworkers > 1) or page_maxworkers > 1:
  19721. fh.set_lock(True)
  19722. lock = fh.lock
  19723. else:
  19724. haslock = True
  19725. filecache = FileCache(size=max(4, maxworkers), lock=lock)
  19726. if tiled is None:
  19727. def func(
  19728. page: TiffPage | TiffFrame | None,
  19729. index: int,
  19730. out: Any = out,
  19731. filecache: FileCache = filecache,
  19732. kwargs: dict[str, Any] = kwargs,
  19733. /,
  19734. ) -> None:
  19735. # read, decode, and copy page data
  19736. if page is None:
  19737. out[index].fill(0)
  19738. else:
  19739. filecache.open(page.parent.filehandle)
  19740. page.asarray(lock=lock, out=out[index], **kwargs)
  19741. filecache.close(page.parent.filehandle)
  19742. if maxworkers < 2:
  19743. for index, page in enumerate(pages):
  19744. func(page, index)
  19745. else:
  19746. page0.decode # noqa: B018 - init TiffPage.decode function
  19747. with ThreadPoolExecutor(maxworkers) as executor:
  19748. for _ in executor.map(func, pages, range(npages)):
  19749. pass
  19750. else:
  19751. # TODO: not used or tested
  19752. def func_tiled(
  19753. page: TiffPage | TiffFrame | None,
  19754. index: tuple[int | slice, ...],
  19755. out: Any = out,
  19756. filecache: FileCache = filecache,
  19757. kwargs: dict[str, Any] = kwargs,
  19758. /,
  19759. ) -> None:
  19760. # read, decode, and copy page data
  19761. if page is None:
  19762. out[index].fill(0)
  19763. else:
  19764. filecache.open(page.parent.filehandle)
  19765. out[index] = page.asarray(lock=lock, **kwargs)
  19766. filecache.close(page.parent.filehandle)
  19767. if maxworkers < 2:
  19768. for index_tiled, page in zip(tiled.slices(), pages, strict=True):
  19769. func_tiled(page, index_tiled)
  19770. else:
  19771. page0.decode # noqa: B018 - init TiffPage.decode function
  19772. with ThreadPoolExecutor(maxworkers) as executor:
  19773. for _ in executor.map(func_tiled, pages, tiled.slices()):
  19774. pass
  19775. filecache.clear()
  19776. if not haslock:
  19777. fh.set_lock(False)
  19778. return out
  19779. def create_output(
  19780. out: OutputType,
  19781. /,
  19782. shape: Sequence[int],
  19783. dtype: DTypeLike | None,
  19784. *,
  19785. mode: Literal['r+', 'w+', 'r', 'c'] = 'w+',
  19786. suffix: str | None = None,
  19787. fillvalue: float | None = None,
  19788. ) -> NDArray[Any] | numpy.memmap[Any, Any]:
  19789. """Return NumPy array where data of shape and dtype can be copied.
  19790. Parameters:
  19791. out:
  19792. Specifies kind of array of `shape` and `dtype` to return:
  19793. `None`:
  19794. Return new array.
  19795. `numpy.ndarray`:
  19796. Return view of existing array.
  19797. `'memmap'` or `'memmap:tempdir'`:
  19798. Return memory-map to array stored in temporary binary file.
  19799. `str` or open file:
  19800. Return memory-map to array stored in specified binary file.
  19801. shape:
  19802. Shape of array to return.
  19803. dtype:
  19804. Data type of array to return.
  19805. If `out` is an existing array, `dtype` must be castable to its
  19806. data type.
  19807. mode:
  19808. File mode to create memory-mapped array.
  19809. The default is 'w+' to create new, or overwrite existing file for
  19810. reading and writing.
  19811. suffix:
  19812. Suffix of `NamedTemporaryFile` if `out` is `'memmap'`.
  19813. The default is '.memmap'.
  19814. fillvalue:
  19815. Value to initialize output array.
  19816. By default, return uninitialized array.
  19817. Returns:
  19818. NumPy array or memory-mapped array of `shape` and `dtype`.
  19819. Raises:
  19820. ValueError:
  19821. Existing array cannot be reshaped to `shape` or cast to `dtype`.
  19822. """
  19823. shape = tuple(shape)
  19824. dtype = numpy.dtype(dtype)
  19825. if out is None:
  19826. if fillvalue is None:
  19827. return numpy.empty(shape, dtype)
  19828. if fillvalue:
  19829. return numpy.full(shape, fillvalue, dtype)
  19830. return numpy.zeros(shape, dtype)
  19831. if isinstance(out, numpy.ndarray):
  19832. if product(shape) != product(out.shape):
  19833. raise ValueError(f'cannot reshape {shape} to {out.shape}')
  19834. if not numpy.can_cast(dtype, out.dtype):
  19835. raise ValueError(f'cannot cast {dtype} to {out.dtype}')
  19836. out = out.reshape(shape)
  19837. if fillvalue is not None:
  19838. out.fill(fillvalue)
  19839. return out
  19840. if isinstance(out, str) and out[:6] == 'memmap':
  19841. import tempfile
  19842. tempdir = out[7:] if len(out) > 7 else None
  19843. if suffix is None:
  19844. suffix = '.memmap'
  19845. with tempfile.NamedTemporaryFile(dir=tempdir, suffix=suffix) as fh:
  19846. out = numpy.memmap(fh, shape=shape, dtype=dtype, mode=mode)
  19847. if fillvalue is not None:
  19848. out.fill(fillvalue)
  19849. return out
  19850. out = numpy.memmap(out, shape=shape, dtype=dtype, mode=mode)
  19851. if fillvalue is not None:
  19852. out.fill(fillvalue)
  19853. return out
  19854. def matlabstr2py(matlabstr: str, /) -> Any:
  19855. r"""Return Python object from Matlab string representation.
  19856. Use to access ScanImage metadata.
  19857. Parameters:
  19858. matlabstr: String representation of Matlab objects.
  19859. Returns:
  19860. Matlab structures are returned as `dict`.
  19861. Matlab arrays or cells are returned as `lists`.
  19862. Other Matlab objects are returned as `str`, `bool`, `int`, or `float`.
  19863. Examples:
  19864. >>> matlabstr2py('1')
  19865. 1
  19866. >>> matlabstr2py("['x y z' true false; 1 2.0 -3e4; NaN Inf @class]")
  19867. [['x y z', True, False], [1, 2.0, -30000.0], [nan, inf, '@class']]
  19868. >>> d = matlabstr2py(
  19869. ... "SI.hChannels.channelType = {'stripe' 'stripe'}\n"
  19870. ... "SI.hChannels.channelsActive = 2"
  19871. ... )
  19872. >>> d['SI.hChannels.channelType']
  19873. ['stripe', 'stripe']
  19874. """
  19875. # TODO: handle invalid input
  19876. # TODO: review unboxing of multidimensional arrays
  19877. def lex(s: str, /) -> list[str]:
  19878. # return sequence of tokens from Matlab string representation
  19879. tokens = ['[']
  19880. while True:
  19881. t, i = next_token(s)
  19882. if t is None:
  19883. break
  19884. if t == ';':
  19885. tokens.extend((']', '['))
  19886. elif t == '[':
  19887. tokens.extend(('[', '['))
  19888. elif t == ']':
  19889. tokens.extend((']', ']'))
  19890. else:
  19891. tokens.append(t)
  19892. s = s[i:]
  19893. tokens.append(']')
  19894. return tokens
  19895. def next_token(s: str, /) -> tuple[str | None, int]:
  19896. # return next token in Matlab string
  19897. length = len(s)
  19898. if length == 0:
  19899. return None, 0
  19900. i = 0
  19901. while i < length and s[i] == ' ':
  19902. i += 1
  19903. if i == length:
  19904. return None, i
  19905. if s[i] in '{[;]}':
  19906. return s[i], i + 1
  19907. if s[i] == "'":
  19908. j = i + 1
  19909. while j < length and s[j] != "'":
  19910. j += 1
  19911. return s[i : j + 1], j + 1
  19912. if s[i] == '<':
  19913. j = i + 1
  19914. while j < length and s[j] != '>':
  19915. j += 1
  19916. return s[i : j + 1], j + 1
  19917. j = i
  19918. while j < length and s[j] not in ' {[;]}':
  19919. j += 1
  19920. return s[i:j], j
  19921. def value(s: str, *, fail: bool = False) -> Any:
  19922. # return Python value of token
  19923. s = s.strip()
  19924. if not s:
  19925. return s
  19926. if len(s) == 1:
  19927. try:
  19928. return int(s)
  19929. except Exception as exc:
  19930. if fail:
  19931. raise ValueError from exc
  19932. return s
  19933. if s[0] == "'":
  19934. if (fail and s[-1] != "'") or "'" in s[1:-1]:
  19935. raise ValueError
  19936. return s[1:-1]
  19937. if s[0] == '<':
  19938. if (fail and s[-1] != '>') or '<' in s[1:-1]:
  19939. raise ValueError
  19940. return s
  19941. if fail and any(i in s for i in " ';[]{}"):
  19942. raise ValueError
  19943. if s[0] == '@':
  19944. return s
  19945. if s in {'true', 'True'}:
  19946. return True
  19947. if s in {'false', 'False'}:
  19948. return False
  19949. if s[:6] == 'zeros(':
  19950. return numpy.zeros([int(i) for i in s[6:-1].split(',')]).tolist()
  19951. if s[:5] == 'ones(':
  19952. return numpy.ones([int(i) for i in s[5:-1].split(',')]).tolist()
  19953. if '.' in s or 'e' in s:
  19954. try:
  19955. return float(s)
  19956. except (TypeError, ValueError):
  19957. pass
  19958. try:
  19959. return int(s)
  19960. except (TypeError, ValueError):
  19961. pass
  19962. try:
  19963. return float(s) # nan, inf
  19964. except (TypeError, ValueError) as exc:
  19965. if fail:
  19966. raise ValueError from exc
  19967. return s
  19968. def parse(s: str, /) -> Any:
  19969. # return Python value from string representation of Matlab value
  19970. s = s.strip()
  19971. try:
  19972. return value(s, fail=True)
  19973. except ValueError:
  19974. pass
  19975. result: list[Any]
  19976. addto: list[Any]
  19977. result = addto = []
  19978. levels = [addto]
  19979. for t in lex(s):
  19980. if t in '[{':
  19981. addto = []
  19982. levels.append(addto)
  19983. elif t in ']}':
  19984. x = levels.pop()
  19985. addto = levels[-1]
  19986. if len(x) == 1 and isinstance(x[0], (list, str)):
  19987. addto.append(x[0])
  19988. else:
  19989. addto.append(x)
  19990. else:
  19991. addto.append(value(t))
  19992. if len(result) == 1 and isinstance(result[0], (list, str)):
  19993. return result[0]
  19994. return result
  19995. if '\r' in matlabstr or '\n' in matlabstr:
  19996. # structure
  19997. d = {}
  19998. for line in matlabstr.splitlines():
  19999. line = line.strip() # noqa: PLW2901
  20000. if not line or line[0] == '%':
  20001. continue
  20002. k, v = line.split('=', 1)
  20003. k = k.strip()
  20004. if any(c in k for c in " ';[]{}<>"):
  20005. continue
  20006. d[k] = parse(v)
  20007. return d
  20008. return parse(matlabstr)
  20009. def strptime(datetime_string: str, fmt: str | None = None, /) -> DateTime:
  20010. """Return datetime corresponding to date string using common formats.
  20011. Parameters:
  20012. datetime_string:
  20013. String representation of date and time.
  20014. fmt:
  20015. Format of `datetime_string`.
  20016. By default, several datetime formats commonly found in TIFF files
  20017. are parsed.
  20018. Raises:
  20019. ValueError: `datetime_string` does not match any known format.
  20020. Examples:
  20021. >>> strptime('2022:08:01 22:23:24')
  20022. datetime.datetime(2022, 8, 1, 22, 23, 24)
  20023. """
  20024. formats = {
  20025. '%Y:%m:%d %H:%M:%S': 1, # TIFF6 specification
  20026. '%Y%m%d %H:%M:%S.%f': 2, # MetaSeries
  20027. '%Y-%m-%dT%H %M %S.%f': 3, # Pilatus
  20028. '%Y-%m-%dT%H:%M:%S.%f': 4, # ISO
  20029. '%Y-%m-%dT%H:%M:%S': 5, # ISO, microsecond is 0
  20030. '%Y:%m:%d %H:%M:%S.%f': 6,
  20031. '%d/%m/%Y %H:%M:%S': 7,
  20032. '%d/%m/%Y %H:%M:%S.%f': 8,
  20033. '%m/%d/%Y %I:%M:%S %p': 9,
  20034. '%m/%d/%Y %I:%M:%S.%f %p': 10,
  20035. '%Y%m%d %H:%M:%S': 11,
  20036. '%Y/%m/%d %H:%M:%S': 12,
  20037. '%Y/%m/%d %H:%M:%S.%f': 13,
  20038. '%Y-%m-%dT%H:%M:%S%z': 14,
  20039. '%Y-%m-%dT%H:%M:%S.%f%z': 15,
  20040. }
  20041. if fmt is not None:
  20042. formats[fmt] = 0 # highest priority; replaces existing key if any
  20043. for fmt_, _ in sorted(formats.items(), key=lambda item: item[1]):
  20044. try:
  20045. return DateTime.strptime(datetime_string, fmt_)
  20046. except ValueError:
  20047. pass
  20048. raise ValueError(
  20049. f'time data {datetime_string!r} does not match any format'
  20050. )
  20051. @overload
  20052. def stripnull(
  20053. string: bytes, /, null: bytes | None = None, *, first: bool = True
  20054. ) -> bytes: ...
  20055. @overload
  20056. def stripnull(
  20057. string: str, /, null: str | None = None, *, first: bool = True
  20058. ) -> str: ...
  20059. def stripnull(
  20060. string: str | bytes,
  20061. /,
  20062. null: str | bytes | None = None,
  20063. *,
  20064. first: bool = True,
  20065. ) -> str | bytes:
  20066. r"""Return string truncated at first null character.
  20067. Use to clean NULL terminated C strings.
  20068. >>> stripnull(b'bytes\x00\x00')
  20069. b'bytes'
  20070. >>> stripnull(b'bytes\x00bytes\x00\x00', first=False)
  20071. b'bytes\x00bytes'
  20072. >>> stripnull('string\x00')
  20073. 'string'
  20074. """
  20075. # TODO: enable deprecation warning
  20076. # warnings.warn(
  20077. # '<tifffile.stripnull is deprecated since 2025.3.18',
  20078. # DeprecationWarning,
  20079. # stacklevel=2,
  20080. # )
  20081. if null is None:
  20082. null = b'\x00' if isinstance(string, bytes) else '\0'
  20083. if first:
  20084. i = string.find(null) # type: ignore[arg-type]
  20085. return string if i < 0 else string[:i]
  20086. return string.rstrip(null) # type: ignore[arg-type]
  20087. def stripascii(string: bytes, /) -> bytes:
  20088. r"""Return string truncated at last byte that is 7-bit ASCII.
  20089. Use to clean NULL separated and terminated TIFF strings.
  20090. >>> stripascii(b'string\x00string\n\x01\x00')
  20091. b'string\x00string\n'
  20092. >>> stripascii(b'\x00')
  20093. b''
  20094. """
  20095. # TODO: pythonize this
  20096. i = len(string)
  20097. while i:
  20098. i -= 1
  20099. if 8 < string[i] < 127:
  20100. break
  20101. else:
  20102. i = -1
  20103. return string[: i + 1]
  20104. @overload
  20105. def asbool(
  20106. value: str,
  20107. /,
  20108. true: Sequence[str] | None = None,
  20109. false: Sequence[str] | None = None,
  20110. ) -> bool: ...
  20111. @overload
  20112. def asbool(
  20113. value: bytes,
  20114. /,
  20115. true: Sequence[bytes] | None = None,
  20116. false: Sequence[bytes] | None = None,
  20117. ) -> bool: ...
  20118. def asbool(
  20119. value: str | bytes,
  20120. /,
  20121. true: Sequence[str | bytes] | None = None,
  20122. false: Sequence[str | bytes] | None = None,
  20123. ) -> bool | bytes:
  20124. """Return string as bool if possible, else raise TypeError.
  20125. >>> asbool(b' False ')
  20126. False
  20127. >>> asbool('ON', ['on'], ['off'])
  20128. True
  20129. """
  20130. value = value.strip().lower()
  20131. isbytes = False
  20132. if true is None:
  20133. if isinstance(value, bytes):
  20134. if value == b'true':
  20135. return True
  20136. isbytes = True
  20137. elif value == 'true':
  20138. return True
  20139. elif value in true:
  20140. return True
  20141. if false is None:
  20142. if isbytes or isinstance(value, bytes):
  20143. if value == b'false':
  20144. return False
  20145. elif value == 'false':
  20146. return False
  20147. elif value in false:
  20148. return False
  20149. raise TypeError
  20150. def astype(value: Any, /, types: Sequence[Any] | None = None) -> Any:
  20151. """Return argument as one of types if possible.
  20152. >>> astype('42')
  20153. 42
  20154. >>> astype('3.14')
  20155. 3.14
  20156. >>> astype('True')
  20157. True
  20158. >>> astype(b'Neee-Wom')
  20159. 'Neee-Wom'
  20160. """
  20161. if types is None:
  20162. types = int, float, asbool, bytes2str
  20163. for typ in types:
  20164. try:
  20165. return typ(value)
  20166. except (ValueError, AttributeError, TypeError, UnicodeEncodeError):
  20167. pass
  20168. return value
  20169. def rational(arg: float | tuple[int, int], /) -> tuple[int, int]:
  20170. """Return rational numerator and denominator from float or two integers."""
  20171. from fractions import Fraction
  20172. if isinstance(arg, Sequence):
  20173. f = Fraction(arg[0], arg[1])
  20174. else:
  20175. f = Fraction.from_float(arg)
  20176. numerator, denominator = f.as_integer_ratio()
  20177. if numerator > 4294967295 or denominator > 4294967295:
  20178. s = 4294967295 / max(numerator, denominator)
  20179. numerator = round(numerator * s)
  20180. denominator = round(denominator * s)
  20181. return numerator, denominator
  20182. def unique_strings(strings: Iterator[str], /) -> Iterator[str]:
  20183. """Return iterator over unique strings.
  20184. >>> list(unique_strings(iter(('a', 'b', 'a'))))
  20185. ['a', 'b', 'a2']
  20186. """
  20187. known = set()
  20188. for i, s in enumerate(strings):
  20189. string = s
  20190. if string in known:
  20191. string += str(i)
  20192. known.add(string)
  20193. yield string
  20194. def format_size(size: float, /, threshold: float = 1536) -> str:
  20195. """Return file size as string from byte size.
  20196. >>> format_size(1234)
  20197. '1234 B'
  20198. >>> format_size(12345678901)
  20199. '11.50 GiB'
  20200. """
  20201. if size < threshold:
  20202. return f'{size} B'
  20203. for unit in ('KiB', 'MiB', 'GiB', 'TiB', 'PiB'):
  20204. size /= 1024.0
  20205. if size < threshold:
  20206. return f'{size:.2f} {unit}'
  20207. return 'ginormous'
  20208. def identityfunc(arg: Any, /, *args: Any, **kwargs: Any) -> Any:
  20209. """Single argument identity function.
  20210. >>> identityfunc('arg')
  20211. 'arg'
  20212. """
  20213. return arg
  20214. def nullfunc(*args: Any, **kwargs: Any) -> None:
  20215. """Null function.
  20216. >>> nullfunc('arg', kwarg='kwarg')
  20217. """
  20218. return
  20219. def sequence(value: Any, /) -> Sequence[Any]:
  20220. """Return tuple containing value if value is not tuple or list.
  20221. >>> sequence(1)
  20222. (1,)
  20223. >>> sequence([1])
  20224. [1]
  20225. >>> sequence('ab')
  20226. ('ab',)
  20227. """
  20228. return value if isinstance(value, (tuple, list)) else (value,)
  20229. def product(iterable: Iterable[int], /) -> int:
  20230. """Return product of integers.
  20231. Equivalent of ``math.prod(iterable)``, but multiplying NumPy integers
  20232. does not overflow.
  20233. >>> product([2**8, 2**30])
  20234. 274877906944
  20235. >>> product([])
  20236. 1
  20237. """
  20238. prod = 1
  20239. for i in iterable:
  20240. prod *= int(i)
  20241. return prod
  20242. def peek_iterator(iterator: Iterator[Any], /) -> tuple[Any, Iterator[Any]]:
  20243. """Return first item of iterator and iterator.
  20244. >>> first, it = peek_iterator(iter((0, 1, 2)))
  20245. >>> first
  20246. 0
  20247. >>> list(it)
  20248. [0, 1, 2]
  20249. """
  20250. first = next(iterator)
  20251. def newiter(
  20252. first: Any = first, iterator: Iterator[Any] = iterator
  20253. ) -> Iterator[Any]:
  20254. yield first
  20255. yield from iterator
  20256. return first, newiter()
  20257. def natural_sorted(iterable: Iterable[str], /) -> list[str]:
  20258. """Return human-sorted list of strings.
  20259. Use to sort file names.
  20260. >>> natural_sorted(['f1', 'f2', 'f10'])
  20261. ['f1', 'f2', 'f10']
  20262. """
  20263. def sortkey(x: str, /) -> list[int | str]:
  20264. return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)]
  20265. numbers = re.compile(r'(\d+)')
  20266. return sorted(iterable, key=sortkey)
  20267. def epics_datetime(sec: int, nsec: int, /) -> DateTime:
  20268. """Return datetime object from epicsTSSec and epicsTSNsec tag values.
  20269. >>> epics_datetime(802117916, 103746502)
  20270. datetime.datetime(2015, 6, 2, 11, 31, 56, 103746)
  20271. """
  20272. return DateTime.fromtimestamp(sec + 631152000 + nsec / 1e9)
  20273. def excel_datetime(timestamp: float, epoch: int | None = None, /) -> DateTime:
  20274. """Return datetime object from timestamp in Excel serial format.
  20275. Use to convert LSM time stamps.
  20276. >>> excel_datetime(40237.029999999795)
  20277. datetime.datetime(2010, 2, 28, 0, 43, 11, 999982)
  20278. """
  20279. if epoch is None:
  20280. epoch = 693594
  20281. return DateTime.fromordinal(epoch) + TimeDelta(timestamp)
  20282. def julian_datetime(julianday: int, millisecond: int = 0, /) -> DateTime:
  20283. """Return datetime from days since 1/1/4713 BC and ms since midnight.
  20284. Convert Julian dates according to MetaMorph.
  20285. >>> julian_datetime(2451576, 54362783)
  20286. datetime.datetime(2000, 2, 2, 15, 6, 2, 783000)
  20287. """
  20288. if julianday <= 1721423:
  20289. # return DateTime.min # ?
  20290. raise ValueError(f'no datetime before year 1 ({julianday=})')
  20291. a = julianday + 1
  20292. if a > 2299160:
  20293. alpha = math.trunc((a - 1867216.25) / 36524.25)
  20294. a += 1 + alpha - alpha // 4
  20295. b = a + (1524 if a > 1721423 else 1158)
  20296. c = math.trunc((b - 122.1) / 365.25)
  20297. d = math.trunc(365.25 * c)
  20298. e = math.trunc((b - d) / 30.6001)
  20299. day = b - d - math.trunc(30.6001 * e)
  20300. month = e - (1 if e < 13.5 else 13)
  20301. year = c - (4716 if month > 2.5 else 4715)
  20302. hour, millisecond = divmod(millisecond, 1000 * 60 * 60)
  20303. minute, millisecond = divmod(millisecond, 1000 * 60)
  20304. second, millisecond = divmod(millisecond, 1000)
  20305. return DateTime(year, month, day, hour, minute, second, millisecond * 1000)
  20306. def byteorder_isnative(byteorder: str, /) -> bool:
  20307. """Return if byteorder matches system's byteorder.
  20308. >>> byteorder_isnative('=')
  20309. True
  20310. """
  20311. if byteorder in {'=', sys.byteorder}:
  20312. return True
  20313. keys = {'big': '>', 'little': '<'}
  20314. return keys.get(byteorder, byteorder) == keys[sys.byteorder]
  20315. def byteorder_compare(byteorder: str, other: str, /) -> bool:
  20316. """Return if byteorders match.
  20317. >>> byteorder_compare('<', '<')
  20318. True
  20319. >>> byteorder_compare('>', '<')
  20320. False
  20321. """
  20322. if byteorder in {other, '|'} or other == '|':
  20323. return True
  20324. if byteorder == '=':
  20325. byteorder = {'big': '>', 'little': '<'}[sys.byteorder]
  20326. elif other == '=':
  20327. other = {'big': '>', 'little': '<'}[sys.byteorder]
  20328. return byteorder == other
  20329. def recarray2dict(recarray: numpy.recarray[Any, Any], /) -> dict[str, Any]:
  20330. """Return numpy.recarray as dictionary.
  20331. >>> r = numpy.array(
  20332. ... [(1.0, 2, 'a'), (3.0, 4, 'bc')],
  20333. ... dtype=[('x', '<f4'), ('y', '<i4'), ('s', 'S2')],
  20334. ... )
  20335. >>> recarray2dict(r)
  20336. {'x': [1.0, 3.0], 'y': [2, 4], 's': ['a', 'bc']}
  20337. >>> recarray2dict(r[1])
  20338. {'x': 3.0, 'y': 4, 's': 'bc'}
  20339. """
  20340. # TODO: subarrays
  20341. value: Any
  20342. result = {}
  20343. for descr in recarray.dtype.descr:
  20344. name, dtype = descr[:2]
  20345. value = recarray[name]
  20346. if value.ndim == 0:
  20347. value = value.tolist()
  20348. if dtype[1] == 'S':
  20349. value = bytes2str(value)
  20350. elif value.ndim == 1:
  20351. value = value.tolist()
  20352. if dtype[1] == 'S':
  20353. value = [bytes2str(v) for v in value]
  20354. result[name] = value
  20355. return result
  20356. def xml2dict(
  20357. xml: str,
  20358. /,
  20359. *,
  20360. sanitize: bool = True,
  20361. prefix: tuple[str, str] | None = None,
  20362. sep: str = ',',
  20363. ) -> dict[str, Any]:
  20364. """Return XML as dictionary.
  20365. Parameters:
  20366. xml: XML data to convert.
  20367. sanitize: Remove prefix from from etree Element.
  20368. prefix: Prefixes for dictionary keys.
  20369. sep: Sequence separator.
  20370. Examples:
  20371. >>> xml2dict(
  20372. ... '<?xml version="1.0" ?><root attr="name"><key>1</key></root>'
  20373. ... )
  20374. {'root': {'key': 1, 'attr': 'name'}}
  20375. >>> xml2dict('<level1><level2>3.5322,-3.14</level2></level1>')
  20376. {'level1': {'level2': (3.5322, -3.14)}}
  20377. """
  20378. try:
  20379. from defusedxml import ElementTree
  20380. except ImportError:
  20381. from xml.etree import ElementTree
  20382. at, tx = prefix if prefix else ('', '')
  20383. def astype(value: Any, /) -> Any:
  20384. # return string value as int, float, bool, tuple, or unchanged
  20385. if not isinstance(value, str):
  20386. return value
  20387. if sep and sep in value:
  20388. # sequence of numbers?
  20389. values = []
  20390. for val in value.split(sep):
  20391. v = astype(val)
  20392. if isinstance(v, str):
  20393. return value
  20394. values.append(v)
  20395. return tuple(values)
  20396. for t in (int, float, asbool):
  20397. try:
  20398. return t(value)
  20399. except (TypeError, ValueError):
  20400. pass
  20401. return value
  20402. def etree2dict(t: Any, /) -> dict[str, Any]:
  20403. # adapted from https://stackoverflow.com/a/10077069/453463
  20404. key = t.tag
  20405. if sanitize:
  20406. key = key.rsplit('}', 1)[-1]
  20407. d: dict[str, Any] = {key: {} if t.attrib else None}
  20408. children = list(t)
  20409. if children:
  20410. dd = collections.defaultdict(list)
  20411. for dc in map(etree2dict, children):
  20412. for k, v in dc.items():
  20413. dd[k].append(astype(v))
  20414. d = {
  20415. key: {
  20416. k: astype(v[0]) if len(v) == 1 else astype(v)
  20417. for k, v in dd.items()
  20418. }
  20419. }
  20420. if t.attrib:
  20421. d[key].update((at + k, astype(v)) for k, v in t.attrib.items())
  20422. if t.text:
  20423. text = t.text.strip()
  20424. if children or t.attrib:
  20425. if text:
  20426. d[key][tx + 'value'] = astype(text)
  20427. else:
  20428. d[key] = astype(text)
  20429. return d
  20430. return etree2dict(ElementTree.fromstring(xml))
  20431. def hexdump(
  20432. data: bytes,
  20433. /,
  20434. *,
  20435. width: int = 75,
  20436. height: int = 24,
  20437. snipat: float | None = 0.75,
  20438. modulo: int = 2,
  20439. ellipsis: str | None = None,
  20440. ) -> str:
  20441. """Return hexdump representation of bytes.
  20442. Parameters:
  20443. data:
  20444. Bytes to represent as hexdump.
  20445. width:
  20446. Maximum width of hexdump.
  20447. height:
  20448. Maximum number of lines of hexdump.
  20449. snipat:
  20450. Approximate position at which to split long hexdump.
  20451. modulo:
  20452. Number of bytes represented in line of hexdump are modulus
  20453. of this value.
  20454. ellipsis:
  20455. Characters to insert for snipped content of long hexdump.
  20456. The default is '...'.
  20457. Examples:
  20458. >>> import binascii
  20459. >>> hexdump(binascii.unhexlify('49492a00080000000e00fe0004000100'))
  20460. '49 49 2a 00 08 00 00 00 0e 00 fe 00 04 00 01 00 II*.............'
  20461. """
  20462. size = len(data)
  20463. if size < 1 or width < 2 or height < 1:
  20464. return ''
  20465. if height == 1:
  20466. addr = b''
  20467. bytesperline = min(
  20468. modulo * (((width - len(addr)) // 4) // modulo), size
  20469. )
  20470. if bytesperline < 1:
  20471. return ''
  20472. nlines = 1
  20473. else:
  20474. addr = b'%%0%ix: ' % len(b'%x' % size)
  20475. bytesperline = min(
  20476. modulo * (((width - len(addr % 1)) // 4) // modulo), size
  20477. )
  20478. if bytesperline < 1:
  20479. return ''
  20480. width = 3 * bytesperline + len(addr % 1)
  20481. nlines = (size - 1) // bytesperline + 1
  20482. if snipat is None or snipat == 1:
  20483. snipat = height
  20484. elif 0 < abs(snipat) < 1:
  20485. snipat = math.floor(height * snipat)
  20486. if snipat < 0:
  20487. snipat += height
  20488. assert isinstance(snipat, int)
  20489. blocks: list[tuple[int, bytes | None]]
  20490. if height == 1 or nlines == 1:
  20491. blocks = [(0, data[:bytesperline])]
  20492. addr = b''
  20493. height = 1
  20494. width = 3 * bytesperline
  20495. elif not height or nlines <= height:
  20496. blocks = [(0, data)]
  20497. elif snipat <= 0:
  20498. start = bytesperline * (nlines - height)
  20499. blocks = [(start, data[start:])] # (start, None)
  20500. elif snipat >= height or height < 3:
  20501. end = bytesperline * height
  20502. blocks = [(0, data[:end])] # (end, None)
  20503. else:
  20504. end1 = bytesperline * snipat
  20505. end2 = bytesperline * (height - snipat - 2)
  20506. if size % bytesperline:
  20507. end2 += size % bytesperline
  20508. else:
  20509. end2 += bytesperline
  20510. blocks = [
  20511. (0, data[:end1]),
  20512. (size - end1 - end2, None),
  20513. (size - end2, data[size - end2 :]),
  20514. ]
  20515. if ellipsis is None:
  20516. if addr and bytesperline > 3:
  20517. elps = b' ' * (len(addr % 1) + bytesperline // 2 * 3 - 2)
  20518. elps += b'...'
  20519. else:
  20520. elps = b'...'
  20521. else:
  20522. elps = ellipsis.encode('cp1252')
  20523. result = []
  20524. for start, bstr in blocks:
  20525. if bstr is None:
  20526. result.append(elps) # 'skip %i bytes' % start)
  20527. continue
  20528. hexstr = binascii.hexlify(bstr)
  20529. strstr = re.sub(br'[^\x20-\x7f]', b'.', bstr)
  20530. for i in range(0, len(bstr), bytesperline):
  20531. h = hexstr[2 * i : 2 * i + bytesperline * 2]
  20532. r = (addr % (i + start)) if height > 1 else addr
  20533. r += b' '.join(h[i : i + 2] for i in range(0, 2 * bytesperline, 2))
  20534. r += b' ' * (width - len(r))
  20535. r += strstr[i : i + bytesperline]
  20536. result.append(r)
  20537. return b'\n'.join(result).decode('ascii')
  20538. def isprintable(string: str | bytes, /) -> bool:
  20539. r"""Return if all characters in string are printable.
  20540. >>> isprintable('abc')
  20541. True
  20542. >>> isprintable(b'\01')
  20543. False
  20544. """
  20545. string = string.strip()
  20546. if not string:
  20547. return True
  20548. if isinstance(string, str):
  20549. return string.isprintable()
  20550. try:
  20551. return string.decode().isprintable()
  20552. except UnicodeDecodeError:
  20553. pass
  20554. return False
  20555. def clean_whitespace(string: str, /, *, compact: bool = False) -> str:
  20556. r"""Return string with compressed whitespace.
  20557. >>> clean_whitespace(' a \n\n b ')
  20558. 'a\n b'
  20559. """
  20560. string = (
  20561. string.replace('\r\n', '\n')
  20562. .replace('\r', '\n')
  20563. .replace('\n\n', '\n')
  20564. .replace('\t', ' ')
  20565. .replace(' ', ' ')
  20566. .replace(' ', ' ')
  20567. .replace(' \n', '\n')
  20568. )
  20569. if compact:
  20570. string = (
  20571. string.replace('\n', ' ')
  20572. .replace('[ ', '[')
  20573. .replace(' ', ' ')
  20574. .replace(' ', ' ')
  20575. .replace(' ', ' ')
  20576. )
  20577. return string.strip()
  20578. def indent(*args: Any) -> str:
  20579. """Return joined string representations of objects with indented lines.
  20580. >>> print(indent('Title:', 'Text'))
  20581. Title:
  20582. Text
  20583. """
  20584. text = '\n'.join(str(arg) for arg in args)
  20585. return '\n'.join(
  20586. (' ' + line if line else line) for line in text.splitlines() if line
  20587. )[2:]
  20588. def pformat_xml(xml: str | bytes, /) -> str:
  20589. """Return pretty formatted XML."""
  20590. try:
  20591. from lxml import etree
  20592. if not isinstance(xml, bytes):
  20593. xml = xml.encode()
  20594. tree = etree.parse(io.BytesIO(xml))
  20595. xml = etree.tostring(
  20596. tree,
  20597. pretty_print=True,
  20598. xml_declaration=True,
  20599. encoding=tree.docinfo.encoding,
  20600. )
  20601. assert isinstance(xml, bytes)
  20602. xml = bytes2str(xml)
  20603. except Exception:
  20604. if isinstance(xml, bytes):
  20605. xml = bytes2str(xml)
  20606. xml = xml.replace('><', '>\n<')
  20607. return xml.replace(' ', ' ').replace('\t', ' ')
  20608. def pformat(
  20609. arg: Any,
  20610. /,
  20611. *,
  20612. height: int | None = 24,
  20613. width: int | None = 79,
  20614. linewidth: int | None = 288,
  20615. compact: bool = True,
  20616. ) -> str:
  20617. """Return pretty formatted representation of object as string.
  20618. Whitespace might be altered. Long lines are cut off.
  20619. """
  20620. if height is None or height < 1:
  20621. height = 1024
  20622. if width is None or width < 1:
  20623. width = 256
  20624. if linewidth is None or linewidth < 1:
  20625. linewidth = width
  20626. npopt = numpy.get_printoptions()
  20627. numpy.set_printoptions(threshold=100, linewidth=width)
  20628. if isinstance(arg, bytes) and (
  20629. arg[:5].lower() == b'<?xml' or arg[-4:] == b'OME>'
  20630. ):
  20631. arg = bytes2str(arg)
  20632. if isinstance(arg, bytes):
  20633. if isprintable(arg):
  20634. arg = bytes2str(arg)
  20635. arg = clean_whitespace(arg)
  20636. else:
  20637. numpy.set_printoptions(**npopt)
  20638. return hexdump(arg, width=width, height=height, modulo=1)
  20639. arg = arg.rstrip()
  20640. elif isinstance(arg, str):
  20641. if arg[:5].lower() == '<?xml' or arg[-4:] == 'OME>':
  20642. arg = arg[: 4 * width] if height == 1 else pformat_xml(arg)
  20643. # too slow
  20644. # else:
  20645. # import textwrap
  20646. # return '\n'.join(
  20647. # textwrap.wrap(arg, width=width, max_lines=height, tabsize=2)
  20648. # )
  20649. arg = arg.rstrip()
  20650. elif isinstance(arg, numpy.record):
  20651. arg = arg.pprint()
  20652. # elif isinstance(arg, dict):
  20653. # from reprlib import Repr
  20654. #
  20655. # arg = Repr(
  20656. # maxlevel=6,
  20657. # maxtuple=height,
  20658. # maxlist=height,
  20659. # maxarray=height,
  20660. # maxdict=height,
  20661. # maxset=height,
  20662. # maxfrozenset=6,
  20663. # maxdeque=6,
  20664. # maxstring=width,
  20665. # maxlong=40,
  20666. # maxother=height,
  20667. # indent=' ',
  20668. # ).repr(arg)
  20669. else:
  20670. import pprint
  20671. arg = pprint.pformat(arg, width=width, compact=compact)
  20672. numpy.set_printoptions(**npopt)
  20673. if height == 1:
  20674. arg = arg[: width * width]
  20675. arg = clean_whitespace(arg, compact=True)
  20676. return arg[:linewidth]
  20677. argl = list(arg.splitlines())
  20678. if len(argl) > height:
  20679. arg = '\n'.join(
  20680. line[:linewidth]
  20681. for line in (*argl[: height // 2], '...', *argl[-height // 2 :])
  20682. )
  20683. else:
  20684. arg = '\n'.join(line[:linewidth] for line in argl[:height])
  20685. return arg
  20686. def snipstr(
  20687. string: str,
  20688. /,
  20689. width: int = 79,
  20690. *,
  20691. snipat: float | None = None,
  20692. ellipsis: str | None = None,
  20693. ) -> str:
  20694. """Return string cut to specified length.
  20695. Parameters:
  20696. string:
  20697. String to snip.
  20698. width:
  20699. Maximum length of returned string.
  20700. snipat:
  20701. Approximate position at which to split long strings.
  20702. The default is 0.5.
  20703. ellipsis:
  20704. Characters to insert between splits of long strings.
  20705. The default is '...'.
  20706. Examples:
  20707. >>> snipstr('abcdefghijklmnop', 8)
  20708. 'abc...op'
  20709. """
  20710. if snipat is None:
  20711. snipat = 0.5
  20712. if ellipsis is None:
  20713. ellipsis = b'...' if isinstance(string, bytes) else '\u2026'
  20714. esize = len(ellipsis)
  20715. splitlines = string.splitlines()
  20716. # TODO: finish and test multiline snip
  20717. result = []
  20718. for line in splitlines:
  20719. if line is None:
  20720. result.append(ellipsis)
  20721. continue
  20722. linelen = len(line)
  20723. if linelen <= width:
  20724. result.append(string)
  20725. continue
  20726. if snipat is None or snipat == 1:
  20727. split = linelen
  20728. elif 0 < abs(snipat) < 1:
  20729. split = math.floor(linelen * snipat)
  20730. else:
  20731. split = int(snipat)
  20732. if split < 0:
  20733. split += linelen
  20734. split = max(split, 0)
  20735. if esize == 0 or width < esize + 1:
  20736. if split <= 0:
  20737. result.append(string[-width:])
  20738. else:
  20739. result.append(string[:width])
  20740. elif split <= 0:
  20741. result.append(ellipsis + string[esize - width :])
  20742. elif split >= linelen or width < esize + 4:
  20743. result.append(string[: width - esize] + ellipsis)
  20744. else:
  20745. splitlen = linelen - width + esize
  20746. end1 = split - splitlen // 2
  20747. end2 = end1 + splitlen
  20748. result.append(string[:end1] + ellipsis + string[end2:])
  20749. if isinstance(string, bytes):
  20750. return b'\n'.join(result)
  20751. return '\n'.join(result)
  20752. def enumstr(enum: Any, /) -> str:
  20753. """Return short string representation of Enum member.
  20754. >>> enumstr(PHOTOMETRIC.RGB)
  20755. 'RGB'
  20756. """
  20757. name = enum.name
  20758. if name is None:
  20759. name = str(enum)
  20760. return name
  20761. def enumarg(enum: type[enum.IntEnum], arg: Any, /) -> enum.IntEnum:
  20762. """Return enum member from its name or value.
  20763. Parameters:
  20764. enum: Type of IntEnum.
  20765. arg: Name or value of enum member.
  20766. Returns:
  20767. Enum member matching name or value.
  20768. Raises:
  20769. ValueError: No enum member matches name or value.
  20770. Examples:
  20771. >>> enumarg(PHOTOMETRIC, 2)
  20772. <PHOTOMETRIC.RGB: 2>
  20773. >>> enumarg(PHOTOMETRIC, 'RGB')
  20774. <PHOTOMETRIC.RGB: 2>
  20775. """
  20776. try:
  20777. return enum(arg)
  20778. except Exception:
  20779. try:
  20780. return enum[arg.upper()]
  20781. except Exception as exc:
  20782. raise ValueError(f'invalid argument {arg!r}') from exc
  20783. def parse_kwargs(
  20784. kwargs: dict[str, Any], /, *keys: str, **keyvalues: Any
  20785. ) -> dict[str, Any]:
  20786. """Return dict with keys from keys|keyvals and values from kwargs|keyvals.
  20787. Existing keys are deleted from `kwargs`.
  20788. >>> kwargs = {'one': 1, 'two': 2, 'four': 4}
  20789. >>> kwargs2 = parse_kwargs(kwargs, 'two', 'three', four=None, five=5)
  20790. >>> kwargs == {'one': 1}
  20791. True
  20792. >>> kwargs2 == {'two': 2, 'four': 4, 'five': 5}
  20793. True
  20794. """
  20795. result = {}
  20796. for key in keys:
  20797. if key in kwargs:
  20798. result[key] = kwargs[key]
  20799. del kwargs[key]
  20800. for key, value in keyvalues.items():
  20801. if key in kwargs:
  20802. result[key] = kwargs[key]
  20803. del kwargs[key]
  20804. else:
  20805. result[key] = value
  20806. return result
  20807. def update_kwargs(kwargs: dict[str, Any], /, **keyvalues: Any) -> None:
  20808. """Update dict with keys and values if keys do not already exist.
  20809. >>> kwargs = {'one': 1}
  20810. >>> update_kwargs(kwargs, one=None, two=2)
  20811. >>> kwargs == {'one': 1, 'two': 2}
  20812. True
  20813. """
  20814. for key, value in keyvalues.items():
  20815. if key not in kwargs:
  20816. kwargs[key] = value
  20817. def kwargs_notnone(**kwargs: Any) -> dict[str, Any]:
  20818. """Return dict of kwargs which values are not None.
  20819. >>> kwargs_notnone(one=1, none=None)
  20820. {'one': 1}
  20821. """
  20822. return dict(item for item in kwargs.items() if item[1] is not None)
  20823. def logger() -> logging.Logger:
  20824. """Return logger for tifffile module."""
  20825. return logging.getLogger('tifffile')
  20826. def validate_jhove(
  20827. filename: str,
  20828. /,
  20829. jhove: str | None = None,
  20830. ignore: Collection[str] | None = None,
  20831. ) -> None:
  20832. """Validate TIFF file with ``jhove -m TIFF-hul``.
  20833. JHOVE does not support the BigTIFF format, more than 50 IFDs, and
  20834. many TIFF extensions.
  20835. Parameters:
  20836. filename:
  20837. Name of TIFF file to validate.
  20838. jhove:
  20839. Path of jhove app. The default is 'jhove'.
  20840. ignore:
  20841. Jhove error message to ignore.
  20842. Raises:
  20843. ValueError:
  20844. Jhove printed error message and did not contain one of strings
  20845. in `ignore`.
  20846. References:
  20847. - `JHOVE TIFF-hul Module <http://jhove.sourceforge.net/tiff-hul.html>`_
  20848. """
  20849. import subprocess
  20850. if ignore is None:
  20851. ignore = {'More than 50 IFDs', 'Predictor value out of range'}
  20852. if jhove is None:
  20853. jhove = 'jhove'
  20854. out = subprocess.check_output( # # noqa: S603
  20855. [jhove, filename, '-m', 'TIFF-hul']
  20856. )
  20857. if b'ErrorMessage: ' in out:
  20858. for line_full in out.splitlines():
  20859. line = line_full.strip()
  20860. if line.startswith(b'ErrorMessage: '):
  20861. error = line[14:].decode()
  20862. for i in ignore:
  20863. if i in error:
  20864. break
  20865. else:
  20866. raise ValueError(error)
  20867. break
  20868. def tiffcomment(
  20869. arg: str | os.PathLike[Any] | FileHandle | IO[bytes],
  20870. /,
  20871. comment: str | bytes | None = None,
  20872. pageindex: int | None = None,
  20873. tagcode: int | str | None = None,
  20874. ) -> str | None:
  20875. """Return or replace ImageDescription value in first page of TIFF file.
  20876. Parameters:
  20877. arg:
  20878. Specifies TIFF file to open.
  20879. comment:
  20880. 7-bit ASCII string or bytes to replace existing tag value.
  20881. The existing value is zeroed.
  20882. pageindex:
  20883. Index of page which ImageDescription tag value to
  20884. read or replace. The default is 0.
  20885. tagcode:
  20886. Code of tag which value to read or replace.
  20887. The default is 270 (ImageDescription).
  20888. Returns:
  20889. None, if `comment` is specified. Else, the current value of the
  20890. specified tag in the specified page.
  20891. """
  20892. if pageindex is None:
  20893. pageindex = 0
  20894. if tagcode is None:
  20895. tagcode = 270
  20896. mode: Any = None if comment is None else 'r+'
  20897. with TiffFile(arg, mode=mode) as tif:
  20898. page = tif.pages[pageindex]
  20899. if not isinstance(page, TiffPage):
  20900. raise IndexError(f'TiffPage {pageindex} not found')
  20901. tag = page.tags.get(tagcode, None)
  20902. if tag is None:
  20903. raise ValueError(f'no {TIFF.TAGS[tagcode]} tag found')
  20904. if comment is None:
  20905. return tag.value
  20906. tag.overwrite(comment)
  20907. return None
  20908. def tiff2fsspec(
  20909. filename: str | os.PathLike[Any],
  20910. /,
  20911. url: str,
  20912. *,
  20913. out: str | None = None,
  20914. key: int | None = None,
  20915. series: int | None = None,
  20916. level: int | None = None,
  20917. chunkmode: CHUNKMODE | int | str | None = None,
  20918. fillvalue: float | None = None,
  20919. zattrs: dict[str, Any] | None = None,
  20920. squeeze: bool | None = None,
  20921. groupname: str | None = None,
  20922. version: int | None = None,
  20923. ) -> None:
  20924. """Write fsspec ReferenceFileSystem in JSON format for data in TIFF file.
  20925. By default, the first series, including all levels, is exported.
  20926. Parameters:
  20927. filename:
  20928. Name of TIFF file to reference.
  20929. url:
  20930. Remote location of TIFF file without file name(s).
  20931. out:
  20932. Name of output JSON file.
  20933. The default is the `filename` with a '.json' extension.
  20934. key, series, level, chunkmode, fillvalue, zattrs, squeeze:
  20935. Passed to :py:meth:`TiffFile.aszarr`.
  20936. groupname, version:
  20937. Passed to :py:meth:`ZarrTiffStore.write_fsspec`.
  20938. """
  20939. if out is None:
  20940. out = os.fspath(filename) + '.json'
  20941. with TiffFile(filename) as tif:
  20942. store: ZarrTiffStore
  20943. with tif.aszarr(
  20944. key=key,
  20945. series=series,
  20946. level=level,
  20947. chunkmode=chunkmode,
  20948. fillvalue=fillvalue,
  20949. zattrs=zattrs,
  20950. squeeze=squeeze,
  20951. ) as store:
  20952. store.write_fsspec(out, url, groupname=groupname, version=version)
  20953. def lsm2bin(
  20954. lsmfile: str,
  20955. /,
  20956. binfile: str | None = None,
  20957. *,
  20958. tile: tuple[int, int] | None = None,
  20959. verbose: bool = True,
  20960. ) -> None:
  20961. """Convert [MP]TZCYX LSM file to series of BIN files.
  20962. One BIN file containing 'ZCYX' data is created for each position, time,
  20963. and tile. The position, time, and tile indices are encoded at the end
  20964. of the filenames.
  20965. Parameters:
  20966. lsmfile:
  20967. Name of LSM file to convert.
  20968. binfile:
  20969. Common name of output BIN files.
  20970. The default is the name of the LSM file without extension.
  20971. tile:
  20972. Y and X dimension sizes of BIN files.
  20973. The default is (256, 256).
  20974. verbose:
  20975. Print status of conversion.
  20976. """
  20977. prints: Any = print if verbose else nullfunc
  20978. if tile is None:
  20979. tile = (256, 256)
  20980. if binfile is None:
  20981. binfile = lsmfile
  20982. elif binfile.lower() == 'none':
  20983. binfile = None
  20984. if binfile:
  20985. binfile += '_(z%ic%iy%ix%i)_m%%ip%%it%%03iy%%ix%%i.bin'
  20986. prints('\nOpening LSM file... ', end='', flush=True)
  20987. timer = Timer()
  20988. with TiffFile(lsmfile) as lsm:
  20989. if not lsm.is_lsm:
  20990. prints('\n', lsm, flush=True)
  20991. raise ValueError('not a LSM file')
  20992. series = lsm.series[0] # first series contains the image
  20993. shape = series.get_shape(squeeze=False)
  20994. axes = series.get_axes(squeeze=False)
  20995. dtype = series.dtype
  20996. size = product(shape) * dtype.itemsize
  20997. prints(timer)
  20998. # verbose(lsm, flush=True)
  20999. prints(
  21000. indent(
  21001. 'Image',
  21002. f'axes: {axes}',
  21003. f'shape: {shape}',
  21004. f'dtype: {dtype}',
  21005. f'size: {size}',
  21006. ),
  21007. flush=True,
  21008. )
  21009. if axes == 'CYX':
  21010. shape = (1, 1, *shape)
  21011. elif axes == 'ZCYX':
  21012. shape = (1, *shape)
  21013. elif axes == 'MPCYX':
  21014. shape = (*shape[:2], 1, 1, *shape[2:])
  21015. elif axes == 'MPZCYX':
  21016. shape = (*shape[:2], 1, *shape[2:])
  21017. elif not axes.endswith('TZCYX'):
  21018. raise ValueError('not a *TZCYX LSM file')
  21019. prints('Copying image from LSM to BIN files', end='', flush=True)
  21020. timer.start()
  21021. tiles = shape[-2] // tile[-2], shape[-1] // tile[-1]
  21022. if binfile:
  21023. binfile = binfile % (shape[-4], shape[-3], tile[0], tile[1])
  21024. shape = (1,) * (7 - len(shape)) + shape
  21025. # cache for ZCYX stacks and output files
  21026. data = numpy.empty(shape[3:], dtype=dtype)
  21027. out = numpy.empty(
  21028. (shape[-4], shape[-3], tile[0], tile[1]), dtype=dtype
  21029. )
  21030. # iterate over Tiff pages containing data
  21031. pages = iter(series.pages)
  21032. for m in range(shape[0]): # mosaic axis
  21033. for p in range(shape[1]): # position axis
  21034. for t in range(shape[2]): # time axis
  21035. for z in range(shape[3]): # z slices
  21036. page = next(pages)
  21037. assert page is not None
  21038. data[z] = page.asarray()
  21039. for y in range(tiles[0]): # tile y
  21040. for x in range(tiles[1]): # tile x
  21041. out[:] = data[
  21042. ...,
  21043. y * tile[0] : (y + 1) * tile[0],
  21044. x * tile[1] : (x + 1) * tile[1],
  21045. ]
  21046. if binfile:
  21047. out.tofile(binfile % (m, p, t, y, x))
  21048. prints('.', end='', flush=True)
  21049. prints(timer, flush=True)
  21050. def imshow(
  21051. data: NDArray[Any],
  21052. /,
  21053. *,
  21054. photometric: PHOTOMETRIC | int | str | None = None,
  21055. planarconfig: PLANARCONFIG | int | str | None = None,
  21056. bitspersample: int | None = None,
  21057. nodata: float = 0,
  21058. interpolation: str | None = None,
  21059. cmap: Any | None = None,
  21060. vmin: float | None = None,
  21061. vmax: float | None = None,
  21062. figure: Any = None,
  21063. subplot: Any = None,
  21064. title: str | None = None,
  21065. window_title: str | None = None,
  21066. dpi: int = 96,
  21067. maxdim: int | None = None,
  21068. background: tuple[float, float, float] | str | None = None,
  21069. show: bool = False,
  21070. **kwargs: Any,
  21071. ) -> tuple[Any, Any, Any]:
  21072. """Plot n-dimensional images with `matplotlib.pyplot`.
  21073. Parameters:
  21074. data:
  21075. Image array to display.
  21076. photometric:
  21077. Color space of image.
  21078. planarconfig:
  21079. How components of each pixel are stored.
  21080. bitspersample:
  21081. Number of bits per channel in integer RGB images.
  21082. interpolation:
  21083. Image interpolation method used in `matplotlib.imshow`.
  21084. The default is 'nearest' for image dimensions > 512,
  21085. else 'bilinear'.
  21086. cmap:
  21087. Colormap mapping non-RGBA scalar data to colors.
  21088. See `matplotlib.colors.Colormap`.
  21089. vmin:
  21090. Minimum of data range covered by colormap.
  21091. By default, the complete range of the data is covered.
  21092. vmax:
  21093. Maximum of data range covered by colormap.
  21094. By default, the complete range of the data is covered.
  21095. figure:
  21096. Matplotlib figure to use for plotting.
  21097. See `matplotlib.figure.Figure`.
  21098. subplot:
  21099. A `matplotlib.pyplot.subplot` axis.
  21100. title:
  21101. Subplot title.
  21102. window_title:
  21103. Window title.
  21104. dpi:
  21105. Resolution of figure.
  21106. maxdim:
  21107. Maximum image width and length.
  21108. background:
  21109. Background color.
  21110. show:
  21111. Display figure.
  21112. **kwargs:
  21113. Additional arguments passed to :py:func:`matplotlib.pyplot.imshow`.
  21114. Returns:
  21115. Matplotlib figure, subplot, and plot axis.
  21116. """
  21117. # TODO: rewrite detection of isrgb, iscontig
  21118. # TODO: use planarconfig
  21119. if photometric is None:
  21120. photometric = 'RGB'
  21121. if maxdim is None:
  21122. maxdim = 2**16
  21123. isrgb = photometric in {'RGB', 'YCBCR'} # 'PALETTE', 'YCBCR'
  21124. if data.dtype == 'float16':
  21125. data = data.astype(numpy.float32)
  21126. if data.dtype.kind == 'b':
  21127. isrgb = False
  21128. if isrgb and not (
  21129. data.shape[-1] in {3, 4}
  21130. or (data.ndim > 2 and data.shape[-3] in {3, 4})
  21131. ):
  21132. isrgb = False
  21133. photometric = 'MINISBLACK'
  21134. data = data.squeeze()
  21135. if photometric in {
  21136. None,
  21137. 'MINISWHITE',
  21138. 'MINISBLACK',
  21139. 'CFA',
  21140. 'MASK',
  21141. 'PALETTE',
  21142. 'LOGL',
  21143. 'LOGLUV',
  21144. 'DEPTH_MAP',
  21145. 'SEMANTIC_MASK',
  21146. }:
  21147. data = reshape_nd(data, 2)
  21148. else:
  21149. data = reshape_nd(data, 3)
  21150. dims = data.ndim
  21151. if dims < 2:
  21152. raise ValueError('not an image')
  21153. if dims == 2:
  21154. dims = 0
  21155. isrgb = False
  21156. else:
  21157. if isrgb and data.shape[-3] in {3, 4} and data.shape[-1] not in {3, 4}:
  21158. data = numpy.swapaxes(data, -3, -2)
  21159. data = numpy.swapaxes(data, -2, -1)
  21160. elif not isrgb and (
  21161. data.shape[-1] < data.shape[-2] // 8
  21162. and data.shape[-1] < data.shape[-3] // 8
  21163. ):
  21164. data = numpy.swapaxes(data, -3, -1)
  21165. data = numpy.swapaxes(data, -2, -1)
  21166. isrgb = isrgb and data.shape[-1] in {3, 4}
  21167. dims -= 3 if isrgb else 2
  21168. if interpolation is None:
  21169. threshold = 512
  21170. elif isinstance(interpolation, int):
  21171. threshold = interpolation
  21172. else:
  21173. threshold = 0
  21174. if isrgb:
  21175. data = data[..., :maxdim, :maxdim, :maxdim]
  21176. if threshold:
  21177. if data.shape[-2] > threshold or data.shape[-3] > threshold:
  21178. interpolation = 'bilinear'
  21179. else:
  21180. interpolation = 'nearest'
  21181. else:
  21182. data = data[..., :maxdim, :maxdim]
  21183. if threshold:
  21184. if data.shape[-1] > threshold or data.shape[-2] > threshold:
  21185. interpolation = 'bilinear'
  21186. else:
  21187. interpolation = 'nearest'
  21188. if photometric == 'PALETTE' and isrgb:
  21189. try:
  21190. datamax = numpy.max(data)
  21191. except ValueError:
  21192. datamax = 1
  21193. if datamax > 255:
  21194. data = data >> 8 # possible precision loss
  21195. data = data.astype('B', copy=False)
  21196. elif data.dtype.kind in 'ui':
  21197. if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None:
  21198. try:
  21199. bitspersample = math.ceil(math.log2(data.max()))
  21200. except Exception:
  21201. bitspersample = data.dtype.itemsize * 8
  21202. elif not isinstance(bitspersample, (int, numpy.integer)):
  21203. # bitspersample can be tuple, such as (5, 6, 5)
  21204. bitspersample = data.dtype.itemsize * 8
  21205. assert bitspersample is not None
  21206. datamax = 2**bitspersample
  21207. if isrgb:
  21208. if bitspersample < 8:
  21209. data = data << (8 - bitspersample)
  21210. elif bitspersample > 8:
  21211. data = data >> (bitspersample - 8) # precision loss
  21212. data = data.astype('B', copy=False)
  21213. elif data.dtype.kind == 'f':
  21214. if nodata:
  21215. data = data.copy()
  21216. data[data == nodata] = numpy.nan
  21217. try:
  21218. datamax = numpy.nanmax(data)
  21219. except ValueError:
  21220. datamax = 1
  21221. if isrgb and datamax > 1.0:
  21222. if data.dtype.char == 'd':
  21223. data = data.astype('f')
  21224. data /= datamax
  21225. else:
  21226. data = data / datamax
  21227. elif data.dtype.kind == 'b':
  21228. datamax = 1
  21229. elif data.dtype.kind == 'c':
  21230. data = numpy.absolute(data)
  21231. try:
  21232. datamax = numpy.nanmax(data)
  21233. except ValueError:
  21234. datamax = 1
  21235. if isrgb:
  21236. vmin = 0
  21237. else:
  21238. if vmax is None:
  21239. vmax = datamax
  21240. if vmin is None:
  21241. if data.dtype.kind == 'i':
  21242. imin = numpy.iinfo(data.dtype).min
  21243. try:
  21244. vmin = numpy.min(data)
  21245. except ValueError:
  21246. vmin = -1
  21247. if vmin == imin:
  21248. vmin = numpy.min(data[data > imin])
  21249. elif data.dtype.kind == 'f':
  21250. fmin = float(numpy.finfo(data.dtype).min)
  21251. try:
  21252. vmin = numpy.nanmin(data)
  21253. except ValueError:
  21254. vmin = 0.0
  21255. if vmin == fmin:
  21256. vmin = numpy.nanmin(data[data > fmin])
  21257. else:
  21258. vmin = 0
  21259. from matplotlib import pyplot
  21260. from matplotlib.widgets import Slider
  21261. if figure is None:
  21262. pyplot.rc('font', family='sans-serif', weight='normal', size=8)
  21263. figure = pyplot.figure(
  21264. dpi=dpi,
  21265. figsize=(10.3, 6.3),
  21266. frameon=True,
  21267. facecolor='1.0',
  21268. edgecolor='w',
  21269. )
  21270. if window_title is not None:
  21271. try:
  21272. figure.canvas.manager.window.title(window_title)
  21273. except Exception: # noqa: S110
  21274. pass
  21275. size = len(title.splitlines()) if title else 1
  21276. pyplot.subplots_adjust(
  21277. bottom=0.03 * (dims + 2),
  21278. top=0.98 - size * 0.03,
  21279. left=0.1,
  21280. right=0.95,
  21281. hspace=0.05,
  21282. wspace=0.0,
  21283. )
  21284. if subplot is None:
  21285. subplot = 111
  21286. subplot = pyplot.subplot(subplot)
  21287. if background is None:
  21288. background = (0.382, 0.382, 0.382)
  21289. subplot.set_facecolor(background)
  21290. if title:
  21291. if isinstance(title, bytes):
  21292. title = title.decode('Windows-1252')
  21293. pyplot.title(title, size=11)
  21294. if cmap is None:
  21295. if data.dtype.char == '?':
  21296. cmap = 'gray'
  21297. elif data.dtype.kind in 'buf' or vmin == 0:
  21298. cmap = 'viridis'
  21299. else:
  21300. cmap = 'coolwarm'
  21301. if photometric == 'MINISWHITE':
  21302. cmap += '_r'
  21303. image = pyplot.imshow(
  21304. numpy.atleast_2d(data[(0,) * dims].squeeze()),
  21305. vmin=vmin,
  21306. vmax=vmax,
  21307. cmap=cmap,
  21308. interpolation=interpolation,
  21309. **kwargs,
  21310. )
  21311. if not isrgb:
  21312. pyplot.colorbar() # panchor=(0.55, 0.5), fraction=0.05
  21313. def format_coord(x: float, y: float, /) -> str:
  21314. # callback function to format coordinate display in toolbar
  21315. x = int(x + 0.5)
  21316. y = int(y + 0.5)
  21317. try:
  21318. if dims:
  21319. return f'{curaxdat[1][y, x]} @ {current} [{y:4}, {x:4}]'
  21320. return f'{data[y, x]} @ [{y:4}, {x:4}]'
  21321. except IndexError:
  21322. return ''
  21323. def none(event: Any) -> str:
  21324. return ''
  21325. subplot.format_coord = format_coord
  21326. image.get_cursor_data = none # type: ignore[assignment, method-assign]
  21327. image.format_cursor_data = none # type: ignore[assignment, method-assign]
  21328. if dims:
  21329. current = list((0,) * dims)
  21330. curaxdat = [0, data[tuple(current)].squeeze()]
  21331. sliders = [
  21332. Slider(
  21333. ax=pyplot.axes((0.125, 0.03 * (axis + 1), 0.725, 0.025)),
  21334. label=f'Dimension {axis}',
  21335. valmin=0,
  21336. valmax=data.shape[axis] - 1,
  21337. valinit=0,
  21338. valfmt=f'%.0f [{data.shape[axis]}]',
  21339. )
  21340. for axis in range(dims)
  21341. ]
  21342. for slider in sliders:
  21343. slider.drawon = False
  21344. def set_image(current, sliders=sliders, data=data):
  21345. # change image and redraw canvas
  21346. curaxdat[1] = data[tuple(current)].squeeze()
  21347. image.set_data(curaxdat[1])
  21348. for ctrl, index in zip(sliders, current, strict=True):
  21349. ctrl.eventson = False
  21350. ctrl.set_val(index)
  21351. ctrl.eventson = True
  21352. figure.canvas.draw()
  21353. def on_changed(index, axis, data=data, current=current):
  21354. # callback function for slider change event
  21355. index = round(index)
  21356. curaxdat[0] = axis
  21357. if index == current[axis]:
  21358. return
  21359. if index >= data.shape[axis]:
  21360. index = 0
  21361. elif index < 0:
  21362. index = data.shape[axis] - 1
  21363. current[axis] = index
  21364. set_image(current)
  21365. def on_keypressed(event, data=data, current=current):
  21366. # callback function for key press event
  21367. key = event.key
  21368. axis = curaxdat[0]
  21369. if str(key) in '0123456789':
  21370. on_changed(key, axis)
  21371. elif key == 'right':
  21372. on_changed(current[axis] + 1, axis)
  21373. elif key == 'left':
  21374. on_changed(current[axis] - 1, axis)
  21375. elif key == 'up':
  21376. curaxdat[0] = 0 if axis == len(data.shape) - 1 else axis + 1
  21377. elif key == 'down':
  21378. curaxdat[0] = len(data.shape) - 1 if axis == 0 else axis - 1
  21379. elif key == 'end':
  21380. on_changed(data.shape[axis] - 1, axis)
  21381. elif key == 'home':
  21382. on_changed(0, axis)
  21383. figure.canvas.mpl_connect('key_press_event', on_keypressed)
  21384. for axis, ctrl in enumerate(sliders):
  21385. ctrl.on_changed(
  21386. lambda k, a=axis: on_changed(k, a) # type: ignore[misc]
  21387. )
  21388. if show:
  21389. pyplot.show()
  21390. return figure, subplot, image
  21391. def askopenfilename(**kwargs: Any) -> str:
  21392. """Return file name(s) from Tkinter's file open dialog."""
  21393. from tkinter import Tk, filedialog
  21394. root = Tk()
  21395. root.withdraw()
  21396. root.update()
  21397. print(kwargs)
  21398. filenames = filedialog.askopenfilename(**kwargs)
  21399. root.destroy()
  21400. return filenames
  21401. def main() -> int:
  21402. """Tifffile command line usage main function."""
  21403. import optparse # TODO: use argparse
  21404. logger().setLevel(logging.INFO)
  21405. parser = optparse.OptionParser(
  21406. usage='usage: %prog [options] path',
  21407. description='Display image and metadata in TIFF file.',
  21408. version=f'%prog {__version__}',
  21409. prog='tifffile',
  21410. )
  21411. opt = parser.add_option
  21412. opt(
  21413. '-p',
  21414. '--page',
  21415. dest='page',
  21416. type='int',
  21417. default=-1,
  21418. help='display single page',
  21419. )
  21420. opt(
  21421. '-s',
  21422. '--series',
  21423. dest='series',
  21424. type='int',
  21425. default=-1,
  21426. help='display select series',
  21427. )
  21428. opt(
  21429. '-l',
  21430. '--level',
  21431. dest='level',
  21432. type='int',
  21433. default=-1,
  21434. help='display pyramid level of series',
  21435. )
  21436. opt(
  21437. '--nomultifile',
  21438. dest='nomultifile',
  21439. action='store_true',
  21440. default=False,
  21441. help='do not read OME series from multiple files',
  21442. )
  21443. opt(
  21444. '--maxplots',
  21445. dest='maxplots',
  21446. type='int',
  21447. default=10,
  21448. help='maximum number of plot windows',
  21449. )
  21450. opt(
  21451. '--interpol',
  21452. dest='interpol',
  21453. metavar='INTERPOL',
  21454. default=None,
  21455. help='image interpolation method',
  21456. )
  21457. opt('--dpi', dest='dpi', type='int', default=96, help='plot resolution')
  21458. opt(
  21459. '--vmin',
  21460. dest='vmin',
  21461. type='int',
  21462. default=None,
  21463. help='minimum value for colormapping',
  21464. )
  21465. opt(
  21466. '--vmax',
  21467. dest='vmax',
  21468. type='int',
  21469. default=None,
  21470. help='maximum value for colormapping',
  21471. )
  21472. opt(
  21473. '--cmap',
  21474. dest='cmap',
  21475. type='str',
  21476. default=None,
  21477. help='colormap name used to map data to colors',
  21478. )
  21479. opt(
  21480. '--maxworkers',
  21481. dest='maxworkers',
  21482. type='int',
  21483. default=0,
  21484. help='maximum number of threads',
  21485. )
  21486. opt(
  21487. '--debug',
  21488. dest='debug',
  21489. action='store_true',
  21490. default=False,
  21491. help='raise exception on failures',
  21492. )
  21493. opt('-v', '--detail', dest='detail', type='int', default=2)
  21494. opt('-q', '--quiet', dest='quiet', action='store_true')
  21495. settings, path_list = parser.parse_args()
  21496. path = ' '.join(path_list)
  21497. if not path:
  21498. path = askopenfilename(
  21499. title='Select a TIFF file', filetypes=TIFF.FILEOPEN_FILTER
  21500. )
  21501. if not path:
  21502. parser.error('No file specified')
  21503. if any(i in path for i in '?*'):
  21504. path_list = glob.glob(path)
  21505. if not path_list:
  21506. print('No files match the pattern')
  21507. return 0
  21508. # TODO: handle image sequences
  21509. path = path_list[0]
  21510. if not settings.quiet:
  21511. print('\nReading TIFF header:', end=' ', flush=True)
  21512. timer = Timer()
  21513. try:
  21514. tif = TiffFile(path, _multifile=not settings.nomultifile)
  21515. except Exception as exc:
  21516. if settings.debug:
  21517. raise
  21518. print(f'\n\n{exc.__class__.__name__}: {exc}')
  21519. return 0
  21520. if not settings.quiet:
  21521. print(timer)
  21522. if tif.is_ome:
  21523. settings.norgb = True
  21524. images: list[tuple[Any, Any, Any]] = []
  21525. if settings.maxplots > 0:
  21526. if not settings.quiet:
  21527. print('Reading image data:', end=' ', flush=True)
  21528. def notnone(x: Any, /) -> Any:
  21529. return next(i for i in x if i is not None)
  21530. timer.start()
  21531. try:
  21532. if settings.page >= 0:
  21533. images = [
  21534. (
  21535. tif.asarray(
  21536. key=settings.page, maxworkers=settings.maxworkers
  21537. ),
  21538. tif.pages[settings.page],
  21539. None,
  21540. )
  21541. ]
  21542. elif settings.series >= 0:
  21543. series = tif.series[settings.series]
  21544. if settings.level >= 0:
  21545. level = settings.level
  21546. elif series.is_pyramidal and product(series.shape) > 2**32:
  21547. level = -1
  21548. for r in series.levels:
  21549. level += 1
  21550. if product(r.shape) < 2**32:
  21551. break
  21552. else:
  21553. level = 0
  21554. images = [
  21555. (
  21556. tif.asarray(
  21557. series=settings.series,
  21558. level=level,
  21559. maxworkers=settings.maxworkers,
  21560. ),
  21561. notnone(tif.series[settings.series]._pages),
  21562. tif.series[settings.series],
  21563. )
  21564. ]
  21565. else:
  21566. for i, s in enumerate(tif.series[: settings.maxplots]):
  21567. if settings.level < 0:
  21568. level = -1
  21569. for r in s.levels:
  21570. level += 1
  21571. if product(r.shape) < 2**31:
  21572. break
  21573. else:
  21574. level = settings.level
  21575. try:
  21576. images.append(
  21577. (
  21578. tif.asarray(
  21579. series=i,
  21580. level=level,
  21581. maxworkers=settings.maxworkers,
  21582. ),
  21583. notnone(s._pages),
  21584. tif.series[i],
  21585. )
  21586. )
  21587. except Exception as exc:
  21588. images.append((None, notnone(s.pages), None))
  21589. if settings.debug:
  21590. raise
  21591. print(f'\nSeries {i} raised {exc!r:.128}... ', end='')
  21592. except Exception as exc:
  21593. if settings.debug:
  21594. raise
  21595. print(f'{exc.__class__.__name__}: {exc}')
  21596. if not settings.quiet:
  21597. print(timer)
  21598. if not settings.quiet:
  21599. print('Generating report:', end=' ', flush=True)
  21600. timer.start()
  21601. try:
  21602. width = os.get_terminal_size()[0]
  21603. except Exception:
  21604. width = 80
  21605. info = tif._str(detail=int(settings.detail), width=width - 1)
  21606. print(timer)
  21607. print()
  21608. print(info)
  21609. print()
  21610. if images and settings.maxplots > 0:
  21611. try:
  21612. from matplotlib import pyplot
  21613. except ImportError as exc:
  21614. logger().warning(f'<tifffile.main> raised {exc!r:.128}')
  21615. else:
  21616. for img, page, series in images:
  21617. if img is None:
  21618. continue
  21619. keyframe = page.keyframe
  21620. vmin, vmax = settings.vmin, settings.vmax
  21621. if keyframe.nodata:
  21622. try:
  21623. if img.dtype.kind == 'f':
  21624. img[img == keyframe.nodata] = numpy.nan
  21625. vmin = numpy.nanmin(img)
  21626. else:
  21627. vmin = numpy.min(img[img > keyframe.nodata])
  21628. except ValueError:
  21629. pass
  21630. if tif.is_stk:
  21631. try:
  21632. vmin = tif.stk_metadata[
  21633. 'MinScale' # type: ignore[index]
  21634. ]
  21635. vmax = tif.stk_metadata[
  21636. 'MaxScale' # type: ignore[index]
  21637. ]
  21638. except KeyError:
  21639. pass
  21640. else:
  21641. if vmax <= vmin:
  21642. vmin, vmax = settings.vmin, settings.vmax
  21643. if series:
  21644. title = f'{tif}\n{page}\n{series}'
  21645. window_title = f'{tif.filename} series {series.index}'
  21646. else:
  21647. title = f'{tif}\n{page}'
  21648. window_title = f'{tif.filename} page {page.index}'
  21649. photometric = 'MINISBLACK'
  21650. if keyframe.photometric != 3:
  21651. photometric = PHOTOMETRIC(keyframe.photometric).name
  21652. imshow(
  21653. img,
  21654. title=title,
  21655. window_title=window_title,
  21656. vmin=vmin,
  21657. vmax=vmax,
  21658. cmap=settings.cmap,
  21659. bitspersample=keyframe.bitspersample,
  21660. nodata=keyframe.nodata,
  21661. photometric=photometric,
  21662. interpolation=settings.interpol,
  21663. dpi=settings.dpi,
  21664. show=False,
  21665. )
  21666. pyplot.show()
  21667. tif.close()
  21668. return 0
  21669. def bytes2str(
  21670. b: bytes, /, encoding: str | None = None, errors: str = 'strict'
  21671. ) -> str:
  21672. """Return Unicode string from encoded bytes up to first NULL character."""
  21673. if encoding is None or '16' not in encoding:
  21674. i = b.find(b'\x00')
  21675. if i >= 0:
  21676. b = b[:i]
  21677. else:
  21678. # utf-16
  21679. i = b.find(b'\x00\x00')
  21680. if i >= 0:
  21681. b = b[: i + i % 2]
  21682. try:
  21683. return b.decode('utf-8' if encoding is None else encoding, errors)
  21684. except UnicodeDecodeError:
  21685. if encoding is not None:
  21686. raise
  21687. return b.decode('cp1252', errors)
  21688. def bytestr(s: str | bytes, /, encoding: str = 'cp1252') -> bytes:
  21689. """Return bytes from Unicode string, else pass through."""
  21690. return s.encode(encoding) if isinstance(s, str) else s
  21691. # aliases and deprecated
  21692. TiffReader = TiffFile
  21693. if TYPE_CHECKING:
  21694. from .zarr import ZarrFileSequenceStore, ZarrStore, ZarrTiffStore
  21695. if __name__ == '__main__':
  21696. sys.exit(main())
  21697. # mypy: allow-untyped-defs, allow-untyped-calls
  21698. # mypy: disable-error-code="no-any-return, unreachable, redundant-expr"