package org.jcodec.codecs.png; import static org.jcodec.common.tools.MathUtil.abs; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.zip.DataFormatException; import java.util.zip.Inflater; import org.jcodec.common.VideoCodecMeta; import org.jcodec.common.VideoDecoder; import org.jcodec.common.io.NIOUtils; import org.jcodec.common.logging.Logger; import org.jcodec.common.model.ColorSpace; import org.jcodec.common.model.Picture8Bit; import org.jcodec.common.model.Size; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * PNG image decoder. * * Supports: RGB, palette, grey, alpha, interlace, transparency. * * @author Stanislav Vitvitskyy * */ public class PNGDecoder extends VideoDecoder { private static final long PNGSIG = 0x89504e470d0a1a0aL; private static final long MNGSIG = 0x8a4d4e470d0a1a0aL; private static final int TAG_IHDR = 0x49484452; private static final int TAG_IDAT = 0x49444154; private static final int TAG_PLTE = 0x504c5445; private static final int TAG_tRNS = 0x74524e53; private static final int TAG_IEND = 0x49454e44; private static final int FILTER_TYPE_LOCO = 64; private static final int FILTER_VALUE_NONE = 0; private static final int FILTER_VALUE_SUB = 1; private static final int FILTER_VALUE_UP = 2; private static final int FILTER_VALUE_AVG = 3; private static final int FILTER_VALUE_PAETH = 4; private static final int PNG_COLOR_MASK_PALETTE = 1; private static final int PNG_COLOR_MASK_COLOR = 2; private static final int PNG_COLOR_MASK_ALPHA = 4; private static final int PNG_COLOR_TYPE_GRAY = 0; private static final int PNG_COLOR_TYPE_PALETTE = (PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_PALETTE); private static final int PNG_COLOR_TYPE_RGB = (PNG_COLOR_MASK_COLOR); private static final int alphaR = 0x7f; private static final int alphaG = 0x7f; private static final int alphaB = 0x7f; public static final int[] logPassStep = { 3, 3, 2, 2, 1, 1, 0 }; public static final int[] logPassRowStep = { 3, 3, 3, 2, 2, 1, 1 }; public static final int[] passOff = { 0, 4, 0, 2, 0, 1, 0 }; public static final int[] passRowOff = { 0, 0, 4, 0, 2, 0, 1 }; private byte[] ca = new byte[4]; @Override public Picture8Bit decodeFrame8Bit(ByteBuffer data, byte[][] buffer) { long sig = data.getLong(); if (sig != PNGSIG && sig != MNGSIG) throw new RuntimeException("Not a PNG file."); IHDR ihdr = null; PLTE plte = null; TRNS trns = null; List<ByteBuffer> list = new ArrayList<ByteBuffer>(); while (data.remaining() >= 8) { int length = data.getInt(); int tag = data.getInt(); if (data.remaining() < length) break; switch (tag) { case TAG_IHDR: ihdr = new IHDR(); ihdr.parse(data); break; case TAG_PLTE: plte = new PLTE(); plte.parse(data, length); break; case TAG_tRNS: trns = new TRNS(ihdr.colorType); trns.parse(data, length); break; case TAG_IDAT: list.add(NIOUtils.read(data, length)); NIOUtils.skip(data, 4); // CRC break; case TAG_IEND: NIOUtils.skip(data, 4); // CRC break; default: data.position(data.position() + length + 4); } } try { decodeData(ihdr, plte, trns, list, buffer); } catch (DataFormatException e) { return null; } return Picture8Bit.createPicture8Bit(ihdr.width, ihdr.height, buffer, ihdr.colorSpace()); } private void decodeData(IHDR ihdr, PLTE plte, TRNS trns, List<ByteBuffer> list, byte[][] buffer) throws DataFormatException { int bpp = (ihdr.getBitsPerPixel() + 7) >> 3; int passes = ihdr.interlaceType == 0 ? 1 : 7; Inflater inflater = new Inflater(); Iterator<ByteBuffer> it = list.iterator(); for (int pass = 0; pass < passes; pass++) { int rowSize, rowStart, rowStep, colStart, colStep; if (ihdr.interlaceType == 0) { rowSize = ihdr.rowSize() + 1; colStart = rowStart = 0; colStep = rowStep = 1; } else { int round = (1 << logPassStep[pass]) - 1; rowSize = ((ihdr.width + round) >> logPassStep[pass]) + 1; rowStart = passRowOff[pass]; rowStep = 1 << logPassRowStep[pass]; colStart = passOff[pass]; colStep = 1 << logPassStep[pass]; } byte[] lastRow = new byte[rowSize - 1]; byte[] uncompressed = new byte[rowSize]; int bptr = 3 * (ihdr.width * rowStart + colStart); for (int row = rowStart; row < ihdr.height; row += rowStep) { int count = inflater.inflate(uncompressed); if (count < uncompressed.length && inflater.needsInput()) { if (!it.hasNext()) { Logger.warn(String.format("Data truncation at row %d", row)); break; } ByteBuffer next = it.next(); inflater.setInput(NIOUtils.toArray(next)); int toRead = uncompressed.length - count; count = inflater.inflate(uncompressed, count, toRead); if (count != toRead) { Logger.warn(String.format("Data truncation at row %d", row)); break; } } int filter = uncompressed[0]; switch (filter) { case FILTER_VALUE_NONE: for (int i = 0; i < rowSize - 1; i++) { lastRow[i] = uncompressed[i + 1]; } break; case FILTER_VALUE_SUB: filterSub(uncompressed, rowSize - 1, lastRow, bpp); break; case FILTER_VALUE_UP: filterUp(uncompressed, rowSize - 1, lastRow); break; case FILTER_VALUE_AVG: filterAvg(uncompressed, rowSize - 1, lastRow, bpp); break; case FILTER_VALUE_PAETH: filterPaeth(uncompressed, rowSize - 1, lastRow, bpp); break; } int bptrWas = bptr; if ((ihdr.colorType & PNG_COLOR_MASK_PALETTE) != 0) { for (int i = 0; i < rowSize - 1; i += bpp, bptr += 3 * colStep) { int plt = plte.palette[lastRow[i] & 0xff]; buffer[0][bptr] = (byte) (((plt >> 16) & 0xff) - 128); buffer[0][bptr + 1] = (byte) (((plt >> 8) & 0xff) - 128); buffer[0][bptr + 2] = (byte) ((plt & 0xff) - 128); } } else if ((ihdr.colorType & PNG_COLOR_MASK_COLOR) != 0) { for (int i = 0; i < rowSize - 1; i += bpp, bptr += 3 * colStep) { buffer[0][bptr] = (byte) ((lastRow[i] & 0xff) - 128); buffer[0][bptr + 1] = (byte) ((lastRow[i + 1] & 0xff) - 128); buffer[0][bptr + 2] = (byte) ((lastRow[i + 2] & 0xff) - 128); } } else { for (int i = 0; i < rowSize - 1; i += bpp, bptr += 3 * colStep) { buffer[0][bptr] = buffer[0][bptr + 1] = buffer[0][bptr + 2] = (byte) ((lastRow[i] & 0xff) - 128); } } if ((ihdr.colorType & PNG_COLOR_MASK_ALPHA) != 0) { for (int i = bpp - 1, j = bptrWas; i < rowSize - 1; i += bpp, j += 3 * colStep) { int alpha = lastRow[i] & 0xff, nalpha = 256 - alpha; buffer[0][j] = (byte) ((alphaR * nalpha + buffer[0][j] * alpha) >> 8); buffer[0][j + 1] = (byte) ((alphaG * nalpha + buffer[0][j + 1] * alpha) >> 8); buffer[0][j + 2] = (byte) ((alphaB * nalpha + buffer[0][j + 2] * alpha) >> 8); } } else if (trns != null) { if (ihdr.colorType == PNG_COLOR_TYPE_PALETTE) { for (int i = 0, j = bptrWas; i < rowSize - 1; i++, j += 3 * colStep) { int alpha = trns.alphaPal[lastRow[i] & 0xff] & 0xff, nalpha = 256 - alpha; buffer[0][j] = (byte) ((alphaR * nalpha + buffer[0][j] * alpha) >> 8); buffer[0][j + 1] = (byte) ((alphaG * nalpha + buffer[0][j + 1] * alpha) >> 8); buffer[0][j + 2] = (byte) ((alphaB * nalpha + buffer[0][j + 2] * alpha) >> 8); } } else if (ihdr.colorType == PNG_COLOR_TYPE_RGB) { int ar = (trns.alphaR & 0xff) - 128; int ag = (trns.alphaG & 0xff) - 128; int ab = (trns.alphaB & 0xff) - 128; if (ab != alphaB || ag != alphaG || ar != alphaR) { for (int i = 0, j = bptrWas; i < rowSize - 1; i += bpp, j += 3 * colStep) { if (buffer[0][j] == ar && buffer[0][j + 1] == ag && buffer[0][j + 2] == ab) { buffer[0][j] = alphaR; buffer[0][j + 1] = alphaG; buffer[0][j + 2] = alphaB; } } } } else if (ihdr.colorType == PNG_COLOR_TYPE_GRAY) { for (int i = 0, j = bptrWas; i < rowSize - 1; i++, j += 3 * colStep) { if (lastRow[i] == trns.alphaGrey) { buffer[0][j] = alphaR; buffer[0][j + 1] = alphaG; buffer[0][j + 2] = alphaB; } } } } bptr = bptrWas + (3 * ihdr.width * rowStep); } } } private void filterPaeth(byte[] uncompressed, int rowSize, byte[] lastRow, int bpp) { for (int i = 0; i < bpp; i++) { ca[i] = lastRow[i]; lastRow[i] = (byte) ((uncompressed[i + 1] & 0xff) + (lastRow[i] & 0xff)); } for (int i = bpp; i < rowSize; i++) { int a = lastRow[i - bpp] & 0xff; int b = lastRow[i] & 0xff; int c = ca[i % bpp] & 0xff; int p = b - c; int pc = a - c; int pa = abs(p); int pb = abs(pc); pc = abs(p + pc); if (pa <= pb && pa <= pc) p = a; else if (pb <= pc) p = b; else p = c; ca[i % bpp] = lastRow[i]; lastRow[i] = (byte) (p + (uncompressed[i + 1] & 0xff)); } } private void filterSub(byte[] uncompressed, int rowSize, byte[] lastRow, int bpp) { switch (bpp) { case 1: filterSub1(uncompressed, lastRow, rowSize); break; case 2: filterSub2(uncompressed, lastRow, rowSize); break; case 3: filterSub3(uncompressed, lastRow, rowSize); break; default: filterSub4(uncompressed, lastRow, rowSize); } } private void filterAvg(byte[] uncompressed, int rowSize, byte[] lastRow, int bpp) { switch (bpp) { case 1: filterAvg1(uncompressed, lastRow, rowSize); break; case 2: filterAvg2(uncompressed, lastRow, rowSize); break; case 3: filterAvg3(uncompressed, lastRow, rowSize); break; default: filterAvg4(uncompressed, lastRow, rowSize); } } private void filterSub1(byte[] uncompressed, byte[] lastRow, int rowSize) { byte p = lastRow[0] = uncompressed[1]; for (int i = 1; i < rowSize; i++) { p = lastRow[i] = (byte) ((p & 0xff) + (uncompressed[i + 1] & 0xff)); } } private void filterUp(byte[] uncompressed, int rowSize, byte[] lastRow) { for (int i = 0; i < rowSize; i++) { lastRow[i] = (byte) ((lastRow[i] & 0xff) + (uncompressed[i + 1] & 0xff)); } } private void filterAvg1(byte[] uncompressed, byte[] lastRow, int rowSize) { byte p = lastRow[0] = (byte) ((uncompressed[1] & 0xff) + ((lastRow[0] & 0xff) >> 1)); for (int i = 1; i < rowSize; i++) { p = lastRow[i] = (byte) ((((lastRow[i] & 0xff) + (p & 0xff)) >> 1) + (uncompressed[i + 1] & 0xff)); } } private void filterSub2(byte[] uncompressed, byte[] lastRow, int rowSize) { byte p0 = lastRow[0] = uncompressed[1]; byte p1 = lastRow[1] = uncompressed[2]; for (int i = 2; i < rowSize; i += 2) { p0 = lastRow[i] = (byte) ((p0 & 0xff) + (uncompressed[1 + i] & 0xff)); p1 = lastRow[i + 1] = (byte) ((p1 & 0xff) + (uncompressed[2 + i] & 0xff)); } } private void filterAvg2(byte[] uncompressed, byte[] lastRow, int rowSize) { byte p0 = lastRow[0] = (byte) ((uncompressed[1] & 0xff) + ((lastRow[0] & 0xff) >> 1)); byte p1 = lastRow[1] = (byte) ((uncompressed[2] & 0xff) + ((lastRow[1] & 0xff) >> 1)); for (int i = 2; i < rowSize; i += 2) { p0 = lastRow[i] = (byte) ((((lastRow[i] & 0xff) + (p0 & 0xff)) >> 1) + (uncompressed[1 + i] & 0xff)); p1 = lastRow[i + 1] = (byte) ((((lastRow[i + 1] & 0xff) + (p1 & 0xff)) >> 1) + (uncompressed[i + 2] & 0xff)); } } private void filterSub3(byte[] uncompressed, byte[] lastRow, int rowSize) { byte p0 = lastRow[0] = uncompressed[1]; byte p1 = lastRow[1] = uncompressed[2]; byte p2 = lastRow[2] = uncompressed[3]; for (int i = 3; i < rowSize; i += 3) { p0 = lastRow[i] = (byte) ((p0 & 0xff) + (uncompressed[i + 1] & 0xff)); p1 = lastRow[i + 1] = (byte) ((p1 & 0xff) + (uncompressed[i + 2] & 0xff)); p2 = lastRow[i + 2] = (byte) ((p2 & 0xff) + (uncompressed[i + 3] & 0xff)); } } private void filterAvg3(byte[] uncompressed, byte[] lastRow, int rowSize) { byte p0 = lastRow[0] = (byte) ((uncompressed[1] & 0xff) + ((lastRow[0] & 0xff) >> 1)); byte p1 = lastRow[1] = (byte) ((uncompressed[2] & 0xff) + ((lastRow[1] & 0xff) >> 1)); byte p2 = lastRow[2] = (byte) ((uncompressed[3] & 0xff) + ((lastRow[2] & 0xff) >> 1)); for (int i = 3; i < rowSize; i += 3) { p0 = lastRow[i] = (byte) ((((lastRow[i] & 0xff) + (p0 & 0xff)) >> 1) + (uncompressed[i + 1] & 0xff)); p1 = lastRow[i + 1] = (byte) ((((lastRow[i + 1] & 0xff) + (p1 & 0xff)) >> 1) + (uncompressed[i + 2] & 0xff)); p2 = lastRow[i + 2] = (byte) ((((lastRow[i + 2] & 0xff) + (p2 & 0xff)) >> 1) + (uncompressed[i + 3] & 0xff)); } } private void filterSub4(byte[] uncompressed, byte[] lastRow, int rowSize) { byte p0 = lastRow[0] = uncompressed[1]; byte p1 = lastRow[1] = uncompressed[2]; byte p2 = lastRow[2] = uncompressed[3]; byte p3 = lastRow[3] = uncompressed[4]; for (int i = 4; i < rowSize; i += 4) { p0 = lastRow[i] = (byte) ((p0 & 0xff) + (uncompressed[i + 1] & 0xff)); p1 = lastRow[i + 1] = (byte) ((p1 & 0xff) + (uncompressed[i + 2] & 0xff)); p2 = lastRow[i + 2] = (byte) ((p2 & 0xff) + (uncompressed[i + 3] & 0xff)); p3 = lastRow[i + 3] = (byte) ((p3 & 0xff) + (uncompressed[i + 4] & 0xff)); } } private void filterAvg4(byte[] uncompressed, byte[] lastRow, int rowSize) { byte p0 = lastRow[0] = (byte) ((uncompressed[1] & 0xff) + ((lastRow[0] & 0xff) >> 1)); byte p1 = lastRow[1] = (byte) ((uncompressed[2] & 0xff) + ((lastRow[1] & 0xff) >> 1)); byte p2 = lastRow[2] = (byte) ((uncompressed[3] & 0xff) + ((lastRow[2] & 0xff) >> 1)); byte p3 = lastRow[3] = (byte) ((uncompressed[4] & 0xff) + ((lastRow[3] & 0xff) >> 1)); for (int i = 4; i < rowSize; i += 4) { p0 = lastRow[i] = (byte) ((((lastRow[i] & 0xff) + (p0 & 0xff)) >> 1) + (uncompressed[i + 1] & 0xff)); p1 = lastRow[i + 1] = (byte) ((((lastRow[i + 1] & 0xff) + (p1 & 0xff)) >> 1) + (uncompressed[i + 2] & 0xff)); p2 = lastRow[i + 2] = (byte) ((((lastRow[i + 2] & 0xff) + (p2 & 0xff)) >> 1) + (uncompressed[i + 3] & 0xff)); p3 = lastRow[i + 3] = (byte) ((((lastRow[i + 3] & 0xff) + (p3 & 0xff)) >> 1) + (uncompressed[i + 4] & 0xff)); } } private static class IHDR { private int width; private int height; private byte bitDepth; private byte colorType; private byte compressionType; private byte filterType; private byte interlaceType; public void parse(ByteBuffer data) { width = data.getInt(); height = data.getInt(); bitDepth = data.get(); colorType = data.get(); compressionType = data.get(); filterType = data.get(); interlaceType = data.get(); data.getInt(); } public int rowSize() { return (width * getBitsPerPixel() + 7) >> 3; } public int getNBChannels() { int channels; channels = 1; if ((colorType & (PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_PALETTE)) == PNG_COLOR_MASK_COLOR) channels = 3; if ((colorType & PNG_COLOR_MASK_ALPHA) != 0) channels++; return channels; } public int getBitsPerPixel() { return bitDepth * getNBChannels(); } public ColorSpace colorSpace() { return ColorSpace.RGB; } } /** * Palette descriptor. */ private static class PLTE { private int[] palette; public void parse(ByteBuffer data, int length) { if ((length % 3) != 0 || length > 256 * 3) throw new RuntimeException("Invalid data"); int n = length / 3; palette = new int[n]; int i = 0; for (i = 0; i < n; i++) { palette[i] = (0xff << 24) | ((data.get() & 0xff) << 16) | ((data.get() & 0xff) << 8) | (data.get() & 0xff); } for (; i < 256; i++) palette[i] = (0xff << 24); data.getInt(); // crc } } /** * Transparency descriptor for paletted data */ public static class TRNS { private int colorType; private byte[] alphaPal; private byte alphaGrey; private byte alphaR; private byte alphaG; private byte alphaB; public TRNS(byte colorType) { this.colorType = colorType; } public void parse(ByteBuffer data, int length) { if (colorType == PNG_COLOR_TYPE_PALETTE) { alphaPal = new byte[256]; data.get(alphaPal, 0, length); for (int i = length; i < 256; i++) { alphaPal[i] = (byte) 0xff; } } else if (colorType == PNG_COLOR_TYPE_GRAY) { alphaGrey = data.get(); } else if (colorType == PNG_COLOR_TYPE_RGB) { alphaR = data.get(); alphaG = data.get(); alphaG = data.get(); } data.getInt(); // crc } } @Override public VideoCodecMeta getCodecMeta(ByteBuffer _data) { ByteBuffer data = _data.duplicate(); long sig = data.getLong(); if (sig != PNGSIG && sig != MNGSIG) throw new RuntimeException("Not a PNG file."); while (data.remaining() >= 8) { int length = data.getInt(); int tag = data.getInt(); if (data.remaining() < length) break; switch (tag) { case TAG_IHDR: IHDR ihdr = new IHDR(); ihdr.parse(data); return new VideoCodecMeta(new Size(ihdr.width, ihdr.height), ColorSpace.RGB); default: data.position(data.position() + length + 4); } } return null; } public static int probe(ByteBuffer data) { long sig = data.getLong(); if (sig == PNGSIG && sig == MNGSIG) return 100; return 0; } public static byte[] deflate(byte[] data, Inflater inflater) throws DataFormatException { inflater.setInput(data); ByteArrayOutputStream baos = new ByteArrayOutputStream(data.length); byte[] buffer = new byte[1 << 14]; while (!inflater.needsInput()) { int count = inflater.inflate(buffer); baos.write(buffer, 0, count); System.out.println(baos.size()); } return baos.toByteArray(); } }