|
|
@@ -7,15 +7,18 @@ 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
|
|
|
@@ -27,6 +30,19 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
|
|
|
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();
|
|
|
@@ -41,8 +57,15 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
|
|
|
_isProcessing = true;
|
|
|
});
|
|
|
|
|
|
+ // 压缩图片后再添加水印
|
|
|
+ final compressedImage = await _compressImage(photo);
|
|
|
// 添加水印并处理图像
|
|
|
- await _addWatermark(photo);
|
|
|
+ await _addWatermark(compressedImage);
|
|
|
+ } else {
|
|
|
+ // 如果用户取消拍照,则关闭页面
|
|
|
+ if (mounted && _initialized) {
|
|
|
+ Navigator.of(context).pop();
|
|
|
+ }
|
|
|
}
|
|
|
} catch (e) {
|
|
|
if (mounted) {
|
|
|
@@ -51,6 +74,114 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
|
|
|
).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;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -58,7 +189,7 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
|
|
|
try {
|
|
|
// 获取水印文本
|
|
|
String watermark =
|
|
|
- widget.watermarkText ??
|
|
|
+ widget.watermarkDateText ??
|
|
|
DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
|
|
|
|
|
|
// 将图片转换为字节以便处理
|
|
|
@@ -76,33 +207,68 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
|
|
|
// 绘制原始图片
|
|
|
canvas.drawImage(image, Offset.zero, ui.Paint());
|
|
|
|
|
|
- // 添加水印文字
|
|
|
- final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
|
|
|
- ui.ParagraphStyle(
|
|
|
- textAlign: TextAlign.left,
|
|
|
- fontSize: 24.0,
|
|
|
- textDirection: ui.TextDirection.ltr,
|
|
|
- ),
|
|
|
+ // 创建段落样式
|
|
|
+ final ui.ParagraphStyle paragraphStyle = ui.ParagraphStyle(
|
|
|
+ textAlign: TextAlign.right,
|
|
|
+ fontSize: 36.0,
|
|
|
+ textDirection: ui.TextDirection.ltr,
|
|
|
);
|
|
|
|
|
|
- paragraphBuilder.pushStyle(
|
|
|
- ui.TextStyle(
|
|
|
- color: const ui.Color(0x80FFFFFF), // 半透明白色
|
|
|
- fontSize: 24.0,
|
|
|
- ),
|
|
|
+ // 创建文字样式
|
|
|
+ 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 - 20,
|
|
|
- image.height.toDouble() - 40,
|
|
|
+ image.width.toDouble() - paragraph.width - 90,
|
|
|
+ verticalOffset + 60,
|
|
|
),
|
|
|
);
|
|
|
|
|
|
@@ -113,7 +279,7 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
|
|
|
image.height,
|
|
|
);
|
|
|
|
|
|
- // 转换为字节数据
|
|
|
+ // 转换为字节数据(PNG格式)
|
|
|
final ByteData? byteData = await watermarkedImage.toByteData(
|
|
|
format: ui.ImageByteFormat.png,
|
|
|
);
|
|
|
@@ -126,10 +292,23 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
|
|
|
);
|
|
|
await file.writeAsBytes(byteData.buffer.asUint8List());
|
|
|
|
|
|
- setState(() {
|
|
|
- _watermarkedImage = file;
|
|
|
- _isProcessing = false;
|
|
|
- });
|
|
|
+ // 检查文件大小,如超过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;
|
|
|
@@ -169,13 +348,16 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
|
|
|
_capturedImage = null;
|
|
|
_watermarkedImage = null;
|
|
|
});
|
|
|
+
|
|
|
+ // 重新拍照
|
|
|
+ _takePicture();
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
return Scaffold(
|
|
|
appBar: AppBar(
|
|
|
- title: const Text('拍照签到'),
|
|
|
+ title: const Text('拍照'),
|
|
|
leading: IconButton(
|
|
|
icon: const Icon(Icons.close),
|
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
|
@@ -184,27 +366,14 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
|
|
|
body: Column(
|
|
|
children: [
|
|
|
Expanded(
|
|
|
- child: _capturedImage == null
|
|
|
- ? Center(
|
|
|
- child: Column(
|
|
|
- mainAxisAlignment: MainAxisAlignment.center,
|
|
|
- children: [
|
|
|
- const Icon(
|
|
|
- Icons.camera_alt,
|
|
|
- size: 100,
|
|
|
- color: Colors.grey,
|
|
|
- ),
|
|
|
- const SizedBox(height: 20),
|
|
|
- const Text('点击按钮拍照', style: TextStyle(fontSize: 18)),
|
|
|
- ],
|
|
|
- ),
|
|
|
- )
|
|
|
- : _isProcessing
|
|
|
+ child: _isProcessing
|
|
|
? const Center(child: CircularProgressIndicator())
|
|
|
: Center(
|
|
|
child: _watermarkedImage != null
|
|
|
? Image.file(_watermarkedImage!)
|
|
|
- : Image.file(File(_capturedImage!.path)),
|
|
|
+ : _capturedImage != null
|
|
|
+ ? Image.file(File(_capturedImage!.path))
|
|
|
+ : const SizedBox(), // 避免显示默认提示信息
|
|
|
),
|
|
|
),
|
|
|
Padding(
|
|
|
@@ -212,12 +381,7 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
|
|
|
child: Row(
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
|
children: [
|
|
|
- if (_capturedImage == null)
|
|
|
- FloatingActionButton(
|
|
|
- onPressed: _takePicture,
|
|
|
- child: const Icon(Icons.camera),
|
|
|
- )
|
|
|
- else if (!_isProcessing)
|
|
|
+ if (_capturedImage != null && !_isProcessing)
|
|
|
Row(
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
|
children: [
|
|
|
@@ -229,6 +393,7 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
|
|
|
),
|
|
|
child: const Text('重新拍照'),
|
|
|
),
|
|
|
+ const SizedBox(width: 16.0),
|
|
|
ElevatedButton(
|
|
|
onPressed: _confirmImage,
|
|
|
style: ElevatedButton.styleFrom(
|