/* * #%L * BSD implementations of Bio-Formats readers and writers * %% * Copyright (C) 2005 - 2015 Open Microscopy Environment: * - Board of Regents of the University of Wisconsin-Madison * - Glencoe Software, Inc. * - University of Dundee * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. 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. * * 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 HOLDERS 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. * #L% */ package loci.formats.in; import java.io.IOException; import java.util.Vector; import loci.common.ByteArrayHandle; import loci.common.DataTools; import loci.common.RandomAccessInputStream; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; import loci.formats.MetadataTools; import loci.formats.codec.CodecOptions; import loci.formats.codec.JPEGCodec; import loci.formats.codec.PackbitsCodec; import loci.formats.gui.AWTImageTools; import loci.formats.gui.LegacyQTTools; import loci.formats.meta.MetadataStore; /** * 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. */ 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; private static final int PICT_JPEG = 0x18; private static final int PICT_TYPE_1 = 0xa9f; private static final int PICT_TYPE_2 = 0x9190; /** Table used in expanding pixels that use less than 8 bits. */ private static final byte[] EXPANSION_TABLE = new byte[256 * 8]; static { for (int i=0; i<256; i++) { for (int j=0; j<8; j++) { EXPANSION_TABLE[i*8 + j] = (byte) ((i & (int) Math.pow(2, 7 - j)) >> 7 - j); } } } // -- Fields -- /** Number of bytes in a row of pixel data (variable). */ protected int rowBytes; /** 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 byte[][] lookup; /** Helper reader in case this one fails. */ protected LegacyQTTools qtTools = new LegacyQTTools(); private boolean legacy = false; private Vector<Long> jpegOffsets = new Vector<Long>(); // -- Constructor -- /** Constructs a new PICT reader. */ public PictReader() { super("PICT", new String[] {"pict", "pct"}); domains = new String[] {FormatTools.GRAPHICS_DOMAIN}; } // -- PictReader API methods -- /** Control whether or not legacy reader (QT Java) is used. */ public void setLegacy(boolean legacy) { this.legacy = legacy; } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#get8BitLookupTable() */ @Override public byte[][] get8BitLookupTable() throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); return lookup; } /** * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int) */ @Override public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) throws FormatException, IOException { FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h); if (jpegOffsets.size() > 0) { ByteArrayHandle v = new ByteArrayHandle(); in.seek(jpegOffsets.get(0)); byte[] b = new byte[(int) (in.length() - in.getFilePointer())]; in.read(b); RandomAccessInputStream s = new RandomAccessInputStream(b); for (long jpegOffset : jpegOffsets) { s.seek(jpegOffset - jpegOffsets.get(0)); CodecOptions options = new CodecOptions(); options.interleaved = isInterleaved(); options.littleEndian = isLittleEndian(); v.write(new JPEGCodec().decompress(s, options)); } s = new RandomAccessInputStream(v); s.seek(0); readPlane(s, x, y, w, h, buf); s.close(); return buf; } if (legacy || strips.size() == 0) { in.seek(512); byte[] pix = new byte[(int) (in.length() - in.getFilePointer())]; in.read(pix); byte[][] b = AWTImageTools.getBytes( AWTImageTools.makeBuffered(qtTools.pictToImage(pix))); pix = null; for (int i=0; i<b.length; i++) { System.arraycopy(b[i], 0, buf, i*b[i].length, b[i].length); } b = null; return buf; } // combine everything in the strips Vector if ((getSizeY()*4 < strips.size()) && (((strips.size() / 3) % getSizeY()) != 0)) { core.get(0).sizeY = strips.size(); } int plane = w * h; if (lookup != null) { // 8 bit data byte[] row; for (int i=y; i<y+h; i++) { row = (byte[]) strips.get(i); int len = (int) Math.min(row.length, w); System.arraycopy(row, x, buf, (i - y) * w, len); } } else if (getSizeY()*3 == strips.size() || getSizeY()*4 == strips.size()) { // 24 or 32 bit data int nc = strips.size() / getSizeY(); byte[] c0 = null; byte[] c1 = null; byte[] c2 = null; for (int i=y; i<h + y; i++) { c0 = (byte[]) strips.get(i * nc + nc - 3); c1 = (byte[]) strips.get(i * nc + nc - 2); c2 = (byte[]) strips.get(i * nc + nc - 1); int baseOffset = (i - y) * w; System.arraycopy(c0, x, buf, baseOffset, w); System.arraycopy(c1, x, buf, plane + baseOffset, w); System.arraycopy(c2, x, buf, 2*plane + baseOffset, w); } } else { // RGB value is packed into a single short: xRRR RRGG GGGB BBBB int[] row = null; for (int i=y; i<h + y; i++) { row = (int[]) strips.get(i); for (int j=x; j<w + x; j++) { int base = (i - y) * w + (j - x); buf[base] = (byte) ((row[j] & 0x7c00) >> 10); buf[plane + base] = (byte) ((row[j] & 0x3e0) >> 5); buf[2 * plane + base] = (byte) (row[j] & 0x1f); } } } return buf; } /* @see loci.formats.IFormatReader#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (!fileOnly) { rowBytes = 0; strips = null; versionOne = false; lookup = null; legacy = false; jpegOffsets.clear(); } } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ @Override protected void initFile(String id) throws FormatException, IOException { super.initFile(id); in = new RandomAccessInputStream(id); CoreMetadata m = core.get(0); m.littleEndian = false; in.seek(518); m.sizeY = in.readShort(); m.sizeX = in.readShort(); m.sizeZ = 1; m.sizeC = 1; m.sizeT = 1; m.dimensionOrder = "XYCZT"; m.imageCount = 1; m.falseColor = false; m.metadataComplete = true; m.interleaved = false; m.pixelType = FormatTools.UINT8; strips = new Vector(); rowBytes = 0; lookup = null; int opcode; int verOpcode = in.read(); int verNumber = in.read(); if (verOpcode == 0x11 && verNumber == 0x01) versionOne = true; else if (verOpcode == 0x00 && verNumber == 0x11) { versionOne = false; int verNumber2 = in.readShort(); if (verNumber2 != 0x02ff) { throw new FormatException("Invalid PICT file : " + verNumber2); } // skip over v2 header -- don't need it here //in.skipBytes(26); in.skipBytes(6); int pixelsPerInchX = in.readInt(); int pixelsPerInchY = in.readInt(); in.skipBytes(4); int y = in.readShort(); int x = in.readShort(); if (y > 0) m.sizeY = y; if (x > 0) m.sizeX = x; in.skipBytes(4); } else throw new FormatException("Invalid PICT file"); addGlobalMeta("Version", versionOne ? 1 : 2); do { if (versionOne) opcode = in.read(); else { // if at odd boundary skip a byte for opcode in PICT v2 if ((in.getFilePointer() & 0x1L) != 0) { in.skipBytes(1); } if (in.getFilePointer() + 2 >= in.length()) { break; } opcode = in.readShort() & 0xffff; } } while (drivePictDecoder(opcode)); m.rgb = getSizeC() > 1; m.indexed = !isRGB() && lookup != null; // The metadata store we're working with. MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels(store, this); } // -- Helper methods -- /** Handles the opcodes in the PICT file. */ private boolean drivePictDecoder(int opcode) throws FormatException, IOException { LOGGER.debug("drivePictDecoder({}) @ {}", opcode, in.getFilePointer()); 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: rowBytes = in.readShort(); if (versionOne || (rowBytes & 0x8000) == 0) handleBitmap(opcode); else handlePixmap(opcode); break; case PICT_9A: handlePixmap(opcode); break; case PICT_CLIP_RGN: int x = in.readShort(); in.skipBytes(x - 2); break; case PICT_LONGCOMMENT: in.skipBytes(2); x = in.readShort(); in.skipBytes(x); break; case PICT_END: // end of PICT return false; case PICT_TYPE_1: case PICT_TYPE_2: x = in.read(); in.skipBytes(x); break; case PICT_JPEG: jpegOffsets.add(in.getFilePointer() + 2); core.get(0).sizeC = 3; core.get(0).rgb = true; while ((in.readShort() & 0xffff) != 0xffd9 && in.getFilePointer() < in.length()); while (in.getFilePointer() < in.length()) { while ((in.readShort() & 0xffff) != 0xffd8 && in.getFilePointer() < in.length()); if (in.getFilePointer() < in.length()) { jpegOffsets.add(in.getFilePointer() - 2); } } core.get(0).interleaved = true; break; default: if (opcode < 0) { //throw new FormatException("Invalid opcode: " + opcode); LOGGER.warn("Invalid opcode: {}", opcode); } } return in.getFilePointer() < in.length(); } private void readImageHeader(int opcode) throws IOException { if (opcode == PICT_9A) in.skipBytes(6); else rowBytes &= 0x3fff; int tlY = in.readShort(); int tlX = in.readShort(); int brY = in.readShort(); int brX = in.readShort(); if (brX - tlX > 0) core.get(0).sizeX = brX - tlX; if (brY - tlY > 0) core.get(0).sizeY = brY - tlY; in.skipBytes(18); } /** Extract the image data in a PICT bitmap structure. */ private void handleBitmap(int opcode) throws FormatException, IOException { readImageHeader(opcode); handlePixmap(1, 1); } /** Extracts the image data in a PICT pixmap structure. */ private void handlePixmap(int opcode) throws FormatException, IOException { readImageHeader(opcode); LOGGER.debug("handlePixmap({})", opcode); int pixelSize = in.readShort(); int compCount = in.readShort(); in.skipBytes(14); if (opcode == PICT_9A) { // rowBytes doesn't exist, so set it to its logical value switch (pixelSize) { case 32: rowBytes = getSizeX() * compCount; break; case 16: rowBytes = getSizeX() * 2; break; default: throw new FormatException("Sorry, vector data not supported."); } } else { // read the lookup table in.skipBytes(4); int flags = in.readShort(); int count = in.readShort(); count++; lookup = new byte[3][count]; for (int i=0; i<count; i++) { in.skipBytes(2); lookup[0][i] = in.readByte(); in.skipBytes(1); lookup[1][i] = in.readByte(); in.skipBytes(1); lookup[2][i] = in.readByte(); in.skipBytes(1); } } // skip over two rectangles in.skipBytes(18); if (opcode == PICT_BITSRGN || opcode == PICT_PACKBITSRGN) in.skipBytes(2); handlePixmap(pixelSize, compCount); } /** Handles the unpacking of the image data. */ private void handlePixmap(int pixelSize, int compCount) throws FormatException, IOException { LOGGER.debug("handlePixmap({}, {}, {})", new Object[] {rowBytes, 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 = rowBytes; int outBufSize = getSizeX(); byte[] outBuf = null; // used to expand pixel data boolean compressed = (rowBytes >= 8) || (pixelSize == 32); // allocate buffers switch (pixelSize) { case 32: if (!compressed) uBufI = new int[getSizeX()]; else uBuf = new byte[bufSize]; break; case 16: uBufI = new int[getSizeX()]; break; case 8: uBuf = new byte[bufSize]; break; default: outBuf = new byte[outBufSize]; uBuf = new byte[bufSize]; break; } if (!compressed) { LOGGER.debug("Pixel data is uncompressed (pixelSize={}).", pixelSize); buf = new byte[bufSize]; for (int row=0; row<getSizeY(); row++) { in.read(buf, 0, rowBytes); switch (pixelSize) { case 16: for (int i=0; i<getSizeX(); i++) { uBufI[i] = DataTools.bytesToShort(buf, i*2, 2, false); } strips.add(uBufI); buf = null; core.get(0).sizeC = 3; break; case 8: strips.add(buf); break; default: // pixel size < 8 expandPixels(pixelSize, buf, outBuf, outBuf.length); strips.add(outBuf); buf = null; } } } else { LOGGER.debug("Pixel data is compressed (pixelSize={}; compCount={}).", pixelSize, compCount); buf = new byte[bufSize + 1 + bufSize / 128]; for (int row=0; row<getSizeY(); row++) { if (rowBytes > 250) rawLen = in.readShort(); else rawLen = in.read(); if (rawLen > buf.length) rawLen = buf.length; if ((in.length() - in.getFilePointer()) <= rawLen) { rawLen = (int) (in.length() - in.getFilePointer() - 1); } if (rawLen < 0) { rawLen = 0; in.seek(in.length() - 1); } in.read(buf, 0, rawLen); if (pixelSize == 16) { uBufI = new int[getSizeX()]; unpackBits(buf, uBufI); strips.add(uBufI); core.get(0).sizeC = 3; } else { PackbitsCodec c = new PackbitsCodec(); CodecOptions options = new CodecOptions(); options.maxBytes = getSizeX() * 4; uBuf = c.decompress(buf, options); } 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; for (int q=0; q<compCount; q++) { int offset = q * getSizeX(); int len = (int) Math.min(getSizeX(), uBuf.length - offset); newBuf = new byte[getSizeX()]; if (offset < uBuf.length) { System.arraycopy(uBuf, offset, newBuf, 0, len); } strips.add(newBuf); } core.get(0).sizeC = 3; } } } } /** Expand an array of bytes. */ private void expandPixels(int bitSize, byte[] ib, byte[] ob, int outLen) throws FormatException { LOGGER.debug("expandPixels({}, {}, {}, {})", new Object[] {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; } 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 tmask; // temp mask if (bitSize != 1 && bitSize != 2 && bitSize != 4) { throw new FormatException("Can only expand 1, 2, and 4 bit values"); } int mask = ((int) Math.pow(2, bitSize) - 1) << (8 - bitSize); int i = 0; for (int o = 0; o < ob.length; i++) { tmask = mask; tpixelshift = pixelshift; v = ib[i]; for (int t = 0; t < count && o < ob.length; t++, o++) { ob[o] = (byte) (((v & tmask) >>> tpixelshift) & 0xff); tmask = (byte) ((tmask & 0xff) >>> maskshift); tpixelshift -= pixelshiftdelta; } } } /** PackBits variant that outputs an int array. */ private void unpackBits(byte[] ib, int[] ob) { LOGGER.debug("unpackBits(...)"); int i = 0; int b; int rep; int end; for (int o=0; o<ob.length;) { if (i+1 < ib.length) { b = ib[i++]; if (b >= 0) { end = o + b + 1; while (o < end && o < ob.length && (i + 1) < ib.length) { ob[o++] = DataTools.bytesToShort(ib, i, 2, false); i += 2; } } else if (b != -128) { rep = DataTools.bytesToShort(ib, i, 2, false); i += 2; end = o - b + 1; while (o < end && o < ob.length) { ob[o++] = rep; } } } else o = ob.length; } } }