blob: 365ea11b50a39cdf09f7480114fdf2ce97590530 [file] [log] [blame]
import 'package:archive/archive.dart';
import '../animation.dart';
import '../color.dart';
import '../icc_profile_data.dart';
import '../image.dart';
import '../image_exception.dart';
import '../transform/copy_into.dart';
import '../util/input_buffer.dart';
import 'decode_info.dart';
import 'decoder.dart';
import 'png/png_frame.dart';
import 'png/png_info.dart';
/// Decode a PNG encoded image.
class PngDecoder extends Decoder {
InternalPngInfo _info;
/// Is the given file a valid PNG image?
bool isValidFile(List<int> data) {
InputBuffer input = InputBuffer(data, bigEndian: true);
InputBuffer pngHeader = input.readBytes(8);
const PNG_HEADER = const [137, 80, 78, 71, 13, 10, 26, 10];
for (int i = 0; i < 8; ++i) {
if (pngHeader[i] != PNG_HEADER[i]) {
return false;
}
}
return true;
}
PngInfo get info => _info;
/// Start decoding the data as an animation sequence, but don't actually
/// process the frames until they are requested with decodeFrame.
DecodeInfo startDecode(List<int> data) {
_input = InputBuffer(data, bigEndian: true);
InputBuffer pngHeader = _input.readBytes(8);
const PNG_HEADER = const [137, 80, 78, 71, 13, 10, 26, 10];
for (int i = 0; i < 8; ++i) {
if (pngHeader[i] != PNG_HEADER[i]) {
return null;
}
}
while (true) {
int inputPos = _input.position;
int chunkSize = _input.readUint32();
String chunkType = _input.readString(4);
switch (chunkType) {
case 'IHDR':
InputBuffer hdr = InputBuffer.from(_input.readBytes(chunkSize));
List<int> hdrBytes = hdr.toUint8List();
_info = InternalPngInfo();
_info.width = hdr.readUint32();
_info.height = hdr.readUint32();
_info.bits = hdr.readByte();
_info.colorType = hdr.readByte();
_info.compressionMethod = hdr.readByte();
_info.filterMethod = hdr.readByte();
_info.interlaceMethod = hdr.readByte();
// Validate some of the info in the header to make sure we support
// the proposed image data.
if (![GRAYSCALE, RGB, INDEXED, GRAYSCALE_ALPHA, RGBA]
.contains(_info.colorType)) {
return null;
}
if (_info.filterMethod != 0) {
return null;
}
switch (_info.colorType) {
case GRAYSCALE:
if (![1, 2, 4, 8, 16].contains(_info.bits)) {
return null;
}
break;
case RGB:
if (![8, 16].contains(_info.bits)) {
return null;
}
break;
case INDEXED:
if (![1, 2, 4, 8].contains(_info.bits)) {
return null;
}
break;
case GRAYSCALE_ALPHA:
if (![8, 16].contains(_info.bits)) {
return null;
}
break;
case RGBA:
if (![8, 16].contains(_info.bits)) {
return null;
}
break;
}
int crc = _input.readUint32();
int computedCrc = _crc(chunkType, hdrBytes);
if (crc != computedCrc) {
throw new ImageException('Invalid $chunkType checksum');
}
break;
case 'PLTE':
_info.palette = _input.readBytes(chunkSize).toUint8List();
int crc = _input.readUint32();
int computedCrc = _crc(chunkType, _info.palette);
if (crc != computedCrc) {
throw new ImageException('Invalid $chunkType checksum');
}
break;
case 'tRNS':
_info.transparency = _input.readBytes(chunkSize).toUint8List();
int crc = _input.readUint32();
int computedCrc = _crc(chunkType, _info.transparency);
if (crc != computedCrc) {
throw new ImageException('Invalid $chunkType checksum');
}
break;
case 'IEND':
// End of the image.
_input.skip(4); // CRC
break;
case 'gAMA':
if (chunkSize != 4) {
throw new ImageException('Invalid gAMA chunk');
}
int gammaInt = _input.readUint32();
_input.skip(4); // CRC
// A gamma of 1.0 doesn't have any affect, so pretend we didn't get
// a gamma in that case.
if (gammaInt != 100000) {
_info.gamma = gammaInt / 100000.0;
}
break;
case 'IDAT':
_info.idat.add(inputPos);
_input.skip(chunkSize);
_input.skip(4); // CRC
break;
case 'acTL': // Animation control chunk
_info.numFrames = _input.readUint32();
_info.repeat = _input.readUint32();
_input.skip(4); // CRC
break;
case 'fcTL': // Frame control chunk
PngFrame frame = InternalPngFrame();
_info.frames.add(frame);
frame.sequenceNumber = _input.readUint32();
frame.width = _input.readUint32();
frame.height = _input.readUint32();
frame.xOffset = _input.readUint32();
frame.yOffset = _input.readUint32();
frame.delayNum = _input.readUint16();
frame.delayDen = _input.readUint16();
frame.dispose = _input.readByte();
frame.blend = _input.readByte();
_input.skip(4); // CRC
break;
case 'fdAT':
/*int sequenceNumber =*/ _input.readUint32();
InternalPngFrame frame = _info.frames.last as InternalPngFrame;
frame.fdat.add(inputPos);
_input.skip(chunkSize - 4);
_input.skip(4); // CRC
break;
case 'bKGD':
if (_info.colorType == 3) {
int paletteIndex = _input.readByte();
chunkSize--;
int p3 = paletteIndex * 3;
int r = _info.palette[p3];
int g = _info.palette[p3 + 1];
int b = _info.palette[p3 + 2];
_info.backgroundColor = Color.fromRgb(r, g, b);
} else if (_info.colorType == 0 || _info.colorType == 4) {
/*int gray =*/ _input.readUint16();
chunkSize -= 2;
} else if (_info.colorType == 2 || _info.colorType == 6) {
/*int r =*/ _input.readUint16();
/*int g =*/ _input.readUint16();
/*int b =*/ _input.readUint16();
chunkSize -= 24;
}
if (chunkSize > 0) {
_input.skip(chunkSize);
}
_input.skip(4); // CRC
break;
case 'iCCP':
_info.iCCPName = _input.readString();
_info.iCCPCompression = _input.readByte(); // 0: deflate
chunkSize -= _info.iCCPName.length + 2;
final profile = _input.readBytes(chunkSize);
_info.iCCPData = profile.toUint8List();
_input.skip(4); // CRC
break;
default:
_input.skip(chunkSize);
_input.skip(4); // CRC
break;
}
if (chunkType == 'IEND') {
break;
}
if (_input.isEOS) {
return null;
}
}
return _info;
}
/// The number of frames that can be decoded.
int numFrames() => _info != null ? _info.numFrames : 0;
/// Decode the frame (assuming [startDecode] has already been called).
Image decodeFrame(int frame) {
if (_info == null) {
return null;
}
List<int> imageData = [];
int width = _info.width;
int height = _info.height;
if (!_info.isAnimated || frame == 0) {
for (int i = 0, len = _info.idat.length; i < len; ++i) {
_input.offset = _info.idat[i];
int chunkSize = _input.readUint32();
String chunkType = _input.readString(4);
List<int> data = _input.readBytes(chunkSize).toUint8List();
imageData.addAll(data);
int crc = _input.readUint32();
int computedCrc = _crc(chunkType, data);
if (crc != computedCrc) {
throw new ImageException('Invalid $chunkType checksum');
}
}
} else {
if (frame < 0 || frame >= _info.frames.length) {
throw new ImageException('Invalid Frame Number: $frame');
}
InternalPngFrame f = _info.frames[frame] as InternalPngFrame;
width = f.width;
height = f.height;
for (int i = 0; i < f.fdat.length; ++i) {
_input.offset = f.fdat[i];
int chunkSize = _input.readUint32();
/*String chunkType =*/ _input.readString(4);
_input.skip(4); // sequence number
List<int> data = _input.readBytes(chunkSize).toUint8List();
imageData.addAll(data);
}
//_frame = frame;
//_numFrames = _info.numFrames;
}
int format;
if (_info.colorType == GRAYSCALE_ALPHA ||
_info.colorType == RGBA ||
_info.transparency != null) {
format = Image.RGBA;
} else {
format = Image.RGB;
}
Image image = Image(width, height, format);
List<int> uncompressed = ZLibDecoder().decodeBytes(imageData);
// input is the decompressed data.
InputBuffer input = InputBuffer(uncompressed, bigEndian: true);
_resetBits();
// Set up a LUT to transform colors for gamma correction.
if (_info.colorLut == null) {
_info.colorLut = List<int>(256);
for (int i = 0; i < 256; ++i) {
int c = i;
/*if (info.gamma != null) {
c = (Math.pow((c / 255.0), info.gamma) * 255.0).toInt();
}*/
_info.colorLut[i] = c;
}
// Apply the LUT to the palette, if necessary.
if (_info.palette != null && _info.gamma != null) {
for (int i = 0; i < _info.palette.length; ++i) {
_info.palette[i] = _info.colorLut[_info.palette[i]];
}
}
}
int origW = _info.width;
int origH = _info.height;
_info.width = width;
_info.height = height;
int w = width;
int h = height;
_progressY = 0;
if (_info.interlaceMethod != 0) {
_processPass(input, image, 0, 0, 8, 8, (w + 7) >> 3, (h + 7) >> 3);
_processPass(input, image, 4, 0, 8, 8, (w + 3) >> 3, (h + 7) >> 3);
_processPass(input, image, 0, 4, 4, 8, (w + 3) >> 2, (h + 3) >> 3);
_processPass(input, image, 2, 0, 4, 4, (w + 1) >> 2, (h + 3) >> 2);
_processPass(input, image, 0, 2, 2, 4, (w + 1) >> 1, (h + 1) >> 2);
_processPass(input, image, 1, 0, 2, 2, w >> 1, (h + 1) >> 1);
_processPass(input, image, 0, 1, 1, 2, w, h >> 1);
} else {
_process(input, image);
}
_info.width = origW;
_info.height = origH;
image.iccProfile =
ICCProfileData(_info.iCCPName, ICCPCompression.deflate, _info.iCCPData);
return image;
}
Image decodeImage(List<int> data, {int frame = 0}) {
if (startDecode(data) == null) {
return null;
}
return decodeFrame(frame);
}
Animation decodeAnimation(List<int> data) {
if (startDecode(data) == null) {
return null;
}
Animation anim = Animation();
anim.width = _info.width;
anim.height = _info.height;
if (!_info.isAnimated) {
Image image = decodeFrame(0);
anim.addFrame(image);
return anim;
}
int dispose = PngFrame.APNG_DISPOSE_OP_BACKGROUND;
Image lastImage = Image(_info.width, _info.height);
for (int i = 0; i < _info.numFrames; ++i) {
//_frame = i;
if (lastImage == null) {
lastImage = Image(_info.width, _info.height);
} else {
lastImage = Image.from(lastImage);
}
PngFrame frame = _info.frames[i];
Image image = decodeFrame(i);
if (image == null) {
continue;
}
if (lastImage != null) {
if (dispose == PngFrame.APNG_DISPOSE_OP_BACKGROUND ||
dispose == PngFrame.APNG_DISPOSE_OP_PREVIOUS) {
lastImage.fill(_info.backgroundColor);
}
copyInto(lastImage, image,
dstX: frame.xOffset,
dstY: frame.yOffset,
blend: frame.blend == PngFrame.APNG_BLEND_OP_OVER);
} else {
lastImage = image;
}
anim.addFrame(lastImage);
dispose = frame.dispose;
}
return anim;
}
/// Process a pass of an interlaced image.
void _processPass(InputBuffer input, Image image, int xOffset, int yOffset,
int xStep, int yStep, int passWidth, int passHeight) {
final int channels = (_info.colorType == GRAYSCALE_ALPHA)
? 2
: (_info.colorType == RGB) ? 3 : (_info.colorType == RGBA) ? 4 : 1;
final int pixelDepth = channels * _info.bits;
final int bpp = (pixelDepth + 7) >> 3;
final int rowBytes = (pixelDepth * passWidth + 7) >> 3;
final List<int> line = List<int>.filled(rowBytes, 0);
final List<List<int>> inData = [line, line];
final List<int> pixel = [0, 0, 0, 0];
//int pi = 0;
for (int srcY = 0, dstY = yOffset, ri = 0;
srcY < passHeight;
++srcY, dstY += yStep, ri = 1 - ri, _progressY++) {
int filterType = input.readByte();
inData[ri] = input.readBytes(rowBytes).toUint8List();
final List<int> row = inData[ri];
final List<int> prevRow = inData[1 - ri];
// Before the image is compressed, it was filtered to improve compression.
// Reverse the filter now.
_unfilter(filterType, bpp, row, prevRow);
// Scanlines are always on byte boundaries, so for bit depths < 8,
// reset the bit stream counter.
_resetBits();
InputBuffer rowInput = InputBuffer(row, bigEndian: true);
final int blockHeight = xStep;
final int blockWidth = xStep - xOffset;
//int yMax = Math.min(dstY + blockHeight, _info.height);
for (int srcX = 0, dstX = xOffset;
srcX < passWidth;
++srcX, dstX += xStep) {
_readPixel(rowInput, pixel);
int c = _getColor(pixel);
image.setPixel(dstX, dstY, c);
if (blockWidth > 1 || blockHeight > 1) {
//int xMax = Math.min(dstX + blockWidth, _info.width);
//int xPixels = xMax - dstX;
for (int i = 0; i < blockHeight; ++i) {
for (int j = 0; j < blockWidth; ++j) {
image.setPixelSafe(dstX + j, dstY + j, c);
}
}
}
}
}
}
void _process(InputBuffer input, Image image) {
final int channels = (_info.colorType == GRAYSCALE_ALPHA)
? 2
: (_info.colorType == RGB) ? 3 : (_info.colorType == RGBA) ? 4 : 1;
final int pixelDepth = channels * _info.bits;
final int w = _info.width;
final int h = _info.height;
final int rowBytes = (((w * pixelDepth + 7)) >> 3);
final int bpp = (pixelDepth + 7) >> 3;
final List<int> line = List<int>.filled(rowBytes, 0);
final List<List<int>> inData = [line, line];
final List<int> pixel = [0, 0, 0, 0];
for (int y = 0, pi = 0, ri = 0; y < h; ++y, ri = 1 - ri) {
int filterType = input.readByte();
inData[ri] = input.readBytes(rowBytes).toUint8List();
List<int> row = inData[ri];
List<int> prevRow = inData[1 - ri];
// Before the image is compressed, it was filtered to improve compression.
// Reverse the filter now.
_unfilter(filterType, bpp, row, prevRow);
// Scanlines are always on byte boundaries, so for bit depths < 8,
// reset the bit stream counter.
_resetBits();
InputBuffer rowInput = InputBuffer(inData[ri], bigEndian: true);
for (int x = 0; x < w; ++x) {
_readPixel(rowInput, pixel);
image[pi++] = _getColor(pixel);
}
}
}
void _unfilter(int filterType, int bpp, List<int> row, List<int> prevRow) {
final int rowBytes = row.length;
switch (filterType) {
case FILTER_NONE:
break;
case FILTER_SUB:
for (int x = bpp; x < rowBytes; ++x) {
row[x] = (row[x] + row[x - bpp]) & 0xff;
}
break;
case FILTER_UP:
for (int x = 0; x < rowBytes; ++x) {
row[x] = (row[x] + prevRow[x]) & 0xff;
}
break;
case FILTER_AVERAGE:
for (int x = 0; x < rowBytes; ++x) {
int a = x < bpp ? 0 : row[x - bpp];
int b = prevRow[x];
row[x] = (row[x] + ((a + b) >> 1)) & 0xff;
}
break;
case FILTER_PAETH:
for (int x = 0; x < rowBytes; ++x) {
int a = x < bpp ? 0 : row[x - bpp];
int b = prevRow[x];
int c = x < bpp ? 0 : prevRow[x - bpp];
int p = a + b - c;
int pa = (p - a).abs();
int pb = (p - b).abs();
int pc = (p - c).abs();
int paeth = 0;
if (pa <= pb && pa <= pc) {
paeth = a;
} else if (pb <= pc) {
paeth = b;
} else {
paeth = c;
}
row[x] = (row[x] + paeth) & 0xff;
}
break;
default:
throw new ImageException('Invalid filter value: ${filterType}');
}
}
int _convert16to8(int c) {
return c >> 8;
}
int _convert1to8(int c) {
return (c == 0) ? 0 : 255;
}
int _convert2to8(int c) {
return c * 85;
}
int _convert4to8(int c) {
return c << 4;
}
/// Return the CRC of the bytes
int _crc(String type, List<int> bytes) {
int crc = getCrc32(type.codeUnits);
return getCrc32(bytes, crc);
}
int _bitBuffer = 0;
int _bitBufferLen = 0;
void _resetBits() {
_bitBuffer = 0;
_bitBufferLen = 0;
}
/// Read a number of bits from the input stream.
int _readBits(InputBuffer input, int numBits) {
if (numBits == 0) {
return 0;
}
if (numBits == 8) {
return input.readByte();
}
if (numBits == 16) {
return input.readUint16();
}
// not enough buffer
while (_bitBufferLen < numBits) {
if (input.isEOS) {
throw new ImageException('Invalid PNG data.');
}
// input byte
int octet = input.readByte();
// concat octet
_bitBuffer = octet << _bitBufferLen;
_bitBufferLen += 8;
}
// output byte
int mask = (numBits == 1)
? 1
: (numBits == 2)
? 3
: (numBits == 4)
? 0xf
: (numBits == 8) ? 0xff : (numBits == 16) ? 0xffff : 0;
int octet = (_bitBuffer >> (_bitBufferLen - numBits)) & mask;
_bitBufferLen -= numBits;
return octet;
}
/// Read the next pixel from the input stream.
void _readPixel(InputBuffer input, List<int> pixel) {
switch (_info.colorType) {
case GRAYSCALE:
pixel[0] = _readBits(input, _info.bits);
return;
case RGB:
pixel[0] = _readBits(input, _info.bits);
pixel[1] = _readBits(input, _info.bits);
pixel[2] = _readBits(input, _info.bits);
return;
case INDEXED:
pixel[0] = _readBits(input, _info.bits);
return;
case GRAYSCALE_ALPHA:
pixel[0] = _readBits(input, _info.bits);
pixel[1] = _readBits(input, _info.bits);
return;
case RGBA:
pixel[0] = _readBits(input, _info.bits);
pixel[1] = _readBits(input, _info.bits);
pixel[2] = _readBits(input, _info.bits);
pixel[3] = _readBits(input, _info.bits);
return;
}
throw new ImageException('Invalid color type: ${_info.colorType}.');
}
/// Get the color with the list of components.
int _getColor(List<int> raw) {
switch (_info.colorType) {
case GRAYSCALE:
int g;
switch (_info.bits) {
case 1:
g = _convert1to8(raw[0]);
break;
case 2:
g = _convert2to8(raw[0]);
break;
case 4:
g = _convert4to8(raw[0]);
break;
case 8:
g = raw[0];
break;
case 16:
g = _convert16to8(raw[0]);
break;
}
g = _info.colorLut[g];
if (_info.transparency != null) {
int a = ((_info.transparency[0] & 0xff) << 24) |
(_info.transparency[1] & 0xff);
if (raw[0] == a) {
return getColor(g, g, g, 0);
}
}
return getColor(g, g, g, 255);
case RGB:
int r, g, b;
switch (_info.bits) {
case 1:
r = _convert1to8(raw[0]);
g = _convert1to8(raw[1]);
b = _convert1to8(raw[2]);
break;
case 2:
r = _convert2to8(raw[0]);
g = _convert2to8(raw[1]);
b = _convert2to8(raw[2]);
break;
case 4:
r = _convert4to8(raw[0]);
g = _convert4to8(raw[1]);
b = _convert4to8(raw[2]);
break;
case 8:
r = raw[0];
g = raw[1];
b = raw[2];
break;
case 16:
r = _convert16to8(raw[0]);
g = _convert16to8(raw[1]);
b = _convert16to8(raw[2]);
break;
}
r = _info.colorLut[r];
g = _info.colorLut[g];
b = _info.colorLut[b];
if (_info.transparency != null) {
int tr = ((_info.transparency[0] & 0xff) << 8) |
(_info.transparency[1] & 0xff);
int tg = ((_info.transparency[2] & 0xff) << 8) |
(_info.transparency[3] & 0xff);
int tb = ((_info.transparency[4] & 0xff) << 8) |
(_info.transparency[5] & 0xff);
if (raw[0] == tr && raw[1] == tg && raw[2] == tb) {
return getColor(r, g, b, 0);
}
}
return getColor(r, g, b, 255);
case INDEXED:
int p = raw[0] * 3;
int a = _info.transparency != null && raw[0] < _info.transparency.length
? _info.transparency[raw[0]]
: 255;
if (p >= _info.palette.length) {
return getColor(255, 255, 255, a);
}
int r = _info.palette[p];
int g = _info.palette[p + 1];
int b = _info.palette[p + 2];
return getColor(r, g, b, a);
case GRAYSCALE_ALPHA:
int g, a;
switch (_info.bits) {
case 1:
g = _convert1to8(raw[0]);
a = _convert1to8(raw[1]);
break;
case 2:
g = _convert2to8(raw[0]);
a = _convert2to8(raw[1]);
break;
case 4:
g = _convert4to8(raw[0]);
a = _convert4to8(raw[1]);
break;
case 8:
g = raw[0];
a = raw[1];
break;
case 16:
g = _convert16to8(raw[0]);
a = _convert16to8(raw[1]);
break;
}
g = _info.colorLut[g];
return getColor(g, g, g, a);
case RGBA:
int r, g, b, a;
switch (_info.bits) {
case 1:
r = _convert1to8(raw[0]);
g = _convert1to8(raw[1]);
b = _convert1to8(raw[2]);
a = _convert1to8(raw[3]);
break;
case 2:
r = _convert2to8(raw[0]);
g = _convert2to8(raw[1]);
b = _convert2to8(raw[2]);
a = _convert2to8(raw[3]);
break;
case 4:
r = _convert4to8(raw[0]);
g = _convert4to8(raw[1]);
b = _convert4to8(raw[2]);
a = _convert4to8(raw[3]);
break;
case 8:
r = raw[0];
g = raw[1];
b = raw[2];
a = raw[3];
break;
case 16:
r = _convert16to8(raw[0]);
g = _convert16to8(raw[1]);
b = _convert16to8(raw[2]);
a = _convert16to8(raw[3]);
break;
}
r = _info.colorLut[r];
g = _info.colorLut[g];
b = _info.colorLut[b];
return getColor(r, g, b, a);
}
throw new ImageException('Invalid color type: ${_info.colorType}.');
}
InputBuffer _input;
int _progressY;
//int _frame = 0;
//int _numFrames = 1;
static const int GRAYSCALE = 0;
static const int RGB = 2;
static const int INDEXED = 3;
static const int GRAYSCALE_ALPHA = 4;
static const int RGBA = 6;
static const int FILTER_NONE = 0;
static const int FILTER_SUB = 1;
static const int FILTER_UP = 2;
static const int FILTER_AVERAGE = 3;
static const int FILTER_PAETH = 4;
}