From 4839c61f5c57f9b611198c0f30580df37beaa3ed Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 19 Mar 2015 14:49:52 +0100 Subject: [PATCH] TMI-106, TMI-118: PICT JDK 8 fix + cleanup --- .../io/enc/PackBits16Decoder.java | 178 ---- .../twelvemonkeys/io/enc/PackBitsDecoder.java | 35 +- .../imageio/plugins/pict/BitMap.java | 11 + .../imageio/plugins/pict/BitMapPattern.java | 93 +- .../imageio/plugins/pict/PICT.java | 37 + .../imageio/plugins/pict/PICTImageReader.java | 832 ++++++------------ .../plugins/pict/PICTImageReaderSpi.java | 10 +- .../imageio/plugins/pict/PICTImageWriter.java | 2 +- .../imageio/plugins/pict/PICTUtil.java | 38 +- .../imageio/plugins/pict/Pattern.java | 10 +- .../imageio/plugins/pict/PixMap.java | 11 + .../imageio/plugins/pict/PixMapPattern.java | 7 +- .../plugins/pict/QuickDrawContext.java | 136 ++- .../plugins/pict/PICTImageReaderTest.java | 202 ++++- .../plugins/pict/PICTImageWriterTest.java | 30 +- .../src/test/resources/pict/FC10.PCT | Bin 0 -> 80809 bytes 16 files changed, 829 insertions(+), 803 deletions(-) delete mode 100644 common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java create mode 100644 imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/BitMap.java create mode 100644 imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMap.java create mode 100644 imageio/imageio-pict/src/test/resources/pict/FC10.PCT diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java deleted file mode 100644 index a25e8671..00000000 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package com.twelvemonkeys.io.enc; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * Decoder implementation for 16 bit-chunked Apple PackBits-like run-length - * encoding. - *

- * This version of the decoder decodes chunk of 16 bit, instead of 8 bit. - * This format is used in certain PICT files. - * - * @see PackBitsDecoder - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java#2 $ - */ -public final class PackBits16Decoder implements Decoder { - // TODO: Refactor this into an option for the PackBitsDecoder (bytesPerSample, default == 1)? - private final boolean disableNoop; - - private int leftOfRun; - private boolean splitRun; - private boolean reachedEOF; - - /** - * Creates a {@code PackBitsDecoder}. - */ - public PackBits16Decoder() { - this(false); - } - - /** - * Creates a {@code PackBitsDecoder}. - *

