From 3e4b14f984729d709d96cc2d020b38c3b77c15a8 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Tue, 12 Feb 2019 20:34:31 +0100 Subject: [PATCH] #473: Fix for ColorMap (Indexed) TIFF with non-alpha ExtraSamples. --- .../color/DiscreteAlphaIndexColorModel.java | 55 ++++++-- .../imageio/util/ImageTypeSpecifiers.java | 7 +- .../imageio/plugins/tiff/TIFFImageReader.java | 127 ++++++++++++++---- .../plugins/tiff/TIFFImageReaderTest.java | 25 ++-- .../tiff/indexed-unspecified-extrasamples.tif | Bin 0 -> 26750 bytes 5 files changed, 165 insertions(+), 49 deletions(-) create mode 100644 imageio/imageio-tiff/src/test/resources/tiff/indexed-unspecified-extrasamples.tif diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/DiscreteAlphaIndexColorModel.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/DiscreteAlphaIndexColorModel.java index b7bdf420..0f5eb4e4 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/DiscreteAlphaIndexColorModel.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/DiscreteAlphaIndexColorModel.java @@ -37,20 +37,26 @@ import static com.twelvemonkeys.lang.Validate.notNull; /** * This class represents a hybrid between an {@link IndexColorModel} and a {@link ComponentColorModel}, - * having both a color map and a full, discrete alpha channel. + * having both a color map and a full, discrete alpha channel and/or one or more "extra" channels. * The color map entries are assumed to be fully opaque and should have no transparent index. *

* ColorSpace will always be the default sRGB color space (as with {@code IndexColorModel}). *

