From 4e03901225fef873217913c3573cc85766e0ee67 Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Wed, 4 Dec 2024 17:19:02 +0800 Subject: [PATCH 01/16] 1 1 --- .../cache_managers/image_cache_manager.dart | 28 ++++++++++++++++++- .../lib/src/compat/file_service_compat.dart | 16 ++++++----- flutter_cache_manager/pubspec.yaml | 2 +- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart index 614d7d9d..d0c1383a 100644 --- a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart +++ b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart @@ -61,6 +61,31 @@ mixin ImageCacheManager on BaseCacheManager { _runningResizes.remove(resizedKey); } + Future getExt(File file) async { + if (!await file.exists()) { + debugPrint('文件不存在: ${file.path}'); + return false; + } + final raf = await file.open(); + final header = await raf.read(8); + await raf.close(); + final hexString = + header.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(''); + if (hexString.startsWith('ffd8')) { + return true; // JPEG + } else if (hexString.startsWith('89504e47')) { + return true; // PNG + } else if (hexString.startsWith('00000200')) { + return true; // TGA + } else if (hexString.startsWith('00000100')) { + return true; // CUR + } else if (hexString.startsWith('00000000')) { + return true; // ICO + } + + return false; // 不支持的文件类型 + } + final Map> _runningResizes = {}; Future _resizeImageFile( @@ -69,9 +94,10 @@ mixin ImageCacheManager on BaseCacheManager { int? maxWidth, int? maxHeight, ) async { + bool isSupported = await getExt(originalFile.file); final originalFileName = originalFile.file.path; final fileExtension = originalFileName.split('.').last; - if (!supportedFileNames.contains(fileExtension)) { + if (isSupported) { return originalFile; } diff --git a/flutter_cache_manager/lib/src/compat/file_service_compat.dart b/flutter_cache_manager/lib/src/compat/file_service_compat.dart index 0df201e1..bbd7a6ff 100644 --- a/flutter_cache_manager/lib/src/compat/file_service_compat.dart +++ b/flutter_cache_manager/lib/src/compat/file_service_compat.dart @@ -63,13 +63,15 @@ class CompatFileServiceGetResponse implements FileServiceResponse { @override String get fileExtension { - var fileExtension = ''; - final contentTypeHeader = _header(HttpHeaders.contentTypeHeader); - if (contentTypeHeader != null) { - final contentType = ContentType.parse(contentTypeHeader); - fileExtension = contentType.fileExtension; - } - return fileExtension; + // var fileExtension = ''; + // final contentTypeHeader = _header(HttpHeaders.contentTypeHeader); + // if (contentTypeHeader != null) { + // final contentType = ContentType.parse(contentTypeHeader); + // fileExtension = contentType.fileExtension; + // } + // return fileExtension; + + return '.jpeg'; } @override diff --git a/flutter_cache_manager/pubspec.yaml b/flutter_cache_manager/pubspec.yaml index 4e49f688..ed849972 100644 --- a/flutter_cache_manager/pubspec.yaml +++ b/flutter_cache_manager/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: uuid: ^4.4.2 dev_dependencies: - build_runner: ^2.4.12 + build_runner: ^2.4.9 flutter_lints: ^4.0.0 flutter_test: sdk: flutter From 74e71ca96a38432cf4b15fb870e2cb1d98fd4e2a Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Wed, 4 Dec 2024 17:37:42 +0800 Subject: [PATCH 02/16] Update file_service_compat.dart --- .../lib/src/compat/file_service_compat.dart | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/flutter_cache_manager/lib/src/compat/file_service_compat.dart b/flutter_cache_manager/lib/src/compat/file_service_compat.dart index bbd7a6ff..0df201e1 100644 --- a/flutter_cache_manager/lib/src/compat/file_service_compat.dart +++ b/flutter_cache_manager/lib/src/compat/file_service_compat.dart @@ -63,15 +63,13 @@ class CompatFileServiceGetResponse implements FileServiceResponse { @override String get fileExtension { - // var fileExtension = ''; - // final contentTypeHeader = _header(HttpHeaders.contentTypeHeader); - // if (contentTypeHeader != null) { - // final contentType = ContentType.parse(contentTypeHeader); - // fileExtension = contentType.fileExtension; - // } - // return fileExtension; - - return '.jpeg'; + var fileExtension = ''; + final contentTypeHeader = _header(HttpHeaders.contentTypeHeader); + if (contentTypeHeader != null) { + final contentType = ContentType.parse(contentTypeHeader); + fileExtension = contentType.fileExtension; + } + return fileExtension; } @override From b6105f7e80d1b60253acae2106a996dfa8c6485b Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Wed, 4 Dec 2024 17:39:30 +0800 Subject: [PATCH 03/16] Update image_cache_manager.dart --- .../lib/src/cache_managers/image_cache_manager.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart index d0c1383a..7de21ac4 100644 --- a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart +++ b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart @@ -63,7 +63,7 @@ mixin ImageCacheManager on BaseCacheManager { Future getExt(File file) async { if (!await file.exists()) { - debugPrint('文件不存在: ${file.path}'); + debugPrint('file is not found: ${file.path}'); return false; } final raf = await file.open(); @@ -83,7 +83,7 @@ mixin ImageCacheManager on BaseCacheManager { return true; // ICO } - return false; // 不支持的文件类型 + return false; } final Map> _runningResizes = {}; From 60286186967dcca9668a68b1dd19100a76506822 Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Wed, 4 Dec 2024 17:57:16 +0800 Subject: [PATCH 04/16] fix: fix method name --- .../lib/src/cache_managers/image_cache_manager.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart index 7de21ac4..57ba59d8 100644 --- a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart +++ b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart @@ -61,7 +61,7 @@ mixin ImageCacheManager on BaseCacheManager { _runningResizes.remove(resizedKey); } - Future getExt(File file) async { + Future _isImage(File file) async { if (!await file.exists()) { debugPrint('file is not found: ${file.path}'); return false; @@ -94,10 +94,10 @@ mixin ImageCacheManager on BaseCacheManager { int? maxWidth, int? maxHeight, ) async { - bool isSupported = await getExt(originalFile.file); + bool isImage = await _isImage(originalFile.file); final originalFileName = originalFile.file.path; final fileExtension = originalFileName.split('.').last; - if (isSupported) { + if (!isImage) { return originalFile; } From 4ec2651d1cc9cd728fa7e9880bb10757aa59c5c0 Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Thu, 5 Dec 2024 11:36:55 +0800 Subject: [PATCH 05/16] Update build.gradle --- flutter_cache_manager/example/android/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter_cache_manager/example/android/build.gradle b/flutter_cache_manager/example/android/build.gradle index d2ffbffa..047207a5 100644 --- a/flutter_cache_manager/example/android/build.gradle +++ b/flutter_cache_manager/example/android/build.gradle @@ -16,3 +16,5 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir } + +ext.kotlin_version = '1.7.10' \ No newline at end of file From b8ebe93a17b3d4212d7e58ca5326ca1100b092c1 Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Thu, 5 Dec 2024 14:20:52 +0800 Subject: [PATCH 06/16] Update build.gradle --- flutter_cache_manager/example/android/build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flutter_cache_manager/example/android/build.gradle b/flutter_cache_manager/example/android/build.gradle index 047207a5..c5e4d410 100644 --- a/flutter_cache_manager/example/android/build.gradle +++ b/flutter_cache_manager/example/android/build.gradle @@ -1,3 +1,7 @@ +buildscript { + ext.kotlin_version = '1.7.10' +} + allprojects { repositories { google() @@ -17,4 +21,3 @@ tasks.register("clean", Delete) { delete rootProject.buildDir } -ext.kotlin_version = '1.7.10' \ No newline at end of file From 1d73d2eb92848b0587ada402673812d3a47cb67f Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Thu, 5 Dec 2024 14:26:13 +0800 Subject: [PATCH 07/16] 1 --- flutter_cache_manager/example/android/build.gradle | 2 +- flutter_cache_manager/example/android/settings.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter_cache_manager/example/android/build.gradle b/flutter_cache_manager/example/android/build.gradle index c5e4d410..2cda7602 100644 --- a/flutter_cache_manager/example/android/build.gradle +++ b/flutter_cache_manager/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.8.22' } allprojects { diff --git a/flutter_cache_manager/example/android/settings.gradle b/flutter_cache_manager/example/android/settings.gradle index 536165d3..8a53adcd 100644 --- a/flutter_cache_manager/example/android/settings.gradle +++ b/flutter_cache_manager/example/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "org.jetbrains.kotlin.android" version "1.8.22" apply false } include ":app" From 64ff206ec8ee542b0c7626fa65fac21c89f94066 Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Thu, 5 Dec 2024 14:34:18 +0800 Subject: [PATCH 08/16] Update build.gradle --- flutter_cache_manager/example/android/build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/flutter_cache_manager/example/android/build.gradle b/flutter_cache_manager/example/android/build.gradle index 2cda7602..457de897 100644 --- a/flutter_cache_manager/example/android/build.gradle +++ b/flutter_cache_manager/example/android/build.gradle @@ -1,7 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.22' -} - allprojects { repositories { google() From 1a55b01c2fcf46a0bb9dc5e47d38727ae7b34b5e Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Thu, 5 Dec 2024 14:36:23 +0800 Subject: [PATCH 09/16] Update build.gradle --- flutter_cache_manager/example/android/build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flutter_cache_manager/example/android/build.gradle b/flutter_cache_manager/example/android/build.gradle index 457de897..8e9de9d8 100644 --- a/flutter_cache_manager/example/android/build.gradle +++ b/flutter_cache_manager/example/android/build.gradle @@ -15,5 +15,4 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir -} - +} \ No newline at end of file From 2132fb42c07144b64684f338fc3b5c2406a0c977 Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Mon, 19 Oct 2026 09:42:27 +0800 Subject: [PATCH 10/16] fix(image): detect resizable images by content instead of url extension The resizer decided whether a file could be resized from the extension of the file on disk. That extension is derived from the url, so images served from extension-less urls, or from urls whose extension does not match the payload, were never resized. It also treated any zero-prefixed binary as an ico. - Detect jpeg, png, ico, cur and webp by their magic number, reading 12 bytes instead of 8 (webp needs the RIFF/WEBP marker at offset 8). - Keep an extension fallback for formats without a signature at the start of the file, like tga, which only stores a signature in its footer. - Read the header in a helper that always closes the file handle. - Restore the trailing newline in example/android/build.gradle, which was dropped by an earlier commit in this branch. - Bump to 3.4.6 with a dated changelog entry, as CONTRIBUTING now requires. Tests: cover an image served without an extension and a non-image payload served with an image extension. --- flutter_cache_manager/CHANGELOG.md | 4 + .../example/android/build.gradle | 2 +- .../cache_managers/image_cache_manager.dart | 78 ++++++++++++++----- flutter_cache_manager/pubspec.yaml | 2 +- .../test/image_cache_manager_test.dart | 37 +++++++++ 5 files changed, 100 insertions(+), 23 deletions(-) diff --git a/flutter_cache_manager/CHANGELOG.md b/flutter_cache_manager/CHANGELOG.md index 9513fb80..8ac9dfb4 100644 --- a/flutter_cache_manager/CHANGELOG.md +++ b/flutter_cache_manager/CHANGELOG.md @@ -1,3 +1,7 @@ +## [3.4.6] - 2026-10-19 + +* Detects whether a downloaded file can be resized from its content instead of from the extension of its url, so images served from extension-less urls or with a non-image `Content-Type` are resized, and responses that are no longer images are returned untouched ([#475](https://github.com/Baseflow/flutter_cache_manager/pull/475)) + ## [3.4.5] - 2026-09-18 * Prevents image resize decoding from completing more than once and propagates decoding errors ([#400](https://github.com/Baseflow/flutter_cache_manager/issues/400)). diff --git a/flutter_cache_manager/example/android/build.gradle b/flutter_cache_manager/example/android/build.gradle index 8e9de9d8..d2ffbffa 100644 --- a/flutter_cache_manager/example/android/build.gradle +++ b/flutter_cache_manager/example/android/build.gradle @@ -15,4 +15,4 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir -} \ No newline at end of file +} diff --git a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart index 6fb17f7d..52aae85a 100644 --- a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart +++ b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart @@ -7,6 +7,21 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; const supportedFileNames = ['jpg', 'jpeg', 'png', 'tga', 'cur', 'ico']; + +/// Signatures (magic numbers) of the image formats that can be recognized by +/// the first bytes of the file, instead of by its extension. +const _imageSignatures = >[ + [0xFF, 0xD8, 0xFF], // jpeg + [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], // png + [0x00, 0x00, 0x01, 0x00], // ico + [0x00, 0x00, 0x02, 0x00], // cur +]; + +/// Extensions of the formats that have no signature at the start of the file, +/// so the only way to recognize them is by their extension. Tga for example +/// only stores a signature in its footer. +const _extensionOnlyFormats = ['tga']; + mixin ImageCacheManager on BaseCacheManager { /// Returns a resized image file to fit within maxHeight and maxWidth. It /// tries to keep the aspect ratio. It stores the resized image by adding @@ -65,30 +80,28 @@ mixin ImageCacheManager on BaseCacheManager { _runningResizes.remove(resizedKey); } + /// Whether [file] holds an image that can be decoded and resized, based on + /// the content of the file rather than on the extension of its url. A + /// response can be served without a file extension, or with an extension + /// that does not match the actual content, for example when it is served + /// with an incorrect `Content-Type` header. Future _isImage(File file) async { if (!await file.exists()) { - debugPrint('file is not found: ${file.path}'); return false; } - final raf = await file.open(); - final header = await raf.read(8); - await raf.close(); - final hexString = header - .map((byte) => byte.toRadixString(16).padLeft(2, '0')) - .join(''); - if (hexString.startsWith('ffd8')) { - return true; // JPEG - } else if (hexString.startsWith('89504e47')) { - return true; // PNG - } else if (hexString.startsWith('00000200')) { - return true; // TGA - } else if (hexString.startsWith('00000100')) { - return true; // CUR - } else if (hexString.startsWith('00000000')) { - return true; // ICO - } - return false; + final header = await _readHeader(file); + if (_imageSignatures.any((signature) => _startsWith(header, signature))) { + return true; + } + // WebP is a RIFF container: "RIFF" + 4 bytes of length + "WEBP". + if (header.length >= 12 && + _startsWith(header, 'RIFF'.codeUnits) && + _startsWith(header.sublist(8), 'WEBP'.codeUnits)) { + return true; + } + final extension = file.path.split('.').last.toLowerCase(); + return _extensionOnlyFormats.contains(extension); } final Map> _runningResizes = {}; @@ -99,10 +112,9 @@ mixin ImageCacheManager on BaseCacheManager { int? maxWidth, int? maxHeight, ) async { - bool isImage = await _isImage(originalFile.file); final originalFileName = originalFile.file.path; final fileExtension = originalFileName.split('.').last; - if (!isImage) { + if (!await _isImage(originalFile.file)) { return originalFile; } @@ -174,6 +186,30 @@ mixin ImageCacheManager on BaseCacheManager { } } +/// Reads the first bytes of [file], up to [length] bytes, which is enough to +/// hold the longest signature that is checked. +Future> _readHeader(File file, {int length = 12}) async { + final raf = await file.open(); + try { + return await raf.read(length); + } finally { + await raf.close(); + } +} + +/// Whether [bytes] starts with [signature]. +bool _startsWith(List bytes, List signature) { + if (bytes.length < signature.length) { + return false; + } + for (var i = 0; i < signature.length; i++) { + if (bytes[i] != signature[i]) { + return false; + } + } + return true; +} + Future _decodeImage( File file, { int? width, diff --git a/flutter_cache_manager/pubspec.yaml b/flutter_cache_manager/pubspec.yaml index 3f0c9ce3..8456c4b5 100644 --- a/flutter_cache_manager/pubspec.yaml +++ b/flutter_cache_manager/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_cache_manager description: Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite. -version: 3.4.5 +version: 3.4.6 homepage: https://github.com/Baseflow/flutter_cache_manager topics: - cache diff --git a/flutter_cache_manager/test/image_cache_manager_test.dart b/flutter_cache_manager/test/image_cache_manager_test.dart index ce8ec599..e2390548 100644 --- a/flutter_cache_manager/test/image_cache_manager_test.dart +++ b/flutter_cache_manager/test/image_cache_manager_test.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; @@ -127,6 +128,42 @@ void main() { expect(progress, isNotEmpty); }); }); + + group('Test image detection', () { + test('Image without file extension should be resized', () async { + const urlWithoutExtension = 'baseflow.com/avatar/123456'; + var config = createTestConfig(); + await config.returnsFile('avatar', data: await getExampleImage()); + config.returnsCacheObject(urlWithoutExtension, 'avatar', validTill); + + var cacheManager = TestCacheManager(config); + var result = + await cacheManager + .getImageFile(urlWithoutExtension, maxWidth: 100) + .last + as FileInfo; + + var image = await result.file.readAsBytes(); + await verifySize(image, 100, 100); + }); + + test('Non image should not be resized', () async { + const urlWithImageExtension = 'baseflow.com/image.png'; + var payload = utf8.encode('{"message":"not an image"}'); + var config = createTestConfig(); + await config.returnsFile('image.png', data: payload); + config.returnsCacheObject(urlWithImageExtension, 'image.png', validTill); + + var cacheManager = TestCacheManager(config); + var result = + await cacheManager + .getImageFile(urlWithImageExtension, maxWidth: 100) + .last + as FileInfo; + + expect(await result.file.readAsBytes(), payload); + }); + }); } Future setupCacheManager() async { From 3a498ba50279af5e7137a602e125a54c986f4b7b Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Mon, 19 Oct 2026 13:33:19 +0800 Subject: [PATCH 11/16] fix(image): also detect bmp by signature and wbmp by extension Flutter decodes bmp and wbmp as well, so they are resizable too. Bmp has a 'BM' signature and can be detected from the content. Wbmp starts with a two byte header that is too generic to match on, so it joins tga in the extension fallback. --- .../lib/src/cache_managers/image_cache_manager.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart index 52aae85a..ff25bebd 100644 --- a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart +++ b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart @@ -13,14 +13,16 @@ const supportedFileNames = ['jpg', 'jpeg', 'png', 'tga', 'cur', 'ico']; const _imageSignatures = >[ [0xFF, 0xD8, 0xFF], // jpeg [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], // png + [0x42, 0x4D], // bmp [0x00, 0x00, 0x01, 0x00], // ico [0x00, 0x00, 0x02, 0x00], // cur ]; -/// Extensions of the formats that have no signature at the start of the file, -/// so the only way to recognize them is by their extension. Tga for example -/// only stores a signature in its footer. -const _extensionOnlyFormats = ['tga']; +/// Extensions of the formats that have no usable signature at the start of the +/// file, so the only way to recognize them is by their extension. Tga only +/// stores a signature in its footer, and wbmp starts with a two byte header +/// (`00 00`) that is too generic to match on. +const _extensionOnlyFormats = ['tga', 'wbmp']; mixin ImageCacheManager on BaseCacheManager { /// Returns a resized image file to fit within maxHeight and maxWidth. It From a7dac2af1dee6dd225349c09be22e3440199758e Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Mon, 19 Oct 2026 13:42:26 +0800 Subject: [PATCH 12/16] fix(image): match jpeg on its two byte signature Two bytes (the SOI marker) are enough to tell jpeg apart from the other formats that are checked, which is what SDWebImage and most detectors do. A file that is detected as an image but fails to decode then errors out of getImageFile, as it already did for a mislabeled extension before this change. --- .../lib/src/cache_managers/image_cache_manager.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart index ff25bebd..470a25a9 100644 --- a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart +++ b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart @@ -11,7 +11,7 @@ const supportedFileNames = ['jpg', 'jpeg', 'png', 'tga', 'cur', 'ico']; /// Signatures (magic numbers) of the image formats that can be recognized by /// the first bytes of the file, instead of by its extension. const _imageSignatures = >[ - [0xFF, 0xD8, 0xFF], // jpeg + [0xFF, 0xD8], // jpeg [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], // png [0x42, 0x4D], // bmp [0x00, 0x00, 0x01, 0x00], // ico From 780d6626a605549566542a1b0d6ee4d2728d8466 Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Mon, 19 Oct 2026 13:51:12 +0800 Subject: [PATCH 13/16] fix(image): store the resized image with a png extension The extension that was passed to putFile came from the cached file path. For a url without an extension that path ends in the key itself, so the extension contained a slash and putFile failed to create the file. This is what the new test for an extension-less url caught. The resized image is always encoded as png anyway, so name it png instead of copying the extension of the url. --- flutter_cache_manager/CHANGELOG.md | 1 + .../lib/src/cache_managers/image_cache_manager.dart | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/flutter_cache_manager/CHANGELOG.md b/flutter_cache_manager/CHANGELOG.md index 8ac9dfb4..c2403c6a 100644 --- a/flutter_cache_manager/CHANGELOG.md +++ b/flutter_cache_manager/CHANGELOG.md @@ -1,6 +1,7 @@ ## [3.4.6] - 2026-10-19 * Detects whether a downloaded file can be resized from its content instead of from the extension of its url, so images served from extension-less urls or with a non-image `Content-Type` are resized, and responses that are no longer images are returned untouched ([#475](https://github.com/Baseflow/flutter_cache_manager/pull/475)) +* Stores resized images with a `.png` extension, matching the encoding that is used, instead of reusing the extension of the url — which broke resizing for urls without an extension ## [3.4.5] - 2026-09-18 diff --git a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart index 470a25a9..6c841913 100644 --- a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart +++ b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart @@ -114,8 +114,6 @@ mixin ImageCacheManager on BaseCacheManager { int? maxWidth, int? maxHeight, ) async { - final originalFileName = originalFile.file.path; - final fileExtension = originalFileName.split('.').last; if (!await _isImage(originalFile.file)) { return originalFile; } @@ -152,7 +150,11 @@ mixin ImageCacheManager on BaseCacheManager { resizedFile, key: key, maxAge: maxAge, - fileExtension: fileExtension, + // The resized image is always encoded as png. The extension of the + // original file cannot be used: it is derived from the url, so it can + // be missing, or be the last segment of the path when the url has no + // extension at all. + fileExtension: 'png', ); return FileInfo( From e9eca3a04bf23edff6cce52d9111f32a72b23fac Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Mon, 19 Oct 2026 14:03:13 +0800 Subject: [PATCH 14/16] fix(image): keep an animated webp out of the resizer The resizer always re-encodes the image to png, which keeps a single frame. Now that webp is recognized by its signature, an animated webp would enter the resizer and silently lose its animation. That is the same reason gif is not recognized at all. Read the flags of the VP8X chunk, which is where an extended webp marks itself as animated, and pass those files through untouched. A static webp, with or without a VP8X chunk, is still resized. --- flutter_cache_manager/CHANGELOG.md | 1 + .../cache_managers/image_cache_manager.dart | 42 ++++++++++++++----- .../test/image_cache_manager_test.dart | 29 +++++++++++++ 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/flutter_cache_manager/CHANGELOG.md b/flutter_cache_manager/CHANGELOG.md index c2403c6a..53534429 100644 --- a/flutter_cache_manager/CHANGELOG.md +++ b/flutter_cache_manager/CHANGELOG.md @@ -2,6 +2,7 @@ * Detects whether a downloaded file can be resized from its content instead of from the extension of its url, so images served from extension-less urls or with a non-image `Content-Type` are resized, and responses that are no longer images are returned untouched ([#475](https://github.com/Baseflow/flutter_cache_manager/pull/475)) * Stores resized images with a `.png` extension, matching the encoding that is used, instead of reusing the extension of the url — which broke resizing for urls without an extension +* Adds webp, bmp and wbmp to the formats that are resized ([#353](https://github.com/Baseflow/flutter_cache_manager/issues/353)). An animated webp is passed through untouched, since the resized image is always encoded as png and would lose its animation, the same reason gif is not resized ## [3.4.5] - 2026-09-18 diff --git a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart index 6c841913..29c86809 100644 --- a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart +++ b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart @@ -10,6 +10,9 @@ const supportedFileNames = ['jpg', 'jpeg', 'png', 'tga', 'cur', 'ico']; /// Signatures (magic numbers) of the image formats that can be recognized by /// the first bytes of the file, instead of by its extension. +/// +/// Gif is deliberately missing: the resizer re-encodes to png, which would turn +/// an animation into a single frame. const _imageSignatures = >[ [0xFF, 0xD8], // jpeg [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], // png @@ -87,20 +90,23 @@ mixin ImageCacheManager on BaseCacheManager { /// response can be served without a file extension, or with an extension /// that does not match the actual content, for example when it is served /// with an incorrect `Content-Type` header. + /// + /// Images that would lose something when they are re-encoded as png, such as + /// an animated gif or an animated webp, are not considered an image here, so + /// they are passed through untouched. Future _isImage(File file) async { if (!await file.exists()) { return false; } final header = await _readHeader(file); - if (_imageSignatures.any((signature) => _startsWith(header, signature))) { + if (_imageSignatures.any((signature) => _matches(header, signature))) { return true; } // WebP is a RIFF container: "RIFF" + 4 bytes of length + "WEBP". - if (header.length >= 12 && - _startsWith(header, 'RIFF'.codeUnits) && - _startsWith(header.sublist(8), 'WEBP'.codeUnits)) { - return true; + if (_matches(header, 'RIFF'.codeUnits) && + _matches(header, 'WEBP'.codeUnits, 8)) { + return !_isAnimatedWebp(header); } final extension = file.path.split('.').last.toLowerCase(); return _extensionOnlyFormats.contains(extension); @@ -191,8 +197,8 @@ mixin ImageCacheManager on BaseCacheManager { } /// Reads the first bytes of [file], up to [length] bytes, which is enough to -/// hold the longest signature that is checked. -Future> _readHeader(File file, {int length = 12}) async { +/// hold the longest signature that is checked, including the webp flags. +Future> _readHeader(File file, {int length = 30}) async { final raf = await file.open(); try { return await raf.read(length); @@ -201,19 +207,33 @@ Future> _readHeader(File file, {int length = 12}) async { } } -/// Whether [bytes] starts with [signature]. -bool _startsWith(List bytes, List signature) { - if (bytes.length < signature.length) { +/// Whether [bytes] holds [signature] at [offset]. +bool _matches(List bytes, List signature, [int offset = 0]) { + if (bytes.length < offset + signature.length) { return false; } for (var i = 0; i < signature.length; i++) { - if (bytes[i] != signature[i]) { + if (bytes[offset + i] != signature[i]) { return false; } } return true; } +/// Whether [header] belongs to a webp that contains an animation. +/// +/// An extended webp starts with a `VP8X` chunk, whose flags byte marks the file +/// as animated. Resizing re-encodes the image to png, which keeps a single +/// frame, so an animated webp is left as it is, the same way gif is. +bool _isAnimatedWebp(List header) { + // "RIFF" + size + "WEBP" + "VP8X" + chunk size, the flags follow at 20. + const flagsOffset = 20; + const animationFlag = 0x02; + return header.length > flagsOffset && + _matches(header, 'VP8X'.codeUnits, 12) && + (header[flagsOffset] & animationFlag) != 0; +} + Future _decodeImage( File file, { int? width, diff --git a/flutter_cache_manager/test/image_cache_manager_test.dart b/flutter_cache_manager/test/image_cache_manager_test.dart index e2390548..3884f292 100644 --- a/flutter_cache_manager/test/image_cache_manager_test.dart +++ b/flutter_cache_manager/test/image_cache_manager_test.dart @@ -163,6 +163,21 @@ void main() { expect(await result.file.readAsBytes(), payload); }); + + test('Animated webp should not be resized', () async { + const urlOfAnimatedWebp = 'baseflow.com/animation.webp'; + var payload = animatedWebpHeader(); + var config = createTestConfig(); + await config.returnsFile('animation.webp', data: payload); + config.returnsCacheObject(urlOfAnimatedWebp, 'animation.webp', validTill); + + var cacheManager = TestCacheManager(config); + var result = + await cacheManager.getImageFile(urlOfAnimatedWebp, maxWidth: 100).last + as FileInfo; + + expect(await result.file.readAsBytes(), payload); + }); }); } @@ -194,3 +209,17 @@ Future verifySize( Future getExampleImage() { return File('test/images/image-120.png').readAsBytes(); } + +/// The header of an animated webp: a `VP8X` chunk with the animation flag set. +/// Only the header is needed, the file is never decoded because it is not +/// resized. +Uint8List animatedWebpHeader() { + return Uint8List.fromList([ + ...'RIFF'.codeUnits, 0x1A, 0x00, 0x00, 0x00, // + ...'WEBP'.codeUnits, // + ...'VP8X'.codeUnits, 0x0A, 0x00, 0x00, 0x00, // + 0x02, // flags: animation + 0x00, 0x00, 0x00, // canvas width - 1 + 0x00, 0x00, 0x00, // canvas height - 1 + ]); +} From fe77da7140f84b086d469220086e43f18ee59f63 Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Mon, 19 Oct 2026 14:36:23 +0800 Subject: [PATCH 15/16] docs(image): explain why ico and cur overlap with tga A tga without an id field and without a colour map starts with the same four bytes as an ico or a cur, so the signatures cannot be told apart. It does not affect the result, since all of them are resizable, but it looks like a mistake without the note. --- .../lib/src/cache_managers/image_cache_manager.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart index 29c86809..0dbe6a78 100644 --- a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart +++ b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart @@ -13,6 +13,12 @@ const supportedFileNames = ['jpg', 'jpeg', 'png', 'tga', 'cur', 'ico']; /// /// Gif is deliberately missing: the resizer re-encodes to png, which would turn /// an animation into a single frame. +/// +/// The ico and cur values overlap with tga, which starts with the same four +/// bytes when it has no id field and no colour map (byte 2 is then the image +/// type, 1 for a colour mapped and 2 for a true colour image). Telling them +/// apart takes more than four bytes, but it does not matter here: all of them +/// are resizable, and only the answer to "is this an image" is used. const _imageSignatures = >[ [0xFF, 0xD8], // jpeg [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], // png From 02775e77ead9b435fc405ed6c470d351f8973012 Mon Sep 17 00:00:00 2001 From: afer <791143125@qq.com> Date: Tue, 27 Oct 2026 15:59:21 +0800 Subject: [PATCH 16/16] refactor(image): let the engine decide whether a file can be resized Instead of matching magic numbers, the file is offered to the image engine: ImageDescriptor.encoded + instantiateCodec report whether the file decodes at all, how many frames it has and how large it is. A file that fails to decode, or that holds an animation (gif, apng, animated webp), is returned untouched instead of erroring the stream or losing its animation. This covers every format the engine supports, including platform ones like heif, without a list to maintain. Also: - Fix shouldResize only checking maxHeight when maxWidth is null. - Fix the comment on the stored extension: it comes from the Content-Type header, not from the url; storing as png was already correct since the resized image is png-encoded. - Deprecate supportedFileNames, which no longer reflects what gets resized. Tests: an image served with a generic content type is resized and stored as .png, static webp and bmp are resized, a file that fails to decode is returned untouched, and animated gif, apng and webp are passed through. --- flutter_cache_manager/CHANGELOG.md | 7 +- .../cache_managers/image_cache_manager.dart | 157 +++++++----------- .../test/image_cache_manager_test.dart | 106 ++++++++++-- .../test/images/animated-120.apng | Bin 0 -> 1675 bytes .../test/images/animated-120.gif | Bin 0 -> 8337 bytes .../test/images/animated-120.webp | Bin 0 -> 3352 bytes .../test/images/image-120.bmp | Bin 0 -> 43254 bytes .../test/images/image-120.webp | Bin 0 -> 11920 bytes 8 files changed, 154 insertions(+), 116 deletions(-) create mode 100644 flutter_cache_manager/test/images/animated-120.apng create mode 100644 flutter_cache_manager/test/images/animated-120.gif create mode 100644 flutter_cache_manager/test/images/animated-120.webp create mode 100644 flutter_cache_manager/test/images/image-120.bmp create mode 100644 flutter_cache_manager/test/images/image-120.webp diff --git a/flutter_cache_manager/CHANGELOG.md b/flutter_cache_manager/CHANGELOG.md index 53534429..cde14705 100644 --- a/flutter_cache_manager/CHANGELOG.md +++ b/flutter_cache_manager/CHANGELOG.md @@ -1,8 +1,9 @@ ## [3.4.6] - 2026-10-19 -* Detects whether a downloaded file can be resized from its content instead of from the extension of its url, so images served from extension-less urls or with a non-image `Content-Type` are resized, and responses that are no longer images are returned untouched ([#475](https://github.com/Baseflow/flutter_cache_manager/pull/475)) -* Stores resized images with a `.png` extension, matching the encoding that is used, instead of reusing the extension of the url — which broke resizing for urls without an extension -* Adds webp, bmp and wbmp to the formats that are resized ([#353](https://github.com/Baseflow/flutter_cache_manager/issues/353)). An animated webp is passed through untouched, since the resized image is always encoded as png and would lose its animation, the same reason gif is not resized +* Detects whether a downloaded file can be resized by letting the image engine decode it, instead of from the extension of the url. Images served with a generic `Content-Type` such as `application/octet-stream` are now resized, and files that are not images are returned untouched ([#475](https://github.com/Baseflow/flutter_cache_manager/pull/475)) +* Detects the image format from the file content, so every format the engine supports is resized, including webp and bmp ([#353](https://github.com/Baseflow/flutter_cache_manager/issues/353)). Animated images (gif, apng, animated webp) are passed through untouched, since the resized image is always encoded as png and would lose its animation +* Stores resized images with a `.png` extension, matching the encoding that is used, instead of reusing the extension derived from the `Content-Type` header +* Deprecates `supportedFileNames`, which is no longer used now that detection is content based ## [3.4.5] - 2026-09-18 diff --git a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart index 0dbe6a78..c59135c1 100644 --- a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart +++ b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart @@ -6,32 +6,17 @@ import 'package:file/file.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -const supportedFileNames = ['jpg', 'jpeg', 'png', 'tga', 'cur', 'ico']; - -/// Signatures (magic numbers) of the image formats that can be recognized by -/// the first bytes of the file, instead of by its extension. -/// -/// Gif is deliberately missing: the resizer re-encodes to png, which would turn -/// an animation into a single frame. +/// Formats that used to be recognized by the extension of the file. /// -/// The ico and cur values overlap with tga, which starts with the same four -/// bytes when it has no id field and no colour map (byte 2 is then the image -/// type, 1 for a colour mapped and 2 for a true colour image). Telling them -/// apart takes more than four bytes, but it does not matter here: all of them -/// are resizable, and only the answer to "is this an image" is used. -const _imageSignatures = >[ - [0xFF, 0xD8], // jpeg - [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], // png - [0x42, 0x4D], // bmp - [0x00, 0x00, 0x01, 0x00], // ico - [0x00, 0x00, 0x02, 0x00], // cur -]; - -/// Extensions of the formats that have no usable signature at the start of the -/// file, so the only way to recognize them is by their extension. Tga only -/// stores a signature in its footer, and wbmp starts with a two byte header -/// (`00 00`) that is too generic to match on. -const _extensionOnlyFormats = ['tga', 'wbmp']; +/// Whether a file can be resized is now decided by the image engine, based on +/// the content of the file, so this list no longer reflects which files are +/// resized. It is kept only for backwards compatibility. +@Deprecated( + 'Image formats are detected from the file content instead of the file ' + 'extension. This list is no longer used and will be removed in a future ' + 'version.', +) +const supportedFileNames = ['jpg', 'jpeg', 'png', 'tga', 'cur', 'ico']; mixin ImageCacheManager on BaseCacheManager { /// Returns a resized image file to fit within maxHeight and maxWidth. It @@ -91,33 +76,6 @@ mixin ImageCacheManager on BaseCacheManager { _runningResizes.remove(resizedKey); } - /// Whether [file] holds an image that can be decoded and resized, based on - /// the content of the file rather than on the extension of its url. A - /// response can be served without a file extension, or with an extension - /// that does not match the actual content, for example when it is served - /// with an incorrect `Content-Type` header. - /// - /// Images that would lose something when they are re-encoded as png, such as - /// an animated gif or an animated webp, are not considered an image here, so - /// they are passed through untouched. - Future _isImage(File file) async { - if (!await file.exists()) { - return false; - } - - final header = await _readHeader(file); - if (_imageSignatures.any((signature) => _matches(header, signature))) { - return true; - } - // WebP is a RIFF container: "RIFF" + 4 bytes of length + "WEBP". - if (_matches(header, 'RIFF'.codeUnits) && - _matches(header, 'WEBP'.codeUnits, 8)) { - return !_isAnimatedWebp(header); - } - final extension = file.path.split('.').last.toLowerCase(); - return _extensionOnlyFormats.contains(extension); - } - final Map> _runningResizes = {}; Future _resizeImageFile( @@ -126,25 +84,33 @@ mixin ImageCacheManager on BaseCacheManager { int? maxWidth, int? maxHeight, ) async { - if (!await _isImage(originalFile.file)) { + final info = await _imageInfo(originalFile.file); + if (info == null) { + // The engine cannot decode this file, so it is not a resizable image. + // The original file is returned untouched instead of erroring the + // stream. return originalFile; } + final (frameCount, imageWidth, imageHeight) = info; - final image = await _decodeImage(originalFile.file); + // Resizing re-encodes the image as png, which keeps a single frame, so + // animated images (gif, apng, animated webp, ...) are passed through + // untouched. + if (frameCount > 1) { + return originalFile; + } - final shouldResize = maxWidth != null - ? image.width > maxWidth - : false || maxHeight != null - ? image.height > maxHeight - : false; + final shouldResize = + (maxWidth != null && imageWidth > maxWidth) || + (maxHeight != null && imageHeight > maxHeight); if (!shouldResize) return originalFile; if (maxWidth != null && maxHeight != null) { - final resizeFactorWidth = image.width / maxWidth; - final resizeFactorHeight = image.height / maxHeight; + final resizeFactorWidth = imageWidth / maxWidth; + final resizeFactorHeight = imageHeight / maxHeight; final resizeFactor = max(resizeFactorHeight, resizeFactorWidth); - maxWidth = (image.width / resizeFactor).round(); - maxHeight = (image.height / resizeFactor).round(); + maxWidth = (imageWidth / resizeFactor).round(); + maxHeight = (imageHeight / resizeFactor).round(); } final resized = await _decodeImage( @@ -162,10 +128,11 @@ mixin ImageCacheManager on BaseCacheManager { resizedFile, key: key, maxAge: maxAge, - // The resized image is always encoded as png. The extension of the - // original file cannot be used: it is derived from the url, so it can - // be missing, or be the last segment of the path when the url has no - // extension at all. + // The resized image is always encoded as png, so it is stored with a + // png extension. The extension of the original file cannot be reused: + // it comes from the Content-Type header of the response, which can be + // something generic like `application/octet-stream` instead of the + // format the image is actually encoded in. fileExtension: 'png', ); @@ -202,42 +169,32 @@ mixin ImageCacheManager on BaseCacheManager { } } -/// Reads the first bytes of [file], up to [length] bytes, which is enough to -/// hold the longest signature that is checked, including the webp flags. -Future> _readHeader(File file, {int length = 30}) async { - final raf = await file.open(); +/// Describes the image in [file], or `null` when the engine cannot decode it. +/// +/// Whether a file is a resizable image is decided entirely by the engine: it +/// knows every format Flutter can decode, including platform specific ones +/// like heif, so there is no list of signatures or extensions to maintain. +/// The returned record holds the frame count of the image, which is how +/// animated images are recognized, and its width and height. +Future<(int frameCount, int width, int height)?> _imageInfo(File file) async { + if (!await file.exists()) { + return null; + } + final bytes = await file.readAsBytes(); + final buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + ui.ImageDescriptor? descriptor; + ui.Codec? codec; try { - return await raf.read(length); + descriptor = await ui.ImageDescriptor.encoded(buffer); + codec = await descriptor.instantiateCodec(); + return (codec.frameCount, descriptor.width, descriptor.height); + } on Object { + return null; } finally { - await raf.close(); - } -} - -/// Whether [bytes] holds [signature] at [offset]. -bool _matches(List bytes, List signature, [int offset = 0]) { - if (bytes.length < offset + signature.length) { - return false; - } - for (var i = 0; i < signature.length; i++) { - if (bytes[offset + i] != signature[i]) { - return false; - } + codec?.dispose(); + descriptor?.dispose(); + buffer.dispose(); } - return true; -} - -/// Whether [header] belongs to a webp that contains an animation. -/// -/// An extended webp starts with a `VP8X` chunk, whose flags byte marks the file -/// as animated. Resizing re-encodes the image to png, which keeps a single -/// frame, so an animated webp is left as it is, the same way gif is. -bool _isAnimatedWebp(List header) { - // "RIFF" + size + "WEBP" + "VP8X" + chunk size, the flags follow at 20. - const flagsOffset = 20; - const animationFlag = 0x02; - return header.length > flagsOffset && - _matches(header, 'VP8X'.codeUnits, 12) && - (header[flagsOffset] & animationFlag) != 0; } Future _decodeImage( diff --git a/flutter_cache_manager/test/image_cache_manager_test.dart b/flutter_cache_manager/test/image_cache_manager_test.dart index 3884f292..6d6867bc 100644 --- a/flutter_cache_manager/test/image_cache_manager_test.dart +++ b/flutter_cache_manager/test/image_cache_manager_test.dart @@ -147,6 +147,62 @@ void main() { await verifySize(image, 100, 100); }); + test('Image with a non-image extension should be resized', () async { + // The stored file's extension does not come from the url, but from the + // Content-Type header of the response. A generic type such as + // application/octet-stream stores the file as .bin, even when the + // payload is an image. + const urlWithBinaryExtension = 'baseflow.com/image.bin'; + var config = createTestConfig(); + await config.returnsFile('image.bin', data: await getExampleImage()); + config.returnsCacheObject(urlWithBinaryExtension, 'image.bin', validTill); + + var cacheManager = TestCacheManager(config); + var result = + await cacheManager + .getImageFile(urlWithBinaryExtension, maxWidth: 100) + .last + as FileInfo; + + var image = await result.file.readAsBytes(); + await verifySize(image, 100, 100); + expect(result.file.path.endsWith('.png'), isTrue); + }); + + test('Static webp should be resized', () async { + const urlWithWebpExtension = 'baseflow.com/image.webp'; + var config = createTestConfig(); + await config.returnsFile('image.webp', data: await getExampleWebp()); + config.returnsCacheObject(urlWithWebpExtension, 'image.webp', validTill); + + var cacheManager = TestCacheManager(config); + var result = + await cacheManager + .getImageFile(urlWithWebpExtension, maxWidth: 100) + .last + as FileInfo; + + var image = await result.file.readAsBytes(); + await verifySize(image, 100, 100); + }); + + test('Bmp should be resized', () async { + const urlWithBmpExtension = 'baseflow.com/image.bmp'; + var config = createTestConfig(); + await config.returnsFile('image.bmp', data: await getExampleBmp()); + config.returnsCacheObject(urlWithBmpExtension, 'image.bmp', validTill); + + var cacheManager = TestCacheManager(config); + var result = + await cacheManager + .getImageFile(urlWithBmpExtension, maxWidth: 100) + .last + as FileInfo; + + var image = await result.file.readAsBytes(); + await verifySize(image, 100, 100); + }); + test('Non image should not be resized', () async { const urlWithImageExtension = 'baseflow.com/image.png'; var payload = utf8.encode('{"message":"not an image"}'); @@ -164,9 +220,24 @@ void main() { expect(await result.file.readAsBytes(), payload); }); + test('Animated gif should not be resized', () async { + const urlOfAnimatedGif = 'baseflow.com/animation.gif'; + var payload = await File('test/images/animated-120.gif').readAsBytes(); + var config = createTestConfig(); + await config.returnsFile('animation.gif', data: payload); + config.returnsCacheObject(urlOfAnimatedGif, 'animation.gif', validTill); + + var cacheManager = TestCacheManager(config); + var result = + await cacheManager.getImageFile(urlOfAnimatedGif, maxWidth: 100).last + as FileInfo; + + expect(await result.file.readAsBytes(), payload); + }); + test('Animated webp should not be resized', () async { const urlOfAnimatedWebp = 'baseflow.com/animation.webp'; - var payload = animatedWebpHeader(); + var payload = await File('test/images/animated-120.webp').readAsBytes(); var config = createTestConfig(); await config.returnsFile('animation.webp', data: payload); config.returnsCacheObject(urlOfAnimatedWebp, 'animation.webp', validTill); @@ -178,6 +249,21 @@ void main() { expect(await result.file.readAsBytes(), payload); }); + + test('Animated png should not be resized', () async { + const urlOfAnimatedPng = 'baseflow.com/animation.png'; + var payload = await File('test/images/animated-120.apng').readAsBytes(); + var config = createTestConfig(); + await config.returnsFile('animation.png', data: payload); + config.returnsCacheObject(urlOfAnimatedPng, 'animation.png', validTill); + + var cacheManager = TestCacheManager(config); + var result = + await cacheManager.getImageFile(urlOfAnimatedPng, maxWidth: 100).last + as FileInfo; + + expect(await result.file.readAsBytes(), payload); + }); }); } @@ -210,16 +296,10 @@ Future getExampleImage() { return File('test/images/image-120.png').readAsBytes(); } -/// The header of an animated webp: a `VP8X` chunk with the animation flag set. -/// Only the header is needed, the file is never decoded because it is not -/// resized. -Uint8List animatedWebpHeader() { - return Uint8List.fromList([ - ...'RIFF'.codeUnits, 0x1A, 0x00, 0x00, 0x00, // - ...'WEBP'.codeUnits, // - ...'VP8X'.codeUnits, 0x0A, 0x00, 0x00, 0x00, // - 0x02, // flags: animation - 0x00, 0x00, 0x00, // canvas width - 1 - 0x00, 0x00, 0x00, // canvas height - 1 - ]); +Future getExampleWebp() { + return File('test/images/image-120.webp').readAsBytes(); +} + +Future getExampleBmp() { + return File('test/images/image-120.bmp').readAsBytes(); } diff --git a/flutter_cache_manager/test/images/animated-120.apng b/flutter_cache_manager/test/images/animated-120.apng new file mode 100644 index 0000000000000000000000000000000000000000..e4016a7ee365bf69454f8c0e788b979c02a685b2 GIT binary patch literal 1675 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q{^bj>O~;A0W*P#0=-&?m7UZq|zW_ zAR({@5CaSt8GwfM+&TDzfr0Ubr;B4q#hkZy9rJEG2rxK$p7_82&7E2 z;Wi=;9~v9qSKMm8z|2(t*ig<%o}=c`PaUWFm%(3Fw!g1*w>?JS|v$ zW+Io(+koX~H(PzUa(Ir_^;HKh^B4ZASa6#+iQax@dyvgu^~1H? z8&==9-*}+Ta&I5&?dSU^^Mbv)!~f~8&`SC6%?Asaj&sVW7tA>TW!zc*X+HDR?}et) zU{QaLJ%4{~t^H?x^TBeKd%IYppZ^n+{$P=&DdTNg ziem^+1rsoJ+;Q}CHWXk9tUto|c=9aiwyXCyFmg8SJ{_8^(pD*@DqUlHtcvgOw{6u7 z)wxWEg(c=4_`uSpZeY{+kdwzhp@POj@0qVu+3ou~v$D2l@~&C)zx?NX@$$jc1NDs6 zTqU9(v=d%l5Iyjp(Yv+pzt~>p^5^euxhlR-5@)a!vq;^kox&$la4RVMhRxLjD^JHP zEDmK{u3y-CUYz&NrahrX=`tG{8*i^m5!_Tb$3njkzIu-NM3q z90}G8JDzH#ot#s6fcNz5ho6r!tLqoGoeO23v*V1UnZIqprJ(c^G8PY7o}MeZQ}JM~ z_dJF>vzGI`uURd`SMxl3bHM}a``vBK^=HNJKRgijL-$tQbRwgn`L4R5d*Rjn_i8U9 zB@a+xf+czUvixxrm^|)7lLrepdGs|Kh_D1ooVB%e@AjW%>BJ=QCpLC#pNqS{*v%uq zv$Nly-SGE}T*7xXdZh_?lKXC5&9LxWUF{#A`~SB|zKwfX&z~V$&?w(lD;4#?bA!Jl zw~getyo`ER@k4tb?_>gzKbgc2%kFi4(6mGOJ4cL2`4zp!?Ynj;e>=*&{n3u_@?*@G zyV>%(=iJk-Ufd|$#hTYW_ul5+$&JBXEP37Y?iv49Zk#LxN*a$EZa;c|fAM`~J_}v+ zoFLAANB!^V{A=GknEkv#Ii#EAUHAU>{x|(Bep+HX^sBo1-}SNjX-e(TFY7x0rkBl6 zLvDwDQP=r*J?wFeKTp{;b}PrnNyIuT)q!WwhzYcASe-b-Y x(KLVd*%g)XC1Gc!@8^d7>moKwemM3{e$MB@tNmpi3BW|a;OXk;vd$@?2>`Um%76d> literal 0 HcmV?d00001 diff --git a/flutter_cache_manager/test/images/animated-120.gif b/flutter_cache_manager/test/images/animated-120.gif new file mode 100644 index 0000000000000000000000000000000000000000..3d03d817807a5b777258405cfd0e27a20fd1ff86 GIT binary patch literal 8337 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 Dre7s~ literal 0 HcmV?d00001 diff --git a/flutter_cache_manager/test/images/animated-120.webp b/flutter_cache_manager/test/images/animated-120.webp new file mode 100644 index 0000000000000000000000000000000000000000..f5b57aa776a467a299cf617150d580806e5e2eef GIT binary patch literal 3352 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 IY_IZv0OxeW_5c6? literal 0 HcmV?d00001 diff --git a/flutter_cache_manager/test/images/image-120.bmp b/flutter_cache_manager/test/images/image-120.bmp new file mode 100644 index 0000000000000000000000000000000000000000..1b93207ef76c63d6b0c6657b1b7e7d8b0cad8bd0 GIT binary patch literal 43254 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 literal 0 HcmV?d00001