- * As some implementations of PackBits-like encoders treat {@code -128} as length of - * a compressed run, instead of a no-op, it's possible to disable no-ops - * for compatibility. - * Should be used with caution, even though, most known encoders never write - * no-ops in the compressed streams. - * - * @param pDisableNoop {@code true} if {@code -128} should be treated as a compressed run, and not a no-op - */ - public PackBits16Decoder(final boolean pDisableNoop) { - disableNoop = pDisableNoop; - } - - /** - * Decodes bytes from the given input stream, to the given buffer. - * - * @param stream the stream to decode from - * @param buffer a byte array, minimum 128 (or 129 if no-op is disabled) - * bytes long - * @return The number of bytes decoded - * - * @throws java.io.IOException - */ - public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { - if (reachedEOF) { - return -1; - } - - while (buffer.hasRemaining()) { - int n; - - if (splitRun) { - // Continue run - n = leftOfRun; - splitRun = false; - } - else { - // Start new run - int b = stream.read(); - if (b < 0) { - reachedEOF = true; - break; - } - n = (byte) b; - } - - // Split run at or before max - if (n >= 0 && 2 * (n + 1) > buffer.remaining()) { - leftOfRun = n; - splitRun = true; - break; - } - else if (n < 0 && 2 * (-n + 1) > buffer.remaining()) { - leftOfRun = n; - splitRun = true; - break; - } - - try { - if (n >= 0) { - // Copy next n + 1 shorts literally - readFully(stream, buffer, 2 * (n + 1)); - } - // Allow -128 for compatibility, see above - else if (disableNoop || n != -128) { - // Replicate the next short -n + 1 times - byte value1 = readByte(stream); - byte value2 = readByte(stream); - - for (int i = -n + 1; i > 0; i--) { - buffer.put(value1); - buffer.put(value2); - } - } - // else NOOP (-128) - } - catch (IndexOutOfBoundsException e) { - throw new DecodeException("Error in PackBits decompression, data seems corrupt", e); - } - } - - return buffer.position(); - } - - private static byte readByte(final InputStream pStream) throws IOException { - int read = pStream.read(); - - if (read < 0) { - throw new EOFException("Unexpected end of PackBits stream"); - } - - return (byte) read; - } - - private static void readFully(final InputStream pStream, final ByteBuffer pBuffer, final int pLength) throws IOException { - if (pLength < 0) { - throw new IndexOutOfBoundsException(); - } - - int total = 0; - - while (total < pLength) { - int count = pStream.read(pBuffer.array(), pBuffer.arrayOffset() + pBuffer.position() + total, pLength - total); - - if (count < 0) { - throw new EOFException("Unexpected end of PackBits stream"); - } - - total += count; - } - - pBuffer.position(pBuffer.position() + total); - } -} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java index 0c173754..86c32a1f 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java @@ -66,7 +66,8 @@ import java.nio.ByteBuffer; public final class PackBitsDecoder implements Decoder { // TODO: Look at ICNSImageReader#unpackbits... What is this weirdness? - private final boolean disableNoop; + private final boolean disableNoOp; + private final byte[] sample; private int leftOfRun; private boolean splitRun; @@ -74,7 +75,7 @@ public final class PackBitsDecoder implements Decoder { /** Creates a {@code PackBitsDecoder}. */ public PackBitsDecoder() { - this(false); + this(1, false); } /** @@ -84,10 +85,24 @@ public final class PackBitsDecoder implements Decoder { * a compressed run, instead of a no-op, it's possible to disable no-ops for compatibility. * Should be used with caution, even though, most known encoders never write no-ops in the compressed streams. * - * @param pDisableNoop {@code true} if {@code -128} should be treated as a compressed run, and not a no-op + * @param disableNoOp {@code true} if {@code -128} should be treated as a compressed run, and not a no-op */ - public PackBitsDecoder(final boolean pDisableNoop) { - disableNoop = pDisableNoop; + public PackBitsDecoder(final boolean disableNoOp) { + this(1, disableNoOp); + } + + /** + * Creates a {@code PackBitsDecoder}, with optional compatibility mode. + *

+ * As some implementations of PackBits-like encoders treat {@code -128} as length of + * a compressed run, instead of a no-op, it's possible to disable no-ops for compatibility. + * Should be used with caution, even though, most known encoders never write no-ops in the compressed streams. + * + * @param disableNoOp {@code true} if {@code -128} should be treated as a compressed run, and not a no-op + */ + public PackBitsDecoder(int sampleSize, final boolean disableNoOp) { + this.sample = new byte[sampleSize]; + this.disableNoOp = disableNoOp; } /** @@ -138,15 +153,17 @@ public final class PackBitsDecoder implements Decoder { try { if (n >= 0) { // Copy next n + 1 bytes literally - readFully(stream, buffer, n + 1); + readFully(stream, buffer, sample.length * (n + 1)); } // Allow -128 for compatibility, see above - else if (disableNoop || n != -128) { + else if (disableNoOp || n != -128) { // Replicate the next byte -n + 1 times - byte value = readByte(stream); + for (int s = 0; s < sample.length; s++) { + sample[s] = readByte(stream); + } for (int i = -n + 1; i > 0; i--) { - buffer.put(value); + buffer.put(sample); } } // else NOOP (-128) diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/BitMap.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/BitMap.java new file mode 100644 index 00000000..20ad04fd --- /dev/null +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/BitMap.java @@ -0,0 +1,11 @@ +package com.twelvemonkeys.imageio.plugins.pict; + +/** + * BitMap. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: BitMap.java,v 1.0 20/02/15 harald.kuhr Exp$ + */ +final class BitMap { +} diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/BitMapPattern.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/BitMapPattern.java index de6f80f2..4eb79f01 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/BitMapPattern.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/BitMapPattern.java @@ -29,10 +29,9 @@ package com.twelvemonkeys.imageio.plugins.pict; import java.awt.*; -import java.awt.image.WritableRaster; -import java.awt.image.DataBufferByte; -import java.awt.image.BufferedImage; -import java.awt.image.Raster; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.*; /** * BitMapPattern @@ -43,22 +42,46 @@ import java.awt.image.Raster; */ final class BitMapPattern extends Pattern { + private final byte[] pattern; + BitMapPattern(final Paint pColor) { - super(pColor); + this(pColor, null); } public BitMapPattern(final byte[] pPattern) { - this(create8x8Pattern(pPattern)); + this(create8x8Pattern(pPattern), pPattern); + } + + private BitMapPattern(final Paint pColor, final byte[] pPattern) { + super(pColor); + + pattern = pPattern; + } + + // TODO: Refactor, don't need both BitMapPattern constructors and create8x8Pattern methods? + public BitMapPattern(final byte[] pPattern, Color fg, Color bg) { + this(create8x8Pattern(pPattern, fg, bg)); } BitMapPattern(final int pPattern) { this(create8x8Pattern(pPattern)); } - private static TexturePaint create8x8Pattern(final int pPattern) { - // TODO: Creating a special purpose Pattern might be faster than piggy-backing on TexturePaint - WritableRaster raster = QuickDraw.MONOCHROME.createCompatibleWritableRaster(8, 8); - byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); + private static Paint create8x8Pattern(final int pPattern) { +// // TODO: Creating a special purpose Pattern might be faster than piggy-backing on TexturePaint +// WritableRaster raster = QuickDraw.MONOCHROME.createCompatibleWritableRaster(8, 8); +// byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); +// +// for (int i = 0; i < data.length; i += 4) { +// data[i ] = (byte) ((pPattern >> 24) & 0xFF); +// data[i + 1] = (byte) ((pPattern >> 16) & 0xFF); +// data[i + 2] = (byte) ((pPattern >> 8) & 0xFF); +// data[i + 3] = (byte) ((pPattern ) & 0xFF); +// } +// +// BufferedImage img = new BufferedImage(QuickDraw.MONOCHROME, raster, false, null); +// return new TexturePaint(img, new Rectangle(8, 8)); + byte[] data = new byte[8]; for (int i = 0; i < data.length; i += 4) { data[i ] = (byte) ((pPattern >> 24) & 0xFF); @@ -67,13 +90,57 @@ final class BitMapPattern extends Pattern { data[i + 3] = (byte) ((pPattern ) & 0xFF); } - BufferedImage img = new BufferedImage(QuickDraw.MONOCHROME, raster, false, null); - return new TexturePaint(img, new Rectangle(8, 8)); + return create8x8Pattern(data); } - private static TexturePaint create8x8Pattern(final byte[] pPattern) { + private static Paint create8x8Pattern(final byte[] pPattern) { WritableRaster raster = Raster.createPackedRaster(new DataBufferByte(pPattern, 8), 8, 8, 1, new Point()); BufferedImage img = new BufferedImage(QuickDraw.MONOCHROME, raster, false, null); return new TexturePaint(img, new Rectangle(8, 8)); } + + private static Paint create8x8Pattern(final byte[] pPattern, Color fg, Color bg) { + switch (isSolid(pPattern)) { + case 0: // 0x00 + return bg; + case -1: // 0xff + return fg; + default: + // Fall through + } + + WritableRaster raster = Raster.createPackedRaster(new DataBufferByte(pPattern, 8), 8, 8, 1, new Point()); + IndexColorModel cm = new IndexColorModel(1, 2, new int[] {bg.getRGB(), fg.getRGB()}, 0, false, -1, DataBuffer.TYPE_BYTE); + BufferedImage img = new BufferedImage(cm, raster, false, null); + return new TexturePaint(img, new Rectangle(8, 8)); + } + + private static int isSolid(byte[] pPattern) { + int prev = pPattern[0]; + + for (int i = 1; i < pPattern.length; i++) { + if (prev != pPattern[i]) { + return 1; + } + } + + return prev; + } + + @Override + public PaintContext createContext(ColorModel pModel, Rectangle pDeviceBounds, Rectangle2D pUserBounds, AffineTransform pTransform, RenderingHints pHints) { +// switch (isSolid(pattern)) { +// } + return super.createContext(pModel, pDeviceBounds, pUserBounds, pTransform, pHints); + } + + @Override + public Pattern derive(final Color foreground, final Color background) { + if (paint instanceof Color) { + // TODO: This only holds for patterns that are already foregrounds... + return new BitMapPattern(foreground); + } + + return null; + } } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICT.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICT.java index eca3c6a4..5038f791 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICT.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICT.java @@ -175,4 +175,41 @@ interface PICT { int OP_UNCOMPRESSED_QUICKTIME = 0x8201; String APPLE_USE_RESERVED_FIELD = "Reserved for Apple use."; + + /* + * Picture comment 'kind' codes from: http://developer.apple.com/technotes/qd/qd_10.html + int TextBegin = 150; + int TextEnd = 151; + int StringBegin = 152; + int StringEnd = 153; + int TextCenter = 154; + int LineLayoutOff = 155; + int LineLayoutOn = 156; + int ClientLineLayout = 157; + int PolyBegin = 160; + int PolyEnd = 161; + int PolyIgnore = 163; + int PolySmooth = 164; + int PolyClose = 165; + int DashedLine = 180; + int DashedStop = 181; + int SetLineWidth = 182; + int PostScriptBegin = 190; + int PostScriptEnd = 191; + int PostScriptHandle = 192; + int PostScriptFile = 193; + int TextIsPostScript = 194; + int ResourcePS = 195; + int PSBeginNoSave = 196; + int SetGrayLevel = 197; + int RotateBegin = 200; + int RotateEnd = 201; + int RotateCenter = 202; + int FormsPrinting = 210; + int EndFormsPrinting = 211; + int ICC_Profile = 224; + int Photoshop_Data = 498; + int BitMapThinningOff = 1000; + int BitMapThinningOn = 1001; + */ } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java index 2cc202d6..c954ff47 100644 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java @@ -64,7 +64,6 @@ import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.io.enc.Decoder; import com.twelvemonkeys.io.enc.DecoderStream; -import com.twelvemonkeys.io.enc.PackBits16Decoder; import com.twelvemonkeys.io.enc.PackBitsDecoder; import javax.imageio.*; @@ -100,23 +99,21 @@ import java.util.List; * - Or methods like frameRect(pen, penmode, penwidth, rect), frameOval(pen, penmode, penwidth, rect), etc? * - Or methods like frameShape(pen, penmode, penwidth, shape), paintShape(pen, penmode, shape) etc?? * QuickDrawContext that wraps an AWT Grpahics, and with methods macthing opcodes, seems like the best fit ATM - * @todo Remove null-checks for Graphics, as null-graphics makes no sense. * @todo Some MAJOR clean up - * @todo Object orientation of different opcodes? * @todo As we now have Graphics2D with more options, support more of the format? - * @todo Support for some other compression (packType 3) that seems to be common... */ public class PICTImageReader extends ImageReaderBase { - static boolean DEBUG = false; + final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.pict.debug")); - // Private fields private QuickDrawContext context; private Rectangle frame; + // TODO: Do we need this? private int version; // Variables for storing draw status + // TODO: Get rid of these, or move to context private Point penPosition = new Point(0, 0); private Rectangle lastRectangle = new Rectangle(0, 0); @@ -180,7 +177,7 @@ public class PICTImageReader extends ImageReaderBase { pStream.seek(0l); // Skip first 512 bytes - skipNullHeader(pStream); + PICTImageReaderSpi.skipNullHeader(pStream); readPICTHeader0(pStream); } } @@ -326,12 +323,6 @@ public class PICTImageReader extends ImageReaderBase { pStream.flushBefore(imageStartStreamPos); } - static void skipNullHeader(final ImageInputStream pStream) throws IOException { - // NOTE: Only skip if FILE FORMAT, not needed for Mac OS DnD - // Spec says "platofrm dependent", may not be all nulls.. - pStream.skipBytes(PICT.PICT_NULL_HEADER_SIZE); - } - /** * Reads the PICT stream. * The contents of the stream will be drawn onto the supplied graphics @@ -369,12 +360,9 @@ public class PICTImageReader extends ImageReaderBase { int opCode, dh, dv, dataLength; byte[] colorBuffer = new byte[3 * PICT.COLOR_COMP_SIZE]; - Pattern fill = QuickDraw.BLACK; Pattern bg; Pattern pen; - Paint foreground; - Paint background; Color hilight = Color.RED; Point origin, dh_dv; @@ -441,39 +429,27 @@ public class PICTImageReader extends ImageReaderBase { } break; - case PICT.OP_TX_FONT:// DIFFICULT TO KNOW THE FONT??? + case PICT.OP_TX_FONT: // Get the data - pStream.readFully(new byte[2], 0, 2); + byte[] fontData = new byte[2]; + pStream.readFully(fontData, 0, 2); // TODO: Font family id, 0 - System font, 1 - Application font. // But how can we get these mappings? if (DEBUG) { - System.out.println("txFont"); + System.out.println("txFont: " + Arrays.toString(fontData)); } break; - case PICT.OP_TX_FACE:// SEE IF IT IS TO BE IMPLEMENTED FOR NOW? + case PICT.OP_TX_FACE: // Get the data - byte txFace = pStream.readByte(); - - //// Construct text face mask -// currentFont = mGraphics.getFont(); - //int awt_face_mask = 0; - //if ((txFace & (byte) QuickDraw.TX_BOLD_MASK) > 0) { - // awt_face_mask |= Font.BOLD; - //} - //if ((txFace & (byte) QuickDraw.TX_ITALIC_MASK) > 0) { - // awt_face_mask |= Font.ITALIC; - //} - // - //// Set the font - //mGraphics.setFont(new Font(currentFont.getName(), awt_face_mask, currentFont.getSize())); - + int txFace = pStream.readUnsignedByte(); + context.setTextFace(txFace); if (DEBUG) { System.out.println("txFace: " + txFace); } break; - case PICT.OP_TX_MODE:// SEE IF IT IS TO BE IMPLEMENTED FOR NOW? + case PICT.OP_TX_MODE: // Get the data byte[] mode_buf = new byte[2]; pStream.readFully(mode_buf, 0, mode_buf.length); @@ -494,25 +470,25 @@ public class PICTImageReader extends ImageReaderBase { // Get the two words // NOTE: This is out of order, compared to other Points Dimension pnsize = new Dimension(pStream.readUnsignedShort(), pStream.readUnsignedShort()); - context.setPenSize(pnsize); if (DEBUG) { System.out.println("pnsize: " + pnsize); } + context.setPenSize(pnsize); + break; - case PICT.OP_PN_MODE:// TRY EMULATING WITH SETXORMODE ETC + case PICT.OP_PN_MODE: // Get the data int mode = pStream.readUnsignedShort(); if (DEBUG) { System.out.println("pnMode: " + mode); } - context.setPenMode(mode); break; case PICT.OP_PN_PAT: - context.setPenPattern(PICTUtil.readPattern(pStream)); + context.setPenPattern(PICTUtil.readPattern(pStream, context.getForeground(), context.getBackground())); if (DEBUG) { System.out.println("pnPat"); } @@ -546,9 +522,6 @@ public class PICTImageReader extends ImageReaderBase { y = getYPtCoord(pStream.readUnsignedShort()); x = getXPtCoord(pStream.readUnsignedShort()); origin = new Point(x, y); - //if (mGraphics != null) { - // mGraphics.translate(origin.x, origin.y); - //} if (DEBUG) { System.out.println("Origin: " + origin); } @@ -557,10 +530,6 @@ public class PICTImageReader extends ImageReaderBase { case PICT.OP_TX_SIZE:// OK // Get the text size int tx_size = getYPtCoord(pStream.readUnsignedShort()); - //if (mGraphics != null) { - // currentFont = mGraphics.getFont(); - // mGraphics.setFont(new Font(currentFont.getName(), currentFont.getStyle(), tx_size)); - //} context.setTextSize(tx_size); if (DEBUG) { System.out.println("txSize: " + tx_size); @@ -604,14 +573,23 @@ public class PICTImageReader extends ImageReaderBase { case 0x0012: // BkPixPat bg = PICTUtil.readColorPattern(pStream); context.setBackgroundPattern(bg); + if (DEBUG) { + System.out.println("BkPixPat"); + } break; case 0x0013: // PnPixPat pen = PICTUtil.readColorPattern(pStream); - context.setBackgroundPattern(pen); + context.setPenPattern(pen); + if (DEBUG) { + System.out.println("PnPixPat"); + } break; case 0x0014: // FillPixPat fill = PICTUtil.readColorPattern(pStream); - context.setBackgroundPattern(fill); + context.setFillPattern(fill); + if (DEBUG) { + System.out.println("FillPixPat"); + } break; case PICT.OP_PN_LOC_H_FRAC:// TO BE DONE??? @@ -633,23 +611,22 @@ public class PICTImageReader extends ImageReaderBase { case PICT.OP_RGB_FG_COL:// OK // Get the color pStream.readFully(colorBuffer, 0, colorBuffer.length); - foreground = new Color((colorBuffer[0] & 0xFF), (colorBuffer[2] & 0xFF), (colorBuffer[4] & 0xFF)); - //if (mGraphics != null) { - // mGraphics.setColor(foreground); - //} + Color foreground = new Color((colorBuffer[0] & 0xFF), (colorBuffer[2] & 0xFF), (colorBuffer[4] & 0xFF)); if (DEBUG) { System.out.println("rgbFgColor: " + foreground); } + context.setForeground(foreground); break; case PICT.OP_RGB_BK_COL:// OK // Get the color pStream.readFully(colorBuffer, 0, colorBuffer.length); - // TODO: The color might be 16 bit per component.. - background = new Color((colorBuffer[0] & 0xFF), (colorBuffer[2] & 0xFF), (colorBuffer[4] & 0xFF)); + // The color might be 16 bit per component.. + Color background = new Color(colorBuffer[0] & 0xFF, colorBuffer[2] & 0xFF, colorBuffer[4] & 0xFF); if (DEBUG) { System.out.println("rgbBgColor: " + background); } + context.setBackground(background); break; case PICT.OP_HILITE_MODE: @@ -725,11 +702,12 @@ public class PICTImageReader extends ImageReaderBase { x = getXPtCoord(pStream.readUnsignedShort()); origin = new Point(x, y); - y = getYPtCoord(pStream.readByte()); x = getXPtCoord(pStream.readByte()); + y = getYPtCoord(pStream.readByte()); dh_dv = new Point(x, y); // Move pen to new position, draw line if we have a graphics + context.moveTo(origin); penPosition.setLocation(origin.x + dh_dv.x, origin.y + dh_dv.y); context.lineTo(penPosition); @@ -740,8 +718,8 @@ public class PICTImageReader extends ImageReaderBase { case PICT.OP_SHORT_LINE_FROM:// OK // Get dh, dv - y = getYPtCoord(pStream.readByte()); x = getXPtCoord(pStream.readByte()); + y = getYPtCoord(pStream.readByte()); // Draw line context.line(x, y); @@ -788,10 +766,6 @@ public class PICTImageReader extends ImageReaderBase { penPosition.translate(dh, 0); context.moveTo(penPosition); text = PICTUtil.readPascalString(pStream); - // TODO -// if (mGraphics != null) { -// mGraphics.drawString(text, penPosition.x, penPosition.y); -// } context.drawString(text); if (DEBUG) { System.out.println("DHText dh: " + dh + ", text:" + text); @@ -804,10 +778,6 @@ public class PICTImageReader extends ImageReaderBase { penPosition.translate(0, dv); context.moveTo(penPosition); text = PICTUtil.readPascalString(pStream); - // TODO - //if (mGraphics != null) { - // mGraphics.drawString(text, penPosition.x, penPosition.y); - //} context.drawString(text); if (DEBUG) { System.out.println("DVText dv: " + dv + ", text:" + text); @@ -821,10 +791,6 @@ public class PICTImageReader extends ImageReaderBase { penPosition.translate(x, y); context.moveTo(penPosition); text = PICTUtil.readPascalString(pStream); - // TODO - //if (mGraphics != null) { - // mGraphics.drawString(text, penPosition.x, penPosition.y); - //} context.drawString(text); if (DEBUG) { System.out.println("DHDVText penPosition: " + penPosition + ", text:" + text); @@ -837,25 +803,20 @@ public class PICTImageReader extends ImageReaderBase { pStream.readShort(); // Get old font ID, ignored -// pStream.readInt(); pStream.readUnsignedShort(); // Get font name and set the new font if we have one - text = PICTUtil.readPascalString(pStream); - // TODO - //if (mGraphics != null) { - // mGraphics.setFont(Font.decode(text) - // .deriveFont(currentFont.getStyle(), currentFont.getSize())); - //} - context.drawString(text); + String fontName = PICTUtil.readPascalString(pStream); + context.setTextFont(fontName); if (DEBUG) { - System.out.println("fontName: \"" + text +"\""); + System.out.println("fontName: \"" + fontName +"\""); } break; - case PICT.OP_LINE_JUSTIFY:// TO BE DONE??? + case PICT.OP_LINE_JUSTIFY:// TODO // Get data - pStream.readFully(new byte[10], 0, 10); + byte[] lineJustifyData = new byte[10]; + pStream.readFully(lineJustifyData, 0, lineJustifyData.length); if (DEBUG) { System.out.println("opLineJustify"); } @@ -863,9 +824,10 @@ public class PICTImageReader extends ImageReaderBase { case PICT.OP_GLYPH_STATE:// TODO: NOT SUPPORTED IN AWT GRAPHICS YET? // Get data - pStream.readFully(new byte[6], 0, 6); + byte[] glyphState = new byte[6]; + pStream.readFully(glyphState, 0, glyphState.length); if (DEBUG) { - System.out.println("glyphState"); + System.out.println("glyphState: " + Arrays.toString(glyphState)); } break; @@ -1319,6 +1281,14 @@ public class PICTImageReader extends ImageReaderBase { // Polygon treatments finished break; + case 0x7d: + case 0x7e: + case 0x7f: + if (DEBUG) { + System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); + } + break; + case 0x75: case 0x76: case 0x77: @@ -1440,14 +1410,14 @@ public class PICTImageReader extends ImageReaderBase { */ int rowBytesRaw = pStream.readUnsignedShort(); - int rowBytes = rowBytesRaw & 0x3FFF; + int rowBytes = rowBytesRaw & 0x7FFF; // TODO: Use rowBytes to determine size of PixMap/ColorTable? if ((rowBytesRaw & 0x8000) > 0) { // Do stuff... } - // Get bounds rectangle. THIS IS NOT TO BE SCALED BY THE RESOLUTION! TODO: ?! + // Get bounds rectangle. THIS IS NOT TO BE SCALED BY THE RESOLUTION! bounds = new Rectangle(); y = pStream.readUnsignedShort(); x = pStream.readUnsignedShort(); @@ -1455,8 +1425,7 @@ public class PICTImageReader extends ImageReaderBase { y = pStream.readUnsignedShort(); x = pStream.readUnsignedShort(); - bounds.setSize(x - bounds.x, - y - bounds.y); + bounds.setSize(x - bounds.x, y - bounds.y); Rectangle srcRect = new Rectangle(); readRectangle(pStream, srcRect); @@ -1465,7 +1434,6 @@ public class PICTImageReader extends ImageReaderBase { readRectangle(pStream, dstRect); mode = pStream.readUnsignedShort(); - context.setPenMode(mode); // TODO: Or parameter? if (DEBUG) { System.out.print("bitsRect, rowBytes: " + rowBytes); @@ -1496,13 +1464,6 @@ public class PICTImageReader extends ImageReaderBase { Rectangle rect = new Rectangle(srcRect); rect.translate(-bounds.x, -bounds.y); context.copyBits(image, rect, dstRect, mode, null); - //mGraphics.drawImage(image, - // dstRect.x, dstRect.y, - // dstRect.x + dstRect.width, dstRect.y + dstRect.height, - // srcRect.x - bounds.x, srcRect.y - bounds.y, - // srcRect.x - bounds.x + srcRect.width, srcRect.y - bounds.y + srcRect.height, - // null); - // break; case PICT.OP_BITS_RGN: @@ -1518,7 +1479,7 @@ public class PICTImageReader extends ImageReaderBase { pixData: PixData; */ if (DEBUG) { - System.out.println("bitsRgn"); + System.out.println("bitsRgn - TODO"); } break; @@ -1531,15 +1492,12 @@ public class PICTImageReader extends ImageReaderBase { dataLength = pStream.readUnsignedShort(); pStream.readFully(new byte[dataLength], 0, dataLength); if (DEBUG) { - System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); + System.out.println(String.format("%s: 0x%04x - length: %d", PICT.APPLE_USE_RESERVED_FIELD, opCode, dataLength)); } break; case PICT.OP_PACK_BITS_RECT: - readOpPackBitsRect(pStream, bounds, pixmapCount++); - if (DEBUG) { - System.out.println("packBitsRect - TODO"); - } + readOpPackBitsRect(pStream, pixmapCount++); break; case PICT.OP_PACK_BITS_RGN: @@ -1551,7 +1509,7 @@ public class PICTImageReader extends ImageReaderBase { break; case PICT.OP_DIRECT_BITS_RECT: - readOpDirectBitsRect(pStream, bounds, pixmapCount++); + readOpDirectBitsRect(pStream, pixmapCount++); break; case PICT.OP_DIRECT_BITS_RGN: @@ -1575,17 +1533,21 @@ public class PICTImageReader extends ImageReaderBase { break; case PICT.OP_SHORT_COMMENT:// NOTHING TO DO, JUST JUMP OVER - pStream.readFully(new byte[2], 0, 2); + byte[] shortComment = new byte[2]; + pStream.readFully(shortComment, 0, 2); if (DEBUG) { - System.out.println("Short comment"); + System.out.println("Short comment: " + Arrays.toString(shortComment)); } break; case PICT.OP_LONG_COMMENT:// NOTHING TO DO, JUST JUMP OVER - readLongComment(pStream); - if (DEBUG) { - System.out.println("Long comment"); - } + /*byte[] longComment =*/ readLongComment(pStream); + // TODO: Don't just skip... + // https://developer.apple.com/legacy/library/documentation/mac/pdf/Imaging_With_QuickDraw/Appendix_B.pdf + // Long comments can be used for PhotoShop IRBs (kind 498) or ICC profiles (224) and other meta data... +// if (DEBUG) { +// System.out.println("Long comment: " + Arrays.toString(longComment)); +// } break; case PICT.OP_END_OF_PICTURE:// OK @@ -1654,6 +1616,8 @@ public class PICTImageReader extends ImageReaderBase { pStream.readFully(new byte[dataLength], 0, dataLength); } else { + // TODO: We could issue a warning and return instead? In any case, can't continue, as we don't know the length of the opcode... +// return; throw new IIOException(String.format("Found unknown opcode: 0x%04x", opCode)); } @@ -1737,7 +1701,7 @@ public class PICTImageReader extends ImageReaderBase { pStream.seek(pos + dataLength); // Might be word-align mismatch here - // Skip "QuickTime? and a ... decompressor required" text + // Skip "QuickTime™ and a ... decompressor required" text // TODO: Verify that this is correct. It works with all my test data, but the algorithm is // reverse-engineered by looking at the input data and not from any spec I've seen... int penSizeMagic = pStream.readInt(); @@ -1768,23 +1732,17 @@ public class PICTImageReader extends ImageReaderBase { */ - private void readOpPackBitsRect(ImageInputStream pStream, Rectangle pBounds, int pPixmapCount) throws IOException { - if (DEBUG) { - System.out.println("packBitsRect"); - } - - // Skip PixMap pointer (always 0x000000FF); -// pStream.skipBytes(4); -// int pixmapPointer = pStream.readInt(); -// System.out.println(String.format("%08d: 0x%08x", pStream.getStreamPosition(), pixmapPointer)); - + private void readOpPackBitsRect(final ImageInputStream pStream, final int pPixmapCount) throws IOException { // Get rowBytes int rowBytesRaw = pStream.readUnsignedShort(); // System.out.println(String.format("%08d: 0x%04x", pStream.getStreamPosition(), rowBytesRaw)); - int rowBytes = rowBytesRaw & 0x3FFF; + // TODO: This way to determine pixmap vs bitmap is for version 2 only! + int rowBytes = rowBytesRaw & 0x7FFF; + boolean isPixMap = (rowBytesRaw & 0x8000) > 0; + if (DEBUG) { System.out.print("packBitsRect, rowBytes: " + rowBytes); - if ((rowBytesRaw & 0x8000) > 0) { + if (isPixMap) { System.out.print(", it is a PixMap"); } else { @@ -1793,98 +1751,114 @@ public class PICTImageReader extends ImageReaderBase { } // Get bounds rectangle. THIS IS NOT TO BE SCALED BY THE RESOLUTION! + // TODO: ...or then again...? :-) + Rectangle bounds = new Rectangle(); int y = pStream.readUnsignedShort(); int x = pStream.readUnsignedShort(); - pBounds.setLocation(x, y); + bounds.setLocation(x, y); y = pStream.readUnsignedShort(); x = pStream.readUnsignedShort(); - pBounds.setSize(x - pBounds.x, y - pBounds.y); + bounds.setSize(x - bounds.x, y - bounds.y); if (DEBUG) { - System.out.print(", bounds: " + pBounds); + System.out.print(", bounds: " + bounds); } - // Get PixMap record version number - int pmVersion = pStream.readUnsignedShort() & 0xFFFF; - if (DEBUG) { - System.out.print(", pmVersion: " + pmVersion); - } - - // Get packing format - int packType = pStream.readUnsignedShort() & 0xFFFF; - if (DEBUG) { - System.out.print(", packType: " + packType); - } - - // Get size of packed data (not used for v2) - int packSize = pStream.readInt(); - if (DEBUG) { - System.out.println(", packSize: " + packSize); - } - - // Get resolution info - double hRes = PICTUtil.readFixedPoint(pStream); - double vRes = PICTUtil.readFixedPoint(pStream); - if (DEBUG) { - System.out.print("hRes: " + hRes + ", vRes: " + vRes); - } - - // Get pixel type - int pixelType = pStream.readUnsignedShort(); - if (DEBUG) { - if (pixelType == 0) { - System.out.print(", indexed pixels"); - } - else { - System.out.print(", RGBDirect"); - } - } - - // Get pixel size - int pixelSize = pStream.readUnsignedShort(); - if (DEBUG) { - System.out.print(", pixelSize:" + pixelSize); - } - - // Get pixel component count - int cmpCount = pStream.readUnsignedShort(); - if (DEBUG) { - System.out.print(", cmpCount:" + cmpCount); - } - - // Get pixel component size - int cmpSize = pStream.readUnsignedShort(); - if (DEBUG) { - System.out.print(", cmpSize:" + cmpSize); - } - - // planeBytes (ignored) - int planeBytes = pStream.readInt(); - if (DEBUG) { - System.out.print(", planeBytes:" + planeBytes); - } - - // Handle to ColorTable record, there should be none for direct - // bits so this should be 0, just skip - int clutId = pStream.readInt(); - if (DEBUG) { - System.out.println(", clutId:" + clutId); - } - - // Reserved - pStream.readInt(); - - // Color table ColorModel colorModel; - if (pixelType == 0) { + int cmpSize; + + if (isPixMap) { + // Get PixMap record version number + int pmVersion = pStream.readUnsignedShort(); + if (DEBUG) { + System.out.print(", pmVersion: " + pmVersion); + } + + // Get packing format + int packType = pStream.readUnsignedShort(); + if (DEBUG) { + System.out.print(", packType: " + packType); + } + + // Get size of packed data (not used for v2) + int packSize = pStream.readInt(); // TODO: Probably not int for BitMap (value seems too high)? + if (DEBUG) { + System.out.println(", packSize: " + packSize); + } + + // Get resolution info + double hRes = PICTUtil.readFixedPoint(pStream); + double vRes = PICTUtil.readFixedPoint(pStream); + if (DEBUG) { + System.out.print("hRes: " + hRes + ", vRes: " + vRes); + } + + // Get pixel type + int pixelType = pStream.readUnsignedShort(); + if (DEBUG) { + if (pixelType == 0) { + System.out.print(", indexed pixels"); + } + else { + System.out.print(", RGBDirect"); + } + } + + // Get pixel size + int pixelSize = pStream.readUnsignedShort(); + if (DEBUG) { + System.out.print(", pixelSize:" + pixelSize); + } + + // Get pixel component count + int cmpCount = pStream.readUnsignedShort(); + if (DEBUG) { + System.out.print(", cmpCount:" + cmpCount); + } + + // Get pixel component size + cmpSize = pStream.readUnsignedShort(); + if (DEBUG) { + System.out.print(", cmpSize:" + cmpSize); + } + + // planeBytes (ignored) + int planeBytes = pStream.readInt(); + if (DEBUG) { + System.out.print(", planeBytes:" + planeBytes); + } + + // Handle to ColorTable record + int clutId = pStream.readInt(); + if (DEBUG) { + System.out.println(", clutId:" + clutId); + } + + // Reserved + pStream.readInt(); + + // TODO: Seems to be packType 0 all the time? + // packType = 0 means default.... + + if (packType != 0) { + throw new IIOException("Unknown pack type: " + packType); + } + if (pixelType != 0) { + throw new IIOException("Unsupported pixel type: " + pixelType); + } + + // Color table colorModel = PICTUtil.readColorTable(pStream, pixelSize); } else { - throw new IIOException("Unsupported pixel type: " + pixelType); + // Old style BitMap record + cmpSize = 1; + colorModel = QuickDraw.MONOCHROME; } // Get source rectangle. We DO NOT scale the coordinates by the // resolution info, since we are in pixmap coordinates here + // TODO: readReactangleNonScaled() Rectangle srcRect = new Rectangle(); y = pStream.readUnsignedShort(); x = pStream.readUnsignedShort(); @@ -1910,157 +1884,58 @@ public class PICTImageReader extends ImageReaderBase { // Get transfer mode int transferMode = pStream.readUnsignedShort(); if (DEBUG) { - System.out.print(", mode: " + transferMode); + System.out.println(", mode: " + transferMode); } // Set up pixel buffer for the RGB values - - // TODO: Seems to be packType 0 all the time? - // packType = 0 means default.... - - - // Read in the RGB arrays - byte[] dstBytes; - /* - if (packType == 1 || rowBytes < 8) { - // TODO: Verify this... - dstBytes = new byte[rowBytes]; - } - else if (packType == 2) { - // TODO: Verify this... - dstBytes = new byte[rowBytes * 3 / 4]; - } - else if (packType == 3) { - dstBytes = new byte[2 * pBounds.width]; - } - else if (packType == 4) { - dstBytes = new byte[cmpCount * pBounds.width]; - } - else { - throw new IIOException("Unknown pack type: " + packType); - } - */ - if (packType == 0) { - dstBytes = new byte[cmpCount * pBounds.width]; - } - else { - throw new IIOException("Unknown pack type: " + packType); - } - -// int[] pixArray = new int[pBounds.height * pBounds.width]; - byte[] pixArray = new byte[pBounds.height * pBounds.width]; + byte[] pixArray = new byte[srcRect.height * rowBytes]; int pixBufOffset = 0; - int packedBytesCount; - for (int scanline = 0; scanline < pBounds.height; scanline++) { - // Get byteCount of the scanline - if (rowBytes > 250) { - packedBytesCount = pStream.readUnsignedShort(); - } - else { - packedBytesCount = pStream.readUnsignedByte(); - } - if (DEBUG) { - System.out.println(); - System.out.print("Line " + scanline + ", byteCount: " + packedBytesCount); - System.out.print(" dstBytes: " + dstBytes.length); - } - + // Read in the RGB arrays + for (int scanline = 0; scanline < srcRect.height; scanline++) { // Read in the scanline - /*if (packType > 2) { - // Unpack them all*/ - Decoder decoder;/* - if (packType == 3) { - decoder = new PackBits16Decoder(); - } - else {*/ - decoder = new PackBitsDecoder(); - /*}*/ - DataInput unPackBits = new DataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(pStream, packedBytesCount), decoder)); -// unPackBits.readFully(dstBytes); - unPackBits.readFully(pixArray, pixBufOffset, pBounds.width); - /*} - else { - imageInput.readFully(dstBytes); - }*/ + if (rowBytes > 8) { + // Get byteCount of the scanline + int packedBytesCount = rowBytes > 250 ? pStream.readUnsignedShort() : pStream.readUnsignedByte(); - // TODO: Use TYPE_USHORT_555_RGB for 16 bit - /* - if (packType == 3) { - for (int i = 0; i < pBounds.width; i++) { - // Set alpha values to all opaque - pixArray[pixBufOffset + i] = 0xFF000000; - - // Get red values - int red = 8 * ((dstBytes[2 * i] & 0x7C) >> 2); - pixArray[pixBufOffset + i] |= red << 16; - // Get green values - int green = 8 * (((dstBytes[2 * i] & 0x07) << 3) + ((dstBytes[2 * i + 1] & 0xE0) >> 5)); - pixArray[pixBufOffset + i] |= green << 8; - // Get blue values - int blue = 8 * ((dstBytes[2 * i + 1] & 0x1F)); - pixArray[pixBufOffset + i] |= blue; - } + // Unpack them all + Decoder decoder = new PackBitsDecoder(); + DataInput unPackBits = new DataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(pStream, packedBytesCount), decoder)); + unPackBits.readFully(pixArray, pixBufOffset, rowBytes); } else { - if (cmpCount == 3) { - for (int i = 0; i < pBounds.width; i++) { - // Set alpha values to all opaque - pixArray[pixBufOffset + i] = 0xFF000000; - // Get red values - pixArray[pixBufOffset + i] |= (dstBytes[i] & 0xFF) << 16; - // Get green values - pixArray[pixBufOffset + i] |= (dstBytes[pBounds.width + i] & 0xFF) << 8; - // Get blue values - pixArray[pixBufOffset + i] |= (dstBytes[2 * pBounds.width + i] & 0xFF); - } - } - else { - for (int i = 0; i < pBounds.width; i++) { -// // Get alpha values -// pixArray[pixBufOffset + i] = (dstBytes[i] & 0xFF) << 24; -// // Get red values -// pixArray[pixBufOffset + i] |= (dstBytes[pBounds.width + i] & 0xFF) << 16; -// // Get green values -// pixArray[pixBufOffset + i] |= (dstBytes[2 * pBounds.width + i] & 0xFF) << 8; -// // Get blue values -// pixArray[pixBufOffset + i] |= (dstBytes[3 * pBounds.width + i] & 0xFF); - - // TODO: Fake it for now... Should ideally just use byte array and use the ICM -// pixArray[pixBufOffset + i] = 0xFF << 24; -// pixArray[pixBufOffset + i] |= colorModel.getRed(dstBytes[i] & 0xFF) << 16; -// pixArray[pixBufOffset + i] |= colorModel.getGreen(dstBytes[i] & 0xFF) << 8; -// pixArray[pixBufOffset + i] |= colorModel.getBlue(dstBytes[i] & 0xFF); - - pixArray[pixBufOffset + i] = dstBytes[i]; - } -// } -// } -*/ + // Uncompressed + imageInput.readFully(pixArray, pixBufOffset, rowBytes); + } // Increment pixel buffer offset - pixBufOffset += pBounds.width; + pixBufOffset += rowBytes; //////////////////////////////////////////////////// // TODO: This works for single image PICTs only... // However, this is the most common case. Ok for now - processImageProgress(scanline * 100 / pBounds.height); + processImageProgress(scanline * 100 / srcRect.height); if (abortRequested()) { processReadAborted(); // Skip rest of image data - for (int skip = scanline + 1; skip < pBounds.height; skip++) { + for (int skip = scanline + 1; skip < srcRect.height; skip++) { // Get byteCount of the scanline - if (rowBytes > 250) { + int packedBytesCount; + + if (rowBytes <= 8) { + packedBytesCount = rowBytes; + } + else if (rowBytes > 250) { packedBytesCount = pStream.readUnsignedShort(); } else { packedBytesCount = pStream.readUnsignedByte(); } + pStream.readFully(new byte[packedBytesCount], 0, packedBytesCount); if (DEBUG) { - System.out.println(); System.out.print("Skip " + skip + ", byteCount: " + packedBytesCount); } } @@ -2074,11 +1949,8 @@ public class PICTImageReader extends ImageReaderBase { // "pPixmapCount" will never be greater than the size of the vector if (images.size() <= pPixmapCount) { // Create BufferedImage and add buffer it for multiple reads -// DirectColorModel cm = (DirectColorModel) ColorModel.getRGBdefault(); -// DataBuffer db = new DataBufferInt(pixArray, pixArray.length); -// WritableRaster raster = Raster.createPackedRaster(db, pBounds.width, pBounds.height, pBounds.width, cm.getMasks(), null); DataBuffer db = new DataBufferByte(pixArray, pixArray.length); - WritableRaster raster = Raster.createPackedRaster(db, pBounds.width, pBounds.height, cmpSize, null); // TODO: last param should ideally be srcRect.getLocation() + WritableRaster raster = Raster.createPackedRaster(db, (rowBytes * 8) / cmpSize, srcRect.height, cmpSize, null); // TODO: last param should ideally be srcRect.getLocation() BufferedImage img = new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null); images.add(img); @@ -2102,24 +1974,23 @@ public class PICTImageReader extends ImageReaderBase { * Reads the data following a {@code directBitsRect} opcode. * * @param pStream the stream to read from - * @param pBounds the bounding rectangle * @param pPixmapCount the index of the bitmap in the PICT file, used for * cahcing. * * @throws javax.imageio.IIOException if the data can not be read. - * @throws IOException if an I/O error occurs while reading the image. + * @throws java.io.IOException if an I/O error occurs while reading the image. */ - private void readOpDirectBitsRect(ImageInputStream pStream, Rectangle pBounds, int pPixmapCount) throws IOException { + private void readOpDirectBitsRect(final ImageInputStream pStream, final int pPixmapCount) throws IOException { if (DEBUG) { System.out.println("directBitsRect"); } // Skip PixMap pointer (always 0x000000FF); - pStream.skipBytes(4); + pStream.readInt(); // Get rowBytes int rowBytesRaw = pStream.readUnsignedShort(); - int rowBytes = rowBytesRaw & 0x3FFF; + int rowBytes = rowBytesRaw & 0x7FFF; if (DEBUG) { System.out.print("directBitsRect, rowBytes: " + rowBytes); if ((rowBytesRaw & 0x8000) > 0) { @@ -2131,25 +2002,27 @@ public class PICTImageReader extends ImageReaderBase { } // Get bounds rectangle. THIS IS NOT TO BE SCALED BY THE RESOLUTION! + // TODO: ...or then again...? :-) + Rectangle bounds = new Rectangle(); int y = pStream.readUnsignedShort(); int x = pStream.readUnsignedShort(); - pBounds.setLocation(x, y); + bounds.setLocation(x, y); y = pStream.readUnsignedShort(); x = pStream.readUnsignedShort(); - pBounds.setSize(x - pBounds.x, y - pBounds.y); + bounds.setSize(x - bounds.x, y - bounds.y); if (DEBUG) { - System.out.print(", bounds: " + pBounds); + System.out.print(", bounds: " + bounds); } // Get PixMap record version number - int pmVersion = pStream.readUnsignedShort() & 0xFFFF; + int pmVersion = pStream.readUnsignedShort(); if (DEBUG) { System.out.print(", pmVersion: " + pmVersion); } // Get packing format - int packType = pStream.readUnsignedShort() & 0xFFFF; + int packType = pStream.readUnsignedShort(); if (DEBUG) { System.out.print(", packType: " + packType); } @@ -2221,7 +2094,6 @@ public class PICTImageReader extends ImageReaderBase { System.out.print("opDirectBitsRect, srcRect:" + srcRect); } - // TODO: FixMe... // Get destination rectangle. We DO scale the coordinates according to // the image resolution, since we are working in display coordinates Rectangle dstRect = new Rectangle(); @@ -2240,19 +2112,11 @@ public class PICTImageReader extends ImageReaderBase { // Read in the RGB arrays byte[] dstBytes; - if (packType == 1 || rowBytes < 8) { - // TODO: Verify this... + if (packType == 1 || packType == 2 || packType == 3) { dstBytes = new byte[rowBytes]; } - else if (packType == 2) { - // TODO: Verify this... - dstBytes = new byte[rowBytes * 3 / 4]; - } - else if (packType == 3) { - dstBytes = new byte[2 * pBounds.width]; - } else if (packType == 4) { - dstBytes = new byte[cmpCount * pBounds.width]; + dstBytes = new byte[cmpCount * rowBytes / 4]; } else { throw new IIOException("Unknown pack type: " + packType); @@ -2261,40 +2125,35 @@ public class PICTImageReader extends ImageReaderBase { int[] pixArray = null; short[] shortArray = null; if (packType == 3) { - shortArray = new short[pBounds.height * pBounds.width]; + shortArray = new short[srcRect.height * (rowBytes + 1) / 2]; } else { - pixArray = new int[pBounds.height * pBounds.width]; + pixArray = new int[srcRect.height * (rowBytes + 3) / 4]; } int pixBufOffset = 0; int packedBytesCount; - for (int scanline = 0; scanline < pBounds.height; scanline++) { - // Get byteCount of the scanline - if (rowBytes > 250) { - packedBytesCount = pStream.readUnsignedShort(); - } - else { - packedBytesCount = pStream.readUnsignedByte(); - } - if (DEBUG) { - System.out.println(); - System.out.print("Line " + scanline + ", byteCount: " + packedBytesCount); - System.out.print(" dstBytes: " + dstBytes.length); - } - + for (int scanline = 0; scanline < srcRect.height; scanline++) { // Read in the scanline if (packType > 2) { - // Unpack them all - Decoder decoder; - if (packType == 3) { - decoder = new PackBits16Decoder(); + // Get byteCount of the scanline + if (rowBytes > 250) { + packedBytesCount = pStream.readUnsignedShort(); } else { - decoder = new PackBitsDecoder(); + packedBytesCount = pStream.readUnsignedByte(); } - DataInput unPackBits = new DataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(pStream, packedBytesCount), decoder)); + + if (DEBUG) { + System.out.print("Line " + scanline + ", byteCount: " + packedBytesCount); + System.out.print(" dstBytes: " + dstBytes.length); + System.out.println(); + } + + // Unpack them all + Decoder decoder = packType == 3 ? new PackBitsDecoder(2, false) : new PackBitsDecoder(); + DataInput unPackBits = new DataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(pStream, packedBytesCount), decoder, dstBytes.length)); unPackBits.readFully(dstBytes); } else { @@ -2303,63 +2162,51 @@ public class PICTImageReader extends ImageReaderBase { if (packType == 3) { // TYPE_USHORT_555_RGB for 16 bit - for (int i = 0; i < pBounds.width; i++) { + for (int i = 0; i < srcRect.width; i++) { shortArray[pixBufOffset + i] = (short) (((0xff & dstBytes[2 * i]) << 8) | (0xff & dstBytes[2 * i + 1])); -// // Set alpha values to all opaque -// pixArray[pixBufOffset + i] = 0xFF000000; -// -// // Get red values -// int red = 8 * ((dstBytes[2 * i] & 0x7C) >> 2); -// pixArray[pixBufOffset + i] |= red << 16; -// // Get green values -// int green = 8 * (((dstBytes[2 * i] & 0x07) << 3) + ((dstBytes[2 * i + 1] & 0xE0) >> 5)); -// pixArray[pixBufOffset + i] |= green << 8; -// // Get blue values -// int blue = 8 * ((dstBytes[2 * i + 1] & 0x1F)); -// pixArray[pixBufOffset + i] |= blue; } } else { if (cmpCount == 3) { // RGB - for (int i = 0; i < pBounds.width; i++) { + for (int i = 0; i < srcRect.width; i++) { // Set alpha values to all opaque - pixArray[pixBufOffset + i] = 0xFF000000; + pixArray[pixBufOffset + i] = 0xFF000000 // Get red values - pixArray[pixBufOffset + i] |= (dstBytes[i] & 0xFF) << 16; + | (dstBytes[/*0* bounds.width*/i] & 0xFF) << 16 // Get green values - pixArray[pixBufOffset + i] |= (dstBytes[pBounds.width + i] & 0xFF) << 8; + | (dstBytes[/**/bounds.width + i] & 0xFF) << 8 // Get blue values - pixArray[pixBufOffset + i] |= (dstBytes[2 * pBounds.width + i] & 0xFF); + | (dstBytes[2 * bounds.width + i] & 0xFF); } } else { // ARGB - for (int i = 0; i < pBounds.width; i++) { + for (int i = 0; i < srcRect.width; i++) { // Get alpha values - pixArray[pixBufOffset + i] = (dstBytes[i] & 0xFF) << 24; + pixArray[pixBufOffset + i] = (dstBytes[/*0* bounds.width*/i] & 0xFF) << 24 // Get red values - pixArray[pixBufOffset + i] |= (dstBytes[pBounds.width + i] & 0xFF) << 16; + | (dstBytes[/**/bounds.width + i] & 0xFF) << 16 // Get green values - pixArray[pixBufOffset + i] |= (dstBytes[2 * pBounds.width + i] & 0xFF) << 8; + | (dstBytes[2 * bounds.width + i] & 0xFF) << 8 // Get blue values - pixArray[pixBufOffset + i] |= (dstBytes[3 * pBounds.width + i] & 0xFF); + | (dstBytes[3 * bounds.width + i] & 0xFF); } } } // Increment pixel buffer offset - pixBufOffset += pBounds.width; + pixBufOffset += srcRect.width; //////////////////////////////////////////////////// // TODO: This works for single image PICTs only... // However, this is the most common case. Ok for now - processImageProgress(scanline * 100 / pBounds.height); + processImageProgress(scanline * 100 / srcRect.height); if (abortRequested()) { processReadAborted(); // Skip rest of image data - for (int skip = scanline + 1; skip < pBounds.height; skip++) { + for (int skip = scanline + 1; skip < srcRect.height; skip++) { // Get byteCount of the scanline if (rowBytes > 250) { packedBytesCount = pStream.readUnsignedShort(); @@ -2390,12 +2237,12 @@ public class PICTImageReader extends ImageReaderBase { if (packType == 3) { cm = new DirectColorModel(15, 0x7C00, 0x03E0, 0x001F); // See BufferedImage TYPE_USHORT_555_RGB DataBuffer db = new DataBufferUShort(shortArray, shortArray.length); - raster = Raster.createPackedRaster(db, pBounds.width, pBounds.height, pBounds.width, cm.getMasks(), null); // TODO: last param should ideally be srcRect.getLocation() + raster = Raster.createPackedRaster(db, srcRect.width, srcRect.height, srcRect.width, cm.getMasks(), null); // TODO: last param should ideally be srcRect.getLocation() } else { cm = (DirectColorModel) ColorModel.getRGBdefault(); DataBuffer db = new DataBufferInt(pixArray, pixArray.length); - raster = Raster.createPackedRaster(db, pBounds.width, pBounds.height, pBounds.width, cm.getMasks(), null); // TODO: last param should ideally be srcRect.getLocation() + raster = Raster.createPackedRaster(db, srcRect.width, srcRect.height, srcRect.width, cm.getMasks(), null); // TODO: last param should ideally be srcRect.getLocation() } BufferedImage img = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); @@ -2540,13 +2387,20 @@ public class PICTImageReader extends ImageReaderBase { /* * Read a long comment from the stream. */ - private void readLongComment(final DataInput pStream) throws IOException { + private byte[] readLongComment(final DataInput pStream) throws IOException { // Comment kind and data byte count - pStream.readShort(); + short kind = pStream.readShort(); + int length = pStream.readUnsignedShort(); + + if (DEBUG) { + System.err.println("Long comment: " + kind + ", " + length + " bytes"); + } // Get as many bytes as indicated by byte count - int length = pStream.readUnsignedShort(); - pStream.readFully(new byte[length], 0, length); + byte[] bytes = new byte[length]; + pStream.readFully(bytes, 0, length); + + return bytes; } /* @@ -2623,12 +2477,19 @@ public class PICTImageReader extends ImageReaderBase { BufferedImage image = getDestination(pParam, getImageTypes(pIndex), getXPtCoord(frame.width), getYPtCoord(frame.height)); Graphics2D g = image.createGraphics(); try { - // TODO: Might need to clear background + // Might need to clear background + g.setComposite(AlphaComposite.Src); + g.setColor(new Color(0x00ffffff, true)); // Transparent white +// g.setColor(Color.WHITE); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + AffineTransform instance = new AffineTransform(); + if (pParam != null && pParam.getSourceRegion() != null) { Rectangle rectangle = pParam.getSourceRegion(); instance.translate(-rectangle.x, -rectangle.y); } + instance.scale(screenImageXRatio / subX, screenImageYRatio / subY); g.setTransform(instance); // try { @@ -2667,158 +2528,37 @@ public class PICTImageReader extends ImageReaderBase { ).iterator(); } - public static void main(String[] pArgs) throws IOException { - ImageReader reader = new PICTImageReader(new PICTImageReaderSpi()); - - ImageInputStream input; - String title; - if (pArgs.length >= 1) { - File file = new File(pArgs[0]); - input = ImageIO.createImageInputStream(file); - title = file.getName(); - } - else { - input = ImageIO.createImageInputStream(new ByteArrayInputStream(DATA_V1_OVERPAINTED_ARC)); - title = "PICT test data"; - } - - System.out.println("canRead: " + reader.getOriginatingProvider().canDecodeInput(input)); - - reader.setInput(input); - long start = System.currentTimeMillis(); - BufferedImage image = reader.read(0); - - System.out.println("time: " + (System.currentTimeMillis() - start)); - - showIt(image, title); - - System.out.println("image = " + image); + protected static void showIt(final BufferedImage pImage, final String pTitle) { + ImageReaderBase.showIt(pImage, pTitle); } - // Sample data from http://developer.apple.com/documentation/mac/QuickDraw/QuickDraw-458.html - // TODO: Create test case(s)! - private static final byte[] DATA_EXT_V2 = { - 0x00, 0x78, /* picture size; don't use this value for picture size */ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x6C, 0x00, (byte) 0xA8, /* bounding rectangle of picture at 72 dpi */ - 0x00, 0x11, /* VersionOp opcode; always $0011 for extended version 2 */ - 0x02, (byte) 0xFF, /* Version opcode; always $02FF for extended version 2 */ - 0x0C, 0x00, /* HeaderOp opcode; always $0C00 for extended version 2 */ - /* next 24 bytes contain header information */ - (byte) 0xFF, (byte) 0xFE, /* version; always -2 for extended version 2 */ - 0x00, 0x00, /* reserved */ - 0x00, 0x48, 0x00, 0x00, /* best horizontal resolution: 72 dpi */ - 0x00, 0x48, 0x00, 0x00, /* best vertical resolution: 72 dpi */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* optimal source rectangle for 72 dpi horizontal - and 72 dpi vertical resolutions */ - 0x00, 0x00, /* reserved */ - 0x00, 0x1E, /* DefHilite opcode to use default hilite color */ - 0x00, 0x01, /* Clip opcode to define clipping region for picture */ - 0x00, 0x0A, /* region size */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for clipping region */ - 0x00, 0x0A, /* FillPat opcode; fill pattern specified in next 8 bytes */ - 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, /* fill pattern */ - 0x00, 0x34, /* fillRect opcode; rectangle specified in next 8 bytes */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* rectangle to fill */ - 0x00, 0x0A, /* FillPat opcode; fill pattern specified in next 8 bytes */ - (byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, /* fill pattern */ - 0x00, 0x5C, /* fillSameOval opcode */ - 0x00, 0x08, /* PnMode opcode */ - 0x00, 0x08, /* pen mode data */ - 0x00, 0x71, /* paintPoly opcode */ - 0x00, 0x1A, /* size of polygon */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for polygon */ - 0x00, 0x6E, 0x00, 0x02, 0x00, 0x02, 0x00, 0x54, 0x00, 0x6E, 0x00, (byte) 0xAA, 0x00, 0x6E, 0x00, 0x02, /* polygon points */ - 0x00, (byte) 0xFF, /* OpEndPic opcode; end of picture */ - }; + public static void main(final String[] pArgs) throws IOException { + ImageReader reader = new PICTImageReader(new PICTImageReaderSpi()); - private static final byte[] DATA_V2 = { - 0x00, 0x78, /* picture size; don't use this value for picture size */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle of picture */ - 0x00, 0x11, /* VersionOp opcode; always $0x00, 0x11, for version 2 */ - 0x02, (byte) 0xFF, /* Version opcode; always $0x02, 0xFF, for version 2 */ - 0x0C, 0x00, /* HeaderOp opcode; always $0C00 for version 2 */ - /* next 24 bytes contain header information */ - (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, /* version; always -1 (long) for version 2 */ - 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, (byte) 0xAA, 0x00, 0x00, 0x00, 0x6E, 0x00, 0x00, /* fixed-point bounding - rectangle for picture */ - 0x00, 0x00, 0x00, 0x00, /* reserved */ - 0x00, 0x1E, /* DefHilite opcode to use default hilite color */ - 0x00, 0x01, /* Clip opcode to define clipping region for picture */ - 0x00, 0x0A, /* region size */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for clipping region */ - 0x00, 0x0A, /* FillPat opcode; fill pattern specifed in next 8 bytes */ - 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, /* fill pattern */ - 0x00, 0x34, /* fillRect opcode; rectangle specified in next 8 bytes */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* rectangle to fill */ - 0x00, 0x0A, /* FillPat opcode; fill pattern specified in next 8 bytes */ - (byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, /* fill pattern */ - 0x00, 0x5C, /* fillSameOval opcode */ - 0x00, 0x08, /* PnMode opcode */ - 0x00, 0x08, /* pen mode data */ - 0x00, 0x71, /* paintPoly opcode */ - 0x00, 0x1A, /* size of polygon */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for polygon */ - 0x00, 0x6E, 0x00, 0x02, 0x00, 0x02, 0x00, 0x54, 0x00, 0x6E, 0x00, (byte) 0xAA, 0x00, 0x6E, 0x00, 0x02, /* polygon points */ - 0x00, (byte) 0xFF, /* OpEndPic opcode; end of picture */ - }; + for (String arg : pArgs) { + File file = new File(arg); + try { + ImageInputStream input = ImageIO.createImageInputStream(file); + String title = file.getName(); - private static final byte[] DATA_V1 = { - 0x00, 0x4F, /* picture size; this value is reliable for version 1 pictures */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle of picture */ - 0x11, /* picVersion opcode for version 1 */ - 0x01, /* version number 1 */ - 0x01, /* ClipRgn opcode to define clipping region for picture */ - 0x00, 0x0A, /* region size */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for region */ - 0x0A, /* FillPat opcode; fill pattern specified in next 8 bytes */ - 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, 0x77, (byte) 0xDD, /* fill pattern */ - 0x34, /* fillRect opcode; rectangle specified in next 8 bytes */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* rectangle to fill */ - 0x0A, /* FillPat opcode; fill pattern specified in next 8 bytes */ - (byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, (byte) 0x88, 0x22, /* fill pattern */ - 0x5C, /* fillSameOval opcode */ - 0x71, /* paintPoly opcode */ - 0x00, 0x1A, /* size of polygon */ - 0x00, 0x02, 0x00, 0x02, 0x00, 0x6E, 0x00, (byte) 0xAA, /* bounding rectangle for polygon */ - 0x00, 0x6E, 0x00, 0x02, 0x00, 0x02, 0x00, 0x54, 0x00, 0x6E, 0x00, (byte) 0xAA, 0x00, 0x6E, 0x00, 0x02, /* polygon points */ - (byte) 0xFF, /* EndOfPicture opcode; end of picture */ - }; + System.out.println("canRead: " + reader.getOriginatingProvider().canDecodeInput(input)); - // Examples from http://developer.apple.com/technotes/qd/qd_14.html - private static final byte[] DATA_V1_OVAL_RECT = { - 0x00, 0x26, /*size */ - 0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, /* picFrame */ - 0x11, 0x01, /* version 1 */ - 0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFA, 0x01, (byte) 0x90, /* clipRgn -- 10 byte region */ - 0x0B, 0x00, 0x04, 0x00, 0x05, /* ovSize point */ - 0x40, 0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, /* frameRRect rectangle */ - (byte) 0xFF, /* fin */ - }; + reader.setInput(input); + // BufferedImage image = reader.getImageTypes(0).next().createBufferedImage(reader.getWidth(0), reader.getHeight(0)); + // ImageReadParam param = reader.getDefaultReadParam(); + // param.setDestination(image); - private static final byte[] DATA_V1_OVERPAINTED_ARC = { - 0x00, 0x36, /* size */ - 0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, /* picFrame */ - 0x11, 0x01, /* version 1 */ - 0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFA, 0x01, (byte) 0x90, /* clipRgn -- 10 byte region */ - 0x61, 0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, 0x00, 0x03, 0x00, 0x2D, /* paintArc rectangle,startangle,endangle */ - 0x08, 0x00, 0x0A, /* pnMode patXor -- note that the pnMode comes before the pnPat */ - 0x09, (byte) 0xAA, 0x55, (byte) 0xAA, 0x55, (byte) 0xAA, 0x55, (byte) 0xAA, 0x55, /* pnPat gray */ - 0x69, 0x00, 0x03, 0x00, 0x2D, /* paintSameArc startangle,endangle */ - (byte) 0xFF, /* fin */ - }; + long start = System.currentTimeMillis(); + BufferedImage image = reader.read(0); + System.out.println("time: " + (System.currentTimeMillis() - start)); - private static final byte[] DATA_V1_COPY_BITS = { - 0x00, 0x48, /* size */ - 0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, /* picFrame */ - 0x11, 0x01, /* version 1 */ - 0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFA, 0x01, (byte) 0x90, /* clipRgn -- 10 byte region */ - 0x31, 0x00, 0x0A, 0x00, 0x14, 0x00, (byte) 0xAF, 0x00, 0x78, /* paintRect rectangle */ - (byte) 0x90, 0x00, 0x02, 0x00, 0x0A, 0x00, 0x14, 0x00, 0x0F, 0x00, 0x1C, /* BitsRect rowbytes bounds (note that bounds is wider than smallr) */ - 0x00, 0x0A, 0x00, 0x14, 0x00, 0x0F, 0x00, 0x19, /* srcRect */ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x1E, /* dstRect */ - 0x00, 0x06, /* mode=notSrcXor */ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 5 rows of empty bitmap (we copied from a - still-blank window) */ - (byte) 0xFF, /* fin */ - }; + showIt(image, title); + + System.out.println("image = " + image); + } + catch (IOException e) { + System.err.println("Could not read " + file.getAbsolutePath() + ": " + e); + } + } + } } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java index bfa92b0c..76a23cab 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java @@ -34,8 +34,8 @@ import com.twelvemonkeys.imageio.util.IIOUtil; import javax.imageio.ImageReader; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; -import java.io.IOException; import java.io.EOFException; +import java.io.IOException; import java.util.Locale; /** @@ -85,7 +85,7 @@ public class PICTImageReaderSpi extends ImageReaderSpi { else { // Skip header 512 bytes for file-based streams stream.reset(); - PICTImageReader.skipNullHeader(stream); + skipNullHeader(stream); } return isPICT(stream); @@ -98,6 +98,12 @@ public class PICTImageReaderSpi extends ImageReaderSpi { } } + static void skipNullHeader(final ImageInputStream pStream) throws IOException { + // NOTE: Only skip if FILE FORMAT, not needed for Mac OS DnD + // Spec says "platofrm dependent", may not be all nulls.. + pStream.skipBytes(PICT.PICT_NULL_HEADER_SIZE); + } + private boolean isPICT(final ImageInputStream pStream) throws IOException { // Size may be 0, so we can't use this for validation... pStream.readUnsignedShort(); diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriter.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriter.java index a4baa53c..df8734b6 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriter.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriter.java @@ -258,7 +258,7 @@ public class PICTImageWriter extends ImageWriterBase { // Treat the scanline. for (int j = 0; j < w; j++) { if (model instanceof ComponentColorModel && model.getColorSpace().getType() == ColorSpace.TYPE_RGB) { - // NOTE: Assumes component order always (A)BGR + // NOTE: Assumes component order always (A)BGR and sRGB // TODO: Alpha support scanlineBytes[x + j] = pixels[off + i * scansize * components + components * j + components - 1]; scanlineBytes[x + w + j] = pixels[off + i * scansize * components + components * j + components - 2]; diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTUtil.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTUtil.java index ef503715..08de9277 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTUtil.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTUtil.java @@ -34,7 +34,8 @@ import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.io.DataInput; import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; /** * PICTUtil @@ -47,15 +48,14 @@ final class PICTUtil { private static final String ENC_MAC_ROMAN = "MacRoman"; - public static final String ENCODING = initEncoding(); + public static final Charset ENCODING = initEncoding(); - private static String initEncoding() { + private static Charset initEncoding() { try { - new String("\uF8FF".getBytes(), ENC_MAC_ROMAN); - return ENC_MAC_ROMAN; + return Charset.forName(ENC_MAC_ROMAN); } - catch (UnsupportedEncodingException e) { - return "ISO-8859-1"; + catch (UnsupportedCharsetException e) { + return Charset.forName("ISO-8859-1"); } } @@ -86,9 +86,9 @@ final class PICTUtil { * @throws java.io.IOException if an I/O error occurs during read */ public static Dimension readDimension(final DataInput pStream) throws IOException { - final int h = pStream.readShort() ; - final int v = pStream.readShort() ; - return new Dimension(h,v); + int h = pStream.readShort(); + int v = pStream.readShort(); + return new Dimension(h, v); } /** @@ -102,8 +102,8 @@ final class PICTUtil { * @throws IOException if an I/O exception occurs during reading */ public static String readStr31(final DataInput pStream) throws IOException { - String text = readPascalString(pStream); - int length = 31 - text.length(); + String text = readPascalString(pStream); + int length = 31 - text.length(); if (length < 0) { throw new IOException("String length exceeds maximum (31): " + text.length()); } @@ -112,7 +112,7 @@ final class PICTUtil { } /** - * Reads a Pascal String from the given strean. + * Reads a Pascal String from the given stream. * The input stream must be positioned at the length byte of the text, * which can thus be a maximum of 255 characters long. * @@ -146,6 +146,14 @@ final class PICTUtil { return new BitMapPattern(data); } + // TODO: Refactor, don't need both readPattern methods + public static Pattern readPattern(final DataInput pStream, final Color fg, final Color bg) throws IOException { + // Get the data (8 bytes) + byte[] data = new byte[8]; + pStream.readFully(data); + return new BitMapPattern(data, fg, bg); + } + /** * Reads a variable width {@link Pattern color pattern} from the given stream * @@ -221,7 +229,7 @@ final class PICTUtil { /** * Reads a {@code ColorTable} data structure from the given stream. * - * @param pStream the input stream + * @param pStream the input stream * @param pPixelSize the pixel size * @return the indexed color model created from the {@code ColorSpec} records read. * @@ -252,7 +260,7 @@ final class PICTUtil { int[] colors = new int[size]; - for (int i = 0; i < size ; i++) { + for (int i = 0; i < size; i++) { // Read ColorSpec records int index = pStream.readUnsignedShort(); Color color = readRGBColor(pStream); diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/Pattern.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/Pattern.java index 32841877..13ceeec1 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/Pattern.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/Pattern.java @@ -29,9 +29,9 @@ package com.twelvemonkeys.imageio.plugins.pict; import java.awt.*; -import java.awt.geom.Rectangle2D; import java.awt.geom.AffineTransform; -import java.awt.image.*; +import java.awt.geom.Rectangle2D; +import java.awt.image.ColorModel; import java.util.Collections; /** @@ -42,7 +42,7 @@ import java.util.Collections; * @version $Id: Pattern.java,v 1.0 Oct 9, 2007 1:21:38 AM haraldk Exp$ */ abstract class Pattern implements Paint { - private final Paint paint; + protected final Paint paint; Pattern(final Paint pPaint) { paint = pPaint; @@ -60,5 +60,7 @@ abstract class Pattern implements Paint { public int getTransparency() { return paint.getTransparency(); - } + } + + public abstract Pattern derive(Color foreground, Color background); } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMap.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMap.java new file mode 100644 index 00000000..43192ca8 --- /dev/null +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMap.java @@ -0,0 +1,11 @@ +package com.twelvemonkeys.imageio.plugins.pict; + +/** + * PixMap. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: PixMap.java,v 1.0 20/02/15 harald.kuhr Exp$ + */ +final class PixMap { +} diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMapPattern.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMapPattern.java index d2108320..d84f0a45 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMapPattern.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMapPattern.java @@ -48,7 +48,12 @@ final class PixMapPattern extends Pattern { /** * @return the fallback B/W pattern */ - public Pattern getPattern() { + public Pattern getFallbackPattern() { return fallback; } + + @Override + public Pattern derive(final Color foreground, final Color background) { + return getFallbackPattern().derive(foreground, background); + } } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDrawContext.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDrawContext.java index e9f52f9d..fd5a0567 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDrawContext.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDrawContext.java @@ -31,8 +31,11 @@ package com.twelvemonkeys.imageio.plugins.pict; import com.twelvemonkeys.lang.Validate; import java.awt.*; -import java.awt.image.BufferedImage; import java.awt.geom.*; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; /** * Emulates an Apple QuickDraw rendering context, backed by a Java {@link Graphics2D}. @@ -121,9 +124,20 @@ class QuickDrawContext { private Dimension2D penSize = new Dimension(); private int penMode; - QuickDrawContext(Graphics2D pGraphics) { + // TODO: Make sure setting bgColor/fgColor does not reset pattern, and pattern not resetting bg/fg! + private Color bgColor = Color.WHITE; + private Color fgColor = Color.BLACK; + + private int textMode; + private Pattern textPattern = new BitMapPattern(Color.BLACK); + private Pattern fillPattern; + + QuickDrawContext(final Graphics2D pGraphics) { graphics = Validate.notNull(pGraphics, "graphics"); - + + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + setPenNormal(); } @@ -144,18 +158,34 @@ class QuickDrawContext { // Font number (sic), integer void setTextFont(int fontFamily) { // ..? - System.err.println("QuickDrawContext.setTextFont"); + System.err.println("QuickDrawContext.setTextFont: " + fontFamily); + } + + public void setTextFont(final String fontName) { + // TODO: Need mapping between known QD font names and Java font names? + Font current = graphics.getFont(); + graphics.setFont(Font.decode(fontName).deriveFont(current.getStyle(), (float) current.getSize())); } // Sets the text's font style (0..255) - void setTextFace(int face) { - // int? - System.err.println("QuickDrawContext.setTextFace"); + void setTextFace(final int face) { + int style = 0; + if ((face & QuickDraw.TX_BOLD_MASK) > 0) { + style |= Font.BOLD; + } + if ((face & QuickDraw.TX_ITALIC_MASK) > 0) { + style |= Font.ITALIC; + } + + // TODO: Other face options, like underline, shadow, etc... + + graphics.setFont(graphics.getFont().deriveFont(style)); } void setTextMode(int pSourceMode) { // ..? System.err.println("QuickDrawContext.setTextMode"); + textMode = pSourceMode; } public void setTextSize(int pSize) { @@ -175,15 +205,24 @@ class QuickDrawContext { graphics.translate(pOrigin.getX(), pOrigin.getY()); } - public void setForeground(Color pColor) { - // TODO: Is this really correct? Or does it depend on pattern mode? + public void setForeground(final Color pColor) { + fgColor = pColor; penPattern = new BitMapPattern(pColor); } - public void setBackground(Color pColor) { + Color getForeground() { + return fgColor; + } + + public void setBackground(final Color pColor) { + bgColor = pColor; background = new BitMapPattern(pColor); } + Color getBackground() { + return bgColor; + } + /* // Pen management: // NOTE: The HidePen procedure is called by the OpenRgn, OpenPicture, and OpenPoly routines so that you can create regions, pictures, and polygons without drawing on the screen. @@ -306,10 +345,14 @@ class QuickDrawContext { BackPat // Used by the Erase* methods *BackPixPat */ - public void setBackgroundPattern(Pattern pPaint) { + public void setBackgroundPattern(final Pattern pPaint) { background = pPaint; } + public void setFillPattern(final Pattern fillPattern) { + this.fillPattern = fillPattern; + } + private Composite getCompositeFor(final int pMode) { switch (pMode & ~QuickDraw.DITHER_COPY) { // Boolean source transfer modes @@ -321,9 +364,10 @@ class QuickDrawContext { return AlphaComposite.Xor; case QuickDraw.SRC_BIC: return AlphaComposite.Clear; + case QuickDraw.NOT_SRC_XOR: + return new NotSrcXor(); case QuickDraw.NOT_SRC_COPY: case QuickDraw.NOT_SRC_OR: - case QuickDraw.NOT_SRC_XOR: case QuickDraw.NOT_SRC_BIC: throw new UnsupportedOperationException("Not implemented for mode " + pMode); // return null; @@ -349,6 +393,15 @@ class QuickDrawContext { } } + /** + * Sets up context for text drawing. + */ + protected void setupForText() { + graphics.setPaint(textPattern); + graphics.setComposite(getCompositeFor(textMode)); + } + + /** * Sets up context for line drawing/painting. */ @@ -415,9 +468,7 @@ class QuickDrawContext { if (isPenVisible()) { // NOTE: Workaround for known Mac JDK bug: Paint, not frame - //graphics.setStroke(getStroke(penSize)); // Make sure we have correct stroke paintShape(graphics.getStroke().createStrokedShape(line)); - } moveTo(pX, pY); @@ -811,13 +862,18 @@ class QuickDrawContext { // TODO: All other operations can delegate to these! :-) private void frameShape(final Shape pShape) { - setupForPaint(); - graphics.draw(pShape); + if (isPenVisible()) { + setupForPaint(); + + Stroke stroke = getStroke(penSize); + Shape shape = stroke.createStrokedShape(pShape); + graphics.draw(shape); + } } private void paintShape(final Shape pShape) { setupForPaint(); - graphics.fill(pShape); + graphics.fill(pShape); // Yes, fill } private void fillShape(final Shape pShape, final Pattern pPattern) { @@ -878,20 +934,22 @@ class QuickDrawContext { pSrcRect.y + pSrcRect.height, null ); + + setClipRegion(null); } /** * CopyMask */ public void copyMask(BufferedImage pSrcBitmap, BufferedImage pMaskBitmap, Rectangle pSrcRect, Rectangle pMaskRect, Rectangle pDstRect, int pSrcCopy, Shape pMaskRgn) { - throw new UnsupportedOperationException("Method copyBits not implemented"); // TODO: Implement + throw new UnsupportedOperationException("Method copyMask not implemented"); // TODO: Implement } /** * CopyDeepMask -- available to basic QuickDraw only in System 7, combines the functionality of both CopyBits and CopyMask */ public void copyDeepMask(BufferedImage pSrcBitmap, BufferedImage pMaskBitmap, Rectangle pSrcRect, Rectangle pMaskRect, Rectangle pDstRect, int pSrcCopy, Shape pMaskRgn) { - throw new UnsupportedOperationException("Method copyBits not implemented"); // TODO: Implement + throw new UnsupportedOperationException("Method copyDeepMask not implemented"); // TODO: Implement } /* @@ -926,7 +984,8 @@ class QuickDrawContext { * @param pString a Pascal string (a string of length less than or equal to 255 chars). */ public void drawString(String pString) { - graphics.drawString(pString, (float) getPenPosition().getX(), (float) getPenPosition().getY()); + setupForText(); + graphics.drawString(pString, (float) getPenPosition().getX(), (float) getPenPosition().getY()); } /* @@ -1049,4 +1108,41 @@ class QuickDrawContext { } } + + private static class NotSrcXor implements Composite { + // TODO: Src can probably be any color model that can be encoded in PICT, dst is always RGB/TYPE_INT + public CompositeContext createContext(final ColorModel srcColorModel, final ColorModel dstColorModel, RenderingHints hints) { + { + if (!srcColorModel.getColorSpace().isCS_sRGB() || !dstColorModel.getColorSpace().isCS_sRGB()) { + throw new IllegalArgumentException("Only sRGB supported"); + } + } + + return new CompositeContext() { + public void dispose() { + + } + + public void compose(Raster src, Raster dstIn, WritableRaster dstOut) { + // We always work in RGB, using DataBuffer.TYPE_INT transfer type. + int[] srcData = null; + int[] dstData = null; + int[] resData = new int[src.getWidth() - src.getMinX()]; + + for (int y = src.getMinY(); y < src.getHeight(); y++) { + srcData = (int[]) src.getDataElements(src.getMinX(), y, src.getWidth(), 1, srcData); + dstData = (int[]) dstIn.getDataElements(src.getMinX(), y, src.getWidth(), 1, dstData); + + for (int x = src.getMinX(); x < src.getWidth(); x++) { + // TODO: Decide how to handle alpha (if at all) + resData[x] = 0xff000000 | ((~ srcData[x] ^ dstData[x])) & 0xffffff ; +// resData[x] = ~ srcData[x] ^ dstData[x]; + } + + dstOut.setDataElements(src.getMinX(), y, src.getWidth(), 1, resData); + } + } + }; + } + } } diff --git a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTest.java b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTest.java index d85e6fb3..41efe9f5 100644 --- a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTest.java +++ b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTest.java @@ -1,15 +1,19 @@ package com.twelvemonkeys.imageio.plugins.pict; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStreamSpi; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; import org.junit.Test; +import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import java.awt.*; +import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Arrays; import java.util.List; -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; /** * ICOImageReaderTestCase @@ -20,6 +24,10 @@ import static org.junit.Assert.*; */ public class PICTImageReaderTest extends ImageReaderAbstractTestCase { + static { + IIORegistry.getDefaultInstance().registerServiceProvider(new ByteArrayImageInputStreamSpi()); + } + static ImageReaderSpi sProvider = new PICTImageReaderSpi(); // TODO: Should also test the clipboard format (without 512 byte header) @@ -32,8 +40,20 @@ public class PICTImageReaderTest extends ImageReaderAbstractTestCase 0); - ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray())); + ImageInputStream input = new ByteArrayImageInputStream(buffer.toByteArray()); BufferedImage written = ImageIO.read(input); assertNotNull(written); @@ -113,16 +112,23 @@ public class PICTImageWriterTest extends ImageWriterAbstractTestCase { int originalRGB = original.getRGB(x, y); int writtenRGB = written.getRGB(x, y); + int expectedR = (originalRGB & 0xff0000) >> 16; + int actualR = (writtenRGB & 0xff0000) >> 16; + int expectedG = (originalRGB & 0x00ff00) >> 8; + int actualG = (writtenRGB & 0x00ff00) >> 8; + int expectedB = originalRGB & 0x0000ff; + int actualB = writtenRGB & 0x0000ff; + if (original.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_GRAY) { // NOTE: For some reason, gray data seems to be one step off... - assertEquals("Test data " + i + " R(" + x + "," + y + ")", originalRGB & 0xff0000, writtenRGB & 0xff0000, 0x10000); - assertEquals("Test data " + i + " G(" + x + "," + y + ")", originalRGB & 0x00ff00, writtenRGB & 0x00ff00, 0x100); - assertEquals("Test data " + i + " B(" + x + "," + y + ")", originalRGB & 0x0000ff, writtenRGB & 0x0000ff, 0x1); + // ...and vary with different backing CMSs... :-( + assertTrue(String.format("original 0x%08x != gray! (%d,%d)", originalRGB, x, y), expectedR == expectedG && expectedG == expectedB); + assertTrue(String.format("written 0x%08x != gray! (%d,%d)", writtenRGB, x, y), actualR == actualG && actualG == actualB); } else { - assertEquals("Test data " + i + " R(" + x + "," + y + ")", originalRGB & 0xff0000, writtenRGB & 0xff0000); - assertEquals("Test data " + i + " G(" + x + "," + y + ")", originalRGB & 0x00ff00, writtenRGB & 0x00ff00); - assertEquals("Test data " + i + " B(" + x + "," + y + ")", originalRGB & 0x0000ff, writtenRGB & 0x0000ff); + assertEquals(String.format("Test data %d R(%d,%d)", i, x, y), expectedR, actualR); + assertEquals(String.format("Test data %d G(%d,%d)", i, x, y), expectedG, actualG); + assertEquals(String.format("Test data %d B(%d,%d)", i, x, y), expectedB, actualB); } } } diff --git a/imageio/imageio-pict/src/test/resources/pict/FC10.PCT b/imageio/imageio-pict/src/test/resources/pict/FC10.PCT new file mode 100644 index 0000000000000000000000000000000000000000..9f80bd9d3eedf6d09f6d0eebbe896009c4564a32 GIT binary patch literal 80809 zcmeFa+p{0pb>Fx9^f{*i%o#LhhC^aV5Di`=%93S`NcV+*6NzW5s@46?> z?d-bu+(zP?#b2p!7k_2n^sjCW*{}UIyzsBwFKLBe@>To(S|Y18KJUjx3y1&p@~(U8 z(&TVr=)4~f6B8Koi81_Htp8a~eX5&e`iLI>w%!bX_`m4)Gpsyw9{zVa(@zM$k@}nC)Yic# zLB@(F{GH7*zP-QmefZVip~c)k-KRrw;7R!(^rY}l_DL@Nvjb21L!Z<(nx^44^SAPf zJdE!*dBVI3V;C@{9}hh7`<@_ne)0j02yNMu(YYh$$*<|@=+gq>JUq92nCT&|PprX5 zUo{V?2EWO(jNq{US;73_nQ6CYsQAPRJdC{0Lz=H5N{Z$gxwYq&5KTpHLz{%-cs z|G)YB6>BB!eaa*aD$!YuVedEkGN>P(WIX8(K9R@M-4&)k_{1+Ul5DFHTo^-bMKguJ zvTyR&I`Z(pf=RRYronsh-@!RvOb;HlqVLCl&3e9~w~N11+V2*BOP}T6qKA19_*Z|g zEcslS4_3;}O8HZtRQn&4LB^rX7>hiPMbMGI*4*~6zn5-$7(Mvd z;qr=o^1uDRgByb%U6RoRu|C8H#ejRy>@n6qw+AaXCX@PY^!#kvb8N?{}fuR-t*f){!_0MT8{&`GVso!PqzyF}(?Y$!p=6Y(!zf!yEQbYU?(DWG2xNOmTOI~h;G+^b+6XEuXz-+<0M7V)wx0NOczyA2v^q83`^gHY?RwBZ~oQDfd zRu6Yq>iuCKGC|z6{p@;p=UY4sc92s(0{M{tWBpj!VInNVM9aY;^xc*G539?mhJZcK zgTrV=pxuXdkN=A;ebD$O8und#9_2medGwKBn-T?{9rp0~w|K}}%gB_EW_br5tQr`; zg{+9AzXNOE=Yr)z$SMgz1B;1LMA>uobS!yFQBF^z0P@a)ha3AJYQ2vS13w#_WK=Hl z>7`VAn(*{j4}D6Wv)-1VO?VjGmms!r-Rp5&ZNi+=0%RBa^>kP7c0J%$upFb{AQ zIX$F`-P2?Lz#m2{BkhNR5~plP;M;hwlM>Zf1=2?}ARE{d2OywA6A3kBacnxMJlD-sj8yKK%!w8` znFQ%|Up!c&sX?TQcFkvxCfc4m>YLJ2*b217rw`X?1vDm#_-^^#+e=zo_yK=&1vJbR zEcsFCc?>;8tM>5TB^fJ%$XaQ(_W~{Yn!#>(z zv`VewSAmbgKqE51tKuu`f?TyE(LtnCMb+bL9`he-n^~$M!$6nvXX4|(u^S4tWq0US1|>bhsIu`Y4XZ`>RIGiZG>lo}XmvXVGbp@TFYUnh}O5_pZ|H#KJB zvXN23nU0xibnxMyQRIS{&cx~iG{zosZO_n0+yb3qCX zW(2t>z7E8yo@B0wt*?Jj5!RJ+gG-|O3lkCVTXYh!9V+f4n*XSfs*qi|T;#gEFljrH z3z18gkw$DbML#JO$eAbg*zC*N$Zg(F7F;72k(~8;w4r$y>l55B_j&yTcZeLm&H5rL zzNB|~^8@!p@3LW?UKl_0KPP(GXE;FU_=114%iw1yZVXJ$H97+??b9X|{od%-<*RtU zeb3`-=?&<2M|Y)+`km6`a!mA1+$}4hJ{{euYR&9rpyn7bGoo{U`Y%n67l^|~Ai9e3 zW(m$oTaYh_T=rEU z`z4u3;4VWRi>_+Kof;Xt5zts`UL};&ml+`dW#drho=3lr1^Vb;zv+cI4Z?$HCBpmT zDDd$K^1++F@nf4yiJ~jI0Lhyl+K*vig}KY<2Taz1Ib(8IU_U?r91L-%&jVu2`dJG5 zw-4SUNS0%^fw}GsSub^A8TMm_V$6=0-d`J1<}Cjok2wtU88BBz7FIu`zEAy(Ved&> zx5j+Teih8_m@vPENrXY;fr>YX+r*UZ@jM7`v4Qo(34!^9_|Q{w5DZ?P!-SPhW+AHj zhvNVb{`DslzyYW$mMdm(3&3Q^MPA>d<@z)-0F<@wu^|x*h$({4_z!?+cF@C>jS$5l z2+kr6QGlp~sEd`1LNY)cM@F5uWm65M{?$yHIuf(j8~}ACZ;RFdO} zkz>$(?vLE?oh!cZ{k|qxO;no~NlD)S@{PUTm1R$bO9purcc& zT(dHIdy*zH78RE`4dQZ?USX~}mcz3NJ=GjZYLO$v0u+i!H-ivEPz0*{TSILrYeMuX=o$1{_heK+6u^{x z#p_hQtap@#f@o9`rrO|V9bTb4LYQwhi4cSu5XewX7ZJqz=k8U)gk%^ZYg`55S)M-%To}|aYIn! z2f@LMIx-=0Aqe+Ekfh2O5rH*BGbs>aQdZ}+!!uNxlLXWBb#kl(`jGTVQ`WdA6(-ff z4nlfjmLSBX2$@a(fgnVPx9BU$2vJEwgs{c+6(RspwI>|CRldy>vLlzph%oy_zCsg> z>1AyNHf}D5tb=BN3xq9q4iOX|R&MbMzJJ~ol5nxq#>FIDGl0#2*L)8EbDejbXx9Mp zp`ya@a2>+qH3%yr*8ZOA^;c2vgS}gztTjq$TVT z8OjK8ECLJ2>z?T{L1-wYAvcf#AgL&gCJ|r>C&vr1gQ;j0VKmYu0b*d zO15P6=#EuSS^_}HwuMB>65FYv-dzBaI7eFTTuVb&4ni*G_Y8t_1M%*>j_>NpF6h#` zlL1oIwAmr$w1U+P$^1P*?l$P++e;3c46H8ekn1cs6NDx5EK<@n$m{I+fLtdCG1GvY zL(0Yku}S9B(0ZUV61A%yu^o+p;teKs zB?q#bg%Y6*%asonkQ7K0A*w$GNOj*JwmtuFfaFZ}%^;9liOx}J_0jOp-5aTPjm(Kl zZd;pJf|6ly{*^M!A0R|m=}V)j=T1r48)WKH!4za25dwLMXa!rqg^(~y&o3ainJncR zB;)>4aZixCXeH$)1^jB9rLdLA(54I!791c{g!*zF!rdr;*QC4%f@yMpN05dPL{Z}= zRsMA;m<&iQT{4D>h6WsB*-(wy3x5lDn;=9Pe|H5T3nMxe#jX@>vQFxfuU7Z=Lyil= zHX%X=ruBYEUN}Lz8B!hwAt#}h!P3M45Ip@bn@+m0`(v#EG6cx3^cUniw0wP35b~9~ z?u;O|^pIn>+$(kjtZNaL36?fFv*6VL!#ROvf@Q-5Lz=kqUCaYaix55>Vfi@22xBg7 zi>otCv!QClg8hMgr*u0lX+$VVQ$qtZU~PoeKeEEr3NX9PS;Jist|>bTK@!)l<1T{R zK`+)2&^lo_1V@V(8J;}g8sTzELC7n7w~zSo7%@=jOaYT-DS5boI_I9km8ZEKgKucq*MjcX$h zO^1bkhPZ{=JZE!BH4Q%(uJHFpaONvi1WDTog`kCVOu>{OR+qhUy9JE!`nqo}McCzP zWa+Egb=XJD#h4-{tt4>cp>2SX6HL;85x)}Onu@0lhC7#%R;^&FR?=wI(ub5@x}B%PMu0W{MUEoxw#5*pvXQ zU2%0;nXG~R1TIX8sssAnQUa)BWUdTYF+y=GROVRfx&H~h3Ne8F<3V9k2*mb=tu>zH z+7s5*e?!;4N6_Cpp)0x<(VHu@&QgWe*;vgx7l*;4a127M0Nwy+0GcJdY~0w>zM`5* zCy6Nnyairi8{o6*SgWFX2;8|^symD_!$~BBHeKGpC^_hh2xMJzE&H5^rncDrB(1H( zgUJ@~&ntB=bTaK4;Fi6uq!Tb&cjXeXL((rK{tN*K_=5+f@7U?{5!&BMh(&meI8wL0({V&#g=E>>2>t%04BQ- zTEV$>=RkL4AZxPaS}WOk4f?#HPvKwKQ&TJ`uI;GM^Z1j8Fe;}BLu@cJe=DHx-emkS z`UJPhW2ykp$TbDu$^2EE5#e`(a0W=SZ9aTRW=^S_xNQTcx!=`}fm2QiUP6hlU`VN+++V9eP zTv1?T2tJ&086G!z9L-_q*I(%fxnyOrb7eE$kY1b#=e z=cjD;tl%n+yk4{uoLPoOZYRQP;Ec@zPETKF>69{cQsb(qjaqNoN(l2)>~%QTmSc zi#w?%y^i>1g;!t6y)MZ#M8uMkd+pr46<*V`Ug44d3iz`Gug0`DJQ&W(o#apW?=$_S z&nwL3Bgk*F)|F2qo)dHlhM|ZJ9As!zb$HylmKn$?xB(Xo7NZlj@G_o&Ml2=9@SI&q z5F60Mr|$~d&|6{835!k3$Gu^1j2@3HIi;PDC22rLC>OHElgn<2tPJ@nlPTnOL_T-h zW!K60C~#LopG&08vt$BpBhrBHNaq#MtQ>q~YCQwmBEI4TND36|h<^Td+~vZi(H&3~xSn&m^V56*$ z1U-o!njN2*$5SYlfRcEWlueKpK^6>?LZTQTrBnh{rXH0arsWM31+4kA0`CY+>cxL! zUjUZ|P65l90W8DsFv$e2o!bpZI%{B!l7(;^{vBqRyJV7gx%r}$K$l?)lg&}04eAn( z>~m->*{#8E1HWPL0(d=1f2HWp6Jr6MHaN-T3@295%?WOr@09&%;4SZwpp0zN0Z%yk z^&5{2UJeV##T7Eip1@26G{3Vaxs00|Cm-J-LknsgXLymT;~6S!_t15`LA7lKeT4wK zg38i5$ML-gPf<$Vn zOb}-6K8ZQFm-3!C7;#L--hE6*YDnbp6CGn9mc*hvC1kCB5{iZyshKl^4c=Wab3Rb^ zU@ZeP>}B_-yuZ!>4G893+YM{*?Xuq#9MdW|Y+Z5x36~)bheJ=)ge{-PN(x0T)e9H+ zw@KH~_ABYEAnWjqxbvw!-L?ES;3TZhwMFZ7AyvA|8QR$_QZ^~&5nB1t{khw0%hq0r zjOedVS&#w>L2M0s*|&B~BW$V320DfxlWb_LqHem{{a3*Cwk2?xa0Rx;Yl#-P0^BP1 z@H(7Xfn)G7ivWI&B$lXd-~zC2KS&YH`h{#pJ}G8+g?FEDJrnq`=56{WUT%)Bx*h)QIR`-H%Rd5@3pg-nS9muF!Wu zmr*vejQwkq`(M*R@{DdK^j3L?5h*~cYCxO^{Z`>`>sZ;>f6Jz%X%dqT%2}qj@^O=W zq%Hg%px(>adoss9s4^-OeB^Rz(0hV%wg5Et@3zd0qLzIH?73(ZAUwh!V(7}Z zSdSc#`=T8T(f_b-a44I{J~FUKasUJaw^#4`c7ku0?OP}hXq%9xIEXsb2Z3`M(ir^K z0$#lk&ihOgyg_;xA#-f~XRBmJCZ45@gv>R6oBu_Tt6oqWA-T1KZdr%hd+xRwy-MKpe>SYRU~+Vo80tSfLFd4JtiTc-xzcMwJ3VKa)fR5%+Q(G5bl+pgU&(s z#RxixE|o%0q79ts(ry6r-;J&q*1=WOV*(f4K$PUtz}=+BRzW0CKS{Y!(iEadJfSP0 zs}^(|NEBX2A2Bqe(?|QYzL|tu5O!Q1*(&j`6#a$f2na_3P5zCw>lIqUt&^roKo^{3 z7+Ti8W@uAotE|J|Z3V?aXVQB@>mD}UnKa+3ngu*Zr!_bxoZ$bFa$*!_#h*0KQR?N~ z|2kzI{EtVBFDCm!MvP0T!9;Gr$!L7i0P`r73W{Qyr1&rlf8pN#FW&sl4y@SE)P z#8VQ=0}t;4kH0_7K4tR8qNs1pl!Hn!IjU90f~B}(_hN>l6GLs=L{65IU=>)cE(pOgX@!v8%+wKWO7LEcDe1p+6H?9(bu`=CWyR|Jj;40#E*x zLOj~g)$qtg9vP2C9-EBjqhcwf87%%LrqbLs7J{);BewGwgSnIkUmBjPEes>4gWwyQ z(6y)jP<%y_ag+lj?=SpCj!PoG1m8`zm6dceg49HY5Cf|9j?0 z1Y`?+LHRZ)FR)&*;c_fG|9!>`-A5SS)ax2B#Q%g$zhrl`35V{gxhit#E?y4@0dyY+ zj!}uDfTQ3An+EyCH$!W0C-lf`I%{ZR;=1|`gCNoDZnzh`0(X-lY&UAqc*CbqiBY8AGlz0-HKq1s6>;oqenXV0OyZE4^4uqDOgmnA-jeKEQ2ZRY(Zu0 zEcs!^_yGP{w9_U7IPPxn*}V)sDu6chfNSsQ&@LE)d>hir{BgyA11uQW#{BN2&ICUp zqnHb$p~0m)(lt2-;7lg#Y3u^{nt0x_Vq|bqdy9CPafMe2(7Fs>@ju|}h_8_^d_T(% zc=DAKDIspcOEJk8-YgG3_~SRhHnxc7B^+mE-{YL@gT`M28pj@gmJ-tMd=sRdYL*~} zh+JrpGt%3ViFM+~y#fCu8(~$P*wT@S=~AIfX4rp#pB;iW$unA0=9VOb<~rFzcBpxn-b-|P zJEHaOZ!>FcXw~qxiX}RE7}310^l+M1Dppv%Z>wNr%p&rugd!WOQP4#;8NVq9{hT>E zw3jCia0JV&w_%pi0+&2RpCvl@t)b;(HuOp)i3!}ZH>`Sq+%P)wl*EuFPBJ4Mv%$6h zK%>)vCCw2`ShGn<)0zk@6Nt5yn|R=we;9^nAB*-dmbEWflpRbgHcsoU%q&G{1(&dd zLM%0{*kHjDCW$F#ER{T+*-g=HVxhK!H5o5g?_&VBWejQ~Feq%?{lhXmGKMm?5JK%f zl|l=3=tauLY}5!xjxY}chx}6mtBoQ$O&r`H8fDzx|Mq&PCwv7yQ%KXSA!%kOCkB>l z7{Xc?8qjo+cZ$K*8kM4T@ltSdfqn9qZ~{bw4huLP7!P5nknc6l zndO?@7tHXkJp?^U6UmlwgKW^2B+ckx_jTFd3JpE&NnO!945HhU4W&y9_hA%N?`3j3L_-xfk8c;8ElO;&zTsS$k4Ad$PJ}^ zV9>r%4A*Hv3^uATgb8jfXmJAUtPWrWDIEbn;!x>%4Tr4X?rY*82isOS+<-|f&0jId zMEjax@N^EW17|TPX=r8!#qne6#6F#1(0TTfA;cVw{?Wit(+MpTjGG$Jsj%=>Y#^)v zib_YT8Xr*dq!4BM1Tj1lgUrgix@^X0okA1F0-J(PLjFG`kK(|ec)jQCDIZxe;A`s9 z8bcq=I?Q-Mj><}$;vd9!;APeR5WLbClTXK|lV0IvRN-TkvOZ%}T~e|1QM&+odxNz| zaG9uEi8pw&g!5zz3qALdgKxD79~TP4NBgMnNy2~RM$b6XYu$-|j7{Na;h+uxT{^@; zKv!|-bp~-TXq)$1_pJLTfv1Ki44m>c8)$VR@X)S!8p3(Y)f-s&qLC9z+$@MiOOfnq zifQyV3vc&xCLTWX^6|uy%V{Qi3W)>|l+z3|_vM^|y7RL)fiUZaWE8c3h zyHe=V5C1fd$fVG5rG3nP+({@!C*heDR0h^qnu*0yF6+0kFsnmAn^-D8(uxK|EMeSb z%oU5et&VKMQW6f*y7C^LOoIqd%#4;i%GG$!&UpG<*F@SevV1k#_PB1eSy!-u1+>6W z`98k|!&PHYTpMArJz`|RQDCtw%96rODx5hdUz=1Ftuq?;VILDFV>81pOg+Qi93~9e zEtn`$%}o8b!IaXIO;xI7HhGJf%xuSHU>nSm3`s2=S4%1Cr4-Xq(z^5l9=gPN%YGi! zk^fmRbw@ikUsmnl4c5*vB|o(D#k4QT@CsLv(Lj^+&SLQVSRa( z`Y3$~94cROohfi|xp3mZOE+%%npVu2U)3&corhMeV4e)xO*iOOJ@xlvKR$Q>uUKz* z-a?!3asW7%Euankpya7(6I-aO$U(n{sfDOfYh%9;UtU!U{~V3s=-@CHqn4~bh@*D} z>L*2PVXH27y;s&QCe4@j5GLJ0)N~OOLkCl{V$y{9c9@!?_4a0vTC#zpB@0NXSIhHJ zkE?gq5&?OeHn|Df#ts#HI_GI~kcE|Njiqe!9+p;(h^5}g(lA=B^=GGIu5z7hEZWRI z2P2-6k-38=HvOYwHICI%@>O!s>%O%2S!bIr7%CWk1P05>4l;Nct`J$S83SE^>hDGA zuWM9P!_e%iFs)tSteA-0pSEu>H2=Z|a$>LofcAj8pfE9{`i#UK-nmezN(_t#47T=u zKrNX2iNRXpLLs}GyLWgiJiu9ifSV(pTEW{WztseMa_0_fmW+z{@=$!x$I_-J{>kmD zoSH^FQv>-n#iRUPI%-qZRLdXu2Q}q4O~2+}1|J;PICvj6a4=l9GCtug2aY)=4tn5* zeH=0-ZNQ=MMH{y$0?!=szWxA*hC;(7j^ZKLK>sWOFKR`vUAK=zKD*A*H@sG>e$x;7 z?OHf2@L11-mxoc-GLb*>+$!STl9A8I@_%sYi(2sf$I4TdJe<^_6S{qh(O>d3KTl7; zCD-@e3|wVwY2vC6>Zce_JH%jD-Q;CzjzuS~nzs1D*jHBi)fTR>!iuLHxI|F>4qj}1 zG9D~3u5rmFjVmmAj!g)+luA43)GY{cX^g~Wp2Kx^wbj5?a}bFaGDfU*j>8*v7i`$E zxW;IJ!+iPM;lOS^c#A<1ztXF+S0FfpQVSeU_z|V_DYX@gx}mcpazaxI!!cW6sg0#F z7JZYgomTj6j_F7tzp5k-b%%-V>-aA z-o{dTvB+GjR-{<&KdV-(Olu9ol04;BK+25}vB;-at*HApE?VOo`qHQs%=YvKEL;rf z|5b3#epYO<8164l1wL|)FT-mrh3{aoZ|5Fl(c0KdEI))Y_teUYrG>?!_8W66TI}RA z*2>jdJ8oXbP#ZQXFxc#bp)n*kb~QMWA#GGL(>EtcVc;n>lJHFa5_q^(8+c@l(bcZN z_w~`qyK0Ef{E5X@VO*>Q+v+t_B_bBfuvg7p>uLMcehHJo)J(duNT7!FgJ}#*h3A}2 z3vptXYsJG?lwA#v&6dc6D%_onJd6fBJBEJ{I)g~nhhdW>op2qzYGMP?r6R^%d5tl)nlOJyul zjBMJ8rGlkjV^J%xRIrq^vSQg4%gt!yeJp6SV!@MTFj}b>EHy0VWUnVT8ygU@&~;i- z@~v@MXZe3@3-z3Pb2#s5YXUckcy{tCj?h4ZPJG~cMi-D7jDgu;*)m%R=L$B;kOh_FHNUY2^@7$waVkMooRv>#ff;{2-X>SNcC6P-F8zfy z90CVWb{AJQh@%C4sroa3^sOuV$}Hl5otl{>$SEaGTeTGp>|Hqt zhZRcTu=y^Iny$mqD;EkJrRU3{;`prpG8|a&|F8uIXRMTu7@qouws2|z1C;tSWXi{S zJPc+ZSgaT0%n;Vm!x&_v4hGXHFbsbCCT@m@SOaar`~KFz@V|s!&-iV0q7NkS$63;3 zfOHot=RLimPAk0abtn0;4-+2NlUxT+03SJv%@b*~_uE1UMdo5q*FnEu6 z)rI>1Jr2SK6OfdxU?>=-hLs$Sj0UhU99RvGLLG*KRUD%l$0kDom@v+YxyEwcx3I{l z4J>9|xu02DSg`KDiy6|yfW3?XkA)#8J;@tq-4Fw{BiAok zD199>7JLg9?$Z<~mo>a?NYBMmMHWmI)S6is$0o2aJ76irLQm8W+OX7BYAqSKElMfy zG~S-!^L0gb(cKB5c=+EU&nx1=K3IS*qMeFCN6*3ICZ0a>^zc}M_zKH6JmJ46#uqW@ z3m1)fC)*WQr5G%BvbAJscoqVKn!$07T&{eL`|CD^u@Jpoa~R7JHU@!(1iV(3EtZy6 zWaN5g37w%8egugB^0`J@88Mw$q?lM_d;DJGSn3{#<(GrW z#$PiBLm)8g3zj0*`|$$LShSuo_EsYhhmOdHeH^*wuqDv47t>^~*1bj+(I#@#GY4{O z4G4ZIjvZ|#vdTZ3IBGc9!MNvlV^v=LU2^cbC3j@I@{eo1p87stl#p=`pfQkXBy?`* zQ)_73KS$Z+q>afgaG9D!ld4FxkfaAZ7DAVRXPQs=wkD0`hRp(RCkj!1CGTY?gyql% z`E@hnvFEQ&(nC)JTHBF`b}iZj=_CuTo$)vi+N|($3Q1pSBTF{mgEkYsiM9d10iORx z?d3oR&~-#JVv^4LDp`#-|JuCge~q%k2LK3B6Pg}xlqP#Z(^JqE0eCqVp%{}T8j*Vf zSN;+I1&nKs2v=8!nL!N)25kx1OlTe0;JUdE%t>VFIVz$JhDyHPg*Nza=={`z zM&20so`o@7jtpu;ofo6U{fvHVwrMWIl407y@3}W=gnos916G}1@knZ%q44C!<00XFf}h?VzQnvCMF00Vq#*e z^uTLar7HzG%Fn{2`iDw6^sT4zw&%14yoPi(_t-O4# zf`@q*=IAzhM;?NPc*qZVix3_!9#3n(qN}Oh%%Q!dwW0*Y>UeDS&=c?_+}38ZZU>9Q zMu@&N>at#tL?M(U5zlHof`jSH6AN4IjT^p&X|h>EbYe7LLNJ?8mGRnycK9mWd1jPr zh%@TTz!G8ch9tK^eY{5BzHcK7AC@oK9vBCK*gSjYkY;Rc;2X}1Mt%cprQnr!E9Ag?EE&Y?4Bo>Z6G4PcYxcKqm_Tq1UOkN#c_S#=MV;DBR?=aY*f0^BCvf@|1ySdUkm#fO3r|!5P_krk>9a2 zYqB(`6oz8O;5{wy@j&fyxm&LWD&|}Zi%vZ1nMH;z)#=f1V?y4WgTsbJfB8o>9IP_Q z!5Dkj`Ohpk^zvVA z^3WaS9F-&ENPmT%h(mr@!(oF?8+>$Por8)5Q}C(P(3Q`~4aHzealuav9Co5WY=U*> zD37TxFwyP9VQ`FW+!h>6r*#f30$bO`pnTHS$u$n8_}ft1&<)x!jz>!lZCEXrGp!~L zx^ICW4i)dBP8Nk{Jkzw%O(5HGNAyw5OiTnnM#E~qYJGS~_=1t(f6+?|zEiJ@zyT8+ z%8|8|wEXO)Om>2U|D_#ycUbkc!I2M|RYBXOTxIBO#cm1}hzSv0jl5~15zw@*C&`Ar zVyDd>&ruX(3K9hQA&7uhXr>lndI;Uil&H#ZtdfIBQbKU+>Ct>2c-m? zXTLCO#_kdGz+=MgJwox+f4)}C*<0}p@Yqb)%vcU-hlBdaV}rpVTvcnjSrJ7+NgQV3 zQ}iWi-;x&fqA#rY;@IkF@x+4^2!gId*UYe8<(PC6NfKCfVdrh=jA zgbS2)EslXUG~b$)PM8&oDVV7Zaqtguw8lYY2mE*|_BynnLZozJRkx;W8EQ=@)xZ?I z_i^Z6P~d2dgS3Md-jYt@&}gVoYL-391p(kS+ZK)|)fMR^j-ovw4pq%$S=C`92Trcy zjB%(dD~_`iPpu8bjusA^P%L%mNy$hYC#0H;EF0&|IlI(PBie#bO>o&n3>;FR9M)3j z#UzH~nzJpnLTx^3IKpt3#=-e3Ce{jOJ zjfLraQ?FO>mj=Ec z*;ef4i8bI$b@@y26)VI)c&2s2JIB)Y5?wbeZGly1L2{hKx|vZzdSB3|Gc;>4=M3$E z+Jz^U*ryFpC4DB9uYIKV36O$DBkF`UfEhZ0tvo9gI=-Oy0I<}J=39XA>Ls|{7Wf$p zhJnppkZkL6N^!T|Qz|PxN*$ls1iLS^4tMPJ70_nrtz5rP!{DF^vxBl~gx?ap2X2P@ zSJ79t^1bn`L~ud_V)-44>n)mN$O$js8di84SvTn;&-i&*+BC1(B&+zy>|E5>HXRz7gp%ldaRVef_s`O>-uBdvAd zNLpYlRwG@p^kttKiv@@k3n|acqMr8)mb}g)3p0zoRcu8I)znG}OEHV$4Xf z+!jlnFmpS18O3V)B5O6U*wo$1`TSaewYbc=;%N9>OyC$9!igbz6vovd91JZs3Qe^7 zLiI&8G00lIE-6j&lUs@oA58E+oN!lu*y@|t7u_jL4BWa6gN4H_g2H3Z=HvAei`x9I z8NH?)E=+j!xnJU+X+!k-xe4!*MtFUh%H&Ib;cdoCK4IgTdlmOUzJ#})Uh;!s-EY{5 z15it@w{_nG#QTohv=J+RFl=ks8mvvrZKYArgdO-@AWtU)He=c3^w>P;Uo6l>CnWC_ z{lytwlk5ewx;TKgU?SNB8PG2k{hR#Dxdz#sg0Y)KwDoT(dIEPVFrkigS%?32{scN& z!$dzra&y8YOKbaf3xe%67NBMgLW#5hn!6cFHh>bkDWFp--O1#nnq)O#-?MWHCrhk= z#IJuIlH{o%SOJtLRwU(W0Gh{8t%bySUoj7(6AMkrKiD$ z*zs;1*aKu}`5F*Xtt+}VqHfnqAnEfAEFw_%Kh)&;5(K4 zECMNyCM*+{*S*IH0%xlDGD?U%4eQCr)_xsm7rmJ zD6m=qb*0g|0?^)>-I|(ysbur*{XOSut0@bqp>Af8{4N%o)c2e^${ROJpmiGHC(Uom zfm&eul=0fr&l)1ILPrS%_fMxjmjiwx6b9VUF3Y~`hhG4sUA@r|rghK|AU)looXo1L z1^l|#om&l{-b{HhlqsDXRQ-N)ZhoKC=Xa3G3=2>?{!iv1oOn=BeKY6xTvxOB{z`;h!SivmY!Qj4IpkP%ng>r#IiF`gCu{`>b!so z^%Yp`tFPHcF2>KSM9B!H|Rqh#PWMYx@NDNG%IWgwvbrtR&tEb zO7&*g}2o467@RGq#%LwzCdwb(yVTnGc3V2gzlfL9T9SG+={IPS|5`6qKV} zro($(>kyXvDF1ls8A#TR-UTFElC%{P-GwxRfwh%~G!0|6ES6B2WqsvXE^(&_mJjy_ z!T5wSjw^U4z$Lf?92Xu*?vPC6ZP58FF%3Rlhd~Wp(OJrMQeqqbz#y#-WUd9ddUr)* zQLlk3z%LazJz(ZsahL8PTVOf#_&+bY$Jazqg08v3w}g=5hW9t_2C%v}8~`6LNsUU5 z&~Uk%c&uW{1HR`dmskTANCw;r%xILTJb}vyo^3(eX5dC@B&gy2H<;v-%-t@@wKRjg zoWTrGfg-d8iVE!GsHFM!AiAgx9ki$enk^j1tiAyq*3KaNU3aWxL`>f%Ajw@e9(n&i zP19R9Nb(wB#U;jC9h-o3Yh1vZ0E3g{sg)|zCFbpKv zLxU9&76Pg128

Ch1{`r5K4YS-eA7m@SzdF;8;dhI%>sKaU~`?+3Tsk7vyKfjD;pPBm5CW4^thh!enb>p zBRGIlU%E_66dXxKbvL~)ZtGA$<>s{%n_s9?UnJU$c*yHO_X=z~R;VDByjFtUki z5nOQeEN~emhdIXk8fTVZR@sV+?qzZxhHVc^T@4)t%wR-lT*>$ z1i$P5;R;CfmjVXFD=C<7oSif<;4~Bws1mpw|EhlO_mT?EUSB-X{L|+jqQfC6VQZ=@ zi~)Xq1=lB4^uC4Qlklo>0nTOu>3Rou`-_!DYS0=EeJx@_XN@W;2oGD20RvOzDQlKy z(DI=*^zDe|Q=4RMyl}n6kEmBV@dqu#wa5+wlKLBs$|%VG4Cq`RX5ZpRCo1J{4|UyK z-%FYFC;~S1Uoz~ifITnl?_iRM4!ddnz=laSbk48laN_9uvMsEa5wGv01bo5|Q%1d! z4t|%E%y&gw)CDF7ugmF6{I%YlHO+-DxtN<@1!SARL;v7%}rLtgv7 zc$U{CdLdux)iO00gWuO2D*jXxJ1 z2FVGM`Cu<=(j`ZIr`-h0YHeRJGRFmLYu$v+$2~6cHASt1s|hLNwIFT&&PaY{>ZJ*& ze1hg>w&qaaTScvE6jOZ-XpX%jJ?SX_M>Kn%46b&323D=>(`04pG9zYK5h9 z*_piIzJcn&6ZmJNVG+x#fhIOnF*G}yBiHG3psqu%Y}Ercm*X;qflV%+EiE){0W)TdDcEfyBq|J8t-M{k)D9Kf-=lXCNjM0|;{nNGq&p+2 zpgHD8sy>dV7b7GWe;2yD6woE;V`sH^KWG+cdX|iJfZIKm?(%V$HjKZ#@~q1OzL9i& zOHXbw{QZ@syDJgiGP#+m0p2pT2QR!#(u2o4Oq(lohhFLc^7sVTJ2r%3vq%S)xn$Uq z5oaQmtQ)+1?C-ZOV?n--;NOQrH&@`QD-MEL3a?XWk6?WwF-q?7%7p1J$=v+m1zLxP zrulu}0@mvvOK{$p8dX$7VASP~x6~*jIQFk+$&k(hOY&|b$s61Wku4(DW-0E`35*&7 z>q>!ZG(3++ul>aj*to=Am~F5FiuYwWC@T03B(UV$cr&$&A!7AzO+R7j6f=6p+RC}j ztSbFO=^$(AGn*DGSo`qFEgNE{|4Yohi>y |>qGQ@`zQmKK15bCU*K0d82~CiM)b;{%*gE(uKoTWzGS&9w;NUqihNW00C7XryAam*#Ko)x4bx{_G(y7>yokIepb z95_-dY&|s$tcV(_!v-@8;-{D}!l>H`#;=Ip^HMOUCnEv_G9-`ZNaOLs((hJ#{#i%q zY>A}LWUq4lbOxs6R(l<<_UTjMrHmMKoPgLy5;NWcy0u5+_XNZ=`rz?G-dW}rOIq6T zyG+mM8*cFRV8a$5gQB!q4GYNP&}DnKd&BkM=&}~Yy0DV91o8785%efnWCbHnv?it5wj=#)CF=l; zMOZNFz@}hW*rY)4K_g*2`G)@=tzgUeM1recNF)+Y}c5cU~(`Gn9~6>KAGru0-atNJ<8yG&!NoG)6O-_i-4)X z)X3ncQ0aTAop;2v+4#VF{rC)+|6$TZp)5&&969e?n|Ts~*rgE+KSFC#g0`DLE|Swm zRfiRTlv~{q;8Fk-=+OfRdZHEn)K$X{wh<+JrS5Ui|iVFs6{bDLV$pbY=O_HNjK`q@avd#0U*oZH4C>@QadvM3b-x+ z)B^ORBs~OB^94a92%w*RA=(mC$OUw}C146n9%sN4pe38<$$6Ex425sueEfM|ybq5$yn`wPQ_kn?+OTV9X_7V^D&; zls!1_AsH@BCR~lc1rB=vcijs8zA!-Cfi%CYzp+}`%zWR3Zo0WWkh}KDku4~Bri$!70rzm$ka(#xn>T{^q?03~8jX_1soJ2*0BF z>(GXMGy=ELGHAVMKa5WAz-JeA>NVO&pt1MdCM9>I^StVgPo*$+E5@QM4A? z8!)o7*lG)W0%u_*=3b=zsHoawj|E9zU=SOYhE~J&p&w>jwbhl4Q8ts7aeLeU6!Jce zQyMAy{5pxGe7h?}LjQD)i%fjGt7^wu3F*uv@bzo->>IS@MSAMq#)!1@As&<9^4O6+ zqgVo=iv%*#tyTlBa5ocvgnvKwTk_DE(24^B`PEi)c^}~o2qaHgb4mnvf~h27;t`2# zC2KZ~bUu>!=xi7?N^3HpY}?>T;qDQXZPJl0YLKM81AMja&SRTWW5ip<>`$&^q{} ziqX)L)2KGGwNa$<7;BEmC|ZTc8rsRf!kRuQqT7Ni%72YTXIialxoCd_ORXqOj$N-9 zZG_h$WPt_3RS0hjzkJM(U`~%EMyZBQ#`F!BXHHfH|G_}m9N4K6vgYR}d0sv8n?277 zQs^UZ7~ps#Q26Y?DauP;LnKlkl>IuNQh|Ud@yJV-1ZzWt3_>Sie--YDCkPWy8%d-l zh|`O)UgIWG1L-4h6|CN>{ZvNUNQdg-im0Xi+a+ebIO`~O!HGBUnWTw$@~=8A-JwNB zH7VQQ4s0L(Z_o@{8)%GcQcbisRx~zq3~N%}r?v4X7+FYJ5GlJS)kF|a4uK4d_&Y)> zZ^1LbbC6VsByk^wi*V^4QMp`k7HK4(hD^v5d7D*(NK#b}uC`}G+b?L*E)y+X z(qtN$8a{4EC531W&7DSO!3o1nZqGK*S@I>fcmlnafuQTef>n4y*B3BfM6QX^Wh9@1 zTjv*%oBgw;ypeiR)Hr186!laE0KTivIVLwQWuVCQF>&cS23uvLlPXggJM%t|QF{FG z&?$qUG%<0r~Z=6Ivv>@$b(e&VEIju2+*rN}Azsq1(xk#-*)?oM(= z)!3QbFY>CmpH{3Y~~O(_1nCOd?Mf`6D8un#q39lxworiS?h6;N)2pd`1nwOX*V| zm=HUeg(I%45K@g(u#FpEIrlN>_)ywVRiXZ2D)c|_R0;b7a<37{ zCj@!ww}!WzyUd0?fS%!ufYNo4J~NP9E&zSuIb4iX^8(O0ZhyEF5mX-n`Y|M(qZX8m zpwdbo5eUr<==4eH#Mkqme}(GQNh>Fvt{oRahw%V|2?Fi;Mt zi=b)&8klOEnLb{Dq#}YoMSJWFvJwu_sApLNN`g#1O?)%_Ops@`VzEb%*{ z#3x8>NW!jJ4$c6sU~SNjVNtUJF%#v#73F&(g~<}=W1ppebm8gy79<4W{o@(ZM2Id6 zvIHoO7(_!DrPM$^jub&y3Jjtn8vk#-Z@v}gy7vk!GJ)iLN0c))WhqQS{Ixp54Ew}7 zjCE);WsfRYv1ii$I=%-;SCFMsiMj^jAAru!AT?jKu{d%p{iJtN`#XhFObs|B@qz>!6J}Am#K{T~ z=P08Z%nm(QupV)r5{%z18I^fbZqz<7=S7e;d27H+0P$V}_Sr$86x3;;LJNUmiOZy5 zNk_V9{|Q%3xWf9I(L@ztvdRLsE&Y0x#MuT1RJ#<$Gw!Adr5PJgiNB$JafH!Z%??d- zaq+kzsUa!JRmO(@V?E`&^#AZPT(C&m6?bn}lPWxfyVcP|jy7hD#j+TM6tc z^y|>?y8!B>8ZQP&plT`D^z8*ZgAlS6vWeSOZM%w1nN~(o_~<{4N^~Ae8LF%bm$1hT~ZB9^xM0>bY?EW|Hm4 zUB@s<#ybOBPrID2)rV%2?EXG^hRYust{e<^8qWN|VK^I*r}wd8gS;yfPMw|fyQ_Bf z>+4$&?|Ui-;IL>?Jd`6ZjM89Iwp!qB-}V#)WM%>LN5Aj?`{2mRENUu_{wn873A-}R zuK3xN10a(6OT-gCq(zVtL}^HNm(d!dB+SbgoXKx`)lE3>ryYS(n>Q1P)$Tfr2_eTs z;UbuykRi~p0DWcwswYsf4|F_&ei=^^sE{OqCY|)A{sdyl2q2{_OAuE?-8Pa^{nADR zl}cGA5NQ|-v=U(~z>W%wVetxj#h$s;9nD?oJt$~zL=M_Xk?n?;6ktqwQrfw-` zfS}Ea@`_s%NXftiQaZZ?-5FIY&ROXhd|6Q0Xc?vG&HLjtle5T2vNFb*%%@w19D2b5 zzo1X@nSgwn0($vP=mm5o2$5eoYvkANfjhvR7L|nE#|sgU4S|6dI!roBhx@E!{DPX% zd%CEswon(}<9|6s8vm>Sna504Fa=^6BoN-rvQsGD)X-YUs!E7kLrGa)%(Bxc-Z}zB zQP8~!Dnp$PSs#yG0!7u5k2ZlRz?@lN)Se`1Kgo;88&sA+m1uMUlvWr?nzZ8GAEAz^ z{&c}z0mW1`l)Cqw$Wr$~aT}AoLp7(mJHy@nq&@_vSFCW}ovvy!UQGTNxHIiOoOEj< zuH|+L)wDa~8TaOdlXhxXxEdTzy0c3a=`o*k=fv=0hde7Y$6kQLWd zwdY_?P#zsqZXVfND7cC+CH80~iGa%s51^9Uo*(SxrB+HOjmw55N{gT7!|<-ldA~ov zD#1)x6@^VO&dPFpA!otR3)Bq~)yw^`8W=Zv3swTbYUiI$um;s8X@V8N>N#>MMm++3j40leZJ_i|O%)B(KJd<&{5w$E7NlsmXtXz3q zMNpe?tnr3Jv&5GPK)mD513nZHfls190m)?h0L&gYOoa04+fH{uOya``Bz-oZTCURP zx0t?V`Zg(paB)nF8FQ@Seh8}=?_FuyPXKlhQOJp^&z}mwuSC=H+EF4TnUq-&U>BB%!R$AYA<^88Kick;#Zt9N{zwgLD)Z}hoGlph5Bp3mB65Wo5snx5s9 z4J?~~dRX+wNvrYJ1^D}rOYYwy_X=Ml5mqZ?n|G&O!NeaNMKiq_GfDrG2N+1V(KT<+ zyVH(~0dAu*hMQ0HSzjsiCsr(%fl{mpEM@a$K4`mPXBfKeWj|9NIv}fIY06kIFb}vB z$=&m^X}GrsgNi8|Bvk)hFbp2jMDWYZ2HgoM_^?f*G$24LLBAV4Sc?}>t`He?x1ONH z1JhadUSQFoRRkw~e``x5KN4e+^eU$dDz>M4Tn-N{#2tu+e5W{(9gbx~4R0 zNdz^UocAu_XtK8C1G1dsqY)b=z9Z_}ALA=Wv;oIvjgwqtA=FDHVqw_ptUeD5$iT8$ zmkiI3o5vVW zW%dUR*h*{Txr~|0l_5H0CoU?BPH5bf5=casEjh^Lyy1w9_p&0o1&v2O_eX{G!-Q=j zw&61&nAlEGRR6^z_hEMmVJ{Um;iT8BKw&Fhf6uJ)O7Kq3%boYR%guYgz4m4$CtRxj z3(W5HsJ^EDv@f4OqJKLZE_-;txq`o^oqS#37V!5*a2vqARvUru+$QN{Bs*>Aly^RS z=7GN{I4}Ilr6s)AvSawGZeWIFt?(&$u4lUDC)YSK|D-SPFF0C8KD)1dn>Ze*-mv7z zu=0bxGLG%zF28-XZuL^n{T2?LpYN$+@>5~-(dy_>47n%X->1B>MQFTw?G{Dl;+MMmC*T3FSqJcQZs_~^f&VHy^W!`2QhJ@+ zBN(x0PM*^A1FoaZPyHx+O8;cfZ+_u{|AkY$z}BVLReG+9<_K3ATor+zFyI5e0RLdv z7OqXNQ$$OvqCWE6wcefi9IJBxA??wFU*0|uP+tahg=uLhoh^qpA0g`YXSPo~Beg-+ z=L)nNDh7nQSKkGML zeydBHh-yK72W~}+><-u*eVWe}B;8qGyDVu7U{!*!J_xe}yx-tmwD-8m(VbP;^ zLdq2fYFwaNeM1acX?K?V^LSdH;+*wz_7yttd9#~k>c|hM#t~mm@%fH(6CsR}^+? zGAX@R-nPsqfPH1a@BVM$1IjeBGoh!L0&DXo$Cx~!_t}Vc6mNBl>XQ-6gwcZCy~CU0 z=RJePU{DbIO@=}Gfg-?Iu_b^`pZ8Z;dI1#zNz@WlNz(S|(^t5+Ju*~93rM%RK8_@k z`eZyq>446ID!wGtAXM{ZT@CxrfKh2i?|%t0j#;;TiZd)qmp5Csd=rj+zTrswN%M;S zO4gh{LHF5?-2m5SQJ!$55>zX|Z53UgH|K4?#%)h<%?7ydpk3i8euQ?n%3DO+E5Z#a zWfe%DU%{wq1lY^v?e3iGFVM4&`*RBzJyr1|wEA*+tvi?WEseBUGr<@DE6a2CNTrHC zly`(W1B$gZMcDn&rVXW`)Pwv^cPS;A*v#Fw!;_L$_w}MMQtKdW>&7o#YSSmc3b*6j zSI{P+*g2D&Mby1tVo-y1j^28xh0~Ydg6MsQh^f(f>J;+yf@DP#;^N*eh)LzGw>8Exn`g=QYoP z;5=le6f00kn37;|h2~H02wAbvWPa zwX`R!Lz6b8zShJ02Pjj4=t?3h$6@Y4#9+jvAS3~K&t)5ACmi3CgxWiE4Qc7=hg@q1DA(A=4Ro z$(<)|2jpoX*;@&z`@3p9rSOoH?BudH2@_mgX9gq&tl1RlSmy7T+jHF>VBx=k>4D{f z?NC2SNn>>!rklh}-X!5Nbby_{1gz%J`BFjtNM>E@l&1xE`iJFS^%J+b1E#<)j`}C; z3>-(3y5$_{7&bZD^K}6Jk^U%#OLV9T{iE=c7K=`X>w5&HOs$E0>W483u^Vbj5(`y} znW$1yRB{t8=|dg4T)js92-Fm7Y@f-}rtsa@Sq^^JQ*qIY3=o<1(mQL4@glVHcUja) z7XB^=JjN-xdGpc!0>pwV5pAG|P50P~`lJ0H;IIcN-u)R-l1TZWHPwj6Qw{u@<9RpB zvgf>rz;io4*UKNYPb*7G`VfbJBDJ~PCRuEAd5cYp-DCOH8ZoCnDIyEFYRoHQ4X22_ zCk8Q9Ax%UKiC7VJ6b8{MU@A$9%7wMqXs#QBzvt4+nCtgFq?13y#F9E(Qa8$*L{Fo9 zXrjF7p!_zSLt6dQTn`bUh2>1rKN3OB>?h{R4$Y z;gN~-ZrS`iqm06H*K zHQM9_cDEC3w|Kn4y|u5>H|{kEEj>&1j9cpZyo~NUtUW)U`DXE4&02rYm3wV?o{Xn{ zCU$s0(An~S2f=RT^85p0(DRGst)A!9d6(!5)qVY7wR}Y5Xq`5`Youm;ZoM7f z#jPQ{1wnS!8@`N;!qs~#iY%>nh5z67&i>7=s><{G+-i zR>K*;oiXS8Za3F!q60W(XIv}wQ|CjD8yYS_Tt?)ynB2O8S&(!c%&vbWA}G`uk8z#~ z;sAehUQOaCEG#?=u@YmsQh6@&x=f9ojR~JF#%7|A3p{K~Tf7|!1vWh0SXCRaZnR;0 zGJCdYI7P!@pu^dd?7ukRM@5pM64--A=sdFet0M8;&atlB<4mjWpKod{fb>;PWQJN zbBCBRQ|n;%YVDN3EVdeR8%(UB()DVI9acVI>C|V#?1ps7jGKti+Gwk;H|Zkcb)JU4;8H|{^P^#U&`+0P%&T;T48O9(1_bQ#1ZrS9*XV^Io(f`^^v z9<9&p86+U_Z8G_6es(kBU9K35m^$>F-LSjv5B{HUXaVRP=T*H-=Yfl{c5w~|83eHm z&cPK+F>&TtZxv(RfN8PvPQd6&6m`K(C7&c*W6YamvTw1yN*E2AH%<=LQ!HEjh9#(BSQJeag?2{!K zs4bwlUks|(ANkMacH(zE<C-4%*IjHphkpJ#A0HDVzJ^muPP|&`zeR5j5J}C6 zgqOD^SrRpfVLLJdhGs`?ZiQU6LztN|&Gtz$JOX}I=?N%N7x+Bsz)Vm4)&nSA##<50 zC@b7G#Bvm+y4g_kL{0K-dwcz42KTe?_5ug~pv z2XaFWgJk=a5q1w@G9ihXuINVnGMUs-Dk98>xTG%y-#O+I&lvvpYF{9CQ8A~!Zbed7Wap`8Ri>w2KqI{Q-vmA z-0dCjohcKdxW2;)dx1wbGEB&t#`4JB-Al0J`{JWj8u0`XYm9vk?C?g0%g#|O6oaV( zdz0nMYghZy>HZG(Mh&@Z)?K}Ij~i#)Fe1FRw-HXU}jjHMRGE%`>B;j&OaUJBocl>v9*GUqA@^1Bf&btX&qcT`C0ssLQ3)i1#i z@1_CP0uBQ_0`OJ>(^ymiZ@Ha_8nW5k``&%Ap-pFB=HXlF~)HzJApxpig*WVQ~C2rAo+l6BT5Wz1K&dHS+?{uBsR%-9W z)Za#OLV_&v?Q9zcWr8_u z`}h!LXyd3Rmi3Wy70z*|0rHP`&3D`yPr%S^T64ZuSyPvrs!i7AH)gblO=P{6M zB$wlnv#XaJ199ylASVsEg*|Pv%Uh3JbVDO3ct=`>ciEs>ui(RwTJTg6WOh@5>%32A z9Q-&9rO{vHEqSeHbxWCbP)4NGy4hn5Ge|iiYVjwTd$~+eu1}v>s1%S?TsM%|9d| zW^U9@2y^C#=UY?~+N)P0LTct~Qtu$jLHtKTh)K>TvlCxg^1Qv*jt?{4^HxifJjy06 zyY4UpwoS!Jf2*P-AwAbB?T}fMoS?Jb#UOeA4L5{{Fvk>biBo=ts$=6kPL-BT^&{@; zl+7bwuyziT=7}GDwvLnz3Zh<``p@4+(lGy)bz%J|(+vgE@s>ED>+0W`Vj-A~EzkR*?S@_!pREfU~;~95DoVVf!sSkA;je z(C@G^@Ta&xBxu*5h!$w6<-E3L^54-w=@L-yk2TQfxBf9z+G+t7AA4R;V`%~>a<27O zCg`k|S>+|b4v%a&BV(O0Vu$HZ%ztWHLR)Ff2%n4EZn#iBrz|sQM(P)Xv#_LI>Jf{*C)c z{0UN}y6nq?EDU>Di9U%TVOl9R_3Nmmz2ZmOs@T4po2;$!R{vS&{*v9j1Oe<$SF-Eu?Z+wM zpJ84!5HKcE+v;&d*yWz0>Upk7LpOA42F;-+%}wWB^hvr>R%%K;R#9}0P*Q(>-v5T` zadGT6GQW`!Fb~`?F8!3A%SDfcBdku3{u-ml0BM1{{*(=;EY~yCIn^i{Q28o?yU2do z3Q(J0lD6c4VqD-wPzFek_U*j_Lv_Fqc+F%e>Q`tw(`I!AhSdNV;<|=WB+FhI1~Wlf ziIrvA=s6-yjFEf_y+&PvxWN#`5j{?eR?}&*FJd57K~b0WG>KsE2NwJO5!Zo@G$P#0^9xVPox?Hid(~?TP_+JtWhPV0Ud2oRoiLF4OG3bg{8;k_RReVZVb;P?=A0L1 z(*lOpF{D#oN*#ch!By7vKrjzv)0+{54m_exnK3PxV=h!iH@{^4QXF+x#nYur|Eu~L zSbs*&n_6r#5%NX#4c|9go*d0tE15zd_Xb&VP=Tr{&l^Cdz6P02XD-$$^_(6$?UvD7 zNj1purWdW=*)idqizPPpZK&dD*P+kSdG#v!-57v5KnZx$VD%ky9rcE$*$z0>5ekl~T6ovdKZ-8nm{ku^o)iDQY+E1Fi%tpQowfUs82Jz`v zh8Oz*f#R2uu4(jP-)wI}iQfxyzRl4)IFrJfH_SSlNFi>#c}Kz&&2pGVj>O~Ol1 zJ95na2i1VL_7@HnpixmA7&$7K1%_M#F;IC@K7O#jIp1)wpxaTP6nLV_Ztp^7bjliV(sn5u2 z4Ou0=G)68fLJ+EA$t!wn(+^&v=ow#Cg07+m^bAWGX@)Ku22Bkm3>bNnQ#Z^&o)kDC zi0XFB$vnMduC=dMQv?yrZVhAX88fBWCLEQ6fs=KshJ*aYL1f~v!xQ(F*$GWc#Zrb_ zV5G)C`ZBldF(-xcA_c1P1dGLVfs^J92Yq_NoJhw9XjF>Nno`hDII@CRW-6Ff`$O?` z6`4`5i-j>FG1gEDBkKvPFr$J5?LPF?$IZo=RvkPO1BVd?WV#75%?sqBLC3x{a%JQP zMP5%PW;8*we?$Nj=bMiZ=J3|Rqee27xH3`Fu(H6SJTs6fyIY*u(BiB1w^=GP1lZ2R zSR_*su*Qx%%{y><8HolfHaM%Ec5Pb0+kb&dO&e;0V6UXEe!7wBzcyfW53tCx`D`G| z-C^aKRFi=$BhO})qzE|`c`azF(yH5Ry{VN0#+s*`<}fZ*EzrFsfKCIW^E`cvwkya; zyn&onJxKxvI4}L`<|&2rp{{{C2DSK}m1Fc=bPTFZkxS^A=sz_9RNu1+UZGCZ0{ZHo z+Pngp${YEj$g6&8wILJQ2Do?8GKZ?OXL_Z)q5DFa{{-~B4^(^LfyEW{G3YW{`MriW z4w7HOsqSU4u?S^_%a9dEd8rIu6qiY{mA2%wYFa)5&$UBkfh4~Z>gs7{hPHpNbO0AA zNPjizj}P^I*BQmOiEQW%8ZWxA6K6ldER2aqR)IP8??_Iw%KdDePGF&oW0i%&4B#qp zb_EXWJc{3NOpBo71RA+0mzjB{-Y-@ewwip(REyn_%d|V9rtzZ=0sYdL40&uK4`#iZN}$F0v&ymV=K8Ft!xE_; zWJ1wE^f*re0%xcf5R48XP!d^ey%p&C&uYSBp{N`h{+1~-icyV|7WEV23ZNL*?5He) zZU97oggz<80rB~4K*x!6VUpA>RvSvD!xVTYKtW-=heXkqlveZC`;Vh(c;go#Hrp|Z z;q`wZ+jorIR7!w;;Dp)NXihO6h3>WHO6X3$`7Ar%fX`XJHvv{sg@L=uc#c;6-v5H( zs^Kfvrww@XGh{}?SXQv_9)P^fzK~@K@^(Ys(jeK5w&K}FkA|>9zmN(?(8jdB%+E}pmF>_Yx2=k z>2B*uH#CZzF>Yjg2ni5h);FnnOvY!=3!i~!L3oZQ@0H?%CtWK&r+M8@O@fYOvb`?@ znzs!>Ps^l%{=f_H5eXs6sNTtz6BCBO6<}IA3Ya*Vf5pJ>2P`E9?1)1ewxDXyH+|A+ zpWE-x8kE(g>z<{}>{rBWLG?e$i2J2_WR8pv_}>`n!!Ic1#fq!o)bD^jZmXy!+E{&( z7U%=ZgwLgshQ9?oheLU|U_WM;3Se=0sWyljEK&V1_tCPC;<7P_ADs8YI356Xg)pv1qkxfL*>rzo1F&(RHo zmfL}9Nn_9&=%Cs+2|BIo$T=#?i#JZkKCz=X3N;pHDG;d>eF1?^#8Uh>r8)-@MuAs5 zDEz~$+gPutN_+YKR;rbM``iUzy@9Tf`Y8DR68z1lT$e@OMZB<_kYuXcCq#)=S#=efFyMICIEOK3BzgUv-C52l7ymZwIHF> z#$R^r)c5_HSV$!ss zl2lKes>_mJGGJ_gAyqEqHIYif`y;0Ov>2vZ9$%4?2Gw|Ysz`q+Qw?CyIRcBy78Vva zRK-e^m|&@GInjf7z$6{T6Vf$SVS$CF3F~3ST7*YepQrsLyoKhGCp=dA5l80D!gnHs z86-Al%z}v%6Ui*X9m$5X5i7-RrSgF*sh)fuORNY^=w0!(8R1K2XY~};ori_cB;!xa za`-UkqoBCUkGp8QG!2u^`D{jqA!2qUv?Clak5R}xcHC4pDA*( zQ(7H#crypp$ECM+*&zob->_&fg_T1#oiVb(^0ia`_b9{Ml??;|qB!GkYZ}v0HH@FH z9zih!-;DCa+e(o!Z6wAFO7SbhQd3OU7N!h!seFX1)X+!hB+~Q)h`IvW%vLhxD+a7# zC5$wZuei%TP**TWt&IdZhS-GM8TIYVX%=$*hntd^bE&$%wBR-} zlvNx2P2*cB#%BbiguSvyz>*3JZd^|gh1vsnE zQ{F}Ykm?HEEax1VK~8vtEMs%lS}I<=OOLp_G4WKr2Vjv*AGTh>JHD(*;g}Qpqo8#N z7_+;E*@`B$4){KFRSg|#=-8gtey{JSYN#P4Oug_(3GK}9C#5PD=x?&zVWiX(xG!BcOwu4v-pcOfg08D*aVX6-25_!Z{ZhygK96{ z*J!ltj{RD}FLZ$8S&P3JYpp_t9JKfhtyVEdQ7ADXoU*pYug7N`RiS@Mf&G`||vYJSOJQibN1)H0iN z_?07H2}(tIOMVp#zf=NEZ+J4E;n#A~k;?){Il@8tm*3gcapCVkAGs4zk)9gD(Jw{m zsl`j-mn|l)_(h$T@Tk+0U*y6s%92MOIA0E$LaI~b$S+N1q+0Tenl!&uHhDC^q>I_4 zI~~$V7s-WR*XSmv>?sNP+53|9e)}i}c7Y%bI?KpdAkhCJAZ?P2k^}{C8jqCla-g77 zP7gg$3X~BrO_}^!{zbV7oLKIlp$E&4%fMH+;HW44@MECB6qar9Omx#3b>`Rf|-*dTvu zeA3}J=qIPcaHkf25`u|lX~1vD^b7iSSor&zFSuxjtUS5G8u=Wh0pFvqEq`2Z26=-;I zs6g{e6Ag9!E$QwEl_OpA%K|I*YdJhC4bJcZj(Y^mflw0ZSsIM+ODQO+`K1$dG?iaH zc*K9&2hD7=I9bVwIP|PC5+y>qmTTkbT%Q?R?@ZD`1KNcGv(qTNo$bb+?&^K$OE@JhQ6LPc3Xikfh6mhDR=UY`1Y+@SVgV5)0lD_eA$o|d zo9FFpJ)lRWVT@r&@lu)U#o!r$P~tmu(w7kvxu@J*q_{w_nu$WT4Mplxa}AMpL0NsL zMKWhkAc|TS11Sdb8p!w;ea+_egk2b%biWUD*Bc&e1q?X5yL?%Ib; zDPXO?)7E#)qn-JTtG7IMGc(|={farD(l)8alrC_Ea$wLHl&MW`Cs)v8@0m)7ob`2? zE#Oyvh2(s6vg$67M!&*67ec;o@}KRK?hn`(P0^?6IsP7D=3jftBy}Ey2=LR*H@RvR)8+{65w(e^acW^lS7XshoIwcs3C)H|dao3p+O za!lni<_figD$h|Zs8Ui4=apU*cu*=fg;{z~iqB0>NQf;`>ei;s(&oVZ1gQUhk>+jK$ zLg(&X0XHUbg>_!wv93~?IXwBcHKWUTWB0frEM~&sdl_+2*8}{kJjF?MZqN5$W;H)W zP?f*YVdMBhoDA z)b-!ePJt(%8gk5X2va#jdUmM;PnNU{JgP1$WgFk5BP5(so0B?8Xn&r-=8|s1x3pBv zXdOJ;T>&=DPi^3nX)Cdg%729nbw<(7=q>WhK*LLPG%=jtBi&j{XR>6Kp7_$n7K}CD za?4Vm(12fxUWr~R!6DmsA&UwICit5$bhu&lArqJ*;47Uuqk;3OK$d%71TyzsoX$?H zIK*n;c{)r6g5M*9-+%D@8a-qh3|~e_=RdCfHh+^1;d}G)iiW1uObJaF#&onqIIT?v zG#x|{O;unM#Abb+S`jUURzTCNUqf5f&~j*)D&*;f{0_C8W3xrdMI1r2)zGx&seVq* zOaTYzS-h}o|Iwg|PFbd+t+^K(vkxZ2^`Xm>RnXVoOQ1f(42nUh%h~3H65NLo)V11R zP@d5c*Abw#_qlfJ%dHp5TS=a=#=wXlaLabb^)3q<{P+@N*hKQIC>>H7e~=U9EH(-J zBK>hp5G^^be0B6`{4l?nMlwu`>QWsKZJQ5AuHfpMc5L!nefn@UEEK8NnXUDQwa+_= zmBIWyy~r!zZF-pBlrqE2lZ8wxAevt$UbHP_*;V7u8ehb;`A~bL$_WSKnYl z(-N|L%DNDP?{)TWEj*?^90ko$@KlV>?X8B9!l+mmJV{ZI3&?99C9l57sL4qfks1$U z4U8@gkZ+y+j7+h;I+D2_x{OQ-{lL3;WJb(KYYX1XWQ*2H$^V|lOfhZl>b~WpLN=VZ zu4Dhn^8GoFTkj*6GsM%{Y z9`{bp`5NwZf!moxKrv300>t!Fn+;~K{2vbybkujFY;l70rbAZ&6kfHxk2&e8+Jk2}KKBEr#Eid0K;Wf?1Qo4_Fo{1~6(&zhKlXgWHp`SA;g{Kp2t zCB*LHWv7=pWf2w*aIb^QoJT>aJ|!vx$?0Jm`2B{>gxPi+FN3WyTf(;S4|{^Y<%)7e zDtu%Vejl2Q%n4t=3!x1-jRVfPX?VA$y$7*nA&=M>cJ5vaBVicGZB#)QGB&ADG=Xj| zxar3kXhtp&Y+Cz18Uth_5PHQ{kZ(~?&1pUr*9NTXDC>@5EVwU17IjW94=EG&tXKlX z$3RL$Vbgn67|H+=pSe8^K(g%zKnZ}}8se+~fV}`Vkpmq&0Q3Q%Rm0Shu4h^gdC-9w zIm9#%05#;4h0hV4U6k9Tjw25~<19$gK#+i9mq$E#Y!2Pe88vO-X{OGnn0SZ5lysh9 zwt%5>xW>K+m~6^jo$6$Z1_ifon~V}xhP+hBWfbyxoa@7c@D0+-)9g-E1D%vD?Z56? z$V_8%&Mzw*ub1l{pxDv}J*X0q4nEPUPCx3Web_p1L?;i}>~YvhsFt|eOo#*5!`;L*I?A*|RaEf-f|;a|TJ16dTM5cSH!vg_>Du-+D|~LokJdZFJ=QqraA}1{K%Z6Hce46R#_V@ z8x|w(uTT#hc_7+QkiF00kHcTl%jdMd;W|gdl~YnQsrPhqq$_(ZJ|ZQxESa8RjyYeu zr^`x*`e^E+3U;UJux?n?Ap$8Fm$3aP6}Mu3ix}d%)5{i+=%{ zzR0NnEPCVtB#&)9#J~D<#1=b)j7Q7!622r)2E&hNevO5p-RHeP*FQ&23{(vNxze5S zsugN?KZAED19jAUW%V4flHq~|=8j{qU{E_@x&;D0LL{X*&kPO$pf{7PhkzeYWV7oz~^w^a*bf6PrN>sGyWWf4!A$$>W7`#FC^x> zL+%(amy>(qOZZ}t0fp!h)$4R5gd<|?Jwf=P*;G!SwKJ>;Y}B>gEnH#jK5UX*B0Gu< z+^Acyrl3Z}50ry^sZ9TV9oOi)!6f{EPGK6%@MTtcvD!99V5T>6kq zUm^8CQ8*_yrGO2Fk{W`2Cva3}U0nf0TB+|7Ui&@MruU@jcquio7e<9sr;8@jpqD{M zq2UJoUnn@4dbji1f1^I2I8JKE0`o(Grj!LVjybtLv{7)j+WI#=a5A8fmvSVpTIs+m zT@5V;Rel>9rQ;G9O~(#vXk+)eXc#xeaRyf(opkxDc(N^&oOOcRhtc72BFMEyKW`#J zm+Ud}&^e0JKWBA@J^?)qbo(q?4To9|Ie0#Qjj3VN4_q`lfbd<(x47Ot*EEHMaw%{5 zA=zOIIaX|L?-^>A_f5#{EYzNLKvZ)5#N7lIC-%FL@q5k0>ATigomOAp5x0m-4e;js z$$(7j|4j|;WRMLot}0L980Z$tl)UB!X}$u;1V7ru%sX0c?)ScP5Zygb;ScS2k~|;a zFjo?HMOzb;U=^cZnisIHFZ^S4`5A--zOr8oU0^^|Gvi+BL^=4K?s zj;R5uwvsB50sdDK&H6b-QIE{b(4wBCg@1fidpdJ5?+7S}Op1>`6soQiS|}@aTI`&M zNJW(J4kau|82g+@3tUSY?egPXnk1=438&aeIIYQxbWoxU3w;%RDT~CRgc@p-=3`A7 z&cl<26J;DhwI1t~dkO`It_jI%rtqzy5(Pzl>@b;;aewFk1Wig!0gAxO=b0{t^_nYA zv=)$UO|@`2dgT6$WHJcFzmOFX)fV#ZHU&^4O2$X=Fil#s%d_j28E@j9KP`%N;MyfC0-iOG7wPAHzFUCC&M-Ek+2>GKW4wNm5(;_%@)KQlKP&)MP-{2ap*oox-eG zDb|$XIEaXfGG{7OvI&$H-~Dg%f4-c@1X?u+n{Dt6ZUAD-b|A8PN4e)*#J*sU)E<`@ z!ngxdrL+|0J!C+We1>i+N1MB#I6=!KS_$-MC@wb;QsaG~=&as^j@plFg;fI@2V^;Y zlg{KAAA%^Twh*5gx^L%KftcI>|2hHinc_7meg=*h-E|eK+`bwCWODV}YZ7c8-Vg zqoke#(CSXoed47r(>EVSCIsjdui%w5YzwXs`b((Co%ZhTIUVweIed;w9t#Rvs>ew; z*i$m*#?Q?Q?+G0)p{mqxUWs zMGSPdwBEA)l+hGWelnDlvd$+PeT<5hxLL6fXUzXAAREairTPJ)Isc+wRM)_LgAl8` z@iEsCKb2^{Y7xjI0MG>j`U+z~SA>?c`5liWg~Q=hpqghn`IaDQ^(|vJ3qnz+Y zJQ$CF#OmgHCXg41RRrEWny*ht^A!`kO*}V85?oLPEXadObkt=dZ!q|k_a(3SMyNj*3E}zKhA0L^pD*ETv_Vf}RG! zqBx#?Xfd`idSO|Rzl2!9$+ii{s}(~17BuD})8tYBiI+gK0C`c)Rc;2>y6J&4VQU_Hp)u`f_%c~-VM z;)Jv4N*~HxuB&XLLLKz9!QDO^7HoM-YbkCB)H427xl?w z3zs($NdpOY0A4|7C`3KIRl>faIzvW#YjQQv;I^FgAUkp>yXco-WM_ zv;zo#4IuKU3djlBATD}pQoCL6fyzNOHA2=0g;H_xm3aw;d7(IesC*aXb6TZnH%f&e z_f|Sn15xXfCYR4S_n(-yvKiMILPoTt^+~U00-_HXrM@Jbn&7dZd)dp*h8*NnPcLi) zq=#|>h}q<{si+2bBK~IXkOCl6h8o!@ic-G^hB3B)As(m)1Ej*-(+K&*!=|1lkHB_JPY!b>)t0?f`lBWaiojrehMohFy<|d@ z{UdZq(mb&5aGivq;a?0xmK=7zN*#f)VP+7vTUx2FF#fhmyA%*yV3WcvAP7O7qz7v+ ztEk&=dLbPEiK9A$B_*RwH~Q-akOM%?KF#BjOmwh$q+)yyhXTOrha@9Ax)#%Z9^JAx za+i?R$vGyz19~OsgzZeKn1C)()-{mQKvEZA5vkF+E9eTyVZ}%ZeHC~e!8oaI$6hn| zp^hDvAlbE5)08vTH)T~9IOvmcR6{vUsn_oLTGDWv&p3mkyV`7LPX|UWM!V~}`40Sy%?25h`I%CKZFqjml z*Nn63FbzbaobWbcln~4eRKx_8AQv_>7In?TYBti~s(IIIZjaid&{soeXbk9k$a;4} zwiX-Qwi<+933@>08*&(7BcUjhjvL$D`pM8V<;e_gB-R$x!Hxcoib2LuOghfhV;-T7 zK^=WW6{Eg(@kH-0_n_Eam8aFbhCqTzHy|jD9aDZ<>Q{HAY{8m2J15YWnDAdNZ?nxCT!pcV*Nj^!&GoJGiZWNN$nA;Vx*C`57 zsK=y(LQNZr-P(mhV;V{#c^E|+t5jCHQKYRFwxajRb$9 z(xtCUDU{hzSZhu?Fm&Jog&IO(JV5D@kJWHm<7NSZ2)T5gdbuw^B8}lEBps=s2&x|h zTo?!xZQ|m#dl%uLGlHn(e-}iAQ5-xQflwGc4aJlY3Pa>w6jC=7ljt#(tw*Jl54}I> zpkNdmN-BvOrD`Zh$td&?l%BjdYHSA63u3Ms6lMZM{6?TK%#4z1f5R}Tqz(q74P_;% z8j8(tZxWQsgG(ZmCZVFC(Qz~%8h?nk5?r7$oicD65-BLaW>`E|%WXhvuO#JLAte-Sn4Jkk0&|ZxLO<6QFy{4d~?dXuNMdLZ6 z2qHqH%@hyqebCzM^<|<-4>MK;E054@0`!v(MWT#`#0mSvNQiPkD%}nv#d_k=67mt7 zeMZ7K4M-f$F%rURNQsebl;pD^VVdg*4y42v15)O8Hze%__T-RIQ_K(LL0ECbxpdND zRLMC2RQTc0Lj>LgfbftqRUmHhH{;`q-;AZ^H&q3Xj_J29*ifqnrE2`{@w;umRm%$v z0VzLdB@75i*a+2?gxLQP!Gjo8AvEhjQ?~4#D=0K9hhhbTd4Lxs;|c;b<6%a3zeJJ! zo-@S!Zs|9}8urzbPG<@EoCH6X>jwp;0|sHnSc!~5v-J0nK$zRBboo7>bpNtQ8nFKK a{~)42$>C3O&`|l49{foU{y)+K|Nj6eO2Kph literal 0 HcmV?d00001