watermarked_camera.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. import 'dart:io';
  2. import 'dart:typed_data';
  3. import 'dart:ui' as ui;
  4. import 'package:flutter/material.dart';
  5. import 'package:image_picker/image_picker.dart';
  6. import 'package:intl/intl.dart';
  7. import 'package:path_provider/path_provider.dart';
  8. import 'package:chicken_farm/core/utils/logger.dart';
  9. import 'package:flutter_image_compress/flutter_image_compress.dart';
  10. class WatermarkedCamera extends StatefulWidget {
  11. final Function(File)? onImageCaptured;
  12. final String? watermarkText;
  13. final String? watermarkDateText;
  14. const WatermarkedCamera({
  15. super.key,
  16. this.onImageCaptured,
  17. this.watermarkText,
  18. this.watermarkDateText,
  19. });
  20. @override
  21. State<WatermarkedCamera> createState() => _WatermarkedCameraState();
  22. }
  23. class _WatermarkedCameraState extends State<WatermarkedCamera> {
  24. XFile? _capturedImage;
  25. File? _watermarkedImage;
  26. bool _isProcessing = false;
  27. // 添加状态标记,用于判断是否已初始化拍照
  28. bool _initialized = false;
  29. @override
  30. void initState() {
  31. super.initState();
  32. // 在初始化时自动启动拍照
  33. WidgetsBinding.instance.addPostFrameCallback((_) {
  34. _takePicture();
  35. _initialized = true;
  36. });
  37. }
  38. Future<void> _takePicture() async {
  39. try {
  40. final ImagePicker picker = ImagePicker();
  41. final XFile? photo = await picker.pickImage(
  42. source: ImageSource.camera,
  43. imageQuality: 80,
  44. );
  45. if (photo != null) {
  46. setState(() {
  47. _capturedImage = photo;
  48. _isProcessing = true;
  49. });
  50. // 压缩图片后再添加水印
  51. final compressedImage = await _compressImage(photo);
  52. // 添加水印并处理图像
  53. await _addWatermark(compressedImage);
  54. } else {
  55. // 如果用户取消拍照,则关闭页面
  56. if (mounted && _initialized) {
  57. Navigator.of(context).pop();
  58. }
  59. }
  60. } catch (e) {
  61. if (mounted) {
  62. ScaffoldMessenger.of(
  63. context,
  64. ).showSnackBar(SnackBar(content: Text('拍照失败: $e')));
  65. }
  66. logger.e('拍照失败: $e');
  67. // 出错时也关闭页面
  68. if (mounted && _initialized) {
  69. Navigator.of(context).pop();
  70. }
  71. }
  72. }
  73. /// 压缩图片以确保文件大小在500KB以内
  74. Future<XFile> _compressImage(XFile imageFile) async {
  75. try {
  76. final tempDir = await getTemporaryDirectory();
  77. final originalFile = File(imageFile.path);
  78. final originalSize = await originalFile.length();
  79. logger.i('原始图片大小: ${originalSize / 1024} KB');
  80. // 如果原始文件已经小于500KB,直接返回
  81. if (originalSize <= 500 * 1024) {
  82. return imageFile;
  83. }
  84. // 初始压缩参数
  85. int quality = 50;
  86. int minWidth = 1024;
  87. int minHeight = 768;
  88. XFile? bestCompressedFile;
  89. int bestFileSize = originalSize.toInt();
  90. String bestFilePath = '';
  91. // 迭代压缩直到文件大小符合要求
  92. while (bestFileSize > 500 * 1024 && (quality >= 20 || minWidth >= 640)) {
  93. final targetPath =
  94. '${tempDir.path}/compressed_${DateTime.now().millisecondsSinceEpoch}_${quality}_$minWidth.jpg';
  95. final compressedFile = await FlutterImageCompress.compressAndGetFile(
  96. imageFile.path,
  97. targetPath,
  98. quality: quality,
  99. minWidth: minWidth,
  100. minHeight: minHeight,
  101. );
  102. if (compressedFile != null) {
  103. final compressedFileSize = await File(compressedFile.path).length();
  104. logger.i(
  105. '压缩后图片大小 ($quality% 质量, ${minWidth}x$minHeight): ${compressedFileSize / 1024} KB',
  106. );
  107. if (compressedFileSize <= 500 * 1024) {
  108. // 找到符合大小要求的文件
  109. if (compressedFileSize < bestFileSize) {
  110. bestCompressedFile = compressedFile;
  111. bestFileSize = compressedFileSize.toInt();
  112. bestFilePath = targetPath;
  113. }
  114. break;
  115. } else if (compressedFileSize < bestFileSize) {
  116. // 记录目前为止最小的文件
  117. bestCompressedFile = compressedFile;
  118. bestFileSize = compressedFileSize.toInt();
  119. bestFilePath = targetPath;
  120. }
  121. // 调整参数继续尝试
  122. if (quality > 20) {
  123. // 优先降低质量
  124. quality -= 10;
  125. } else if (minWidth > 640) {
  126. // 质量不能再低了,减小尺寸
  127. minWidth -= 128;
  128. minHeight = (minHeight * minWidth / (minWidth + 128))
  129. .toInt(); // 按比例缩小
  130. quality = 30; // 恢复一定质量
  131. } else {
  132. // 已经达到极限
  133. break;
  134. }
  135. } else {
  136. // 压缩失败,跳出循环
  137. break;
  138. }
  139. }
  140. // 清理其他临时文件,只保留最佳文件
  141. final dir = Directory(tempDir.path);
  142. await for (final file in dir.list()) {
  143. if (file.path.contains('compressed_') && file.path != bestFilePath) {
  144. try {
  145. await file.delete();
  146. } catch (e) {
  147. logger.w('删除临时文件失败: $e');
  148. }
  149. }
  150. }
  151. if (bestCompressedFile != null) {
  152. logger.i('最终图片大小: ${bestFileSize / 1024} KB');
  153. return bestCompressedFile;
  154. } else {
  155. // 如果所有尝试都失败,返回原始文件
  156. return imageFile;
  157. }
  158. } catch (e) {
  159. logger.e('图片压缩失败: $e');
  160. // 如果压缩过程中出错,返回原始文件
  161. return imageFile;
  162. }
  163. }
  164. Future<void> _addWatermark(XFile imageFile) async {
  165. try {
  166. // 获取水印文本
  167. String watermark =
  168. widget.watermarkDateText ??
  169. DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
  170. // 将图片转换为字节以便处理
  171. final Uint8List imageBytes = await imageFile.readAsBytes();
  172. // 创建图片对象
  173. final ui.Codec codec = await ui.instantiateImageCodec(imageBytes);
  174. final ui.FrameInfo frameInfo = await codec.getNextFrame();
  175. final ui.Image image = frameInfo.image;
  176. // 创建画布
  177. final ui.PictureRecorder recorder = ui.PictureRecorder();
  178. final ui.Canvas canvas = ui.Canvas(recorder);
  179. // 绘制原始图片
  180. canvas.drawImage(image, Offset.zero, ui.Paint());
  181. // 创建段落样式
  182. final ui.ParagraphStyle paragraphStyle = ui.ParagraphStyle(
  183. textAlign: TextAlign.right,
  184. fontSize: 36.0,
  185. textDirection: ui.TextDirection.ltr,
  186. );
  187. // 创建文字样式
  188. final ui.TextStyle textStyle = ui.TextStyle(
  189. color: const ui.Color(0xFFFFFFFF), // 纯白色
  190. fontSize: 36.0,
  191. fontWeight: ui.FontWeight.bold, // 加粗
  192. shadows: [
  193. ui.Shadow(
  194. blurRadius: 3.0,
  195. color: const ui.Color(0xFF000000), // 黑色阴影增强可读性
  196. offset: const Offset(2.0, 2.0),
  197. ),
  198. ],
  199. );
  200. double verticalOffset = image.height.toDouble() - 100;
  201. // 如果有watermarkText,则先绘制watermarkText
  202. if (widget.watermarkText != null && widget.watermarkText!.isNotEmpty) {
  203. final ui.ParagraphBuilder additionalParagraphBuilder =
  204. ui.ParagraphBuilder(paragraphStyle);
  205. additionalParagraphBuilder.pushStyle(textStyle);
  206. additionalParagraphBuilder.addText(widget.watermarkText!);
  207. final ui.Paragraph additionalParagraph =
  208. additionalParagraphBuilder.build()
  209. ..layout(ui.ParagraphConstraints(width: image.width.toDouble()));
  210. verticalOffset -= 70;
  211. // 绘制watermarkText(在时间戳上方)
  212. canvas.drawParagraph(
  213. additionalParagraph,
  214. Offset(
  215. image.width.toDouble() - additionalParagraph.width - 90,
  216. verticalOffset,
  217. ),
  218. );
  219. }
  220. // 绘制时间戳水印
  221. final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
  222. paragraphStyle,
  223. );
  224. paragraphBuilder.pushStyle(textStyle);
  225. paragraphBuilder.addText(watermark);
  226. final ui.Paragraph paragraph = paragraphBuilder.build()
  227. ..layout(ui.ParagraphConstraints(width: image.width.toDouble()));
  228. // 在右下角绘制时间戳水印
  229. canvas.drawParagraph(
  230. paragraph,
  231. Offset(
  232. image.width.toDouble() - paragraph.width - 90,
  233. verticalOffset + 60,
  234. ),
  235. );
  236. // 完成绘制
  237. final ui.Picture picture = recorder.endRecording();
  238. final ui.Image watermarkedImage = await picture.toImage(
  239. image.width,
  240. image.height,
  241. );
  242. // 转换为字节数据(PNG格式)
  243. final ByteData? byteData = await watermarkedImage.toByteData(
  244. format: ui.ImageByteFormat.png,
  245. );
  246. if (byteData != null) {
  247. // 保存到临时文件
  248. final tempDir = await getTemporaryDirectory();
  249. final file = File(
  250. '${tempDir.path}/watermarked_${DateTime.now().millisecondsSinceEpoch}.png',
  251. );
  252. await file.writeAsBytes(byteData.buffer.asUint8List());
  253. // 检查文件大小,如超过500KB则再压缩一次
  254. final fileSize = await file.length();
  255. logger.i('添加水印后图片大小: ${fileSize / 1024} KB');
  256. if (fileSize > 500 * 1024) {
  257. // 再次压缩
  258. final reCompressedFile = await _compressImage(XFile(file.path));
  259. setState(() {
  260. _watermarkedImage = File(reCompressedFile.path);
  261. _isProcessing = false;
  262. });
  263. } else {
  264. setState(() {
  265. _watermarkedImage = file;
  266. _isProcessing = false;
  267. });
  268. }
  269. } else {
  270. setState(() {
  271. _isProcessing = false;
  272. });
  273. if (mounted) {
  274. ScaffoldMessenger.of(
  275. context,
  276. ).showSnackBar(const SnackBar(content: Text('处理图片失败')));
  277. }
  278. logger.e('处理图片失败');
  279. }
  280. } catch (e) {
  281. setState(() {
  282. _isProcessing = false;
  283. });
  284. if (mounted) {
  285. ScaffoldMessenger.of(
  286. context,
  287. ).showSnackBar(SnackBar(content: Text('添加水印失败: $e')));
  288. }
  289. logger.e('添加水印失败: $e');
  290. }
  291. }
  292. void _confirmImage() {
  293. if (_watermarkedImage != null && widget.onImageCaptured != null) {
  294. widget.onImageCaptured!(_watermarkedImage!);
  295. }
  296. Navigator.of(context).pop();
  297. }
  298. void _retakePicture() {
  299. setState(() {
  300. _capturedImage = null;
  301. _watermarkedImage = null;
  302. });
  303. // 重新拍照
  304. _takePicture();
  305. }
  306. @override
  307. Widget build(BuildContext context) {
  308. return Scaffold(
  309. appBar: AppBar(
  310. title: const Text('拍照'),
  311. leading: IconButton(
  312. icon: const Icon(Icons.close),
  313. onPressed: () => Navigator.of(context).pop(),
  314. ),
  315. ),
  316. body: Column(
  317. children: [
  318. Expanded(
  319. child: _isProcessing
  320. ? const Center(child: CircularProgressIndicator())
  321. : Center(
  322. child: _watermarkedImage != null
  323. ? Image.file(_watermarkedImage!)
  324. : _capturedImage != null
  325. ? Image.file(File(_capturedImage!.path))
  326. : const SizedBox(), // 避免显示默认提示信息
  327. ),
  328. ),
  329. Padding(
  330. padding: const EdgeInsets.all(16.0),
  331. child: Row(
  332. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  333. children: [
  334. if (_capturedImage != null && !_isProcessing)
  335. Row(
  336. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  337. children: [
  338. ElevatedButton(
  339. onPressed: _retakePicture,
  340. style: ElevatedButton.styleFrom(
  341. backgroundColor: Colors.grey,
  342. foregroundColor: Colors.white,
  343. ),
  344. child: const Text('重新拍照'),
  345. ),
  346. const SizedBox(width: 16.0),
  347. ElevatedButton(
  348. onPressed: _confirmImage,
  349. style: ElevatedButton.styleFrom(
  350. backgroundColor: Colors.green,
  351. foregroundColor: Colors.white,
  352. ),
  353. child: const Text('确认提交'),
  354. ),
  355. ],
  356. ),
  357. ],
  358. ),
  359. ),
  360. ],
  361. ),
  362. );
  363. }
  364. }