Image decoding without dart:ui: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: jpeg-source (line 19), png (line 38), jpeg (line 55), hdr (line 58), report (line 86), check (line 113).
1/// PNG, JPEG and Radiance `.hdr`, decoded with no `dart:ui` in sight.
2///
3/// Quoted by `image_decode.md` and shown whole in the Source tab.
4library;
6import 'dart:convert';
7import 'dart:typed_data';
9import 'package:flutter/widgets.dart';
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:vector_math/vector_math.dart';
14final class ImageDecodeDemo extends ShowcaseDemo {
15 late final DecodedImage _png;
16 late final DecodedImage _jpeg;
17 late final HdrImage _hdr;
19 // A real JPEG, one 8x8 MCU encoded at quality 100 from a flat
20 // (200, 100, 50) fill, the same fixture the decoder's own test checks
21 // against that exact colour.
22 static const String _jpegBase64 =
23 '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB'
24 'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEB'
25 'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAAR'
26 'CAAIAAgDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA'
27 'AgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK'
28 'FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG'
29 'h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl'
30 '5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA'
31 'AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk'
32 'NOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE'
33 'hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk'
34 '5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDn6/yvP9sD/9k=';
37 Scene build(DemoContext context) {
38 // A tiny checkerboard, encoded here and decoded straight back: a real
39 // compressed PNG, not a fixture on disk.
40 final width = 4, height = 4;
41 final rgba = Uint8List(width * height * 4);
42 for (var y = 0; y < height; y++) {
43 for (var x = 0; x < width; x++) {
44 final lit = (x + y).isEven;
45 final o = (y * width + x) * 4;
46 rgba[o] = lit ? 240 : 20;
47 rgba[o + 1] = lit ? 240 : 20;
48 rgba[o + 2] = lit ? 240 : 20;
49 rgba[o + 3] = 255;
50 }
51 }
52 final pngBytes = encodeCompressedPng(width, height, rgba);
53 _png = decodePng(pngBytes)!;
55 final jpegBytes = base64Decode(_jpegBase64);
56 _jpeg = decodeJpeg(jpegBytes)!;
58 // Radiance's old, flat scanline encoding: a header, a resolution line,
59 // then every pixel as four raw bytes (R, G, B, a shared exponent). Two
60 // by two is under the width the new run-length form requires, so this
61 // is read the simple way.
62 final hdrText = '#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n-Y 2 +X 2\n';
63 final hdrBytes = Uint8List.fromList(<int>[
64 ...utf8.encode(hdrText),
65 // Top row: a dim red pixel, a bright red pixel.
66 255, 0, 0, 120, 255, 0, 0, 128,
67 // Bottom row: two pixels at half that brightness again.
68 255, 0, 0, 119, 255, 0, 0, 127,
69 ]);
70 _hdr = readHdr(hdrBytes);
72 return Scene()
73 ..add(
74 MeshNode(
75 DeviceMesh.upload(context.device, CuboidShape().build()),
76 Material(baseColor: Vector4(0.6, 0.6, 0.6, 1.0)),
77 name: 'block',
78 ),
79 )
80 ..add(
81 LightNode(name: 'sun', intensity: 3.0)
82 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
83 );
84 }
86 String _report() =>
87 'PNG: ${_png.width}x${_png.height}, top-left pixel '
88 '${_png.rgba[0]},${_png.rgba[1]},${_png.rgba[2]}\n'
89 'JPEG: ${_jpeg.width}x${_jpeg.height}, decodes back to about '
90 '${_jpeg.rgba[0]},${_jpeg.rgba[1]},${_jpeg.rgba[2]} (encoded from '
91 '200,100,50)\n'
92 'HDR: ${_hdr.width}x${_hdr.height}, top-left red channel '
93 '${_hdr.rgb[0].toStringAsFixed(3)}';
96 Widget? customBody(BuildContext buildContext, DemoContext context) =>
97 Container(
98 color: const Color(0xFF14161A),
99 padding: const EdgeInsets.all(24),
100 alignment: Alignment.topLeft,
101 child: DefaultTextStyle(
102 style: const TextStyle(
103 color: Color(0xFFE8E8EC),
104 fontSize: 16,
105 fontFamily: 'monospace',
106 ),
107 child: Text(_report()),
108 ),
109 );
112 void verify(Scene scene, FrameResult frame) {
113 if (_png.width != 4 || _png.height != 4) {
114 throw StateError('the PNG did not round-trip its own size');
115 }
116 if (_png.rgba[0] < 200) {
117 throw StateError('the PNG round trip lost its lit corner');
118 }
119 if ((_jpeg.rgba[0] - 200).abs() > 5 || (_jpeg.rgba[1] - 100).abs() > 5) {
120 throw StateError('the JPEG did not decode close to its own fill');
121 }
122 if (_hdr.rgb[0] <= 0.0) {
123 throw StateError('the HDR pixel decoded to nothing');
124 }
125 if (frame.drawCalls < 1) {
126 throw StateError('the block was not drawn');
127 }
128 }
129}