Merge pull request #314 from cogwheel0/flutter-image-compress-media
feat(media): add flutter_image_compress for efficient image handling
This commit is contained in:
@@ -48,6 +48,11 @@ PODS:
|
|||||||
- flutter_callkit_incoming (0.0.1):
|
- flutter_callkit_incoming (0.0.1):
|
||||||
- CryptoSwift
|
- CryptoSwift
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_image_compress_common (1.0.0):
|
||||||
|
- Flutter
|
||||||
|
- Mantle
|
||||||
|
- SDWebImage
|
||||||
|
- SDWebImageWebPCoder
|
||||||
- flutter_local_notifications (0.0.1):
|
- flutter_local_notifications (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_native_splash (2.4.3):
|
- flutter_native_splash (2.4.3):
|
||||||
@@ -63,6 +68,21 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- irondash_engine_context (0.0.1):
|
- irondash_engine_context (0.0.1):
|
||||||
- Flutter
|
- 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-c (1.22.0)
|
||||||
- onnxruntime-objc (1.22.0):
|
- onnxruntime-objc (1.22.0):
|
||||||
- onnxruntime-objc/Core (= 1.22.0)
|
- onnxruntime-objc/Core (= 1.22.0)
|
||||||
@@ -82,6 +102,9 @@ PODS:
|
|||||||
- SDWebImage (5.21.5):
|
- SDWebImage (5.21.5):
|
||||||
- SDWebImage/Core (= 5.21.5)
|
- SDWebImage/Core (= 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):
|
- share_handler_ios (0.0.14):
|
||||||
- Flutter
|
- Flutter
|
||||||
- share_handler_ios/share_handler_ios_models (= 0.0.14)
|
- share_handler_ios/share_handler_ios_models (= 0.0.14)
|
||||||
@@ -123,6 +146,7 @@ DEPENDENCIES:
|
|||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_callkit_incoming (from `.symlinks/plugins/flutter_callkit_incoming/ios`)
|
- 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_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
||||||
@@ -154,9 +178,12 @@ SPEC REPOS:
|
|||||||
- CwlCatchExceptionSupport
|
- CwlCatchExceptionSupport
|
||||||
- DKImagePickerController
|
- DKImagePickerController
|
||||||
- DKPhotoGallery
|
- DKPhotoGallery
|
||||||
|
- libwebp
|
||||||
|
- Mantle
|
||||||
- onnxruntime-c
|
- onnxruntime-c
|
||||||
- onnxruntime-objc
|
- onnxruntime-objc
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
|
- SDWebImageWebPCoder
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
@@ -172,6 +199,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_callkit_incoming:
|
flutter_callkit_incoming:
|
||||||
:path: ".symlinks/plugins/flutter_callkit_incoming/ios"
|
:path: ".symlinks/plugins/flutter_callkit_incoming/ios"
|
||||||
|
flutter_image_compress_common:
|
||||||
|
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
|
||||||
flutter_local_notifications:
|
flutter_local_notifications:
|
||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
@@ -231,6 +260,7 @@ SPEC CHECKSUMS:
|
|||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_callkit_incoming: cb8138af67cda6dd981f7101a5d709003af21502
|
flutter_callkit_incoming: cb8138af67cda6dd981f7101a5d709003af21502
|
||||||
|
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
||||||
@@ -238,6 +268,8 @@ SPEC CHECKSUMS:
|
|||||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||||
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
|
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||||
onnxruntime-c: 7f778680e96145956c0a31945f260321eed2611a
|
onnxruntime-c: 7f778680e96145956c0a31945f260321eed2611a
|
||||||
onnxruntime-objc: 83d28b87525bd971259a66e153ea32b5d023de19
|
onnxruntime-objc: 83d28b87525bd971259a66e153ea32b5d023de19
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
@@ -246,6 +278,7 @@ SPEC CHECKSUMS:
|
|||||||
quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779
|
quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779
|
||||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||||
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
|
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
|
||||||
|
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
|
||||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
|
|||||||
@@ -4,19 +4,86 @@ import 'dart:ui' as ui;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:file_picker/file_picker.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:image_picker/image_picker.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../../../core/utils/debug_logger.dart';
|
import '../../../core/utils/debug_logger.dart';
|
||||||
|
|
||||||
|
/// Standard web image formats that LLMs can process directly.
|
||||||
|
const Set<String> _standardImageFormats = {
|
||||||
|
'.jpg',
|
||||||
|
'.jpeg',
|
||||||
|
'.png',
|
||||||
|
'.gif',
|
||||||
|
'.webp',
|
||||||
|
'.bmp',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// iOS-specific formats that need conversion to JPEG before LLM submission.
|
||||||
|
const Set<String> _iosImageFormats = {
|
||||||
|
'.heic',
|
||||||
|
'.heif',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// RAW image formats that need conversion to JPEG before LLM submission.
|
||||||
|
const Set<String> _rawImageFormats = {
|
||||||
|
'.dng',
|
||||||
|
'.raw',
|
||||||
|
'.cr2',
|
||||||
|
'.nef',
|
||||||
|
'.arw',
|
||||||
|
'.orf',
|
||||||
|
'.rw2',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// All supported image formats (both standard and those requiring conversion).
|
||||||
|
const Set<String> 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.
|
/// Converts an image file to a base64 data URL.
|
||||||
/// This is a standalone utility used by both FileAttachmentService and TaskWorker.
|
/// 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.
|
/// Returns null if conversion fails.
|
||||||
Future<String?> convertImageFileToDataUrl(File imageFile) async {
|
Future<String?> convertImageFileToDataUrl(File imageFile) async {
|
||||||
try {
|
try {
|
||||||
final bytes = await imageFile.readAsBytes();
|
|
||||||
final ext = path.extension(imageFile.path).toLowerCase();
|
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';
|
String mimeType = 'image/png';
|
||||||
if (ext == '.jpg' || ext == '.jpeg') {
|
if (ext == '.jpg' || ext == '.jpeg') {
|
||||||
mimeType = 'image/jpeg';
|
mimeType = 'image/jpeg';
|
||||||
@@ -33,6 +100,40 @@ Future<String?> 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<List<int>?> _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({
|
String _deriveDisplayName({
|
||||||
required String? preferredName,
|
required String? preferredName,
|
||||||
required String filePath,
|
required String filePath,
|
||||||
@@ -85,14 +186,7 @@ class LocalAttachment {
|
|||||||
return path.extension(file.path).toLowerCase();
|
return path.extension(file.path).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isImage => <String>{
|
bool get isImage => allSupportedImageFormats.contains(extension);
|
||||||
'.jpg',
|
|
||||||
'.jpeg',
|
|
||||||
'.png',
|
|
||||||
'.gif',
|
|
||||||
'.webp',
|
|
||||||
'.bmp',
|
|
||||||
}.contains(extension);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileAttachmentService {
|
class FileAttachmentService {
|
||||||
@@ -329,8 +423,8 @@ class FileAttachmentService {
|
|||||||
if (['.xls', '.xlsx'].contains(ext)) return '📊';
|
if (['.xls', '.xlsx'].contains(ext)) return '📊';
|
||||||
if (['.ppt', '.pptx'].contains(ext)) return '📊';
|
if (['.ppt', '.pptx'].contains(ext)) return '📊';
|
||||||
|
|
||||||
// Images
|
// Images (including iOS and RAW formats)
|
||||||
if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].contains(ext)) return '🖼️';
|
if (allSupportedImageFormats.contains(ext)) return '🖼️';
|
||||||
|
|
||||||
// Code
|
// Code
|
||||||
if (['.js', '.ts', '.py', '.dart', '.java', '.cpp'].contains(ext)) {
|
if (['.js', '.ts', '.py', '.dart', '.java', '.cpp'].contains(ext)) {
|
||||||
@@ -395,8 +489,8 @@ class FileUploadState {
|
|||||||
if (['.xls', '.xlsx'].contains(ext)) return '📊';
|
if (['.xls', '.xlsx'].contains(ext)) return '📊';
|
||||||
if (['.ppt', '.pptx'].contains(ext)) return '📊';
|
if (['.ppt', '.pptx'].contains(ext)) return '📊';
|
||||||
|
|
||||||
// Images
|
// Images (including iOS and RAW formats)
|
||||||
if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].contains(ext)) return '🖼️';
|
if (allSupportedImageFormats.contains(ext)) return '🖼️';
|
||||||
|
|
||||||
// Code
|
// Code
|
||||||
if (['.js', '.ts', '.py', '.dart', '.java', '.cpp'].contains(ext)) {
|
if (['.js', '.ts', '.py', '.dart', '.java', '.cpp'].contains(ext)) {
|
||||||
|
|||||||
@@ -74,9 +74,8 @@ class TaskWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _performUploadMedia(UploadMediaTask task) async {
|
Future<void> _performUploadMedia(UploadMediaTask task) async {
|
||||||
const imageExts = <String>{'.jpg', '.jpeg', '.png', '.gif', '.webp'};
|
|
||||||
final lowerName = task.fileName.toLowerCase();
|
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)
|
// For images: read as base64 locally (matching web client behavior)
|
||||||
// Web client never uploads images to /api/v1/files/
|
// Web client never uploads images to /api/v1/files/
|
||||||
|
|||||||
48
pubspec.lock
48
pubspec.lock
@@ -518,6 +518,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0"
|
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:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ dependencies:
|
|||||||
audioplayers: ^6.5.1
|
audioplayers: ^6.5.1
|
||||||
image_picker: ^1.2.0
|
image_picker: ^1.2.0
|
||||||
file_picker: ^10.3.7
|
file_picker: ^10.3.7
|
||||||
|
flutter_image_compress: ^2.1.0
|
||||||
path_provider: ^2.1.4
|
path_provider: ^2.1.4
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
|
|||||||
Reference in New Issue
Block a user