Skip to content

Instantly share code, notes, and snippets.

@karnadii
Last active March 30, 2024 23:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save karnadii/a9f2fdac2186cc2ea713201a7efc2ca2 to your computer and use it in GitHub Desktop.
Save karnadii/a9f2fdac2186cc2ea713201a7efc2ca2 to your computer and use it in GitHub Desktop.
prefetch image size
import 'dart:async';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
enum ImageFormat { jpg, gif, png, webp }
class Size {
final int width;
final int height;
Size(this.width, this.height);
}
class ImageInfo {
final String url;
final ImageFormat? format;
final Size? size;
ImageInfo(this.url, this.format, this.size);
}
class ImageSizeFetcher {
Future<List<ImageInfo>> fetchImageSizes(List<String> imageUrls) async {
final List<ImageInfo> imageInfoList = [];
for (String url in imageUrls) {
final client = http.Client();
final request = http.Request('GET', Uri.parse(url));
final response = await client.send(request);
final completer = Completer<ImageInfo>();
var bytesRead = 0;
var buffer = BytesBuilder();
// Determine the image format based on the URL extension
final imageFormat = _getImageFormatFromUrl(url);
late StreamSubscription subscription;
subscription = response.stream.listen(
(List<int> data) {
buffer.add(data);
bytesRead += data.length;
if (imageFormat != null) {
// Extract the image size based on the format
final size = _extractImageSize(buffer.toBytes(), imageFormat);
if (size != null) {
completer.complete(ImageInfo(url, imageFormat, size));
subscription.cancel();
client.close(); // Cancel the subscription to stop receiving more data
}
} else {
// If no format was determined from the URL, check the header
final headerFormat = _getImageFormatFromHeader(buffer.toBytes());
if (headerFormat != null) {
final size = _extractImageSize(buffer.toBytes(), headerFormat);
if (size != null) {
completer.complete(ImageInfo(url, headerFormat, size));
subscription.cancel();
client.close(); // Cancel the subscription to stop receiving more data
}
}
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete(ImageInfo(url, null, null));
}
},
onError: (error) {
completer.completeError(error);
},
cancelOnError: true,
);
final imageInfo = await completer.future;
imageInfoList.add(imageInfo);
}
return imageInfoList;
}
ImageFormat? _getImageFormatFromUrl(String url) {
final uri = Uri.parse(url);
final extension = uri.pathSegments.last.split('.').last.toLowerCase();
switch (extension) {
case 'jpg':
case 'jpeg':
return ImageFormat.jpg;
case 'gif':
return ImageFormat.gif;
case 'png':
return ImageFormat.png;
case 'webp':
return ImageFormat.webp;
default:
return null;
}
}
ImageFormat? _getImageFormatFromHeader(Uint8List bytes) {
if (bytes.length >= 2) {
if (bytes[0] == 0xFF && bytes[1] == 0xD8) {
return ImageFormat.jpg;
} else if (bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46) {
return ImageFormat.gif;
} else if (bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47) {
return ImageFormat.png;
} else if (bytes[0] == 0x52 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x46) {
return ImageFormat.webp;
}
}
return null;
}
Size? _extractImageSize(Uint8List bytes, ImageFormat format) {
switch (format) {
case ImageFormat.jpg:
int i = 0;
// check for valid JPEG image
// http://www.fastgraph.com/help/jpeg_header_format.html
if (bytes[i] != 0xFF || bytes[i + 1] != 0xD8 || bytes[i + 2] != 0xFF) {
throw Exception('Not a valid JPEG image'); // Not a valid SOI header
}
i += 4;
// Check for valid JPEG header (null terminated JFIF)
// if (bytes[i + 2] != 0x4A ||
// bytes[i + 3] != 0x46 ||
// bytes[i + 4] != 0x49 ||
// bytes[i + 5] != 0x46 ||
// bytes[i + 6] != 0x00) {
// throw Exception('Not a valid JFIF image'); // Not a valid JFIF string
// }
// Retrieve the block length of the first block since the
// first block will not contain the size of file
int blockLength = bytes[i] * 256 + bytes[i + 1];
do {
i += blockLength; // Increase the file index to get to the next block
if (i >= bytes.length) {
// Check to protect against segmentation faults
return null;
}
if (bytes[i] != 0xFF) {
// Check that we are truly at the start of another block
return null;
}
if (bytes[i + 1] >= 0xC0 && bytes[i + 1] <= 0xC3) {
// if marker type is SOF0, SOF1, SOF2
// "Start of frame" marker which contains the file size
int height = bytes[i + 5] << 8 | bytes[i + 6];
int width = bytes[i + 7] << 8 | bytes[i + 8];
final size = Size(width, height);
return size;
} else {
// Skip the block marker
i += 2;
blockLength = bytes[i] * 256 + bytes[i + 1]; // Go to the next block
}
} while (i < bytes.length);
return null;
case ImageFormat.gif:
// GIF header format: GIF89a<width><height>
if (bytes.length >= 10) {
final width = bytes[6] | (bytes[7] << 8);
final height = bytes[8] | (bytes[9] << 8);
return Size(width, height);
}
break;
case ImageFormat.png:
// PNG header format: \x89PNG\r\n\x1a\n<length><IHDR>
if (bytes.length >= 24) {
final width = (bytes[16] << 24) | (bytes[17] << 16) | (bytes[18] << 8) | bytes[19];
final height = (bytes[20] << 24) | (bytes[21] << 16) | (bytes[22] << 8) | bytes[23];
return Size(width, height);
}
break;
case ImageFormat.webp:
// WebP header format: RIFF<fileSize>WEBPVP8<width><height>
if (bytes.length >= 30) {
final width = (bytes[26] << 8) | bytes[27];
final height = (bytes[28] << 8) | bytes[29];
return Size(width, height);
}
break;
}
return null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment