From 6d424770418b1a889ffef4470e3554fb73636fb6 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Fri, 16 Oct 2026 20:35:42 -0400 Subject: [PATCH] add support for avif images Fixes #138 --- android/build.gradle.kts | 17 +++ lib/helpers/mxc_image.dart | 42 +++++- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + linux/nix/devshell.nix | 17 ++- macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 136 ++++++++++++++++++ pubspec.yaml | 2 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 10 files changed, 219 insertions(+), 8 deletions(-) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 441eb71..f82adc9 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -16,6 +16,23 @@ subprojects { project.layout.buildDirectory.value(newSubprojectBuildDir) } +subprojects { + if (name == "flutter_avif_android") { + tasks.withType().configureEach { + exclude("**/FlutterAvifPlugin.kt") + } + afterEvaluate { + extensions.configure { + compileSdk = 36 + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + } + } + } +} + subprojects { project.evaluationDependsOn(":app") } diff --git a/lib/helpers/mxc_image.dart b/lib/helpers/mxc_image.dart index 83a345a..83bcf14 100644 --- a/lib/helpers/mxc_image.dart +++ b/lib/helpers/mxc_image.dart @@ -1,6 +1,8 @@ -import "dart:ui"; +import "dart:io"; +import "dart:ui" as ui; import "package:flutter/widgets.dart"; +import "package:flutter_avif/flutter_avif.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/models/requests/download_media.dart"; @@ -15,13 +17,23 @@ class MxcImage(final WidgetRef ref, final DownloadMediaRequest request) ImageStreamCompleter loadImage(MxcImage key, ImageDecoderCallback decode) => MultiFrameImageStreamCompleter(codec: _loadAsync(decode), scale: 1.0); - Future _loadAsync(ImageDecoderCallback decode) async { + Future _loadAsync(ImageDecoderCallback decode) async { final file = await ref .read(ClientController.provider.notifier) .downloadMedia(request); - final buffer = await ImmutableBuffer.fromFilePath(file.path); + final bytes = await File(file.path).readAsBytes(); - return decode(buffer); + // Flutter can't decode AVIF on Linux/Windows (or older Android/iOS), so we do it ourselves + final avifType = isAvifFile(bytes.sublist(0, 16.clamp(0, bytes.length))); + if (avifType != .unknown) { + final codec = avifType == .avif + ? SingleFrameAvifCodec(bytes: bytes) + : MultiFrameAvifCodec(key: hashCode, avifBytes: bytes); + await codec.ready(); + return AvifCodecAdapter(codec); + } + + return decode(await ui.ImmutableBuffer.fromUint8List(bytes)); } @override @@ -31,3 +43,25 @@ class MxcImage(final WidgetRef ref, final DownloadMediaRequest request) @override int get hashCode => request.hashCode; } + +class AvifCodecAdapter(final AvifCodec codec) implements ui.Codec { + @override + int get frameCount => codec.frameCount; + + @override + int get repetitionCount => frameCount > 1 ? -1 : 0; + + @override + Future getNextFrame() async { + final frame = await codec.getNextFrame(); + return AvifFrameInfo(frame.duration, frame.image); + } + + @override + void dispose() => codec.dispose(); +} + +class AvifFrameInfo( + @override final Duration duration, + @override final ui.Image image, +) implements ui.FrameInfo; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index b2ee366..d95018b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_avif_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterAvifLinuxPlugin"); + flutter_avif_linux_plugin_register_with_registrar(flutter_avif_linux_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index eea220b..7d212bd 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links_linux dynamic_color file_selector_linux + flutter_avif_linux media_kit_libs_linux media_kit_video pasteboard diff --git a/linux/nix/devshell.nix b/linux/nix/devshell.nix index 6b2b94d..bfa5408 100644 --- a/linux/nix/devshell.nix +++ b/linux/nix/devshell.nix @@ -9,6 +9,7 @@ let ]; cmakeVersions = [ "3.22.1" ]; platformVersions = [ + "31" "34" "36" ]; @@ -47,7 +48,17 @@ pkgs.mkShell { GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_HOME}/build-tools/36.0.0/aapt2"; } - // lib.optionalAttrs pkgs.stdenv.isLinux { - LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}"; - }; + // lib.optionalAttrs pkgs.stdenv.isLinux ( + let + arch = if pkgs.stdenv.hostPlatform.isAarch64 then "arm64" else "x64"; + in + { + LD_LIBRARY_PATH = lib.concatStringsSep ":" [ + "./build/native_assets/linux" + "./build/linux/${arch}/debug/bundle/lib" + "./build/linux/${arch}/release/bundle/lib" + (lib.makeLibraryPath [ pkgs.zlib ]) + ]; + } + ); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 2a15f97..c801836 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import app_links import dynamic_color import file_selector_macos +import flutter_avif_macos import flutter_local_notifications import media_kit_libs_macos_video import media_kit_video @@ -15,6 +16,7 @@ import package_info_plus import pasteboard import screen_retriever_macos import shared_preferences_foundation +import sqflite_darwin import url_launcher_macos import wakelock_plus import webcrypto @@ -24,6 +26,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterAvifPlugin.register(with: registry.registrar(forPlugin: "FlutterAvifPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) @@ -31,6 +34,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WebcryptoPlugin.register(with: registry.registrar(forPlugin: "WebcryptoPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 51443ce..bf9c326 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -313,6 +313,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + exif: + dependency: transitive + description: + name: exif + sha256: a7980fdb3b7ffcd0b035e5b8a5e1eef7cadfe90ea6a4e85ebb62f87b96c7a172 + url: "https://pub.dev" + source: hosted + version: "3.3.0" fab_m3e: dependency: "direct overridden" description: @@ -447,6 +455,70 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_avif: + dependency: "direct main" + description: + name: flutter_avif + sha256: "6035f073189c1ae134affa73338c2eca79bf412e9abdb8f8628fdefcccff4444" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + flutter_avif_android: + dependency: transitive + description: + name: flutter_avif_android + sha256: "1da135ea0d74225fae3154f954f9ba4cf7aa5e56b30d9851fcfc0ddecab9d8c5" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + flutter_avif_ios: + dependency: transitive + description: + name: flutter_avif_ios + sha256: "3daaa599d8fe0193d3ade6cafa1fd4f166d4063b8d499c7c311ce1d170b2759f" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + flutter_avif_linux: + dependency: transitive + description: + name: flutter_avif_linux + sha256: dd6179edca12c720761b3acc678d7f15208eb8647adbac5a6a90f52d4ba56e2d + url: "https://pub.dev" + source: hosted + version: "3.1.0" + flutter_avif_macos: + dependency: transitive + description: + name: flutter_avif_macos + sha256: "8e39f365dfc5713d4527f07a92399184857ea30eced85f82171bfd0a45e2c73b" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + flutter_avif_platform_interface: + dependency: transitive + description: + name: flutter_avif_platform_interface + sha256: f5c110bdb7de2d4ab4902bfe2ec331fe24bc19b6d54f00ac4fa165effc0edcda + url: "https://pub.dev" + source: hosted + version: "3.1.0" + flutter_avif_web: + dependency: transitive + description: + name: flutter_avif_web + sha256: db2ff395b08cbfe9116e48ac65c428ba21a9159972ea019f8d7be580ff6cf6e8 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + flutter_avif_windows: + dependency: transitive + description: + name: flutter_avif_windows + sha256: e0c7722df305a6621f4ecdd83098025c705bcf0e8ce9519e03c6f515abcf351d + url: "https://pub.dev" + source: hosted + version: "3.1.0" flutter_blurhash: dependency: "direct main" description: @@ -455,6 +527,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.1" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: f94fc6f5480977e3eb7b622ed488f671ba96c971bd0e274e6bd0f0ba9b044816 + url: "https://pub.dev" + source: hosted + version: "3.4.5" flutter_file_dialog: dependency: "direct main" description: @@ -1185,6 +1265,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.6" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" pub_semver: dependency: transitive description: @@ -1398,6 +1486,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "273218a1678360a3127fcf15e543381d9b813d413d783bc6e9a53eed4d2eb8fe" + url: "https://pub.dev" + source: hosted + version: "2.4.4+2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: e4f659c14bdd14464127fcd234a7a96f61fe0f539b8568e5a38f99f5a568a835 + url: "https://pub.dev" + source: hosted + version: "2.4.4+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: b80e94ff0d551d5f1f8c7d14348d29b576cca3ae30f6e195a7c380552c32b1e0 + url: "https://pub.dev" + source: hosted + version: "2.5.14" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "8de68fc306040f992aaf378750c16f95e4298c4bb150dd45091d75e42b1f08ee" + url: "https://pub.dev" + source: hosted + version: "2.4.4+1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: c9177b2f3150ed0647b90a94307c28452fe0a5401863044216aed83addd6c7c5 + url: "https://pub.dev" + source: hosted + version: "2.4.2" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8802215..c4313d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -109,7 +109,7 @@ dependencies: pasteboard: 0.5.0 flutter_file_dialog: 3.3.3 mime: 2.1.0 - + flutter_avif: 3.1.0 dev_dependencies: build_runner: 2.16.2 flutter_lints: 6.0.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 95357c4..13fb733 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterAvifWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterAvifWindowsPlugin")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); MediaKitVideoPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a7b2473..1822f53 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links dynamic_color file_selector_windows + flutter_avif_windows media_kit_libs_windows_video media_kit_video pasteboard