| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- 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<WatermarkedCamera> createState() => _WatermarkedCameraState();
- }
- class _WatermarkedCameraState extends State<WatermarkedCamera> {
- XFile? _capturedImage;
- File? _watermarkedImage;
- bool _isProcessing = false;
- // 添加状态标记,用于判断是否已初始化拍照
- bool _initialized = false;
- @override
- void initState() {
- super.initState();
- // 在初始化时自动启动拍照
- WidgetsBinding.instance.addPostFrameCallback((_) {
- _takePicture();
- _initialized = true;
- });
- }
- Future<void> _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<XFile> _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<void> _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('确认提交'),
- ),
- ],
- ),
- ],
- ),
- ),
- ],
- ),
- );
- }
- }
|