/*
* #%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.EOFException;
import java.io.InputStream;
import java.io.IOException;
import java.util.Vector;
import java.util.zip.InflaterInputStream;
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.UnsupportedCompressionException;
import loci.formats.meta.MetadataStore;
/**
* APNGReader is the file format reader for
* Animated Portable Network Graphics (APNG) images.
*
* @author Melissa Linkert melissa at glencoesoftware.com
*/
public class APNGReader extends FormatReader {
// -- Constants --
/** Color types. */
private static final int GRAYSCALE = 0;
private static final int TRUE_COLOR = 2;
private static final int INDEXED = 3;
private static final int GRAY_ALPHA = 4;
private static final int TRUE_ALPHA = 6;
/** Filter types. */
private static final int NONE = 0;
private static final int SUB = 1;
private static final int UP = 2;
private static final int AVERAGE = 3;
private static final int PAETH = 4;
/** Interlacing pass dimensions. */
private static final int[] PASS_WIDTHS = {1, 1, 2, 2, 4, 4, 8};
private static final int[] PASS_HEIGHTS = {1, 1, 1, 2, 2, 4, 4};
// -- Fields --
private Vector<PNGBlock> blocks;
private Vector<int[]> frameCoordinates;
private byte[][] lut;
private byte[] lastImage;
private int lastImageIndex = -1;
private int lastImageRow = -1;
private int compression;
private int interlace;
private int idatCount = 0;
// -- Constructor --
/** Constructs a new APNGReader. */
public APNGReader() {
super("Animated PNG", "png");
domains = new String[] {FormatTools.GRAPHICS_DOMAIN};
suffixNecessary = false;
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
@Override
public boolean isThisType(RandomAccessInputStream stream) throws IOException {
final int blockLen = 8;
if (!FormatTools.validStream(stream, blockLen, false)) return false;
byte[] signature = new byte[blockLen];
stream.read(signature);
if (signature[0] != (byte) 0x89 || signature[1] != 0x50 ||
signature[2] != 0x4e || signature[3] != 0x47 || signature[4] != 0x0d ||
signature[5] != 0x0a || signature[6] != 0x1a || signature[7] != 0x0a)
{
return false;
}
return true;
}
/* @see loci.formats.IFormatReader#get8BitLookupTable() */
@Override
public byte[][] get8BitLookupTable() {
FormatTools.assertId(currentId, true, 1);
return lut;
}
/**
* @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 (no == lastImageIndex && lastImage != null && y + h <= lastImageRow) {
RandomAccessInputStream s = new RandomAccessInputStream(lastImage);
readPlane(s, x, y, w, h, buf);
s.close();
s = null;
return buf;
}
if (no == 0) {
lastImage = null;
PNGInputStream stream = new PNGInputStream("IDAT");
int decodeHeight = y + h;
if (decodeHeight < getSizeY() && decodeHeight % 8 != 0) {
decodeHeight += 8 - (decodeHeight % 8);
}
lastImage = decode(stream, getSizeX(), decodeHeight);
stream.close();
lastImageIndex = 0;
lastImageRow = y + h;
RandomAccessInputStream pix = new RandomAccessInputStream(lastImage);
readPlane(pix, x, y, w, h, buf);
pix.close();
pix = null;
if (y + h < getSizeY()) {
lastImage = null;
}
return buf;
}
int[] coords = frameCoordinates.get(no);
lastImage = openBytes(0);
lastImageRow = getSizeY();
PNGInputStream stream = new PNGInputStream("fdAT", no);
byte[] newImage = decode(stream, coords[2], coords[3]);
stream.close();
// paste current image onto first image
int bpp = FormatTools.getBytesPerPixel(getPixelType());
int len = coords[2] * bpp;
int plane = getSizeX() * getSizeY() * bpp;
int newPlane = len * coords[3];
if (!isInterleaved()) {
for (int c=0; c<getRGBChannelCount(); c++) {
for (int row=0; row<coords[3]; row++) {
System.arraycopy(newImage, c * newPlane + row * len, lastImage,
c * plane + (coords[1] + row) * getSizeX() * bpp + coords[0] * bpp,
len);
}
}
}
else {
len *= getRGBChannelCount();
for (int row=0; row<coords[3]; row++) {
System.arraycopy(newImage, row * len, lastImage,
(coords[1] + row) * getSizeX() * bpp * getRGBChannelCount() +
coords[0] * bpp * getRGBChannelCount(), len);
}
}
lastImageIndex = no;
RandomAccessInputStream pix = new RandomAccessInputStream(lastImage);
readPlane(pix, x, y, w, h, buf);
pix.close();
return buf;
}
/* @see loci.formats.IFormatReader#close(boolean) */
@Override
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
if (!fileOnly) {
lut = null;
frameCoordinates = null;
blocks = null;
lastImage = null;
lastImageIndex = -1;
lastImageRow = -1;
compression = 0;
interlace = 0;
idatCount = 0;
}
}
// -- Internal FormatReader 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);
// check that this is a valid PNG file
byte[] signature = new byte[8];
in.read(signature);
if (signature[0] != (byte) 0x89 || signature[1] != 0x50 ||
signature[2] != 0x4e || signature[3] != 0x47 || signature[4] != 0x0d ||
signature[5] != 0x0a || signature[6] != 0x1a || signature[7] != 0x0a)
{
throw new FormatException("Invalid PNG signature.");
}
// read data chunks - each chunk consists of the following:
// 1) 32 bit length
// 2) 4 char type
// 3) 'length' bytes of data
// 4) 32 bit CRC
blocks = new Vector<PNGBlock>();
frameCoordinates = new Vector<int[]>();
while (in.getFilePointer() < in.length()) {
int length = in.readInt();
String type = in.readString(4);
PNGBlock block = new PNGBlock();
block.length = length;
block.type = type;
block.offset = in.getFilePointer();
blocks.add(block);
if (type.equals("acTL")) {
// APNG-specific chunk
m.imageCount = in.readInt();
int loop = in.readInt();
addGlobalMeta("Loop count", loop);
}
else if (type.equals("fcTL")) {
in.skipBytes(4);
int w = in.readInt();
int h = in.readInt();
int x = in.readInt();
int y = in.readInt();
frameCoordinates.add(new int[] {x, y, w, h});
in.skipBytes(length - 20);
}
else if (type.equals("IDAT")) {
idatCount++;
}
else if (type.equals("PLTE")) {
// lookup table
m.indexed = true;
lut = new byte[3][256];
for (int i=0; i<length/3; i++) {
for (int c=0; c<3; c++) {
lut[c][i] = in.readByte();
}
}
}
else if (type.equals("IHDR")) {
m.sizeX = in.readInt();
m.sizeY = in.readInt();
m.bitsPerPixel = in.read();
int colorType = in.read();
compression = in.read();
int filter = in.read();
interlace = in.read();
if (filter != 0) {
throw new FormatException("Invalid filter mode: " + filter);
}
switch (colorType) {
case GRAYSCALE:
case INDEXED:
m.sizeC = 1;
break;
case GRAY_ALPHA:
m.sizeC = 2;
break;
case TRUE_COLOR:
m.sizeC = 3;
break;
case TRUE_ALPHA:
m.sizeC = 4;
break;
}
m.pixelType = getBitsPerPixel() <= 8 ?
FormatTools.UINT8 : FormatTools.UINT16;
m.rgb = getSizeC() > 1;
}
else if (type.equals("IEND")) {
break;
}
in.seek(block.offset + length);
if (in.getFilePointer() < in.length() - 4) {
in.skipBytes(4); // skip the CRC
}
}
if (m.imageCount == 0) m.imageCount = 1;
m.sizeZ = 1;
m.sizeT = getImageCount();
m.dimensionOrder = "XYCTZ";
m.interleaved = isRGB();
m.falseColor = false;
MetadataStore store = makeFilterMetadata();
MetadataTools.populatePixels(store, this);
}
private byte[] decode(PNGInputStream bytes) throws FormatException, IOException {
return decode(bytes, getSizeX(), getSizeY());
}
private byte[] decode(PNGInputStream bytes, int width, int height)
throws FormatException, IOException
{
int bpp = FormatTools.getBytesPerPixel(getPixelType());
int rowLen = width * getRGBChannelCount() * bpp;
if (getBitsPerPixel() < bpp * 8) {
int div = (bpp * 8) / getBitsPerPixel();
if (div < rowLen) {
int originalRowLen = rowLen;
rowLen /= div;
if (rowLen * div < originalRowLen) {
rowLen++;
}
}
else {
rowLen = 1;
}
}
byte[] image = null;
if (compression == 0 && interlace == 0) {
// decompress the image
byte[] filters = new byte[height];
image = new byte[rowLen * height];
InflaterInputStream decompressor = new InflaterInputStream(bytes);
try {
int n = 0;
for (int row=0; row<height; row++) {
n = 0;
while (n < 1) {
n = decompressor.read(filters, row, 1);
}
n = 0;
while (n < rowLen) {
n += decompressor.read(image, row * rowLen + n, rowLen - n);
}
}
}
finally {
decompressor.close();
decompressor = null;
}
// perform any necessary unfiltering
unfilter(filters, image, width, height);
}
else if (compression == 0) {
// see: http://www.w3.org/TR/PNG/#8Interlace
int byteCount = 0;
byte[][] passImages = new byte[7][];
int nRowBlocks = getSizeY() / 8;
int nColBlocks = getSizeX() / 8;
// row and column counts have to be adjusted when the image
// dimensions are not multiples of 8
if (8 * nRowBlocks != getSizeY()) {
nRowBlocks++;
}
if (8 * nColBlocks != getSizeX()) {
nColBlocks++;
}
if (nRowBlocks <= 0) {
nRowBlocks = 1;
}
if (nColBlocks <= 0) {
nColBlocks = 1;
}
image = new byte[FormatTools.getPlaneSize(this)];
InflaterInputStream decompressor = new InflaterInputStream(bytes);
try {
for (int i=0; i<passImages.length; i++) {
int passWidth = PASS_WIDTHS[i] * nColBlocks;
int passHeight = PASS_HEIGHTS[i] * nRowBlocks;
// see http://www.libpng.org/pub/png/spec/1.2/PNG-DataRep.html#DR.Interlaced-data-order
// for clarification of the pass width / pass row adjustments
if (nColBlocks * 8 != width) {
int extraCols = getSizeX() - (nColBlocks - 1) * 8;
switch (extraCols) {
case 1:
if (i == 1 || i == 3 || i == 5) {
passWidth -= PASS_WIDTHS[i];
}
if (i == 2 || i == 4 || i == 6) {
passWidth -= (PASS_WIDTHS[i] - 1);
}
break;
case 2:
if (i == 1 || i == 3) {
passWidth -= PASS_WIDTHS[i];
}
if (i == 2 || i == 4 || i == 5) {
passWidth -= (PASS_WIDTHS[i] - 1);
}
if (i == 6) {
passWidth -= (PASS_WIDTHS[i] - 2);
}
break;
case 3:
if (i == 1) {
passWidth -= PASS_WIDTHS[i];
}
if (i == 2 || i == 3 || i == 5) {
passWidth -= (PASS_WIDTHS[i] - 1);
}
if (i == 4) {
passWidth -= (PASS_WIDTHS[i] - 2);
}
if (i == 6) {
passWidth -= (PASS_WIDTHS[i] - 3);
}
break;
case 4:
if (i == 1) {
passWidth -= PASS_WIDTHS[i];
}
if (i == 2 || i == 3) {
passWidth -= (PASS_WIDTHS[i] - 1);
}
if (i == 4 || i == 5) {
passWidth -= (PASS_WIDTHS[i] - 2);
}
if (i == 6) {
passWidth -= (PASS_WIDTHS[i] - 4);
}
break;
case 5:
if (i == 3) {
passWidth -= (PASS_WIDTHS[i] - 1);
}
if (i == 5) {
passWidth -= (PASS_WIDTHS[i] - 2);
}
if (i == 4) {
passWidth -= (PASS_WIDTHS[i] - 3);
}
if (i == 6) {
passWidth -= (PASS_WIDTHS[i] - 5);
}
break;
case 6:
if (i == 3) {
passWidth -= (PASS_WIDTHS[i] - 1);
}
if (i == 4 || i == 5) {
passWidth -= (PASS_WIDTHS[i] - 3);
}
if (i == 6) {
passWidth -= (PASS_WIDTHS[i] - 6);
}
break;
case 7:
if (i == 5 || i == 6) {
passWidth--;
}
break;
}
}
int rowSize = passWidth * bpp * getRGBChannelCount();
byte[] filters = new byte[passHeight];
passImages[i] = new byte[rowSize * passHeight];
for (int row=0; row<passHeight; row++) {
if (passWidth == 0) {
continue;
}
if (nRowBlocks * 8 != getSizeY() && row >= PASS_HEIGHTS[i] * (nRowBlocks - 1)) {
int extraRows = getSizeY() - (nRowBlocks - 1) * 8;
switch (extraRows) {
case 1:
if (i == 2 || i == 4 || i == 6) {
continue;
}
if ((i == 3 || i == 5) && (row % PASS_HEIGHTS[i]) > 0) {
continue;
}
break;
case 2:
if (i == 4 || i == 2) {
continue;
}
if ((i == 3 || i == 5 || i == 6) && (row % PASS_HEIGHTS[i]) > 0) {
continue;
}
break;
case 3:
if (i == 2) {
continue;
}
if ((i == 3 || i == 4 || i == 6) && (row % PASS_HEIGHTS[i]) > 0) {
continue;
}
if (i == 5 && (row % PASS_HEIGHTS[i] > 1)) {
continue;
}
break;
case 4:
if (i == 2) {
continue;
}
if ((i == 3 || i == 4) && (row % PASS_HEIGHTS[i]) > 0) {
continue;
}
if ((i == 5 || i == 6) && (row % PASS_HEIGHTS[i]) > 1) {
continue;
}
break;
case 5:
if ((i == 4) && (row % PASS_HEIGHTS[i]) > 0) {
continue;
}
if ((i == 6) && (row % PASS_HEIGHTS[i]) > 1) {
continue;
}
if ((i == 5) && (row % PASS_HEIGHTS[i]) > 2) {
continue;
}
break;
case 6:
if ((i == 4) && (row % PASS_HEIGHTS[i]) > 0) {
continue;
}
if ((i == 5 || i == 6) && (row % PASS_HEIGHTS[i]) > 2) {
continue;
}
break;
case 7:
if (i == 6 && (row % PASS_HEIGHTS[i]) > 2) {
continue;
}
break;
}
}
int n = 0;
while (n < 1) {
n = decompressor.read(filters, row, 1);
}
n = 0;
while (n < rowSize) {
n += decompressor.read(passImages[i],
row * rowSize + n, rowSize - n);
}
}
unfilter(filters, passImages[i], passWidth, passHeight);
}
}
finally {
decompressor.close();
decompressor = null;
}
int chunk = bpp * getRGBChannelCount();
int[] passOffset = new int[7];
for (int row=0; row<8 * (height / 8); row++) {
int rowOffset = row * width * chunk;
for (int col=0; col<width; col++) {
int blockRow = row % 8;
int blockCol = col % 8;
int pass = -1;
if ((blockRow % 2) == 1) {
pass = 6;
}
else if (blockRow == 0 || blockRow == 4) {
if ((blockCol % 2) == 1) {
pass = 5;
}
else if (blockCol == 0) {
pass = blockRow == 0 ? 0 : 2;
}
else if (blockCol == 4) {
pass = blockRow == 0 ? 1 : 2;
}
else {
pass = 3;
}
}
else {
pass = 4 + (blockCol % 2);
}
int colOffset = col * chunk;
for (int c=0; c<chunk; c++) {
if (passOffset[pass] < passImages[pass].length) {
image[rowOffset + colOffset + c] =
passImages[pass][passOffset[pass]++];
}
}
}
}
}
else {
throw new UnsupportedCompressionException(
"Compression type " + compression + " not supported");
}
// de-interleave
if (getBitsPerPixel() < 8) {
byte[] expandedImage = new byte[FormatTools.getPlaneSize(this)];
RandomAccessInputStream bits = new RandomAccessInputStream(image);
int skipBits = rowLen * 8 - getSizeX() * getBitsPerPixel();
for (int row=0; row<getSizeY(); row++) {
for (int col=0; col<getSizeX(); col++) {
int index = row * getSizeX() + col;
expandedImage[index] =
(byte) (bits.readBits(getBitsPerPixel()) & 0xff);
}
bits.skipBits(skipBits);
}
bits.close();
bits = null;
image = expandedImage;
}
return image;
}
/** See http://www.w3.org/TR/PNG/#9Filters. */
private void unfilter(byte[] filters, byte[] image, int width, int height)
throws FormatException
{
int bpp =
getRGBChannelCount() * FormatTools.getBytesPerPixel(getPixelType());
int rowLen = width * bpp;
for (int row=0; row<height; row++) {
int filter = filters[row];
if (filter == NONE) {
continue;
}
for (int col=0; col<rowLen; col++) {
int q = row * rowLen + col;
int xx = image[q] & 0xff;
int a = col >= bpp ? image[q - bpp] & 0xff : 0;
int b = row > 0 ? image[q - bpp * width] & 0xff : 0;
int c = row > 0 && col >= bpp ?
image[q - bpp * (width + 1)] & 0xff : 0;
switch (filter) {
case SUB:
image[q] = (byte) ((xx + a) & 0xff);
break;
case UP:
image[q] = (byte) ((xx + b) & 0xff);
break;
case AVERAGE:
image[q] = (byte) ((xx + ((int) Math.floor(a + b) / 2)) & 0xff);
break;
case PAETH:
int p = a + b - c;
int pa = (int) Math.abs(p - a);
int pb = (int) Math.abs(p - b);
int pc = (int) Math.abs(p - c);
if (pa <= pb && pa <= pc) {
image[q] = (byte) ((xx + a) & 0xff);
}
else if (pb <= pc) {
image[q] = (byte) ((xx + b) & 0xff);
}
else {
image[q] = (byte) ((xx + c) & 0xff);
}
break;
default:
throw new FormatException("Unknown filter: " + filter);
}
}
}
}
// -- Helper class --
class PNGBlock {
public long offset;
public int length;
public String type;
}
/**
* InputStream implementation that stitches together IDAT blocks into
* a seamless zlib stream. This allows us to decompress the image data
* without reading the entire compressed stream into a byte array.
*/
class PNGInputStream extends InputStream {
private int currentBlock = -1;
private int blockPointer = 0;
private int blockLength = 0;
private String blockType;
private int imageNumber;
private int fctlCount = 0;
private boolean fdatValid = false;
public PNGInputStream(String blockType) throws IOException {
this(blockType, 0);
}
public PNGInputStream(String blockType, int imageNumber) throws IOException {
this.imageNumber = imageNumber;
this.blockType = blockType;
fctlCount = 0;
fdatValid = false;
advanceBlock();
}
@Override
public int available() throws IOException {
if (blockPointer == blockLength) {
advanceBlock();
}
if (currentBlock < 0 || in.getFilePointer() == in.length()) {
return -1;
}
return (int) Math.min(blockLength - blockPointer, in.length() - in.getFilePointer());
}
@Override
public int read() throws IOException {
if (blockPointer < blockLength) {
blockPointer++;
return in.read();
}
advanceBlock();
if (currentBlock < 0) {
throw new EOFException();
}
blockPointer++;
return in.read();
}
public byte readByte() throws IOException {
if (blockPointer < blockLength) {
blockPointer++;
return in.readByte();
}
advanceBlock();
if (currentBlock < 0) {
throw new EOFException();
}
blockPointer++;
return in.readByte();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = 0;
for (int i=0; i<len; i++) {
if (available() > 0) {
b[off + i] = readByte();
read++;
}
else {
break;
}
}
return read;
}
private void advanceBlock() throws IOException {
if (currentBlock < blocks.size() - 1) {
while (currentBlock < blocks.size()) {
currentBlock++;
if (currentBlock == blocks.size()) {
currentBlock = -1;
break;
}
if (blockType.equals("fdAT") && blocks.get(currentBlock).type.equals("fcTL")) {
fdatValid = fctlCount == imageNumber;
fctlCount++;
if (fctlCount > imageNumber + 1) {
currentBlock = -1;
break;
}
}
else if (blockType.equals(blocks.get(currentBlock).type)) {
if (fdatValid || !blockType.equals("fdAT")) {
break;
}
}
}
if (currentBlock >= 0) {
blockPointer = 0;
blockLength = blocks.get(currentBlock).length;
in.seek(blocks.get(currentBlock).offset);
if (blocks.get(currentBlock).type.equals("fdAT")) {
blockLength -= 4;
in.skipBytes(4);
}
}
}
else {
currentBlock = -1;
blockPointer = 0;
blockLength = 0;
}
}
}
}