- * Component order is always P, A, where P is a palette index, and A is the alpha value. + * Component order is always I, A, X1, X2... Xn, + * where I is a palette index, A is the alpha value and Xn are extra samples (ignored for display). * * @see IndexColorModel * @see ComponentColorModel */ +// TODO: ExtraSamplesIndexColorModel might be a better name? +// TODO: Allow specifying which channel is the transparency mask? public final class DiscreteAlphaIndexColorModel extends ColorModel { // Our IndexColorModel delegate private final IndexColorModel icm; + private final int samples; + private final boolean hasAlpha; + /** * Creates a {@code DiscreteAlphaIndexColorModel}, delegating color map look-ups * to the given {@code IndexColorModel}. @@ -59,13 +65,34 @@ public final class DiscreteAlphaIndexColorModel extends ColorModel { * fully opaque, any transparency or transparent index will be ignored. */ public DiscreteAlphaIndexColorModel(final IndexColorModel icm) { + this(icm, 1, true); + } + + /** + * Creates a {@code DiscreteAlphaIndexColorModel}, delegating color map look-ups + * to the given {@code IndexColorModel}. + * + * @param icm The {@code IndexColorModel} delegate. Color map entries are assumed to be + * fully opaque, any transparency or transparent index will be ignored. + * @param extraSamples the number of extra samples in the color model. + * @param hasAlpha {@code true} if the extra samples contains alpha, otherwise {@code false}. + */ + public DiscreteAlphaIndexColorModel(final IndexColorModel icm, int extraSamples, boolean hasAlpha) { super( - notNull(icm, "IndexColorModel").getPixelSize() * 2, + notNull(icm, "IndexColorModel").getPixelSize() * (1 + extraSamples), new int[] {icm.getPixelSize(), icm.getPixelSize(), icm.getPixelSize(), icm.getPixelSize()}, - icm.getColorSpace(), true, false, Transparency.TRANSLUCENT, icm.getTransferType() + icm.getColorSpace(), hasAlpha, false, hasAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE, + icm.getTransferType() ); this.icm = icm; + this.samples = 1 + extraSamples; + this.hasAlpha = hasAlpha; + } + + @Override + public int getNumComponents() { + return samples; } @Override @@ -85,7 +112,7 @@ public final class DiscreteAlphaIndexColorModel extends ColorModel { @Override public final int getAlpha(final int pixel) { - return (int) ((((float) pixel) / ((1 << getComponentSize(3))-1)) * 255.0f + 0.5f); + return hasAlpha ? (int) ((((float) pixel) / ((1 << getComponentSize(3))-1)) * 255.0f + 0.5f) : 0xff; } private int getSample(final Object inData, final int index) { @@ -128,17 +155,27 @@ public final class DiscreteAlphaIndexColorModel extends ColorModel { @Override public final int getAlpha(final Object inData) { - return getAlpha(getSample(inData, 1)); + return hasAlpha ? getAlpha(getSample(inData, 1)) : 0xff; } @Override public final SampleModel createCompatibleSampleModel(final int w, final int h) { - return new PixelInterleavedSampleModel(transferType, w, h, 2, w * 2, new int[] {0, 1}); + return new PixelInterleavedSampleModel(transferType, w, h, samples, w * samples, createOffsets(samples)); + } + + private int[] createOffsets(int samples) { + int[] offsets = new int[samples]; + + for (int i = 0; i < samples; i++) { + offsets[i] = i; + } + + return offsets; } @Override public final boolean isCompatibleSampleModel(final SampleModel sm) { - return sm instanceof PixelInterleavedSampleModel && sm.getNumBands() == 2; + return sm instanceof PixelInterleavedSampleModel && sm.getNumBands() == samples; } @Override @@ -150,7 +187,7 @@ public final class DiscreteAlphaIndexColorModel extends ColorModel { public final boolean isCompatibleRaster(final Raster raster) { int size = raster.getSampleModel().getSampleSize(0); return ((raster.getTransferType() == transferType) && - (raster.getNumBands() == 2) && ((1 << size) >= icm.getMapSize())); + (raster.getNumBands() == samples) && ((1 << size) >= icm.getMapSize())); } @Override diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java index 0f3f6259..aaaa618c 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ImageTypeSpecifiers.java @@ -44,7 +44,7 @@ import static com.twelvemonkeys.lang.Validate.notNull; * Fixes some subtle bugs in {@code ImageTypeSpecifier}'s factory methods, but * in most cases, this class will delegate to the corresponding methods in {@link ImageTypeSpecifier}. * - * @see javax.imageio.ImageTypeSpecifier + * @see ImageTypeSpecifier * @author Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: ImageTypeSpecifiers.java,v 1.0 24.01.11 17.51 haraldk Exp$ @@ -186,4 +186,9 @@ public final class ImageTypeSpecifiers { ColorModel colorModel = new DiscreteAlphaIndexColorModel(pColorModel); return new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(1, 1)); } + + public static ImageTypeSpecifier createDiscreteExtraSamplesIndexedFromIndexColorModel(final IndexColorModel pColorModel, int extraSamples, boolean hasAlpha) { + ColorModel colorModel = new DiscreteAlphaIndexColorModel(pColorModel, extraSamples, hasAlpha); + return new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(1, 1)); + } } 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 da664ce5..59466d61 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 @@ -55,11 +55,14 @@ import com.twelvemonkeys.io.LittleEndianDataInputStream; import com.twelvemonkeys.io.enc.DecoderStream; import com.twelvemonkeys.io.enc.PackBitsDecoder; import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.xml.XMLSerializer; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.imageio.*; import javax.imageio.event.IIOReadWarningListener; import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.plugins.jpeg.JPEGImageReadParam; import javax.imageio.spi.IIORegistry; @@ -71,6 +74,7 @@ import java.awt.color.ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.*; import java.io.*; +import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; @@ -80,6 +84,7 @@ import java.util.zip.InflaterInputStream; import static com.twelvemonkeys.imageio.util.IIOUtil.createStreamAdapter; import static com.twelvemonkeys.imageio.util.IIOUtil.lookupProviderByName; +import static java.util.Arrays.asList; /** * ImageReader implementation for Aldus/Adobe Tagged Image File Format (TIFF). @@ -111,7 +116,8 @@ import static com.twelvemonkeys.imageio.util.IIOUtil.lookupProviderByName; * * * @see Adobe TIFF developer resources - * @see Wikipedia + * @see TIFF 6.0 specification + * @see Wikipedia TIFF * @see AWare Systems TIFF pages * * @author Harald Kuhr @@ -122,10 +128,6 @@ public final class TIFFImageReader extends ImageReaderBase { // TODOs ImageIO basic functionality: // TODO: Thumbnail support (what is a TIFF thumbnail anyway? Photoshop way? Or use subfiletype?) - // TODOs Full BaseLine support: - // TODO: Support ExtraSamples (an array, if multiple extra samples!) - // (0: Unspecified (not alpha), 1: Associated Alpha (pre-multiplied), 2: Unassociated Alpha (non-multiplied) - // TODOs ImageIO advanced functionality: // TODO: Tiling support (readTile, readTileRaster) // TODO: Implement readAsRenderedImage to allow tiled RenderedImage? @@ -148,6 +150,8 @@ public final class TIFFImageReader extends ImageReaderBase { // Support ICCProfile // Support PlanarConfiguration 2 // Support Compression 3 & 4 (CCITT T.4 & T.6) + // Support ExtraSamples (an array, if multiple extra samples!) + // (0: Unspecified (not alpha), 1: Associated Alpha (pre-multiplied), 2: Unassociated Alpha (non-multiplied) final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug")); @@ -469,6 +473,10 @@ public final class TIFFImageReader extends ImageReaderBase { // as some software will treat black/white runs as-is, regardless of photometric. // Special handling is also in the normalizeColor method if (significantSamples == 1 && bitsPerSample == 1) { + if (profile != null) { + processWarningOccurred("Ignoring embedded ICC color profile for Bi-level/Gray TIFF"); + } + byte[] lut = new byte[] {-1, 0}; return ImageTypeSpecifier.createIndexed(lut, lut, lut, null, bitsPerSample, dataType); } @@ -591,8 +599,8 @@ public final class TIFFImageReader extends ImageReaderBase { IndexColorModel icm = createIndexColorModel(bitsPerSample, dataType, (int[]) colorMap.getValue()); - if (hasAlpha) { - return ImageTypeSpecifiers.createDiscreteAlphaIndexedFromIndexColorModel(icm); + if (extraSamples != null) { + return ImageTypeSpecifiers.createDiscreteExtraSamplesIndexedFromIndexColorModel(icm, extraSamples.length, hasAlpha); } return ImageTypeSpecifiers.createFromIndexColorModel(icm); @@ -921,6 +929,10 @@ public final class TIFFImageReader extends ImageReaderBase { if (stripTileByteCounts == null) { processWarningOccurred("Missing TileByteCounts for tiled TIFF with compression: " + compression); } + else if (stripTileByteCounts.length == 0 || containsZero(stripTileByteCounts)) { + stripTileByteCounts = null; + processWarningOccurred("Ignoring all-zero TileByteCounts for tiled TIFF with compression: " + compression); + } stripTileWidth = getValueAsInt(TIFF.TAG_TILE_WIDTH, "TileWidth"); stripTileHeight = getValueAsInt(TIFF.TAG_TILE_HEIGTH, "TileHeight"); @@ -931,6 +943,10 @@ public final class TIFFImageReader extends ImageReaderBase { if (stripTileByteCounts == null) { processWarningOccurred("Missing StripByteCounts for TIFF with compression: " + compression); } + else if (stripTileByteCounts.length == 0 || containsZero(stripTileByteCounts)) { + stripTileByteCounts = null; + processWarningOccurred("Ignoring all-zero StripByteCounts for TIFF with compression: " + compression); + } // NOTE: This is really against the spec, but libTiff seems to handle it. TIFF 6.0 says: // "Do not use both strip- oriented and tile-oriented fields in the same TIFF file". @@ -1309,13 +1325,11 @@ public final class TIFFImageReader extends ImageReaderBase { int len = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Integer.MAX_VALUE; imageInput.seek(stripTileOffsets != null ? stripTileOffsets[i] : realJPEGOffset); - try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration( - Arrays.asList( - new ByteArrayInputStream(jpegHeader), - createStreamAdapter(imageInput, len), - new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI - ) - )))) { + try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(asList( + new ByteArrayInputStream(jpegHeader), + createStreamAdapter(imageInput, len), + new ByteArrayInputStream(new byte[]{(byte) 0xff, (byte) 0xd9}) // EOI + ))))) { jpegReader.setInput(stream); jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile)); jpegParam.setSourceSubsampling(xSub, ySub, 0, 0); @@ -1460,7 +1474,7 @@ public final class TIFFImageReader extends ImageReaderBase { } try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration( - Arrays.asList( + asList( createJFIFStream(destRaster.getNumBands(), stripTileWidth, stripTileHeight, qTables, dcTables, acTables, subsampling), createStreamAdapter(imageInput, length), new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI @@ -1538,6 +1552,16 @@ public final class TIFFImageReader extends ImageReaderBase { return destination; } + private boolean containsZero(long[] byteCounts) { + for (long byteCount : byteCounts) { + if (byteCount <= 0) { + return true; + } + } + + return false; + } + private IIOMetadata readJPEGMetadataSafe(final ImageReader jpegReader) throws IOException { try { return jpegReader.getImageMetadata(0); @@ -2026,7 +2050,7 @@ public final class TIFFImageReader extends ImageReaderBase { } } - private void normalizeColor(int photometricInterpretation, byte[] data) throws IIOException { + private void normalizeColor(int photometricInterpretation, byte[] data) throws IOException { switch (photometricInterpretation) { case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: // NOTE: Preserve WhiteIsZero for 1 bit monochrome, for CCITT compatibility @@ -2543,6 +2567,13 @@ public final class TIFFImageReader extends ImageReaderBase { try { ImageReadParam param = reader.getDefaultReadParam(); + + if (param.getClass().getName().equals("com.twelvemonkeys.imageio.plugins.svg.SVGReadParam")) { + Method setBaseURI = param.getClass().getMethod("setBaseURI", String.class); + String uri = file.getAbsoluteFile().toURI().toString(); + setBaseURI.invoke(param, uri); + } + int numImages = reader.getNumImages(true); for (int imageNo = 0; imageNo < numImages; imageNo++) { // if (args.length > 1) { @@ -2557,6 +2588,7 @@ public final class TIFFImageReader extends ImageReaderBase { // int height = reader.getHeight(imageNo); // param.setSourceRegion(new Rectangle(width / 4, height / 4, width / 2, height / 2)); // param.setSourceRegion(new Rectangle(100, 300, 400, 400)); +// param.setSourceRegion(new Rectangle(95, 105, 100, 100)); // param.setSourceRegion(new Rectangle(3, 3, 9, 9)); // param.setDestinationOffset(new Point(50, 150)); // param.setSourceSubsampling(2, 2, 0, 0); @@ -2564,16 +2596,18 @@ public final class TIFFImageReader extends ImageReaderBase { BufferedImage image = reader.read(imageNo, param); System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms"); -// IIOMetadata metadata = reader.getImageMetadata(imageNo); -// if (metadata != null) { -// if (metadata.getNativeMetadataFormatName() != null) { -// new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false); -// } -// /*else*/ -// if (metadata.isStandardMetadataFormatSupported()) { -// new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false); -// } -// } + IIOMetadata metadata = reader.getImageMetadata(imageNo); + if (metadata != null) { + if (metadata.getNativeMetadataFormatName() != null) { + Node tree = metadata.getAsTree(metadata.getNativeMetadataFormatName()); + replaceBytesWithUndefined((IIOMetadataNode) tree); + new XMLSerializer(System.out, "UTF-8").serialize(tree, false); + } + /*else*/ + if (metadata.isStandardMetadataFormatSupported()) { + new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false); + } + } System.err.println("image: " + image); @@ -2659,6 +2693,47 @@ public final class TIFFImageReader extends ImageReaderBase { } } + // XMP Spec says "The field type should be UNDEFINED (7) or BYTE (1)" + // Adobe Photoshop® TIFF Technical Notes says (for Image Source Data): "Type: UNDEFINED" + private static final Set BYTE_TO_UNDEFINED_NODES = new HashSet<>(asList( + "700", // XMP + "34377", // Photoshop Image Resources + "37724" // Image Source Data + )); + + private static void replaceBytesWithUndefined(IIOMetadataNode tree) { + // The output of the TIFFUndefined tag is just much more readable (or easier to skip) + + NodeList nodes = tree.getElementsByTagName("TIFFBytes"); + for (int i = 0; i < nodes.getLength(); i++) { + IIOMetadataNode node = (IIOMetadataNode) nodes.item(i); + + IIOMetadataNode parentNode = (IIOMetadataNode) node.getParentNode(); + + NodeList childNodes = node.getChildNodes(); + if (BYTE_TO_UNDEFINED_NODES.contains(parentNode.getAttribute("number")) && childNodes.getLength() > 16) { + IIOMetadataNode undefined = new IIOMetadataNode("TIFFUndefined"); + StringBuilder values = new StringBuilder(); + + IIOMetadataNode child = (IIOMetadataNode) node.getFirstChild(); + while (child != null) { + if (values.length() > 0) { + values.append(", "); + } + + String value = child.getAttribute("value"); + values.append(value); + + child = (IIOMetadataNode) child.getNextSibling(); + } + + undefined.setAttribute("value", values.toString()); + + parentNode.replaceChild(undefined, node); + } + } + } + protected static void showIt(BufferedImage image, String title) { ImageReaderBase.showIt(image, title); } 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 0ce685b0..89ad7e50 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 @@ -43,10 +43,8 @@ import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import java.awt.*; -import java.awt.color.ICC_ColorSpace; -import java.awt.image.BufferedImage; -import java.awt.image.Raster; -import java.awt.image.WritableRaster; +import java.awt.color.ColorSpace; +import java.awt.image.*; import java.io.IOException; import java.nio.ByteOrder; import java.util.Arrays; @@ -108,6 +106,7 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestkKe8D*hpKAhkp%P{grJX`PWy zr!&xYkQM59fe-}Rkfcf8_T0O>@9n*NH!mUk z(mb* z+DvO{8O@}^lpZRfEt+KN${lpMWzl5@Z4FY}8rD`xm!`BucSgnZPqMoy$7vyv(;k)4 z;ZVtDx{4XDOrx!x{z&7av);8U9fM6B^iu*M9RY*z)=BzCv=nLMAX+_3H$=!_(>ru= zloSSrChiYiTAx{Q-VDujggP~CHciej!{dbXkk;y+5QevCgIiY9NaJ`|$+Ztso2GAG zrWv@^ayq57x<7#crvT5(8yS3x;u8cJO8hL(kUf{-2iw!7{xakmQ8H8v=$!B8oHKfb?`uNx z)*JaQDzzGlv;N8XdKQPXbTnChXJmMurqd2eXbcOT6(^e4KIYzj@75U^{1U12+oQ4G z(ei&`I6OBd2CrB=P} zTYOz%KwC=Y9-2lK(MBLzU&PvC55yjbJrH{!_CV}`!R`Uf!vZc5=Ue^bRUC$s@d>QO zKVu`8I(!b7;PqJGZyj@SI^O9kx8QZMOu#F!3@=h-^YJ#Hr2ncrai-AAo>H}`GWjf# z$CCb9w*_wN4qyFEqFuJ_UVIb(i?&u;aW7V4o@W~l$A4)O&K2$0gdei(QXG|(y^L#g zectaUk@q!mJRS>`u#xwlu@svXiMft;oQ+++`UaLPz^4uNd+oL1v&i@H;s|B%WE1!{ zPI6^4!LS=IkalPci1>_;$+O1)-iaG=NRJrARr_#+l+6S~2Tn9HOjP1xV*=l2J31Av z7QDH~3|&~2F$No-M7}@O`x1U%vDU^DSj4^SU9I+KAq<B#v`S zh3K0GhV@B_abk8iFxdDVuGh!IR;7I#9flXQ+PAx57|XHqN8%vzQ#1w!-evY51_l}J zl#G#~5r+gA8kF|K7>)|fhIvWEL1*}zoc)7wh=!qqS1mmbeP!jn83`$xk$l)FrZ@o)}Lxn--+ZeVClO8h~rX2$U#+Nw{-fP_;5MLrK@ zVDMXS<5@hwKFXCA2K!P<_D>-UDTz9vC6u8<@tUD8;l+xCqc?0!r&_hK9d~jT$FpX} z8T_rS^mBdhQ6>0suP+&LKDMAQamXaY7(5e*L<8!%_hH$WmTQu|=}Tb@+xcF@D>K+n!Fg#{ z^6xb`l_m6CV;I-Hq**4$!+fo-mG=j3!1uJ;C>Wf`JFnY~41S5MAW5iUPX} zt$11^=zeb1$8=a6{1%=+LqT9~rGaVZYWHFOw!HHJ69=|;o#tV|fShHkcVfcwHe7`* zN$G*aFj?D4Vg_+=-u_$sBx!L6zJv2HpLdr2?v_`jR59QGdo|Ac0={-MzOK~P;ZUv} zg|8^J>UB@EL; zak2wstzM1u&bx=4tE;t_%Ka0<5>3{BrP!Uw(XR9VeWdh*-X8DENV|#@&Ab}LN9=*v z1F;8U55yjbJrH{!_CW7F0Qs;D_6EuxsD)?YM{qm1-j-d8IA()jGfOB!jQ}Jp?{ue zf(cp25R60iF_gfkN*q20x&M*TF&$-JIs~g>4;#=pUuiBgTLLcM99zu`(aUPzQ{@j;hRo44E!mAII@P}Q|_0a`9_1_IO61| zS;O#q$bm6zwZDHR;D;HH!)aK?K5~kZ@yjmI&5L17i*-65b}I}v)MlI^%DGiA=P=0&Bcyy?F29oLD{u*v|~d;TPb3BAKo)NzO7@2Q9{Ed#ISgmD-G_d%7h%!e6pGmHh_w{74mqoi}a+%<6RD~7qS5gK_cd@M@?JO!0r1YDWx zy3tkTdCigaUVmL?S>h+J)UvMTXG-hGTGmYdra&?Oi_IL1A3k>uHd&lM$Uoqoi+@;v zK$iH>IFAeePbn!Q