import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:chicken_farm/core/utils/logger.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; class WatermarkedCamera extends StatefulWidget { final Function(File)? onImageCaptured; final String? watermarkText; final String? watermarkDateText; const WatermarkedCamera({ super.key, this.onImageCaptured, this.watermarkText, this.watermarkDateText, }); @override State createState() => _WatermarkedCameraState(); } class _WatermarkedCameraState extends State { XFile? _capturedImage; File? _watermarkedImage; bool _isProcessing = false; // 添加状态标记,用于判断是否已初始化拍照 bool _initialized = false; @override void initState() { super.initState(); // 在初始化时自动启动拍照 WidgetsBinding.instance.addPostFrameCallback((_) { _takePicture(); _initialized = true; }); } Future _takePicture() async { try { final ImagePicker picker = ImagePicker(); final XFile? photo = await picker.pickImage( source: ImageSource.camera, imageQuality: 80, ); if (photo != null) { setState(() { _capturedImage = photo; _isProcessing = true; }); // 压缩图片后再添加水印 final compressedImage = await _compressImage(photo); // 添加水印并处理图像 await _addWatermark(compressedImage); } else { // 如果用户取消拍照,则关闭页面 if (mounted && _initialized) { Navigator.of(context).pop(); } } } catch (e) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('拍照失败: $e'))); } logger.e('拍照失败: $e'); // 出错时也关闭页面 if (mounted && _initialized) { Navigator.of(context).pop(); } } } /// 压缩图片以确保文件大小在500KB以内 Future _compressImage(XFile imageFile) async { try { final tempDir = await getTemporaryDirectory(); final originalFile = File(imageFile.path); final originalSize = await originalFile.length(); logger.i('原始图片大小: ${originalSize / 1024} KB'); // 如果原始文件已经小于500KB,直接返回 if (originalSize <= 500 * 1024) { return imageFile; } // 初始压缩参数 int quality = 50; int minWidth = 1024; int minHeight = 768; XFile? bestCompressedFile; int bestFileSize = originalSize.toInt(); String bestFilePath = ''; // 迭代压缩直到文件大小符合要求 while (bestFileSize > 500 * 1024 && (quality >= 20 || minWidth >= 640)) { final targetPath = '${tempDir.path}/compressed_${DateTime.now().millisecondsSinceEpoch}_${quality}_$minWidth.jpg'; final compressedFile = await FlutterImageCompress.compressAndGetFile( imageFile.path, targetPath, quality: quality, minWidth: minWidth, minHeight: minHeight, ); if (compressedFile != null) { final compressedFileSize = await File(compressedFile.path).length(); logger.i( '压缩后图片大小 ($quality% 质量, ${minWidth}x$minHeight): ${compressedFileSize / 1024} KB', ); if (compressedFileSize <= 500 * 1024) { // 找到符合大小要求的文件 if (compressedFileSize < bestFileSize) { bestCompressedFile = compressedFile; bestFileSize = compressedFileSize.toInt(); bestFilePath = targetPath; } break; } else if (compressedFileSize < bestFileSize) { // 记录目前为止最小的文件 bestCompressedFile = compressedFile; bestFileSize = compressedFileSize.toInt(); bestFilePath = targetPath; } // 调整参数继续尝试 if (quality > 20) { // 优先降低质量 quality -= 10; } else if (minWidth > 640) { // 质量不能再低了,减小尺寸 minWidth -= 128; minHeight = (minHeight * minWidth / (minWidth + 128)) .toInt(); // 按比例缩小 quality = 30; // 恢复一定质量 } else { // 已经达到极限 break; } } else { // 压缩失败,跳出循环 break; } } // 清理其他临时文件,只保留最佳文件 final dir = Directory(tempDir.path); await for (final file in dir.list()) { if (file.path.contains('compressed_') && file.path != bestFilePath) { try { await file.delete(); } catch (e) { logger.w('删除临时文件失败: $e'); } } } if (bestCompressedFile != null) { logger.i('最终图片大小: ${bestFileSize / 1024} KB'); return bestCompressedFile; } else { // 如果所有尝试都失败,返回原始文件 return imageFile; } } catch (e) { logger.e('图片压缩失败: $e'); // 如果压缩过程中出错,返回原始文件 return imageFile; } } Future _addWatermark(XFile imageFile) async { try { // 获取水印文本 String watermark = widget.watermarkDateText ?? DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()); // 将图片转换为字节以便处理 final Uint8List imageBytes = await imageFile.readAsBytes(); // 创建图片对象 final ui.Codec codec = await ui.instantiateImageCodec(imageBytes); final ui.FrameInfo frameInfo = await codec.getNextFrame(); final ui.Image image = frameInfo.image; // 创建画布 final ui.PictureRecorder recorder = ui.PictureRecorder(); final ui.Canvas canvas = ui.Canvas(recorder); // 绘制原始图片 canvas.drawImage(image, Offset.zero, ui.Paint()); // 创建段落样式 final ui.ParagraphStyle paragraphStyle = ui.ParagraphStyle( textAlign: TextAlign.right, fontSize: 36.0, textDirection: ui.TextDirection.ltr, ); // 创建文字样式 final ui.TextStyle textStyle = ui.TextStyle( color: const ui.Color(0xFFFFFFFF), // 纯白色 fontSize: 36.0, fontWeight: ui.FontWeight.bold, // 加粗 shadows: [ ui.Shadow( blurRadius: 3.0, color: const ui.Color(0xFF000000), // 黑色阴影增强可读性 offset: const Offset(2.0, 2.0), ), ], ); double verticalOffset = image.height.toDouble() - 100; // 如果有watermarkText,则先绘制watermarkText if (widget.watermarkText != null && widget.watermarkText!.isNotEmpty) { final ui.ParagraphBuilder additionalParagraphBuilder = ui.ParagraphBuilder(paragraphStyle); additionalParagraphBuilder.pushStyle(textStyle); additionalParagraphBuilder.addText(widget.watermarkText!); final ui.Paragraph additionalParagraph = additionalParagraphBuilder.build() ..layout(ui.ParagraphConstraints(width: image.width.toDouble())); verticalOffset -= 70; // 绘制watermarkText(在时间戳上方) canvas.drawParagraph( additionalParagraph, Offset( image.width.toDouble() - additionalParagraph.width - 90, verticalOffset, ), ); } // 绘制时间戳水印 final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder( paragraphStyle, ); paragraphBuilder.pushStyle(textStyle); paragraphBuilder.addText(watermark); final ui.Paragraph paragraph = paragraphBuilder.build() ..layout(ui.ParagraphConstraints(width: image.width.toDouble())); // 在右下角绘制时间戳水印 canvas.drawParagraph( paragraph, Offset( image.width.toDouble() - paragraph.width - 90, verticalOffset + 60, ), ); // 完成绘制 final ui.Picture picture = recorder.endRecording(); final ui.Image watermarkedImage = await picture.toImage( image.width, image.height, ); // 转换为字节数据(PNG格式) final ByteData? byteData = await watermarkedImage.toByteData( format: ui.ImageByteFormat.png, ); if (byteData != null) { // 保存到临时文件 final tempDir = await getTemporaryDirectory(); final file = File( '${tempDir.path}/watermarked_${DateTime.now().millisecondsSinceEpoch}.png', ); await file.writeAsBytes(byteData.buffer.asUint8List()); // 检查文件大小,如超过500KB则再压缩一次 final fileSize = await file.length(); logger.i('添加水印后图片大小: ${fileSize / 1024} KB'); if (fileSize > 500 * 1024) { // 再次压缩 final reCompressedFile = await _compressImage(XFile(file.path)); setState(() { _watermarkedImage = File(reCompressedFile.path); _isProcessing = false; }); } else { setState(() { _watermarkedImage = file; _isProcessing = false; }); } } else { setState(() { _isProcessing = false; }); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('处理图片失败'))); } logger.e('处理图片失败'); } } catch (e) { setState(() { _isProcessing = false; }); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('添加水印失败: $e'))); } logger.e('添加水印失败: $e'); } } void _confirmImage() { if (_watermarkedImage != null && widget.onImageCaptured != null) { widget.onImageCaptured!(_watermarkedImage!); } Navigator.of(context).pop(); } void _retakePicture() { setState(() { _capturedImage = null; _watermarkedImage = null; }); // 重新拍照 _takePicture(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('拍照'), leading: IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), ), body: Column( children: [ Expanded( child: _isProcessing ? const Center(child: CircularProgressIndicator()) : Center( child: _watermarkedImage != null ? Image.file(_watermarkedImage!) : _capturedImage != null ? Image.file(File(_capturedImage!.path)) : const SizedBox(), // 避免显示默认提示信息 ), ), Padding( padding: const EdgeInsets.all(16.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (_capturedImage != null && !_isProcessing) Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: _retakePicture, style: ElevatedButton.styleFrom( backgroundColor: Colors.grey, foregroundColor: Colors.white, ), child: const Text('重新拍照'), ), const SizedBox(width: 16.0), ElevatedButton( onPressed: _confirmImage, style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, ), child: const Text('确认提交'), ), ], ), ], ), ), ], ), ); } }