Skip to content

Instantly share code, notes, and snippets.

@jonahwilliams
Last active October 4, 2022 22:11
Show Gist options
  • Save jonahwilliams/56bc6212445d813a8579e6a395327a29 to your computer and use it in GitHub Desktop.
Save jonahwilliams/56bc6212445d813a8579e6a395327a29 to your computer and use it in GitHub Desktop.
Smart-er image provider
class SmartFileProvider extends ImageProvider<SmartFileProvider> {
const SmartFileProvider({required this.file, this.scale = 1.0});
/// The file to decode into an image.
final File file;
/// The scale to place in the [ImageInfo] object of the image.
final double scale;
static const int _kMaxFileSize = 2147483647; /* exercise left for the reader */
static bool _isValidImage(Uint8List bytes) {
return isPng(bytes) || isGif(bytes) || isJpeg(bytes) || isWebP(bytes);
}
/// Returns true if `bytes` starts with the expected header for a PNG image.
static bool isPng(Uint8List bytes) {
return bytes.lengthInBytes > 20 &&
bytes[0] == 0x89 &&
bytes[1] == 0x50 &&
bytes[2] == 0x4E &&
bytes[3] == 0x47 &&
bytes[4] == 0x0D &&
bytes[5] == 0x0A &&
bytes[6] == 0x1A &&
bytes[7] == 0x0A;
}
/// Returns true if `bytes` starts with the expected header for a GIF image.
static bool isGif(Uint8List bytes) {
return bytes.lengthInBytes > 8 &&
bytes[0] == 0x47 &&
bytes[1] == 0x49 &&
bytes[2] == 0x46 &&
bytes[3] == 0x38 &&
(bytes[4] == 0x37 || bytes[4] == 0x39) // 7 or 9
&&
bytes[5] == 0x61;
}
/// Returns true if `bytes` starts with the expected header for a JPEG image.
static bool isJpeg(Uint8List bytes) {
return bytes.lengthInBytes > 12 &&
bytes[0] == 0xFF &&
bytes[1] == 0xD8 &&
bytes[2] == 0xFF;
}
/// Returns true if `bytes` starts with the expected header for a WebP image.
static bool isWebP(Uint8List bytes) {
return bytes.lengthInBytes > 28 &&
bytes[0] == 0x52 // R
&&
bytes[1] == 0x49 // I
&&
bytes[2] == 0x46 // F
&&
bytes[3] == 0x46 // F
&&
bytes[8] == 0x57 // W
&&
bytes[9] == 0x45 // E
&&
bytes[10] == 0x42 // B
&&
bytes[11] == 0x50; // P
}
@override
Future<SmartFileProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<SmartFileProvider>(this);
}
@override
ImageStreamCompleter load(SmartFileProvider key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, null, decode),
scale: key.scale,
debugLabel: key.file.path,
informationCollector: () => <DiagnosticsNode>[
ErrorDescription('Path: ${file.path}'),
],
);
}
@override
ImageStreamCompleter loadBuffer(
SmartFileProvider key, DecoderBufferCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode, null),
scale: key.scale,
debugLabel: key.file.path,
informationCollector: () => <DiagnosticsNode>[
ErrorDescription('Path: ${file.path}'),
],
);
}
Future<ui.Codec> _loadAsync(SmartFileProvider key,
DecoderBufferCallback? decode, DecoderCallback? decodeDeprecated) async {
assert(key == this);
// First check if file is too big.
final int length = file.lengthSync();
if (length >= _kMaxFileSize) {
throw Exception('File was too big!');
}
// Then check if is probably an image, if the size is greater
// than the biggest known header. Otherwise, just read it all.
if (length > 28) {
final Uint8List header = Uint8List(28);
final RandomAccessFile raf = file.openSync();
raf.readIntoSync(header, 0, 28);
raf.closeSync();
if (!_isValidImage(header)) {
throw Exception('Not a known image type!');
}
}
final Uint8List bytes = await file.readAsBytes();
if (bytes.lengthInBytes == 0) {
// The file may become available later.
PaintingBinding.instance.imageCache.evict(key);
throw StateError('$file is empty and cannot be loaded as an image.');
}
if (decode != null) {
return decode(await ui.ImmutableBuffer.fromUint8List(bytes));
}
return decodeDeprecated!(bytes);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is SmartFileProvider &&
other.file.path == file.path &&
other.scale == scale;
}
@override
int get hashCode => Object.hash(file.path, scale);
@override
String toString() =>
'${objectRuntimeType(this, 'SmartFileImage')}("${file.path}", scale: $scale)';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment