blob: f8daab348e82650944aa7a8ff5f1012c1cc69230 [file] [log] [blame]
import 'dart:typed_data';
import 'package:archive/archive.dart';
import '../color.dart';
import '../animation.dart';
import '../icc_profile_data.dart';
import '../image.dart';
import '../util/output_buffer.dart';
import 'encoder.dart';
/// Encode an image to the PNG format.
class PngEncoder extends Encoder {
PngEncoder({this.filter = FILTER_PAETH, this.level});
void addFrame(Image image) {
xOffset = image.xOffset;
yOffset = image.xOffset;
delay = image.duration;
disposeMethod = image.disposeMethod;
blendMethod = image.blendMethod;
if (output == null) {
output = OutputBuffer(bigEndian: true);
format = image.format;
_width = image.width;
_height = image.height;
_writeHeader(_width, _height);
_writeICCPChunk(output, image.iccProfile);
if (isAnimated) {
_writeAnimationControlChunk();
}
}
// Include room for the filter bytes (1 byte per row).
List<int> filteredImage =
Uint8List((image.width * image.height * image.format) + image.height);
_filter(image, filteredImage);
List<int> compressed = ZLibEncoder().encode(filteredImage, level: level);
if (isAnimated) {
_writeFrameControlChunk();
sequenceNumber++;
}
if (sequenceNumber <= 1) {
_writeChunk(output, 'IDAT', compressed);
} else {
// fdAT chunk
OutputBuffer fdat = OutputBuffer(bigEndian: true);
fdat.writeUint32(sequenceNumber);
fdat.writeBytes(compressed);
_writeChunk(output, 'fdAT', fdat.getBytes());
sequenceNumber++;
}
}
List<int> finish() {
List<int> bytes;
if (output == null) {
return bytes;
}
_writeChunk(output, 'IEND', []);
sequenceNumber = 0;
bytes = output.getBytes();
output = null;
return bytes;
}
/// Does this encoder support animation?
bool get supportsAnimation => true;
/// Encode an animation.
List<int> encodeAnimation(Animation anim) {
isAnimated = true;
_frames = anim.frames.length;
repeat = anim.loopCount;
for (Image f in anim) {
addFrame(f);
}
return finish();
}
/// Encode a single frame image.
List<int> encodeImage(Image image) {
isAnimated = false;
addFrame(image);
return finish();
}
void _writeHeader(int width, int height) {
// PNG file signature
output.writeBytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
// IHDR chunk
OutputBuffer chunk = OutputBuffer(bigEndian: true);
chunk.writeUint32(width);
chunk.writeUint32(height);
chunk.writeByte(8);
chunk.writeByte(format == Image.RGB ? 2 : 6);
chunk.writeByte(0); // compression method
chunk.writeByte(0); // filter method
chunk.writeByte(0); // interlace method
_writeChunk(output, 'IHDR', chunk.getBytes());
}
void _writeAnimationControlChunk() {
OutputBuffer chunk = OutputBuffer(bigEndian: true);
chunk.writeUint32(_frames); // number of frames
chunk.writeUint32(repeat); // loop count
_writeChunk(output, 'acTL', chunk.getBytes());
}
void _writeFrameControlChunk() {
OutputBuffer chunk = OutputBuffer(bigEndian: true);
chunk.writeUint32(sequenceNumber);
chunk.writeUint32(_width);
chunk.writeUint32(_height);
chunk.writeUint32(xOffset);
chunk.writeUint32(yOffset);
chunk.writeUint16(delay);
chunk.writeUint16(0); // delay denominator
chunk.writeByte(disposeMethod);
chunk.writeByte(blendMethod);
_writeChunk(output, 'fcTL', chunk.getBytes());
}
void _writeICCPChunk(OutputBuffer out, ICCProfileData iccp) {
if (iccp == null || iccp.data == null) {
return;
}
OutputBuffer chunk = OutputBuffer(bigEndian: true);
// name
chunk.writeBytes(iccp.name.codeUnits);
chunk.writeByte(0);
// compression
chunk.writeByte(0); // 0 - deflate
// profile data
chunk.writeBytes(iccp.compressed());
_writeChunk(output, 'iCCP', chunk.getBytes());
}
void _writeChunk(OutputBuffer out, String type, List<int> chunk) {
out.writeUint32(chunk.length);
out.writeBytes(type.codeUnits);
out.writeBytes(chunk);
int crc = _crc(type, chunk);
out.writeUint32(crc);
}
void _filter(Image image, List<int> out) {
int oi = 0;
for (int y = 0; y < image.height; ++y) {
switch (filter) {
case FILTER_SUB:
oi = _filterSub(image, oi, y, out);
break;
case FILTER_UP:
oi = _filterUp(image, oi, y, out);
break;
case FILTER_AVERAGE:
oi = _filterAverage(image, oi, y, out);
break;
case FILTER_PAETH:
oi = _filterPaeth(image, oi, y, out);
break;
case FILTER_AGRESSIVE:
// TODO Apply all five filters and select the filter that produces
// the smallest sum of absolute values per row.
oi = _filterPaeth(image, oi, y, out);
break;
default:
oi = _filterNone(image, oi, y, out);
break;
}
}
}
int _filterNone(Image image, int oi, int row, List<int> out) {
out[oi++] = FILTER_NONE;
for (int x = 0; x < image.width; ++x) {
int c = image.getPixel(x, row);
out[oi++] = getRed(c);
out[oi++] = getGreen(c);
out[oi++] = getBlue(c);
if (image.format == Image.RGBA) {
out[oi++] = getAlpha(image.getPixel(x, row));
}
}
return oi;
}
int _filterSub(Image image, int oi, int row, List<int> out) {
out[oi++] = FILTER_SUB;
out[oi++] = getRed(image.getPixel(0, row));
out[oi++] = getGreen(image.getPixel(0, row));
out[oi++] = getBlue(image.getPixel(0, row));
if (image.format == Image.RGBA) {
out[oi++] = getAlpha(image.getPixel(0, row));
}
for (int x = 1; x < image.width; ++x) {
int ar = getRed(image.getPixel(x - 1, row));
int ag = getGreen(image.getPixel(x - 1, row));
int ab = getBlue(image.getPixel(x - 1, row));
int r = getRed(image.getPixel(x, row));
int g = getGreen(image.getPixel(x, row));
int b = getBlue(image.getPixel(x, row));
out[oi++] = ((r - ar)) & 0xff;
out[oi++] = ((g - ag)) & 0xff;
out[oi++] = ((b - ab)) & 0xff;
if (image.format == Image.RGBA) {
int aa = getAlpha(image.getPixel(x - 1, row));
int a = getAlpha(image.getPixel(x, row));
out[oi++] = ((a - aa)) & 0xff;
}
}
return oi;
}
int _filterUp(Image image, int oi, int row, List<int> out) {
out[oi++] = FILTER_UP;
for (int x = 0; x < image.width; ++x) {
int br = (row == 0) ? 0 : getRed(image.getPixel(x, row - 1));
int bg = (row == 0) ? 0 : getGreen(image.getPixel(x, row - 1));
int bb = (row == 0) ? 0 : getBlue(image.getPixel(x, row - 1));
int xr = getRed(image.getPixel(x, row));
int xg = getGreen(image.getPixel(x, row));
int xb = getBlue(image.getPixel(x, row));
out[oi++] = (xr - br) & 0xff;
out[oi++] = (xg - bg) & 0xff;
out[oi++] = (xb - bb) & 0xff;
if (image.format == Image.RGBA) {
int ba = (row == 0) ? 0 : getAlpha(image.getPixel(x, row - 1));
int xa = getAlpha(image.getPixel(x, row));
out[oi++] = (xa - ba) & 0xff;
;
}
}
return oi;
}
int _filterAverage(Image image, int oi, int row, List<int> out) {
out[oi++] = FILTER_AVERAGE;
for (int x = 0; x < image.width; ++x) {
int ar = (x == 0) ? 0 : getRed(image.getPixel(x - 1, row));
int ag = (x == 0) ? 0 : getGreen(image.getPixel(x - 1, row));
int ab = (x == 0) ? 0 : getBlue(image.getPixel(x - 1, row));
int br = (row == 0) ? 0 : getRed(image.getPixel(x, row - 1));
int bg = (row == 0) ? 0 : getGreen(image.getPixel(x, row - 1));
int bb = (row == 0) ? 0 : getBlue(image.getPixel(x, row - 1));
int xr = getRed(image.getPixel(x, row));
int xg = getGreen(image.getPixel(x, row));
int xb = getBlue(image.getPixel(x, row));
out[oi++] = (xr - ((ar + br) >> 1)) & 0xff;
out[oi++] = (xg - ((ag + bg) >> 1)) & 0xff;
out[oi++] = (xb - ((ab + bb) >> 1)) & 0xff;
if (image.format == Image.RGBA) {
int aa = (x == 0) ? 0 : getAlpha(image.getPixel(x - 1, row));
int ba = (row == 0) ? 0 : getAlpha(image.getPixel(x, row - 1));
int xa = getAlpha(image.getPixel(x, row));
out[oi++] = (xa - ((aa + ba) >> 1)) & 0xff;
;
}
}
return oi;
}
int _paethPredictor(int a, int b, int c) {
int p = a + b - c;
int pa = (p > a) ? p - a : a - p;
int pb = (p > b) ? p - b : b - p;
int pc = (p > c) ? p - c : c - p;
if (pa <= pb && pa <= pc) {
return a;
} else if (pb <= pc) {
return b;
}
return c;
}
int _filterPaeth(Image image, int oi, int row, List<int> out) {
out[oi++] = FILTER_PAETH;
for (int x = 0; x < image.width; ++x) {
int ar = (x == 0) ? 0 : getRed(image.getPixel(x - 1, row));
int ag = (x == 0) ? 0 : getGreen(image.getPixel(x - 1, row));
int ab = (x == 0) ? 0 : getBlue(image.getPixel(x - 1, row));
int br = (row == 0) ? 0 : getRed(image.getPixel(x, row - 1));
int bg = (row == 0) ? 0 : getGreen(image.getPixel(x, row - 1));
int bb = (row == 0) ? 0 : getBlue(image.getPixel(x, row - 1));
int cr =
(row == 0 || x == 0) ? 0 : getRed(image.getPixel(x - 1, row - 1));
int cg =
(row == 0 || x == 0) ? 0 : getGreen(image.getPixel(x - 1, row - 1));
int cb =
(row == 0 || x == 0) ? 0 : getBlue(image.getPixel(x - 1, row - 1));
int xr = getRed(image.getPixel(x, row));
int xg = getGreen(image.getPixel(x, row));
int xb = getBlue(image.getPixel(x, row));
int pr = _paethPredictor(ar, br, cr);
int pg = _paethPredictor(ag, bg, cg);
int pb = _paethPredictor(ab, bb, cb);
out[oi++] = (xr - pr) & 0xff;
out[oi++] = (xg - pg) & 0xff;
out[oi++] = (xb - pb) & 0xff;
if (image.format == Image.RGBA) {
int aa = (x == 0) ? 0 : getAlpha(image.getPixel(x - 1, row));
int ba = (row == 0) ? 0 : getAlpha(image.getPixel(x, row - 1));
int ca =
(row == 0 || x == 0) ? 0 : getAlpha(image.getPixel(x - 1, row - 1));
int xa = getAlpha(image.getPixel(x, row));
int pa = _paethPredictor(aa, ba, ca);
out[oi++] = (xa - pa) & 0xff;
;
}
}
return oi;
}
/// Return the CRC of the bytes
int _crc(String type, List<int> bytes) {
int crc = getCrc32(type.codeUnits);
return getCrc32(bytes, crc);
}
int format;
int filter;
int repeat;
int level;
int xOffset;
int yOffset;
int delay;
int disposeMethod;
int blendMethod;
int _width;
int _height;
int _frames;
int sequenceNumber = 0;
bool isAnimated;
OutputBuffer output;
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;
static const int FILTER_AGRESSIVE = 5;
// Table of CRCs of all 8-bit messages.
//final List<int> _crcTable = List<int>(256);
}