From 50ce1e51deaecad2e2a026bb7f3506bcee2ea54f Mon Sep 17 00:00:00 2001 From: cogwheel <172976095+cogwheel0@users.noreply.github.com> Date: Mon, 22 Dec 2025 11:20:00 +0530 Subject: [PATCH] feat(media): add flutter_image_compress for efficient image handling --- ios/Podfile.lock | 33 +++++ .../services/file_attachment_service.dart | 120 ++++++++++++++++-- lib/shared/services/tasks/task_worker.dart | 3 +- pubspec.lock | 48 +++++++ pubspec.yaml | 1 + 5 files changed, 190 insertions(+), 15 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 74682cf..813f652 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -48,6 +48,11 @@ PODS: - flutter_callkit_incoming (0.0.1): - CryptoSwift - Flutter + - flutter_image_compress_common (1.0.0): + - Flutter + - Mantle + - SDWebImage + - SDWebImageWebPCoder - flutter_local_notifications (0.0.1): - Flutter - flutter_native_splash (2.4.3): @@ -63,6 +68,21 @@ PODS: - Flutter - irondash_engine_context (0.0.1): - Flutter + - libwebp (1.5.0): + - libwebp/demux (= 1.5.0) + - libwebp/mux (= 1.5.0) + - libwebp/sharpyuv (= 1.5.0) + - libwebp/webp (= 1.5.0) + - libwebp/demux (1.5.0): + - libwebp/webp + - libwebp/mux (1.5.0): + - libwebp/demux + - libwebp/sharpyuv (1.5.0) + - libwebp/webp (1.5.0): + - libwebp/sharpyuv + - Mantle (2.2.0): + - Mantle/extobjc (= 2.2.0) + - Mantle/extobjc (2.2.0) - onnxruntime-c (1.22.0) - onnxruntime-objc (1.22.0): - onnxruntime-objc/Core (= 1.22.0) @@ -82,6 +102,9 @@ PODS: - SDWebImage (5.21.5): - SDWebImage/Core (= 5.21.5) - SDWebImage/Core (5.21.5) + - SDWebImageWebPCoder (0.15.0): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.17) - share_handler_ios (0.0.14): - Flutter - share_handler_ios/share_handler_ios_models (= 0.0.14) @@ -123,6 +146,7 @@ DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_callkit_incoming (from `.symlinks/plugins/flutter_callkit_incoming/ios`) + - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) @@ -154,9 +178,12 @@ SPEC REPOS: - CwlCatchExceptionSupport - DKImagePickerController - DKPhotoGallery + - libwebp + - Mantle - onnxruntime-c - onnxruntime-objc - SDWebImage + - SDWebImageWebPCoder - SwiftyGif EXTERNAL SOURCES: @@ -172,6 +199,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_callkit_incoming: :path: ".symlinks/plugins/flutter_callkit_incoming/ios" + flutter_image_compress_common: + :path: ".symlinks/plugins/flutter_image_compress_common/ios" flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: @@ -231,6 +260,7 @@ SPEC CHECKSUMS: file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_callkit_incoming: cb8138af67cda6dd981f7101a5d709003af21502 + flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 @@ -238,6 +268,8 @@ SPEC CHECKSUMS: home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 + libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 + Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d onnxruntime-c: 7f778680e96145956c0a31945f260321eed2611a onnxruntime-objc: 83d28b87525bd971259a66e153ea32b5d023de19 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 @@ -246,6 +278,7 @@ SPEC CHECKSUMS: quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779 record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374 SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838 + SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a diff --git a/lib/features/chat/services/file_attachment_service.dart b/lib/features/chat/services/file_attachment_service.dart index 625ffb5..c12b720 100644 --- a/lib/features/chat/services/file_attachment_service.dart +++ b/lib/features/chat/services/file_attachment_service.dart @@ -4,19 +4,86 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart' as path; import '../../../core/providers/app_providers.dart'; import '../../../core/utils/debug_logger.dart'; +/// Standard web image formats that LLMs can process directly. +const Set _standardImageFormats = { + '.jpg', + '.jpeg', + '.png', + '.gif', + '.webp', + '.bmp', +}; + +/// iOS-specific formats that need conversion to JPEG before LLM submission. +const Set _iosImageFormats = { + '.heic', + '.heif', +}; + +/// RAW image formats that need conversion to JPEG before LLM submission. +const Set _rawImageFormats = { + '.dng', + '.raw', + '.cr2', + '.nef', + '.arw', + '.orf', + '.rw2', +}; + +/// All supported image formats (both standard and those requiring conversion). +const Set allSupportedImageFormats = { + ..._standardImageFormats, + ..._iosImageFormats, + ..._rawImageFormats, +}; + +/// Returns true if the extension requires conversion to a standard format. +bool _needsConversion(String extension) { + return _iosImageFormats.contains(extension) || + _rawImageFormats.contains(extension); +} + /// Converts an image file to a base64 data URL. /// This is a standalone utility used by both FileAttachmentService and TaskWorker. +/// +/// Handles iOS-specific formats (HEIC, HEIF) and RAW formats (DNG, CR2, etc.) +/// by converting them to JPEG before encoding. +/// /// Returns null if conversion fails. Future convertImageFileToDataUrl(File imageFile) async { try { - final bytes = await imageFile.readAsBytes(); final ext = path.extension(imageFile.path).toLowerCase(); + // Check if we need to convert the image format + if (_needsConversion(ext)) { + DebugLogger.log( + 'Converting image from $ext to JPEG', + scope: 'attachments', + data: {'path': imageFile.path}, + ); + + final convertedBytes = await _convertImageToJpeg(imageFile); + if (convertedBytes != null) { + return 'data:image/jpeg;base64,${base64Encode(convertedBytes)}'; + } + + // Conversion failed - return null rather than sending unusable raw data + DebugLogger.warning( + 'Conversion failed for $ext format, cannot process image', + ); + return null; + } + + // Standard format - read directly + final bytes = await imageFile.readAsBytes(); + String mimeType = 'image/png'; if (ext == '.jpg' || ext == '.jpeg') { mimeType = 'image/jpeg'; @@ -33,6 +100,40 @@ Future convertImageFileToDataUrl(File imageFile) async { } } +/// Converts an image file to JPEG bytes using flutter_image_compress. +/// This handles iOS-specific formats (HEIC, HEIF) and RAW formats (DNG, etc.) +Future?> _convertImageToJpeg(File imageFile) async { + try { + // Use flutter_image_compress for native iOS/Android conversion + final result = await FlutterImageCompress.compressWithFile( + imageFile.absolute.path, + format: CompressFormat.jpeg, + quality: 90, + ); + + if (result != null && result.isNotEmpty) { + DebugLogger.log( + 'Image converted successfully', + scope: 'attachments', + data: { + 'originalPath': imageFile.path, + 'resultSize': result.length, + }, + ); + return result; + } + + return null; + } catch (e) { + DebugLogger.error( + 'image-conversion-failed', + scope: 'attachments', + error: e, + ); + return null; + } +} + String _deriveDisplayName({ required String? preferredName, required String filePath, @@ -85,14 +186,7 @@ class LocalAttachment { return path.extension(file.path).toLowerCase(); } - bool get isImage => { - '.jpg', - '.jpeg', - '.png', - '.gif', - '.webp', - '.bmp', - }.contains(extension); + bool get isImage => allSupportedImageFormats.contains(extension); } class FileAttachmentService { @@ -329,8 +423,8 @@ class FileAttachmentService { if (['.xls', '.xlsx'].contains(ext)) return '📊'; if (['.ppt', '.pptx'].contains(ext)) return '📊'; - // Images - if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].contains(ext)) return '🖼️'; + // Images (including iOS and RAW formats) + if (allSupportedImageFormats.contains(ext)) return '🖼️'; // Code if (['.js', '.ts', '.py', '.dart', '.java', '.cpp'].contains(ext)) { @@ -395,8 +489,8 @@ class FileUploadState { if (['.xls', '.xlsx'].contains(ext)) return '📊'; if (['.ppt', '.pptx'].contains(ext)) return '📊'; - // Images - if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].contains(ext)) return '🖼️'; + // Images (including iOS and RAW formats) + if (allSupportedImageFormats.contains(ext)) return '🖼️'; // Code if (['.js', '.ts', '.py', '.dart', '.java', '.cpp'].contains(ext)) { diff --git a/lib/shared/services/tasks/task_worker.dart b/lib/shared/services/tasks/task_worker.dart index 7a34aee..111b1b2 100644 --- a/lib/shared/services/tasks/task_worker.dart +++ b/lib/shared/services/tasks/task_worker.dart @@ -74,9 +74,8 @@ class TaskWorker { } Future _performUploadMedia(UploadMediaTask task) async { - const imageExts = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}; final lowerName = task.fileName.toLowerCase(); - final bool isImage = imageExts.any(lowerName.endsWith); + final bool isImage = allSupportedImageFormats.any(lowerName.endsWith); // For images: read as base64 locally (matching web client behavior) // Web client never uploads images to /api/v1/files/ diff --git a/pubspec.lock b/pubspec.lock index c40efc2..7bc82e2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -518,6 +518,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + flutter_image_compress: + dependency: "direct main" + description: + name: flutter_image_compress + sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb + url: "https://pub.dev" + source: hosted + version: "1.0.6" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 + url: "https://pub.dev" + source: hosted + version: "0.1.5" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 6533d03..9421942 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,6 +51,7 @@ dependencies: audioplayers: ^6.5.1 image_picker: ^1.2.0 file_picker: ^10.3.7 + flutter_image_compress: ^2.1.0 path_provider: ^2.1.4 # Utilities