| import 'dart:typed_data'; |
| |
| import 'package:archive/archive.dart'; |
| |
| import '../animation.dart'; |
| import '../color.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); |
| |
| channels = image.channels; |
| _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). |
| final filteredImage = Uint8List( |
| (image.width * image.height * image.numberOfChannels) + image.height); |
| |
| _filter(image, filteredImage); |
| |
| final compressed = ZLibEncoder().encode(filteredImage, level: level); |
| |
| if (isAnimated) { |
| _writeFrameControlChunk(); |
| sequenceNumber++; |
| } |
| |
| if (sequenceNumber <= 1) { |
| _writeChunk(output!, 'IDAT', compressed); |
| } else { |
| // fdAT chunk |
| final 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? |
| @override |
| bool get supportsAnimation => true; |
| |
| /// Encode an animation. |
| @override |
| List<int>? encodeAnimation(Animation anim) { |
| isAnimated = true; |
| _frames = anim.frames.length; |
| repeat = anim.loopCount; |
| |
| for (var f in anim) { |
| addFrame(f); |
| } |
| return finish(); |
| } |
| |
| /// Encode a single frame image. |
| @override |
| 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 |
| final chunk = OutputBuffer(bigEndian: true); |
| chunk.writeUint32(width); |
| chunk.writeUint32(height); |
| chunk.writeByte(8); |
| chunk.writeByte(channels == Channels.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() { |
| final chunk = OutputBuffer(bigEndian: true); |
| chunk.writeUint32(_frames); // number of frames |
| chunk.writeUint32(repeat); // loop count |
| _writeChunk(output!, 'acTL', chunk.getBytes()); |
| } |
| |
| void _writeFrameControlChunk() { |
| final chunk = OutputBuffer(bigEndian: true); |
| chunk.writeUint32(sequenceNumber); |
| chunk.writeUint32(_width); |
| chunk.writeUint32(_height); |
| chunk.writeUint32(xOffset); |
| chunk.writeUint32(yOffset); |
| chunk.writeUint16(delay!); |
| chunk.writeUint16(1000); // delay denominator |
| chunk.writeByte(disposeMethod.index); |
| chunk.writeByte(blendMethod.index); |
| _writeChunk(output!, 'fcTL', chunk.getBytes()); |
| } |
| |
| void _writeICCPChunk(OutputBuffer? out, ICCProfileData? iccp) { |
| if (iccp == null) { |
| return; |
| } |
| |
| final 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); |
| final crc = _crc(type, chunk); |
| out.writeUint32(crc); |
| } |
| |
| void _filter(Image image, List<int> out) { |
| var oi = 0; |
| for (var 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 (var x = 0; x < image.width; ++x) { |
| final c = image.getPixel(x, row); |
| out[oi++] = getRed(c); |
| out[oi++] = getGreen(c); |
| out[oi++] = getBlue(c); |
| if (image.channels == Channels.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.channels == Channels.rgba) { |
| out[oi++] = getAlpha(image.getPixel(0, row)); |
| } |
| |
| for (var x = 1; x < image.width; ++x) { |
| final ar = getRed(image.getPixel(x - 1, row)); |
| final ag = getGreen(image.getPixel(x - 1, row)); |
| final ab = getBlue(image.getPixel(x - 1, row)); |
| |
| final r = getRed(image.getPixel(x, row)); |
| final g = getGreen(image.getPixel(x, row)); |
| final b = getBlue(image.getPixel(x, row)); |
| |
| out[oi++] = ((r - ar)) & 0xff; |
| out[oi++] = ((g - ag)) & 0xff; |
| out[oi++] = ((b - ab)) & 0xff; |
| if (image.channels == Channels.rgba) { |
| final aa = getAlpha(image.getPixel(x - 1, row)); |
| final 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 (var x = 0; x < image.width; ++x) { |
| final br = (row == 0) ? 0 : getRed(image.getPixel(x, row - 1)); |
| final bg = (row == 0) ? 0 : getGreen(image.getPixel(x, row - 1)); |
| final bb = (row == 0) ? 0 : getBlue(image.getPixel(x, row - 1)); |
| |
| final xr = getRed(image.getPixel(x, row)); |
| final xg = getGreen(image.getPixel(x, row)); |
| final xb = getBlue(image.getPixel(x, row)); |
| |
| out[oi++] = (xr - br) & 0xff; |
| out[oi++] = (xg - bg) & 0xff; |
| out[oi++] = (xb - bb) & 0xff; |
| if (image.channels == Channels.rgba) { |
| final ba = (row == 0) ? 0 : getAlpha(image.getPixel(x, row - 1)); |
| final 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 (var x = 0; x < image.width; ++x) { |
| final ar = (x == 0) ? 0 : getRed(image.getPixel(x - 1, row)); |
| final ag = (x == 0) ? 0 : getGreen(image.getPixel(x - 1, row)); |
| final ab = (x == 0) ? 0 : getBlue(image.getPixel(x - 1, row)); |
| |
| final br = (row == 0) ? 0 : getRed(image.getPixel(x, row - 1)); |
| final bg = (row == 0) ? 0 : getGreen(image.getPixel(x, row - 1)); |
| final bb = (row == 0) ? 0 : getBlue(image.getPixel(x, row - 1)); |
| |
| final xr = getRed(image.getPixel(x, row)); |
| final xg = getGreen(image.getPixel(x, row)); |
| final 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.channels == Channels.rgba) { |
| final aa = (x == 0) ? 0 : getAlpha(image.getPixel(x - 1, row)); |
| final ba = (row == 0) ? 0 : getAlpha(image.getPixel(x, row - 1)); |
| final xa = getAlpha(image.getPixel(x, row)); |
| out[oi++] = (xa - ((aa + ba) >> 1)) & 0xff; |
| } |
| } |
| |
| return oi; |
| } |
| |
| int _paethPredictor(int a, int b, int c) { |
| final p = a + b - c; |
| final pa = (p > a) ? p - a : a - p; |
| final pb = (p > b) ? p - b : b - p; |
| final 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 (var x = 0; x < image.width; ++x) { |
| final ar = (x == 0) ? 0 : getRed(image.getPixel(x - 1, row)); |
| final ag = (x == 0) ? 0 : getGreen(image.getPixel(x - 1, row)); |
| final ab = (x == 0) ? 0 : getBlue(image.getPixel(x - 1, row)); |
| |
| final br = (row == 0) ? 0 : getRed(image.getPixel(x, row - 1)); |
| final bg = (row == 0) ? 0 : getGreen(image.getPixel(x, row - 1)); |
| final bb = (row == 0) ? 0 : getBlue(image.getPixel(x, row - 1)); |
| |
| final cr = |
| (row == 0 || x == 0) ? 0 : getRed(image.getPixel(x - 1, row - 1)); |
| final cg = |
| (row == 0 || x == 0) ? 0 : getGreen(image.getPixel(x - 1, row - 1)); |
| final cb = |
| (row == 0 || x == 0) ? 0 : getBlue(image.getPixel(x - 1, row - 1)); |
| |
| final xr = getRed(image.getPixel(x, row)); |
| final xg = getGreen(image.getPixel(x, row)); |
| final xb = getBlue(image.getPixel(x, row)); |
| |
| final pr = _paethPredictor(ar, br, cr); |
| final pg = _paethPredictor(ag, bg, cg); |
| final pb = _paethPredictor(ab, bb, cb); |
| |
| out[oi++] = (xr - pr) & 0xff; |
| out[oi++] = (xg - pg) & 0xff; |
| out[oi++] = (xb - pb) & 0xff; |
| if (image.channels == Channels.rgba) { |
| final aa = (x == 0) ? 0 : getAlpha(image.getPixel(x - 1, row)); |
| final ba = (row == 0) ? 0 : getAlpha(image.getPixel(x, row - 1)); |
| final ca = |
| (row == 0 || x == 0) ? 0 : getAlpha(image.getPixel(x - 1, row - 1)); |
| final xa = getAlpha(image.getPixel(x, row)); |
| final 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) { |
| final crc = getCrc32(type.codeUnits); |
| return getCrc32(bytes, crc); |
| } |
| |
| Channels? channels; |
| int filter; |
| int repeat = 0; |
| int? level; |
| late int xOffset; |
| late int yOffset; |
| int? delay; |
| late DisposeMode disposeMethod; |
| late BlendMode blendMethod; |
| late int _width; |
| late int _height; |
| late int _frames; |
| int sequenceNumber = 0; |
| bool isAnimated = false; |
| OutputBuffer? output; |
| |
| static const FILTER_NONE = 0; |
| static const FILTER_SUB = 1; |
| static const FILTER_UP = 2; |
| static const FILTER_AVERAGE = 3; |
| static const FILTER_PAETH = 4; |
| static const FILTER_AGRESSIVE = 5; |
| |
| // Table of CRCs of all 8-bit messages. |
| //final List<int> _crcTable = List<int>(256); |
| } |