libjpeg_turbo_dart 1.1.0
libjpeg_turbo_dart: ^1.1.0 copied to clipboard
Pure-Dart port of libjpeg-turbo: JPEG decode, encode, and lossless transforms with output verified byte-for-byte against the C library. On web, requires WebAssembly.
// Encode and decode a JPEG entirely in memory.
//
// This example touches no files, so it runs on every supported platform,
// including Flutter web built with `flutter build web --wasm`.
//
// Run it with: dart run example/example.dart
import 'dart:typed_data';
import 'package:libjpeg_turbo_dart/libjpeg_turbo_dart.dart';
const int width = 256;
const int height = 192;
/// Build an interleaved RGB test image (a smooth gradient plus a grid).
Uint8List buildRgbImage() {
final pixels = Uint8List(width * height * 3);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
final i = (y * width + x) * 3;
final onGrid = x % 32 == 0 || y % 32 == 0;
pixels[i + 0] = onGrid ? 255 : x;
pixels[i + 1] = onGrid ? 255 : y * 255 ~/ (height - 1);
pixels[i + 2] = onGrid ? 255 : 128;
}
}
return pixels;
}
/// Compress interleaved RGB to a JPEG at the given quality.
Uint8List encode(Uint8List rgb, {int quality = 90}) {
final cinfo = JpegCompress();
cinfo.err = jpegStdError();
jpegCreateCompress(cinfo);
try {
// Destination buffer; it grows automatically if the image needs more.
jpegMemDest(cinfo, 1 << 16);
cinfo.imageWidth = width;
cinfo.imageHeight = height;
cinfo.inputComponents = 3;
cinfo.inColorSpace = JColorSpace.rgb;
jpegSetDefaults(cinfo);
jpegSetQuality(cinfo, quality, false);
jpegStartCompress(cinfo, true);
final row = Uint8List(width * 3);
while (cinfo.nextScanline < cinfo.imageHeight) {
final start = cinfo.nextScanline * width * 3;
row.setRange(0, width * 3, rgb, start);
jpegWriteScanlines(cinfo, [row], 1);
}
jpegFinishCompress(cinfo);
return jpegMemDestGetBuffer(cinfo);
} finally {
jpegDestroyCompress(cinfo);
}
}
/// Decompress a JPEG to interleaved RGB.
({Uint8List pixels, int width, int height, int components}) decode(
Uint8List jpeg,
) {
final cinfo = JpegDecompress();
cinfo.err = jpegStdError();
jpegCreateDecompress(cinfo, jpegLibVersion, 0);
try {
jpegMemSrc(cinfo, jpeg);
jpegReadHeader(cinfo, true);
jpegStartDecompress(cinfo);
final w = cinfo.outputWidth;
final h = cinfo.outputHeight;
final nc = cinfo.outputComponents;
final pixels = Uint8List(w * h * nc);
// Uint8List rows take a fast path in the color converters.
final row = Uint8List(w * nc);
while (cinfo.outputScanline < h) {
final y = cinfo.outputScanline;
jpegReadScanlines(cinfo, [row], 1);
pixels.setRange(y * w * nc, (y + 1) * w * nc, row);
}
jpegFinishDecompress(cinfo);
return (pixels: pixels, width: w, height: h, components: nc);
} finally {
jpegDestroyDecompress(cinfo);
}
}
void main() {
final original = buildRgbImage();
print('original: $width x $height RGB, ${original.length} bytes');
for (final quality in [50, 90]) {
final jpeg = encode(original, quality: quality);
final decoded = decode(jpeg);
// Mean absolute error against the source pixels, as a sanity check that
// the round trip actually reproduces the image.
int total = 0;
for (int i = 0; i < original.length; i++) {
total += (original[i] - decoded.pixels[i]).abs();
}
final mae = total / original.length;
final ratio = original.length / jpeg.length;
print(
'quality $quality: ${jpeg.length} bytes '
'(${ratio.toStringAsFixed(1)}x smaller), '
'decoded ${decoded.width}x${decoded.height}'
' x${decoded.components}, mean error ${mae.toStringAsFixed(2)}',
);
}
// Errors in the JPEG data surface as catchable JpegError exceptions.
try {
decode(Uint8List.fromList([0x00, 0x01, 0x02, 0x03]));
} on JpegError catch (e) {
print('rejected non-JPEG input as expected: ${e.message}');
}
}