Last active
October 4, 2022 22:11
-
-
Save jonahwilliams/56bc6212445d813a8579e6a395327a29 to your computer and use it in GitHub Desktop.
Smart-er image provider
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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