Image decoding without dart:ui
Open the live demo · Read the source · View on GitHub
flutter3d_core cannot import dart:ui: it has no window, and a texture
node's own bake needs pixels somewhere that does not require one either.
So PNG, JPEG and Radiance .hdr are each read by a decoder written here,
not borrowed from Flutter. This page runs all three.
Step 1: A real JPEG, encoded elsewhere #
This is a real, quality-100 JPEG of a flat colour, one 8x8 block, the smallest a baseline JPEG can be.
// A real JPEG, one 8x8 MCU encoded at quality 100 from a flat
// (200, 100, 50) fill, the same fixture the decoder's own test checks
// against that exact colour.
static const String _jpegBase64 =
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB'
'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEB'
'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAAR'
'CAAIAAgDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA'
'AgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK'
'FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG'
'h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl'
'5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA'
'AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk'
'NOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE'
'hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk'
'5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDn6/yvP9sD/9k=';
Step 2: Encode a PNG, and decode it back #
Unlike the JPEG, the PNG here is built by this page: encodeCompressedPng
writes a small checkerboard, and decodePng reads it straight back.
// A tiny checkerboard, encoded here and decoded straight back: a real
// compressed PNG, not a fixture on disk.
final width = 4, height = 4;
final rgba = Uint8List(width * height * 4);
for (var y = 0; y < height; y++) {
for (var x = 0; x < width; x++) {
final lit = (x + y).isEven;
final o = (y * width + x) * 4;
rgba[o] = lit ? 240 : 20;
rgba[o + 1] = lit ? 240 : 20;
rgba[o + 2] = lit ? 240 : 20;
rgba[o + 3] = 255;
}
}
final pngBytes = encodeCompressedPng(width, height, rgba);
_png = decodePng(pngBytes)!;
Step 3: Decode the JPEG #
decodeJpeg is this package's own baseline decoder: Huffman tables,
chroma upsampling, the inverse DCT, all of it. A flat fill has no block
edges to hide a rounding error behind, which is why the source file is
exactly that.
final jpegBytes = base64Decode(_jpegBase64);
_jpeg = decodeJpeg(jpegBytes)!;
Step 4: Decode a hand-built HDR #
Radiance's .hdr stores three floats a pixel as four bytes: a shared
exponent and three mantissas. This file is two pixels wide, which is
under the width the newer run-length form requires, so it is written the
old, flat way: a header, then every pixel's four raw bytes in order.
// Radiance's old, flat scanline encoding: a header, a resolution line,
// then every pixel as four raw bytes (R, G, B, a shared exponent). Two
// by two is under the width the new run-length form requires, so this
// is read the simple way.
final hdrText = '#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n-Y 2 +X 2\n';
final hdrBytes = Uint8List.fromList(<int>[
...utf8.encode(hdrText),
// Top row: a dim red pixel, a bright red pixel.
255, 0, 0, 120, 255, 0, 0, 128,
// Bottom row: two pixels at half that brightness again.
255, 0, 0, 119, 255, 0, 0, 127,
]);
_hdr = readHdr(hdrBytes);
Step 5: Read the numbers back #
Three decoders, three answers, none of them touching dart:ui.
String _report() =>
'PNG: ${_png.width}x${_png.height}, top-left pixel '
'${_png.rgba[0]},${_png.rgba[1]},${_png.rgba[2]}\n'
'JPEG: ${_jpeg.width}x${_jpeg.height}, decodes back to about '
'${_jpeg.rgba[0]},${_jpeg.rgba[1]},${_jpeg.rgba[2]} (encoded from '
'200,100,50)\n'
'HDR: ${_hdr.width}x${_hdr.height}, top-left red channel '
'${_hdr.rgb[0].toStringAsFixed(3)}';
Step 6: Check the claim #
The PNG has to survive its own round trip, the JPEG has to decode close to the colour it was encoded from, and the HDR pixel has to come back brighter than black.
if (_png.width != 4 || _png.height != 4) {
throw StateError('the PNG did not round-trip its own size');
}
if (_png.rgba[0] < 200) {
throw StateError('the PNG round trip lost its lit corner');
}
if ((_jpeg.rgba[0] - 200).abs() > 5 || (_jpeg.rgba[1] - 100).abs() > 5) {
throw StateError('the JPEG did not decode close to its own fill');
}
if (_hdr.rgb[0] <= 0.0) {
throw StateError('the HDR pixel decoded to nothing');
}
if (frame.drawCalls < 1) {
throw StateError('the block was not drawn');
}