import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_general_repository_imp.dart'; import 'package:yumi/modules/user/crop/crop_image_page.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; class SCPickUtils { static final ImagePicker _pkr = ImagePicker(); static const int _defaultMaxImageBytes = 4000000; static const int _gifOriginalMaxImageBytes = 10 * 1024 * 1024; static void pickImage( BuildContext context, OnUpLoadCallBack onUpLoadCallBack, { double? aspectRatio, bool? backOriginalFile = true, bool? neeCrop = true, bool allowGif = false, bool skipCropForGif = true, }) async { try { final XFile? pickedFile = allowGif ? await _pkr.pickImage(source: ImageSource.gallery) : await _pkr.pickImage( source: ImageSource.gallery, imageQuality: 90, maxWidth: 1920, maxHeight: 1080, ); if (pickedFile == null) return; final isGif = await _igif(pickedFile); if (!context.mounted) { onUpLoadCallBack(false, ""); return; } if (isGif) { if (!allowGif) { SCTts.show("GIF sc_images are not supported"); return; } } else if (!SCPathUtils.fileTypeIsPic(pickedFile.path)) { SCTts.show( allowGif ? "Please select sc_images in .jpg, .jpeg, .png, .gif format." : "Please select sc_images in .jpg, .jpeg, .png format.", ); return; } final File imageFile = File(pickedFile.path); if (isGif && skipCropForGif) { await _uploadOriginalImage( context, imageFile, onUpLoadCallBack, maxBytes: _gifOriginalMaxImageBytes, oversizeMessage: "The GIF image size cannot exceed 10M", ); return; } if (neeCrop ?? false) { cropImage( imageFile, context, aspectRatio, backOriginalFile, onUpLoadCallBack, ); } else { await _uploadOriginalImage( context, imageFile, onUpLoadCallBack, maxBytes: _defaultMaxImageBytes, oversizeMessage: SCAppLocalizations.of(context)!.theImageSizeCannotExceed, ); } } catch (e) { onUpLoadCallBack(false, ""); SCTts.show("Failed to select image"); } } static Future _uploadOriginalImage( BuildContext context, File imageFile, OnUpLoadCallBack onUpLoadCallBack, { required int maxBytes, required String oversizeMessage, }) async { if (imageFile.lengthSync() > maxBytes) { SCTts.show(oversizeMessage); onUpLoadCallBack(false, ""); return; } SCLoadingManager.show(context: context); try { String fileUrl = await SCGeneralRepositoryImp().upload(imageFile); SCLoadingManager.hide(); onUpLoadCallBack(true, fileUrl); } catch (e) { SCTts.show("upload fail $e"); onUpLoadCallBack(false, ""); SCLoadingManager.hide(); } } /// 检查图像是否为 GIF 格式 static Future _igif(XFile file) async { try { // 方法1: 通过文件扩展名检查 if (file.path.toLowerCase().endsWith('.gif')) { return true; } // 方法2: 通过读取文件头检查 (更可靠) final bytes = await file.readAsBytes(); if (bytes.length >= 6) { // GIF 文件头特征: "GIF87a" 或 "GIF89a" final header = String.fromCharCodes(bytes.sublist(0, 6)); return header == 'GIF87a' || header == 'GIF89a'; } return false; } catch (e) { return false; } } static Future _iwp(XFile file) async { try { // 方法1: 通过文件扩展名检查 if (file.path.toLowerCase().endsWith('.webp')) { return true; } // 方法2: 通过读取文件头检查 final bytes = await file.readAsBytes(); if (bytes.length < 12) { return false; } // 检查基本的 WebP 文件头 final riffHeader = String.fromCharCodes(bytes.sublist(0, 4)); final webpHeader = String.fromCharCodes(bytes.sublist(8, 12)); if (riffHeader == 'RIFF' && webpHeader == 'WEBP') { return true; } // 检查扩展的 WebP 格式(如果有更多字节可用) if (bytes.length >= 16) { // 检查 VP8 (有损) WebP if (bytes[12] == 0x56 && bytes[13] == 0x50 && bytes[14] == 0x38 && bytes[15] == 0x20) { return true; } // 检查 VP8L (无损) WebP if (bytes[12] == 0x56 && bytes[13] == 0x50 && bytes[14] == 0x38 && bytes[15] == 0x4C) { return true; } // 检查 VP8X (扩展) WebP if (bytes[12] == 0x56 && bytes[13] == 0x50 && bytes[14] == 0x38 && bytes[15] == 0x58) { return true; } } return false; } catch (e) { return false; } } /// 拍照并过滤 GIF static void takePhoto( BuildContext context, OnUpLoadCallBack onUpLoadCallBack, { double? aspectRatio, bool? backOriginalFile = true, }) async { try { final XFile? pickedFile = await _pkr.pickImage( source: ImageSource.camera, imageQuality: 90, maxWidth: 1920, maxHeight: 1080, ); if (pickedFile == null) return; // 相机拍摄的照片不会是 GIF,但为了安全也可以检查 final isGif = await _igif(pickedFile); if (!context.mounted) { onUpLoadCallBack(false, ""); return; } if (isGif) { SCTts.show("Unsupported image format"); return; } final File imageFile = File(pickedFile.path); cropImage( imageFile, context, aspectRatio, backOriginalFile, onUpLoadCallBack, ); } catch (e) { SCTts.show("Camera failed"); } } /// 选择视频 (不涉及 GIF 过滤) static Future pickVideo(BuildContext context) async { try { final XFile? videoFile = await _pkr.pickVideo( source: ImageSource.gallery, ); if (videoFile != null) { return File(videoFile.path); } return null; } catch (e) { SCTts.show("Failed to select video_pic"); return null; } } // 原有的 cropImage 方法保持不变 static void cropImage( File originalImage, BuildContext context, double? aspectRatio, bool? backOriginalFile, OnUpLoadCallBack onUpLoadCallBack, { bool needUpload = true, }) async { await Navigator.push( context, MaterialPageRoute( builder: (context) => CropImagePage( originalImage, aspectRatio: aspectRatio, backOriginalFile: backOriginalFile, needUpload: needUpload, onUpLoadCallBack: (success, url) { if (url.isNotEmpty) { onUpLoadCallBack(success, url); } }, ), ), ); } }