From 3e7ad0597311505bbad6ccbaab99cb43244f217e Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Tue, 28 May 2024 20:18:54 +0200 Subject: [PATCH] #948: TIFF 64 bit FP support --- .../twelvemonkeys/imageio/util/IIOUtil.java | 25 ++++- .../imageio/plugins/tiff/TIFFImageReader.java | 106 ++++++++++++++---- .../plugins/tiff/TIFFImageReaderTest.java | 9 +- .../resources/tiff/floatingpoint-64bit.tif | Bin 0 -> 23942 bytes 4 files changed, 112 insertions(+), 28 deletions(-) create mode 100644 imageio/imageio-tiff/src/test/resources/tiff/floatingpoint-64bit.tif diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java index 3e053f85..c3009826 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java @@ -40,7 +40,7 @@ import javax.imageio.spi.ServiceRegistry; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import java.awt.*; -import java.awt.image.BufferedImage; +import java.awt.image.*; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.InputStream; @@ -283,7 +283,7 @@ public final class IIOUtil { "bitsPerSample must be > 0 and <= 16 and a power of 2"); Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0"); Validate.isTrue(samplesPerPixel * bitsPerSample <= 16 || samplesPerPixel * bitsPerSample % 16 == 0, - "samplesPerPixel * bitsPerSample must be < 16 or a multiple of 16 "); + "samplesPerPixel * bitsPerSample must be < 16 or a multiple of 16"); int pixelStride = bitsPerSample * samplesPerPixel / 16; for (int x = 0; x < srcWidth * pixelStride; x += samplePeriod * pixelStride) { @@ -305,7 +305,7 @@ public final class IIOUtil { "bitsPerSample must be > 0 and <= 32 and a power of 2"); Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0"); Validate.isTrue(samplesPerPixel * bitsPerSample <= 32 || samplesPerPixel * bitsPerSample % 32 == 0, - "samplesPerPixel * bitsPerSample must be < 32 or a multiple of 32 "); + "samplesPerPixel * bitsPerSample must be < 32 or a multiple of 32"); int pixelStride = bitsPerSample * samplesPerPixel / 32; for (int x = 0; x < srcWidth * pixelStride; x += samplePeriod * pixelStride) { @@ -322,7 +322,7 @@ public final class IIOUtil { "bitsPerSample must be > 0 and <= 32 and a power of 2"); Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0"); Validate.isTrue(samplesPerPixel * bitsPerSample <= 32 || samplesPerPixel * bitsPerSample % 32 == 0, - "samplesPerPixel * bitsPerSample must be < 32 or a multiple of 32 "); + "samplesPerPixel * bitsPerSample must be < 32 or a multiple of 32"); int pixelStride = bitsPerSample * samplesPerPixel / 32; for (int x = 0; x < srcWidth * pixelStride; x += samplePeriod * pixelStride) { @@ -330,4 +330,21 @@ public final class IIOUtil { System.arraycopy(srcRow, srcPos + x, destRow, destPos + x / samplePeriod, pixelStride); } } + + public static void subsampleRow(double[] srcRow, int srcPos, int srcWidth, + double[] destRow, int destPos, + int samplesPerPixel, int bitsPerSample, int samplePeriod) { + Validate.isTrue(samplePeriod > 1, "samplePeriod must be > 1"); // Period == 1 could be a no-op... + Validate.isTrue(bitsPerSample > 0 && bitsPerSample <= 64 && (bitsPerSample == 1 || bitsPerSample % 2 == 0), + "bitsPerSample must be > 0 and <= 64 and a power of 2"); + Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0"); + Validate.isTrue(samplesPerPixel * bitsPerSample <= 64 || samplesPerPixel * bitsPerSample % 64 == 0, + "samplesPerPixel * bitsPerSample must be < 64 or a multiple of 64"); + + int pixelStride = bitsPerSample * samplesPerPixel / 64; + for (int x = 0; x < srcWidth * pixelStride; x += samplePeriod * pixelStride) { + // System.arraycopy should be intrinsic, but consider using direct array access for pixelStride == 1 + System.arraycopy(srcRow, srcPos + x, destRow, destPos + x / samplePeriod, pixelStride); + } + } } \ No newline at end of file diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index a99d0302..21879b90 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -815,8 +815,11 @@ public final class TIFFImageReader extends ImageReaderBase { if (bitsPerSample == 16 || bitsPerSample == 32) { return DataBuffer.TYPE_FLOAT; } + else if (bitsPerSample == 64) { + return DataBuffer.TYPE_DOUBLE; + } - throw new IIOException("Unsupported BitsPerSample for SampleFormat 3/Floating Point (expected 16/32): " + bitsPerSample); + throw new IIOException("Unsupported BitsPerSample for SampleFormat 3/Floating Point (expected 16/32/64): " + bitsPerSample); default: throw new IIOException("Unknown TIFF SampleFormat (expected 1, 2, 3 or 4): " + sampleFormat); } @@ -1153,10 +1156,8 @@ public final class TIFFImageReader extends ImageReaderBase { } // Need to do color normalization after reading all bands for planar - if (planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR) { - if (normalize) { - normalizeColorPlanar(interpretation, destRaster); - } + if (normalize && planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR) { + normalizeColorPlanar(interpretation, destRaster); } col += colsInTile; @@ -1252,19 +1253,17 @@ public final class TIFFImageReader extends ImageReaderBase { // We'll have to use readAsRaster and later apply color space conversion ourselves Raster raster = jpegReader.readRaster(0, jpegParam); // TODO: Refactor + duplicate this for all JPEG-in-TIFF cases - switch (raster.getTransferType()) { - case DataBuffer.TYPE_BYTE: - if (normalize) { + if (normalize) { + switch (raster.getTransferType()) { + case DataBuffer.TYPE_BYTE: normalizeColor(interpretation, samplesInTile, ((DataBufferByte) raster.getDataBuffer()).getData()); - } - break; - case DataBuffer.TYPE_USHORT: - if (normalize) { + break; + case DataBuffer.TYPE_USHORT: normalizeColor(interpretation, samplesInTile, ((DataBufferUShort) raster.getDataBuffer()).getData()); - } - break; - default: - throw new IllegalStateException("Unsupported transfer type: " + raster.getTransferType()); + break; + default: + throw new IllegalStateException("Unsupported transfer type: " + raster.getTransferType()); + } } destination.getRaster().setDataElements(offset.x, offset.y, raster); @@ -2080,6 +2079,36 @@ public final class TIFFImageReader extends ImageReaderBase { break; + case DataBuffer.TYPE_DOUBLE: + /*for (int band = 0; band < bands; band++)*/ { + double[] rowDataDouble = ((DataBufferDouble) tileRowRaster.getDataBuffer()).getData(band); + + for (int row = startRow; row < startRow + rowsInTile; row++) { + if (row >= srcRegion.y + srcRegion.height) { + break; // We're done with this tile + } + + input.readFully(rowDataDouble, 0, rowDataDouble.length); + + if (row >= srcRegion.y) { + if (normalize) { + normalizeColor(interpretation, numBands, rowDataDouble); + } + + // Subsample horizontal + if (xSub != 1) { + subsampleRow(rowDataDouble, srcRegion.x * numBands, colsInTile, + rowDataDouble, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub); + } + + destChannel.setDataElements(startCol, row - srcRegion.y, srcChannel); + } + // Else skip data + } + } + + break; + default: throw new AssertionError("Unsupported data type: " + tileRowRaster.getTransferType()); } @@ -2091,13 +2120,24 @@ public final class TIFFImageReader extends ImageReaderBase { } } - private void clamp(final float[] rowDataFloat) { - for (int i = 0; i < rowDataFloat.length; i++) { - if (rowDataFloat[i] > 1f) { - rowDataFloat[i] = 1f; + private void clamp(final float[] data) { + for (int i = 0; i < data.length; i++) { + if (data[i] > 1f) { + data[i] = 1f; } - else if (rowDataFloat[i] < 0f) { - rowDataFloat[i] = 0f; + else if (data[i] < 0f) { + data[i] = 0f; + } + } + } + + private void clamp(final double[] data) { + for (int i = 0; i < data.length; i++) { + if (data[i] > 1d) { + data[i] = 1d; + } + else if (data[i] < 0d) { + data[i] = 0d; } } } @@ -2395,6 +2435,28 @@ public final class TIFFImageReader extends ImageReaderBase { } } + private void normalizeColor(int photometricInterpretation, @SuppressWarnings("unused") int numBands, double[] data) { + // TODO: Allow param to decide tone mapping strategy, like in the HDRImageReader + clamp(data); + + switch (photometricInterpretation) { + case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: + // Inverse values + for (int i = 0; i < data.length; i++) { + data[i] = 1d - data[i]; + } + + break; + + case TIFFExtension.PHOTOMETRIC_CIELAB: + case TIFFExtension.PHOTOMETRIC_ICCLAB: + case TIFFExtension.PHOTOMETRIC_ITULAB: + case TIFFExtension.PHOTOMETRIC_YCBCR: + // Not supported + break; + } + } + private void convertYCbCr2RGB(final short[] yCbCr, final short[] rgb, final double[] coefficients, final double[] referenceBW, final int offset) { double y; double cb; diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java index ac46736b..d7658d19 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java @@ -32,6 +32,7 @@ package com.twelvemonkeys.imageio.plugins.tiff; import com.twelvemonkeys.imageio.color.ColorSpaces; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; + import org.junit.Test; import javax.imageio.IIOException; @@ -43,7 +44,7 @@ import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import java.awt.*; -import java.awt.color.ColorSpace; +import java.awt.color.*; import java.awt.image.*; import java.io.IOException; import java.nio.ByteOrder; @@ -55,7 +56,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.AdditionalMatchers.and; import static org.mockito.Mockito.*; @@ -100,6 +104,7 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest-VMWRBbBvgb5YdwofcXBMrDWOm)a>zM3L?Jm-LUKwegp@?ep>n94 zBBvq|kwg)ZsQj+>xAz#|c=u@R|M%O!@Bh8u*t>Vsvz})?hx?iHn%BJMyzljDYOZ5q zXJTUFW@2KV&BTmP1b>oyEk0&$2Wc{>2|5{I~oJzB#iBf44F*rSjZ1j(;dZ{DU!U>9f7#Ar$qnSgDa`D-fox2COmkpNW~K-cCeB}5 z_meDt*@}zoQP&q@$O}me?Kapc% z&ubs;VkqKE%eCcCr%1Hv7VF^S6p3-s_xyC8BEP%--^YJh;P37Fzr`*B=K_@SIw=zI zYQOEC0E%q5#d&;wBt^zoFY(SkO_9@FoXtvyC}MK?DWBa5iiC<~cNe-*#A-EPO`IP^ zepF`EKRZSd8CSZ>_M;SWKKDbn)086X_1Xts0YBAJG?O!46cLL(KmSI~zj5?u!-b*X zhL)FKDe@_V@BK(7MOM{_e$T!}kz(=wH??W-<3gR3qahS2>lw)x4y6dI^u9$QmK512 zEpSlMfg-i%SIX9UQ)K7KshCz*ip;GQoVguNk+w2F{)fOtT*4jPPL2$*Mcc>HrvkUA&!U0uIA>& z^$B|-N;}Oc!Z|X^wHp4H&7S#^u1^vEd)sZLttlcY(>%ohPrRUL;X=w33Cl~$XVRjG zhgsZ*)^~K0W#u9zi>>C5^Y8Wn7uQ)YyVvZb2-|a!4f*vHiG8bIYg|SV|0=EVlTH*7 zUpTZlk&hx1W?T1N=%bUGGi+97?Q|04WZwV$2JqA>^Dt77PV{8<*{n6DksEBBEm_Po z^3U4wf9v&CHqYEV6e+SmCXM4M_&lb))!ys?MfP-A-hR59A{~464wb{-8Z}`8!5S2~ zaesGG*%nhTL)8TKz(ML5dhm zt55TBAa4})#0D*-$Qrxu%qJ!ksk^zn_TwYOe`C&J<~oY(Xx>5Bf?mm%_4de8qsaL+ zZD;au{f>+;Nv=#3`O3`b(rBfV+OC;%n)Y;ZQf&Pbt!Ff{O5dC5y*rKMyq|r2KE$`PNuak_(J1G72G%FQLxNmT$g;u z=YT(jvO6LibT!Tro!P*n#C*WqC|=`q(r-O z0T;##M^$1kQKVNp;H3W(;I-`rX)dS8?HYPb^=y zfU{Yhx?7*n$)`C<)qT6^WSPDG>(~++F#+lkh~<}E?9%?%^MCc}+8^5Yb#QcL5R#&vJ{PCkmj zzQLf;%es>kk!D%^**BRY+p0YVUcRKr=Q>3)c?Ef-+;i&WRf+`pC|lTLe8QnGwK;IS zsbznX*)2S;NnLjXcqi`bW{2E6@UwK_7&d|xw zOM^Y{(&;4QlwW1BHl4_*{V1LM@vrCizvZiT_KQm*!SAOCMwOBnsscj>{uBm49Q z_fh1EHQR!y1d2?)e=4&PI2an9nX?>q#H)!d~Q77`IE56@iC$jhyMV6-U23`f9g)r$za3UXY>^?5-HcBUb?Mp8;nL}52 zIa&2^(f_ph|GwD1>*wv{$?+Fsx)bU&GEZH3^!d0vx!CKkByfX9*1Yafev?ik<3&@e z_`l2lm*1o2sOOArHS83}sIPlOk^N7C95l-iPlK{qD`5B9tCF2gh`+hDYI|3vQ$&@s z^$Z8%Ff}oKw+nPzc9#746!?2xVR}!~F4TWY0gW!eg|ZDR#RDD?`^2Lh??REyHB;JO zk>6J*soJW9lsZt+`nE5!DN<~1& zxT+joAo`Rvw3IKTM^y(@rDiWEh}*KYgs`rmei>i(wd@`U}&Qqg!F z8rjHR?Kt^Oo}6_I>RJ>}BaxN04eePpvWrK+uw@gC*c>U)iv=!*s!YpYwa63KPUmd{ z2EWI}j}wLcCa{|aKg+GWB=~Vw@>|^s=qAs2kH&h6Ea8r1KH36Z_KHhpdJuUvujp+n z^ww3r1>M(fARp_RM|UD_m-x>OTNI4^FFejS8@&6pP9a_l{@0z=l9YawB9^z-EmT0< z7F(rM*FqmuyPns4c^&zC&ES2vSop1f-r2*iDI#IDN$Sl5=*Tm^VZz?XBe6xQbKy^& z6pq#(a;Oi?YZi^@BX7Jfb>EC4%dghB?lq=}^>cHUGpHx;XqDLWCjWW;zdc-Y34e|}Nx1*RLPb@?hL!ODl{vPZm%#6mvP%3s@Jo*7SZ8MuMIKE(9hfeKpB4E-P2V7HKM&8! zYo$owWYal$_^DOLT>sHainM<#b~ejEoIP3XYG6wdMF$eT2so%(x!3dqbXY@9d4v@7 z#PhziW4iFa<=t6b9C(h@%FI`xxQ??$IeG3U@b=2*snOsc1O5eVixI!FjF&q(5bqB; z1Sx5XA_bPyX$CgnbC+kI<&%*g%DP{w7E^@F;q2(ZQ{=Jzizk}ES1E(snt?GG$BDNl zb1x!qaUJ!q0RF@u&+5(wKFqB5?mf4fA{mi38)L`+y#9Z4?@e%oAdOfv37468(Z~^* zqY=C(XvC-7TH-nwogW5=ZD*9rkS3Z_4sy} z|AzJ|gLp5*YklV3I%kT!F7Fx@IS4!6TR&dvg8J)*&-^==DDv!K|J+FEEps9ENyB#3 z3Ch0pdq1M?`ZAU}w+4PKXh~X+{Zpp>>y%0mZ!gBzn*m1!BY7vkI-sAlXwmp2^1*hU z4_x9qDDs|XuBSMj`z=R6H|H|+<+zziA$UCGVn~rn6?DMTNyYCs;J;&~D>#C|^BODq zxDdx;$HR9VhOTq_meJ6M`&ZEH@-`qJ-G9!`shdxc@9(`{_rHQptbJ;f2j05w8^Cw1 z0M{EzH|8U+1!p~pw@U!Njkl%OCt_T)mZ_%#r#cPUKV{YaynQ66?@_D!xS?00wYXB& z(#b7dkNM9e>4a;xuDCDsOS7Lhso=_T$HFK9&Z&e@(^?uD0yr3|8b5F}67gqy(fc+0`}hh`Jl+63z;0k~@*a7@ zc?DnKFzndu7paeU&1lGLW1r6;Zd}L2tYP;zTQ}}*L0sp?8dpa{zugz|(euK6MWm}b z9zZwTptg^jAkSwU>;3o=erAkp+`A0dty(vBb35WbXk60L5xUNCXYaFF;S`B~CRE7d zi1Y1(`LhxCigOa!M9))XWXJGg#cLF~SWioEN~1{9>YrZsF+LqBwae4s0m+O~b_)1t z4|o^57SBr)i$0oc0zPS}S{ZBv9O}QET#vfoclPh8TlC+BQR z?p>**lb-jA`4VGvVmbQ$h)@Hav`glk*x^ej^uB9el3VFyU-!qkH6FjOYt9`F9Thc2 zAI|;l4=d>Jl{Mb7Uf|mo`ZBdMrzzt8p7+Bd*iV6HgX1m4>6-Be)Gqi%TF!83DR^IG ze0Qu-HR3KzUuqcpRG6^+{`=5>XCyidke8L?)|P)n{^q3_&e`mV{98Cx<7)~1C%bC8 z2Y66l$8lQe80z+xMUT4T;Fn#)mCE3S=Cva_>OpvZLb>WwTwi!d*f`7sIzV>iyC(4c zV^&USu3HqD%}UkljR9WVb4RWnKz->>bqst(UqZ*O z&iJw(-lK8L4f`$VU!3wxdMp0t_y4+UPKU-G&`H<3DW{|AsE?A5l)Hlm)4uEGrXsIx za*a@(gx}`OwavY72z|ENc{dMl$Gl9tV~W7+-@ji0%QxR)N$77igUl!3eL=m`z8Scl zEjF!0-Wq-QvNxK%z{UD~Y!R~XOV>JE@uSe~5edH5dtvup_9~Nsa*EW~l#APde={i! zYhB=B=$633>=5LUP1Yy2;y$t;O#)giaKDZibyesuqxLef68Jyt#ZZeZ{A%6SulyN) z%Y0Rzmy7%RCCTL~13!YJ`x_=4DB=}$Y{93)(4{v5v|Pd8A7&L!HY2YaF5HJ8-SG2crs1~!n%q*TV^6glG4vSyczBaO(U#FNHB z^sQXOawoxetrxTpA3?lDM$nE_!rsi97Ovm!f=5#vC)paYpAx))H;N*I=RY`@!=Jz7 zv9lrNV#i^(p!+}cC*bF}?$4GM@EgNtFHiAV^z9|}!{)+2!fU^qFGL+(F}yrR75F>r zlrFBZpCV_%KU;o&-@N>IZ$xH1peC}zwd)8^7MsOiIp0-uUHbN zie6s}a zm|R({w6l#OYgq(uY^|k8P4rjGx!|P)YOUa5H;Pzf91b-_+{ZdB5dT%@Z<2Ve6tfO^ zy?E#f+g!|7m`cqSs0Kf>%KK!t(TR445VzTj|2hBjg{twCh{+RH-MLu_ujGh^d)(o# z-{gpshea$M`o2r*Xr=cD8ku0_I9xFYeb#ptV%#aEknmc^Zf4?`Baa->9{-01%*QF!AF1advpM-tWN*f+kTtYpj_%f;l z`(eEKD~;eE^PDepPQtEB6dvVrY(qc5W;#|J*F9A_BG?FgJnzpu^Q*rr$X|_NW6x59eqgp3`iu z`!WM38&{b4N<2Zofprff0^`XDG|c{l`y9O;kf2ozT{$~riAXAVpiove(jEOHIRPba zJiq1Zhxq_ONY=L6W;$6@aU-OmgHCo%Dr`K~ zNhe9`wpLI459t4A2bGT3Rm&4En^B$!)b}X{DeSABps%BH_u!SQG~zY=_Ib ze!q+VMaOsNhi9O_$1=E=!u@VqYjwxMe|{Oc)%CZ2<+mlR}95nOd6lF#M7z_k7!;tKjdBh4X@{DRQsHrs+J!>9%W;yacXq3N^fH4Ib*O zzqWFx9?s853XKMTFmYN*?L0`4BV)!f2QE%23mtGo2{k8t0*Wo7U{2%XXXDY$dEY${e@W(x==PZsM6dB!< zf5jj1D)w=<>__l3d*E_vsztH|bW)>)vC~BexC& z2I75=T)8cN*9QL7`7S!81;1w$D=z`Ariw>hBlIzj)`XVZ=D!paa$%<%rR-22Y3u_6mYu;>!d`h&FhSUCXr<{(sK8{p~Sl^aBJU z+Ga^XFW7r@-a!BV>PgL^_PMA#YgoVD5<`D*^vbey1@LE&MBQBriu5drHD0dy-;aZ9 zMjC0EMX)o+QlU+-w|$P(&mPoUr4~>rRTR-q-59f=3-8r#5%UxN8L2pMs-h0|NLZh9 zrxg8jzan1MT<~RnjmSOtNxsl*TNvW^%(ECzF}2^1e<7oR#?eEOEzBK-PjElC&`%7d z7tm!h_a98NG02<{1BI%0n9s{sP~X-#7i-_6m5h5uce_tb_T%+=1ekxL-r! zJDWn}jd_**^W=@8J9=xExuK3f)RS)U!4iE1+v^h3$vFSw^Ug_ZcU_rytqi|bY&aOQ zZ6nsNjuod52+PdmjMt~nu2gre*C)3gXs9bd-I_1kJqrKJ+7EW!L!1tO z;i+-z#Cs^^vOmH1kF_t0hag|-tr$v?JO};1>HX~ch%@^myzhkYp1*tUDOPvaySUFs zmg8FKZK#*1{C*kmwx#2z%Rcby`aq+zqPYKlozVVHjHmScW61*eeKORvT>J{=`54v} z+_=u~igU1OJ~DUZSb z^i&!55X8Mp8&|;(0r2wbvFCRcfHSrl4&zn8)jf6&-3{nJ95TG}3I4HN#Jx-$d86>v z{%1Kp(1EtLbml;c$o;Hbf7gQ|T29x_{NnkL9eZ-Vz#e{UkIU3u2L5(`y_tyoc0{dD zZ6uT;=?f*o*zEWB!;7Ra(tYh2kort#| z{08yA^ndbeOU+O(_*wGlPiJ57itWnx-l*@(`afFOSV7;{v&MBDhCM8*$|v?B?icJo zuMXZm^u@lqTiK@@;m5@5XE&H3 z-Zw0dn>Y^LvF?%+FF&qha7Ktyzc@ z-m=NX!!a1&nZ)=_;DN>G-V_@F4}JX)4t0X}7dtLj7y{mzu1s%^0IqzakDp^hoc-~+ zf7`c;9!!oNrHJdZu_+hu_kk0619kA9&WfV7Yj4B9ehnLwV-XKr$7{=hqlQh2AB;08 zvQ<`+&$y5x0@sbtU+u-bzq8>XrEbjcv+aLRL)`m`Sj`OFLHr9J)%~RtG*%ux=8}f- zTU%Ch;C_M)Wwwf8;Q2?a8~7tom*+;ief5ODW4CGj0DeXLR7;1FkY5sCmd=A;%v=sU z?sfschp{_svc&frTlTi?MExip)t>-AoVGQ&Qep_d#w%`T+l_d>UpmZ+ecL;VkLs}B zEPU)B19>A#r=nv5_Na3Dk(LWQEz~*2wGw!(Z2LDSXpB-5)f_NzZka`06<%m?X(FaZh{J81Cu=lyjrOvCjQ)G;O#rteI zojmcce{d}b{;jgVz!^v<+>si$BCpcP)|*duc)p>N2ODactkdXZ-f3Ico00H)Q}|w{ z{d5vLd`e>35jyF&7G9@aL?^QAR_2OAue4w3Kj*q1d3{ZP!659nWV(7!0qkQG&&q!Z zar*w_uKaiK57(i&E%wke-@PP!^S=H*UrmNjmXXKk+f0}&l!U)R*MzesBA)tK7&qqm z;l63N9$kgs4-Y)ItU3r?>S&Pq#18$tfKN{%!3PDd!d*Xe;lF1iuI2@Z{|icD5!J|3 zjU1uJfP?2JW1jqo1V6}1AD7NUoE_aJ9R=LD&s={kf_P0m&*q<5hwbnlol4|4Vct_W z&cgnDexu8+phI>(lu^+~oQFg-jQ;A^HfU=+G{QK_g68Ed0Kdr`)Bc(UTs?F$l5u>4 zn>3<)nM*G z0)F8ME>aW)@3=0iwd;m|3Un2-(_YcZDehe-NEUR%fsFRz7&@`YQ)VAR-&V-o^HRxu zI$7}W*+kVzI%&O?c#+o@ef)NVO@S-vByUZN(HUdl!TI8XHSu&Zx{^hmg6@dQ^}1gp zhdk_AJ=B#W;I?6c`%thi<_ZsX3@?MBR^>7$yAP|I;+-rtB~9 zbC!`l193d!@N>2G6WFt^e2EC`8pFZc9~uoER`K2647?M~bb@Iy{PX!x+{Lka@OXgf z&$HkksYPox7s09h=A%pfXT56)Ym%ph^G5AvqgFo@njblgY0Pm!U=n(b z<^8s*&vf!$>Wd~9^jv}Yk|jYD_=8^K=?LC2VOw9Y`Yc6u55HT$XAM2I`(alz@TUL8 zGwGrgc#Jo?_q`eJ_uzS7I_zwgkt3~*xKnHF9#grBezvZMd{>O#+}=O zz|G89^c$Di(4n`Z3&}d@|DZMjc}?`?m#-XkF#vCUePenHap3cILqlT#>^t?;q`n09 zf2O;J1O2U*6UL3Kdw%c#iRC+kFVA6+4~>UL0@}gb^B-vanulo}k!>hBhrC(d&9Vyl za?8M%Z@i}|Qf?u=+XMdoJk~X<1NObTOs+l`e!UxY^K1kB(z8A$V{Zd+ArnS@<=6Jy%USaNttc!>Wh(==E9QkP5yXExPBz z@AxZ^bG+W44!*YBs}uhc_vfu#uw^;?emQ^1k^~0XCFxmGmc}3{Ms07?CK-hIc2~ab zz}D|eUakv+)Y&(BrtmPxK#aEZ8Y7CV5MvS&mmMJIld4rZp(&?$cT8t%vk zF4pdCRw2My@7(?Vh$nBcHJ`2^KdtAVQFF%oR|zOMMe9X#iLsynze2X)hO z2d5<9T6CPa^Qo9ht_`1 z!@Bk#if3FBBIu<0^zl6&@K^PopiQ3hkk4IR#~U``{dX_Bc@X(#B0=EfXD8^W7hjH9 z?ZES1cN=ZphWK(V-8qQ+M=p!;c#iy?`E~SSi!%_&8C^$*U{%`Ip^F~3>|V)WCaUw;X1f8>M?L|wp~^#zY2VE z==2Wzi{R;%mhQvGh_CEVoiCIz-`n&hE*!kJLUGSN?a#=wW#Y^CdN7FE^VGoL&kVB1 zA;MWFP=V}L`H^$%ngUrD`caDKnF3Ls31j}2uRvyE^)?L|E07N3uY0)q7{r=g`BU{u z2AMTmag$a!@}j3>#fzoL7uG8_=_}y5Hgk*W9Deb++a5=J@3u>&dEZ_1AG|~!96-ME zDjC!5xCXuAy;G- zb-(gKq+0s!Jp$0LZo=7&tg6yvo%S{ijIv5r32yS1WR5aV@xWbs@P>p1efLW|abS8hw3s0A)I z^leO@8w}j$a7N^S7oPX6S^c07dG|zokq{^1?A0D4-WSNzi!N~# z%%8u{aW~wHdD4QOqHy51GCFe80_aN9Er}t?l{BIpIX0+eK_dg}oI3_te((R$t!M5Y zFQ5}^!KT{89@H5=OHVk14=-l&%Q<78zb4*oBlzK{#YD{uJWp$Oy>dEmV7KR_ffe+j z_dDr#g4M{ICJ$tou-}>yEpQom!IS&Mm?+}CGriH#ItqBdp_;NE{6E~YaX}k+^Gd++ zIis-S9kKkpCh)4s;^D>|JSY8aD|^R7I#J9le)2YxPR{I~lduhV7|{xk5ySTaEYcb| zz?o{yfn#o)8D#1#H?_2xK{BisItuPlAmq|J^^pbzGM!T+lKWGEaJqJ`-&qa6zp*I2 zenx>P&hnr2`a1G~pR>Fva2&f^X2e(%c-&RKuAU3yQj}Qz5$~z*$~9b$_b_=~udjf- zRwwz4D_|7w*FEX}Yh9$9Zgi%58F<(IzMRoR%p;!soM;Z-;7N{H@)SB_`;i(OrR&hC z&QU!5z(c!WrOYPasWSOM&k0V{NfK>sD$x6SC3p9)wxAPUy^oVzh+i>9OWT_xbdqvm zyZm(#;Hga|eh1<`L{_pZ{}k5O9un@U_kvvyh;bZ8ys*TN`aWEX=hvHFa08AO=Qw*v zC(%f3^es7wVR>@WR;y;mw%@P!xJpI7JZnfN5)Sq=>wtqv1MBXU-E^|n*;;ZM{B-kt zXPk`^>=IS+Lc<)oU{`aPMJVzNo7b8B;5WVtkGO*}V85lTHj#no%W)0yKRpY5Wu10L z1Nbl07na z|2DN=(KQ=>Z~oL7WXgp32tWFkU~ycR#xfiLzvXnUXfDacJkimlS?S~GLzIaf^}NC$ zKVLRgN{cCwOonIhP=Eq?T`lwQ+I0ouvO_Ju-dut3?YaD2)R#fN9;YR2$8)tyRlUOI zg2#93D%3s4ypNx$z!y2JW9U)uJzRu7WOoBQKjMGx=B={{^45{KZE;eW$PabBc4k?~ z!%k@qf(7uew`x7BE$WtY!@@1F&vwUo$DX7@5A9`poO2uc*+k$<=>@EdrTZAftwNt} z&4q3Wtlzj+)a)*dIxzjSm|4pLtUro~x*TZ}k5;uFq)li5B4{Mj@6 zy8VoUJ`yzyD?5OCEq99RI_{@^B7dv&2I$Cb%hV^X(THut^z_~&d6MCAyF0Pr@7H<# zF%SH0znOF#x-d;AgF_Q9k3PWqp~D}anII2H$ClnWzY%(fzc|Xt34PMc?Y8rw)3WpC zjZXugCj=HHMsEY&W$R8|0#9j}o-*{odA+VhmXG28O`kGNKf&KWi*hc^(!%cWCWO=$SVRlOFUv)KmgmP3!4|h3%?%?jz`A54Cj);7g_m5_=eU zuWM0}xe33WbIJUWDg=8SSaXaWI#XrO`yjhO2D$E1E3U+X_?Mi|mUBdbtUue`prxun zBooSy4IN^T!8=P01P|l-fCVqELjUVIF)z4=K1-8w&XEHXSXaS2I_!+`3Vg7!`n9eo zzRA>|D*^Kx%lC2?B!b8EJx@x6LH`MLz1{{~FEVuq{04g{2PXD1;rqd`)>+!X&Ep$< z`|KVAzpJ;rw*zj?Z1!q7V7zATUW!Tg=p;<`X6Ujv;Qdaa!JPrHUrU)rHiJQ4TxO(8 z=QBue^8R@zY8ga#uT%SKe+D@iyzH_U^c9CoeP;DTjDr?zE$@Q&zC9E!a~XBsqCVd; z=!v(U+S{`1Y2^E@a}r-;e;@yU^ZP&A{#aAu7PB7jOS8NhW&pgiv*+_0V*Pcu+kw{% z0@lB*nr46?tV{p_`M3Zz;!yRc@K0_oql z?(r@W)QR#&G1qY27q2INBiHDpc$-m(pBv%u`3Ui=ErcErQCqS33gYZSs+rhc2Js8+HW%kn zAnH6Oj5%}#qFrTCeEtiAn6NY6_-tj6g_D_TwcnsqJ{Rsu10R`mKe)adynS}{(25My zl@Emd&olG>UibeafB&QJ!va>D+|)xoIDPNVEcmlOczs6;JNRz!3v)C0W74s>qh&Gr z1(_2)i*`aM3ZxkQ>c=I_HWcHtg8icFH%@~ethf}YFPjiIKOVf^j5vHhpg7G1-p}5( z`O;nd9%J1$!)Knb5BDjzQ|hL$Z{p)`zt*E&IL~x2SsQtYciuprB=TdAZOU;4@Q71< zp9Sn;+zcU#XipFeoc`iW9G zL47`)$F!COx~uY==z@)OVrQ7}($N!jc(y_L7w}dF~~5BR}PKT@8JQ{^aQ+9>w?ZeT4Rm3G`d?^U?XoD(S@j z&X|t@cr4HP>J8~^2F$0shaXQH?h?#fcJ?^XL_(a3yWsGvaj9h@**x1o*;hU+srx)OA-^ZED=XAgs@$zb>JnuGRdZVyOZhGyZAn zY8QhXdKZ6=(qRzAOKIN@entOXFF5pMKKOV3-L6FNBa42w#u?zVBhrDKK!4$ne)|7w zf5y(z>*Y~8kvn_;(s^?_d1QL)N!20rJ?~xr!Hsx({4!dpD2Yy(^W zQ5rcUxT|`66P-j9WE-nWV!dj}?Jq|Vmo~e0HtP@5iO-Jm!X+$-r^lHID!`HB(e0e} z$m1QG)qdOs4nnf$n^}NovcC$)oq_&blc&S!P zdcO1$`nDyO4UD(w2lE;b!;R>RH;#)pftOeec8`_LfFEbx26b>KkVP@PD*D_CWW;4F zYai;Cuq~18=_U;FW_eGWav$oE=8y*x38+K*rd^VFFdoOC-AdrKYiG@Q$L{{m`TvhN zt5SdPq+t%7bPqn+r7}YU&mMV~(nTYVZmN|_kJCu;`2?#mb{cVT9~n--?-VwzyU=Z| zLL;?DQu_JudwhD|ibE1I_?^CE=|g7^{61&BQ2IFZ!A2JUS*O6ioXR$}*AYkY%-f{n zUURk4nH#%<=mVN7Bh{aoV@wTso2ZJTx>og?03CcN1*T zck|Xi=)Nu#xa;AH6V*f=UdUYQzJpFYuC?rS2X3^V@5(nqoK5g97`uN6^+AfdxSl2K z-`-?9UWECWEj0qyn-GtW>Vr1}2iZZra#EA1j|HtoS5;#@lAK-BnIP0JL+*8v$Zt|d zRn}-BA1blWn8d z4e6yr-~4P`A$p^TPDF;dxhLVr$w-NmKJ1%u%(crz9dYZjz$;DUA=RA1;tB`Mt1>v< ziw|S|fz>zfDdR;Z%R|I{*6YvGDMYIEYkYVgNi`o+iH(62IT)Fyfj zg9yk!XMSOeey7hDfg5e$F~fYT8^DiVn1q}K>aO9oLT`5H@F3=O&aZ&S3#-VxAa&qC zqoLv$^wgil-~VVFTx@I`#zf_ag6`aOlR8<_xJ+ar-*#DI>Q$R=-@SpnZR(~kkCi2x z+Ua8VgXBnRmIb$>hCFeZ4Bzz5l17p*3e-O4rehsQ%fJcn_Q{LJNz2ebHM&u^%s~(M zc=!6kMPWMW$d*ieLD7kH2lZ3;7IejpU5mVtPon*t7;=~=$+@;zXJ8rhn0Wkc#aif+ z)#Yk_SMdJ{nbT>Z0K014wBJP0(FbV~A7{XiSJv`AxR;K;0Gsu9j}FXZvGXhE!heNr zLJVkiGL+(BWh=@cwruaVJGSHZ6h7*64NTzYDQWq`;A!$=ab6GfLt30u|E*x$*Kozi zqZ;r_`m>tjs8ho$o;H7x1|Kx~>~Qr3f4nd<3c&SS%PK|!jWCa$^(CfN4SF*41#>g< z$ASZ=#ip;JKX{jCdLzF7M|S_Se(x@jV6O9BhD4ZOv5hH_A&x74aJ=f1AyFQwAHq{M zkYz@525cxcaC+H`9(CyQ_xC4#cR;5o#%}a? z$8p&YL(T56`z<|Hsg5Y%C2HeHAo$nm{2_krVf5d4gE(J|qrO#roODEhLE5r$i zJmlqB>q~_hBs*VlmheZ^&l;i<v_Xqyf64 z%(?dTYxr*@Sx0jM>)b-+w!1)|@ulxM<&C@^(i|f*i0@+sU5Z~LUv%!L_m3fu*ma*? zybOB9m|6e2`v&y?o=!ym`aOEdnqGm}WTycPm({#iT!Z@K=bu(tKxBhutROM=F|*D_>J`ho>B zb2pIT)|Hj}>9XX!chwEGK3S5YASb2)ec}Hz^}wq4^5p&1^)Wl{(TJbTCK;7A=$j97 zrgR^K0nPEz}z)+QdzL=VIOR4k|nj@yquk@tMsL%wLDJn@Rx>nH#q`)uIj? zY&l2@qpL*s}JFFY(Ny?k=gSd}6`mXB|&a(+tjs$MftPRwDtt*@KnOkuYebRY$ z`YdneP-MVrMrU>}ogDUxY-Xz3OcsL!a~b6Q22hhKicJAegDwF$GdhBAqE zmkJmfSbw>rqI01)`uU%)=d41U3!e?SuXhylzLPsT`&`h^Hu|u=0{%~(N{+dC8G7+s zlC5qQ`jq7tdVKwn&pnTNdpSV&>CTnr6vg_Um!EtVe#d+;twL=1W;|!5(WzV|^tXG> z!ugQrO$F6Mjo+dFRWUAUjdf2>UVI;p#P3>vP|Mvg?YrNlIw~U7! zesIv2y}}E>Rvoad|Ac%z)~6BhJOt}wqIcE1oW*>2=7p(oGxUFIZ{0qPx<1#uW9D!Y z*3qzqy!0x?e8n7=F*cLvKN_?~ECl~>oqqbw40vIf*78{tqK@?939BE#?{FTsD5+yShsJc*-9-NQ zfBpXdzTYnk{Qdtg;lFB^G?DZ(n_AE>*w^7)jC#%A#qLAFE#&nBac2$$;&)dEqpAm> zFPmgI$n3SKC{PX_(@3vz}gT5^t^C3|)!sIyWg&V!wxo>+wZ?Z31@(ud& Izxw?D1H-47eE