// // PictReader.java // /* LOCI Bio-Formats package for reading and converting biological file formats. Copyright (C) 2005-@year@ Melissa Linkert, Curtis Rueden, Chris Allan, Eric Kjellman and Brian Loranger. This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package loci.formats.in; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.io.*; import java.util.*; import loci.formats.*; import loci.formats.codec.PackbitsCodec; /** * PictReader is the file format reader for Apple PICT files. * Most of this code was adapted from the PICT readers in JIMI * (http://java.sun.com/products/jimi/index.html), ImageMagick * (http://www.imagemagick.org), and Java QuickDraw. * * <dl><dt><b>Source code:</b></dt> * <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/in/PictReader.java">Trac</a>, * <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/in/PictReader.java">SVN</a></dd></dl> */ public class PictReader extends FormatReader { // -- Constants -- // opcodes that we need private static final int PICT_CLIP_RGN = 1; private static final int PICT_BITSRECT = 0x90; private static final int PICT_BITSRGN = 0x91; private static final int PICT_PACKBITSRECT = 0x98; private static final int PICT_PACKBITSRGN = 0x99; private static final int PICT_9A = 0x9a; private static final int PICT_END = 0xff; private static final int PICT_LONGCOMMENT = 0xa1; // possible image states private static final int INITIAL = 1; private static final int STATE2 = 2; // other stuff? private static final int INFOAVAIL = 2; private static final int IMAGEAVAIL = 4; /** Table used in expanding pixels that use less than 8 bits. */ private static final byte[] EXPANSION_TABLE = new byte[256 * 8]; static { int index = 0; for (int i=0; i<256; i++) { EXPANSION_TABLE[index++] = (i & 128) == 0 ? (byte) 0 : (byte) 1; EXPANSION_TABLE[index++] = (i & 64) == 0 ? (byte) 0 : (byte) 1; EXPANSION_TABLE[index++] = (i & 32) == 0 ? (byte) 0 : (byte) 1; EXPANSION_TABLE[index++] = (i & 16) == 0 ? (byte) 0 : (byte) 1; EXPANSION_TABLE[index++] = (i & 8) == 0 ? (byte) 0 : (byte) 1; EXPANSION_TABLE[index++] = (i & 4) == 0 ? (byte) 0 : (byte) 1; EXPANSION_TABLE[index++] = (i & 2) == 0 ? (byte) 0 : (byte) 1; EXPANSION_TABLE[index++] = (i & 1) == 0 ? (byte) 0 : (byte) 1; } } // -- Fields -- /** Stream for reading pixel data. */ protected RandomAccessStream ras; /** Pixel bytes. */ protected byte[] bytes; /** Number of bytes in a row of pixel data (variable). */ protected int rowBytes; /** Decoder state. */ protected int state; /** Image state. */ protected int pictState; /** Vector of byte arrays representing individual rows. */ protected Vector strips; /** Whether or not the file is PICT v1. */ protected boolean versionOne; /** Color lookup table for palette color images. */ protected short[][] lookup; /** Helper reader in case this one fails. */ protected LegacyQTTools qtTools = new LegacyQTTools(); // -- Constructor -- /** Constructs a new PICT reader. */ public PictReader() { super("PICT", new String[] {"pict", "pct"}); } // -- PictReader API methods -- /** Get the dimensions of a PICT file from the first 4 bytes after header. */ public Dimension getDimensions(byte[] stuff) throws FormatException { if (stuff.length < 10) { throw new FormatException("Need 10 bytes to calculate dimension"); } int w = DataTools.bytesToInt(stuff, 6, 2, core.littleEndian[0]); int h = DataTools.bytesToInt(stuff, 8, 2, core.littleEndian[0]); if (debug) debug("getDimensions: " + w + " x " + h); return new Dimension(h, w); } /** Open a PICT image from an array of bytes (used by OpenlabReader). */ public BufferedImage open(byte[] pix) throws FormatException, IOException { // handles case when we call this method directly, instead of // through initFile(String) if (debug) debug("open"); if (core == null) core = new CoreMetadata(1); strips = new Vector(); state = 0; pictState = INITIAL; bytes = pix; ras = new RandomAccessStream(bytes); ras.order(false); try { while (driveDecoder()) { } } catch (FormatException exc) { trace(exc); return ImageTools.makeBuffered(qtTools.pictToImage(pix)); } // combine everything in the strips Vector if ((core.sizeY[0]*4 < strips.size()) && (((strips.size() / 3) % core.sizeY[0]) != 0)) { core.sizeY[0] = strips.size(); } if (strips.size() == 0) { return ImageTools.makeBuffered(qtTools.pictToImage(pix)); } if (lookup != null) { // 8 bit data short[][] data = new short[3][core.sizeY[0] * core.sizeX[0]]; byte[] row; for (int i=0; i<core.sizeY[0]; i++) { row = (byte[]) strips.get(i); for (int j=0; j<row.length; j++) { if (j < core.sizeX[0]) { int ndx = row[j]; if (ndx < 0) ndx += lookup[0].length; ndx = ndx % lookup[0].length; int outIndex = i*core.sizeX[0] + j; if (outIndex >= data[0].length) outIndex = data[0].length - 1; data[0][outIndex] = lookup[0][ndx]; data[1][outIndex] = lookup[1][ndx]; data[2][outIndex] = lookup[2][ndx]; } else j = row.length; } } if (debug) { debug("openBytes: 8-bit data, " + core.sizeX[0] + " x " + core.sizeY[0] + ", length=" + data.length + "x" + data[0].length); } return ImageTools.makeImage(data, core.sizeX[0], core.sizeY[0]); } else if (core.sizeY[0]*3 == strips.size()) { // 24 bit data byte[][] data = new byte[3][core.sizeX[0] * core.sizeY[0]]; int outIndex = 0; for (int i=0; i<3*core.sizeY[0]; i+=3) { byte[] c0 = (byte[]) strips.get(i); byte[] c1 = (byte[]) strips.get(i+1); byte[] c2 = (byte[]) strips.get(i+2); System.arraycopy(c0, 0, data[0], outIndex, c0.length); System.arraycopy(c1, 0, data[1], outIndex, c1.length); System.arraycopy(c2, 0, data[2], outIndex, c2.length); outIndex += core.sizeX[0]; } if (debug) { debug("openBytes: 24-bit data, " + core.sizeX[0] + " x " + core.sizeY[0] + ", length=" + data.length + "x" + data[0].length); } return ImageTools.makeImage(data, core.sizeX[0], core.sizeY[0]); } else if (core.sizeY[0]*4 == strips.size()) { // 32 bit data byte[][] data = new byte[3][core.sizeX[0] * core.sizeY[0]]; int outIndex = 0; for (int i=0; i<4*core.sizeY[0]; i+=4) { //byte[] a = (byte[]) strips.get(i); byte[] r = (byte[]) strips.get(i+1); byte[] g = (byte[]) strips.get(i+2); byte[] b = (byte[]) strips.get(i+3); System.arraycopy(r, 0, data[0], outIndex, r.length); System.arraycopy(g, 0, data[1], outIndex, g.length); System.arraycopy(b, 0, data[2], outIndex, b.length); //System.arraycopy(a, 0, data[3], outIndex, a.length); outIndex += core.sizeX[0]; } if (debug) { debug("openBytes: 32-bit data, " + core.sizeX[0] + " x " + core.sizeY[0] + ", length=" + data.length + "x" + data[0].length); } return ImageTools.makeImage(data, core.sizeX[0], core.sizeY[0]); } else { // 16 bit data short[] data = new short[3 * core.sizeY[0] * core.sizeX[0]]; int outIndex = 0; for (int i=0; i<core.sizeY[0]; i++) { int[] row = (int[]) strips.get(i); for (int j=0; j<row.length; j++, outIndex+=3) { if (j < core.sizeX[0]) { if (outIndex >= data.length - 2) break; int s0 = (row[j] & 0x1f); int s1 = (row[j] & 0x3e0) >> 5; // 0x1f << 5; int s2 = (row[j] & 0x7c00) >> 10; // 0x1f << 10; data[outIndex] = (short) s2; data[outIndex+1] = (short) s1; data[outIndex+2] = (short) s0; } else j = row.length; } } if (debug) { debug("openBytes: 16-bit data, " + core.sizeX[0] + " x " + core.sizeY[0] + ", length=" + data.length); } return ImageTools.makeImage(data, core.sizeX[0], core.sizeY[0], 3, true); } } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isThisType(byte[]) */ public boolean isThisType(byte[] block) { if (block.length < 528) return false; return true; } /* @see loci.formats.IFormatReader#openBytes(int, byte[]) */ public byte[] openBytes(int no, byte[] buf) throws FormatException, IOException { buf = ImageTools.getBytes(openImage(no), false, no % 3); return buf; } /* @see loci.formats.IFormatReader#openImage(int) */ public BufferedImage openImage(int no) throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); FormatTools.checkPlaneNumber(this, no); return open(bytes); } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ protected void initFile(String id) throws FormatException, IOException { if (debug) debug("PictReader.initFile(" + id + ")"); super.initFile(id); in = new RandomAccessStream(id); status("Populating metadata"); core.littleEndian[0] = false; // skip the header and read in the remaining bytes int len = (int) (in.length() - 512); bytes = new byte[len]; in.seek(512); in.read(bytes); byte[] b = new byte[20]; in.seek(512); in.read(b); Dimension d = getDimensions(b); core.sizeX[0] = d.width; while (core.sizeX[0] % 8 != 0) core.sizeX[0]++; core.sizeY[0] = d.height; core.sizeZ[0] = 1; core.sizeC[0] = 3; core.sizeT[0] = 1; core.currentOrder[0] = "XYCZT"; core.rgb[0] = true; core.interleaved[0] = false; core.imageCount[0] = 1; core.indexed[0] = false; core.falseColor[0] = false; core.metadataComplete[0] = true; // The metadata store we're working with. MetadataStore store = getMetadataStore(); store.setImage(currentId, null, null, null); core.pixelType[0] = ImageTools.getPixelType(openImage(0)); FormatTools.populatePixels(store, this); for (int i=0; i<core.sizeC[0]; i++) { store.setLogicalChannel(i, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); } } // -- Helper methods -- /** Loop through the remainder of the file and find relevant opcodes. */ private boolean driveDecoder() throws FormatException, IOException { if (debug) debug("driveDecoder"); int opcode; switch (pictState) { case INITIAL: ras.skipBytes(10); int verOpcode = ras.read(); int verNumber = ras.read(); if (verOpcode == 0x11 && verNumber == 0x01) versionOne = true; else if (verOpcode == 0x00 && verNumber == 0x11) { versionOne = false; int verNumber2 = ras.readShort(); if (verNumber2 != 0x02ff) { throw new FormatException("Invalid PICT file : " + verNumber2); } // skip over v2 header -- don't need it here ras.skipBytes(26); } else throw new FormatException("Invalid PICT file"); pictState = STATE2; state |= INFOAVAIL; return true; case STATE2: if (versionOne) opcode = ras.read(); else { // if at odd boundary skip a byte for opcode in PICT v2 if ((ras.getFilePointer() & 0x1L) != 0) { ras.skipBytes(1); } opcode = ras.readShort(); } return drivePictDecoder(opcode); } return true; } /** Handles the opcodes in the PICT file. */ private boolean drivePictDecoder(int opcode) throws FormatException, IOException { if (debug) debug("drivePictDecoder"); switch (opcode) { case PICT_BITSRGN: // rowBytes must be < 8 case PICT_PACKBITSRGN: // rowBytes must be < 8 case PICT_BITSRECT: // rowBytes must be < 8 case PICT_PACKBITSRECT: case PICT_9A: handlePackBits(opcode); break; case PICT_CLIP_RGN: int x = ras.readShort(); ras.skipBytes(x - 2); break; case PICT_LONGCOMMENT: ras.skipBytes(2); x = ras.readShort(); ras.skipBytes(x); break; case PICT_END: // end of PICT state |= IMAGEAVAIL; return false; } return ras.getFilePointer() < ras.length(); } /** Handles bitmap and pixmap opcodes of PICT format. */ private void handlePackBits(int opcode) throws FormatException, IOException { if (debug) debug("handlePackBits(" + opcode + ")"); if (opcode == PICT_9A) { // special case handlePixmap(opcode); } else { rowBytes = ras.readShort(); if (versionOne || (rowBytes & 0x8000) == 0) handleBitmap(opcode); else handlePixmap(opcode); } } /** Extract the image data in a PICT bitmap structure. */ private void handleBitmap(int opcode) throws FormatException, IOException { if (debug) debug("handleBitmap(" + opcode + ")"); int row; byte[] buf; // raw byte buffer for data from file byte[] uBuf; // uncompressed data -- possibly still pixel packed byte[] outBuf; // expanded pixel data rowBytes &= 0x3fff; // mask off flags // read the bitmap data -- 3 rectangles + mode int tlY = ras.readShort(); int tlX = ras.readShort(); int brY = ras.readShort(); int brX = ras.readShort(); // skip next two rectangles ras.skipBytes(18); core.sizeX[0] = brX - tlX; core.sizeY[0] = brY - tlY; // allocate enough space to handle compressed data length for rowBytes try { buf = new byte[rowBytes + 1 + rowBytes/128]; uBuf = new byte[rowBytes]; outBuf = new byte[core.sizeX[0]]; } catch (NegativeArraySizeException n) { throw new FormatException("Sorry, vector data not supported."); } for (row=0; row < core.sizeY[0]; ++row) { if (rowBytes < 8) { // data is not compressed ras.read(buf, 0, rowBytes); for (int j=buf.length; --j >= 0;) { buf[j] = (byte) ~buf[j]; } expandPixels(1, buf, outBuf, outBuf.length); } else { int rawLen; if (rowBytes > 250) rawLen = ras.readShort(); else rawLen = ras.read(); try { ras.read(buf, 0, rawLen); } catch (ArrayIndexOutOfBoundsException e) { throw new FormatException("Sorry, vector data not supported."); } PackbitsCodec c = new PackbitsCodec(); uBuf = c.decompress(buf); //uBuf = Compression.packBitsUncompress(buf); // invert the pixels -- PICT images map zero to white for (int j=0; j<uBuf.length; j++) uBuf[j] = (byte) ~uBuf[j]; expandPixels(1, uBuf, outBuf, outBuf.length); } strips.add(outBuf); } } /** Extracts the image data in a PICT pixmap structure. */ private void handlePixmap(int opcode) throws FormatException, IOException { if (debug) debug("handlePixmap(" + opcode + ")"); int pixelSize; int compCount; // handle 9A variation if (opcode == PICT_9A) { // this is the only opcode that holds 16, 24, and 32 bit data // read the pixmap (9A) ras.skipBytes(6); // read the bounding box int tlY = ras.readShort(); int tlX = ras.readShort(); int brY = ras.readShort(); int brX = ras.readShort(); ras.skipBytes(18); pixelSize = ras.readShort(); compCount = ras.readShort(); ras.skipBytes(14); core.sizeX[0] = brX - tlX; core.sizeY[0] = brY - tlY; // rowBytes doesn't exist, so set it to its logical value switch (pixelSize) { case 32: rowBytes = core.sizeX[0] * compCount; break; case 16: rowBytes = core.sizeX[0] * 2; break; default: throw new FormatException("Sorry, vector data not supported."); } } else { rowBytes &= 0x3fff; // mask off flags int tlY = ras.readShort(); int tlX = ras.readShort(); int brY = ras.readShort(); int brX = ras.readShort(); ras.skipBytes(18); pixelSize = ras.readShort(); compCount = ras.readShort(); ras.skipBytes(14); // read the lookup table ras.skipBytes(4); int flags = ras.readShort(); int count = ras.readShort(); count++; lookup = new short[3][count]; for (int i=0; i<count; i++) { int index = ras.readShort(); if ((flags & 0x8000) != 0) index = i; lookup[0][index] = ras.readShort(); lookup[1][index] = ras.readShort(); lookup[2][index] = ras.readShort(); } core.sizeX[0] = brX - tlX; core.sizeY[0] = brY - tlY; } // skip over two rectangles ras.skipBytes(18); if (opcode == PICT_BITSRGN || opcode == PICT_PACKBITSRGN) ras.skipBytes(2); handlePixmap(rowBytes, pixelSize, compCount); } /** Handles the unpacking of the image data. */ private void handlePixmap(int rBytes, int pixelSize, int compCount) throws FormatException, IOException { if (debug) { debug("handlePixmap(" + rBytes + ", " + pixelSize + ", " + compCount + ")"); } int rawLen; byte[] buf; // row raw bytes byte[] uBuf = null; // row uncompressed data int[] uBufI = null; // row uncompressed data - 16+ bit pixels int bufSize; int outBufSize; byte[] outBuf = null; // used to expand pixel data boolean compressed = (rBytes >= 8) || (pixelSize == 32); bufSize = rBytes; outBufSize = core.sizeX[0]; // allocate buffers switch (pixelSize) { case 32: if (!compressed) uBufI = new int[core.sizeX[0]]; else uBuf = new byte[bufSize]; break; case 16: uBufI = new int[core.sizeX[0]]; break; case 8: uBuf = new byte[bufSize]; break; default: outBuf = new byte[outBufSize]; uBuf = new byte[bufSize]; break; } if (!compressed) { if (debug) { debug("Pixel data is uncompressed (pixelSize=" + pixelSize + ")."); } buf = new byte[bufSize]; for (int row=0; row<core.sizeY[0]; row++) { ras.read(buf, 0, rBytes); switch (pixelSize) { case 16: for (int i=0; i<core.sizeX[0]; i++) { uBufI[i] = ((buf[i*2] & 0xff) << 8) + (buf[i*2+1] & 0xff); } strips.add(uBufI); break; case 8: strips.add(buf); break; default: // pixel size < 8 expandPixels(pixelSize, buf, outBuf, outBuf.length); strips.add(outBuf); } } } else { if (debug) { debug("Pixel data is compressed (pixelSize=" + pixelSize + "; compCount=" + compCount + ")."); } buf = new byte[bufSize + 1 + bufSize / 128]; for (int row=0; row<core.sizeY[0]; row++) { if (rBytes > 250) rawLen = ras.readShort(); else rawLen = ras.read(); if (rawLen > buf.length) rawLen = buf.length; if ((ras.length() - ras.getFilePointer()) <= rawLen) { rawLen = (int) (ras.length() - ras.getFilePointer() - 1); } if (rawLen < 0) { rawLen = 0; ras.seek(ras.length() - 1); } ras.read(buf, 0, rawLen); if (pixelSize == 16) { uBufI = new int[core.sizeX[0]]; unpackBits(buf, uBufI); strips.add(uBufI); } else { PackbitsCodec c = new PackbitsCodec(); uBuf = c.decompress(buf); //uBuf = Compression.packBitsUncompress(buf); } if (pixelSize < 8) { expandPixels(pixelSize, uBuf, outBuf, outBuf.length); strips.add(outBuf); } else if (pixelSize == 8) strips.add(uBuf); else if (pixelSize == 24 || pixelSize == 32) { byte[] newBuf = null; int offset = 0; if (compCount == 4) { // alpha channel //newBuf = new byte[width]; //System.arraycopy(uBuf, offset, newBuf, 0, width); strips.add(newBuf); offset += core.sizeX[0]; } // red channel newBuf = new byte[core.sizeX[0]]; System.arraycopy(uBuf, offset, newBuf, 0, core.sizeX[0]); strips.add(newBuf); offset += core.sizeX[0]; // green channel newBuf = new byte[core.sizeX[0]]; System.arraycopy(uBuf, offset, newBuf, 0, core.sizeX[0]); strips.add(newBuf); offset += core.sizeX[0]; // blue channel newBuf = new byte[core.sizeX[0]]; System.arraycopy(uBuf, offset, newBuf, 0, core.sizeX[0]); strips.add(newBuf); } } } } /** Expand an array of bytes. */ private void expandPixels(int bitSize, byte[] ib, byte[] ob, int outLen) throws FormatException { if (debug) { debug("expandPixels(" + bitSize + ", " + ib.length + ", " + ob.length + ", " + outLen + ")"); } if (bitSize == 1) { int remainder = outLen % 8; int max = outLen / 8; for (int i=0; i<max; i++) { if (i < ib.length) { int look = (ib[i] & 0xff) * 8; System.arraycopy(EXPANSION_TABLE, look, ob, i*8, 8); } else i = max; } if (remainder != 0) { if (max < ib.length) { System.arraycopy(EXPANSION_TABLE, (ib[max] & 0xff) * 8, ob, max*8, remainder); } } return; } int i; int o; int t; byte v; int count = 8 / bitSize; // number of pixels in a byte int maskshift = bitSize; // num bits to shift mask int pixelshift = 8 - bitSize; // num bits to shift pixel int tpixelshift = 0; int pixelshiftdelta = bitSize; int mask = 0; int tmask; // temp mask if (bitSize != 1 && bitSize != 2 && bitSize != 4) { throw new FormatException("Can only expand 1, 2, and 4 bit values"); } switch (bitSize) { case 1: mask = 0x80; break; case 2: mask = 0xC0; break; case 4: mask = 0xF0; break; } i = 0; for (o = 0; o < ob.length;) { tmask = mask; tpixelshift = pixelshift; v = ib[i]; for (t = 0; t < count && o < ob.length; ++t, ++o) { ob[o] = (byte) (((v & tmask) >>> tpixelshift) & 0xff); tmask = (byte) ((tmask & 0xff) >>> maskshift); tpixelshift -= pixelshiftdelta; } ++i; } } /** PackBits variant that outputs an int array. */ private void unpackBits(byte[] ib, int[] ob) { if (debug) debug("unpackBits(" + ib + ", " + ob + ")"); int i = 0; int o = 0; int b; int rep; int end; for (o=0; o<ob.length;) { if (i+1 < ib.length) { b = ib[i++]; if (b >= 0) { b++; end = o + b; for(; o < end; o++, i+=2) { if (o < ob.length && (i+1) < ib.length) { ob[o] = (((ib[i] & 0xff) << 8) + (ib[i+1] & 0xff)) & 0xffff; } else o = end; } } else if (b != -128) { rep = (((ib[i] & 0xff) << 8) + (ib[i+1] & 0xff)) & 0xffff; i += 2; end = o - b + 1; for (; o < end; o++) { if (o < ob.length) ob[o] = rep; else o = end; } } } else o = ob.length; } } }