logo和名称替换

This commit is contained in:
谢宇宁
2026-10-14 11:23:36 +07:00
parent 1cb514cd02
commit c5c04a7967
478 changed files with 8990 additions and 4948 deletions
+39 -15
View File
@@ -10,8 +10,8 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_native_image/flutter_native_image.dart';
import 'package:image/image.dart' as img;
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:hgmj/tools_base/debug_log.dart';
import 'package:hgmj/tools_base/toast.dart';
import 'package:wydj/tools_base/debug_log.dart';
import 'package:wydj/tools_base/toast.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:zxing2/qrcode.dart';
@@ -23,7 +23,8 @@ class ImageUtil {
static Future<String?> decodeQr(String path) async {
var decodePath = path;
try {
final converted = await FlutterNativeImage.compressImage(path, percentage: 100, quality: 100);
final converted = await FlutterNativeImage.compressImage(path,
percentage: 100, quality: 100);
decodePath = converted.path;
} catch (_) {
// 转换失败就拿原图碰运气(本就是 jpg/png 时不影响)
@@ -39,7 +40,11 @@ class ImageUtil {
final source = RGBLuminanceSource(
image.width,
image.height,
image.convert(numChannels: 4).getBytes(order: img.ChannelOrder.rgba).buffer.asInt32List(),
image
.convert(numChannels: 4)
.getBytes(order: img.ChannelOrder.rgba)
.buffer
.asInt32List(),
);
final bitmap = BinaryBitmap(HybridBinarizer(source));
// tryHarder:相册照片有边框/轻微旋转/噪点,放开更费时但识别率更高
@@ -54,7 +59,8 @@ class ImageUtil {
/// 注:boundary 必须完整渲染在屏幕内,被滚动容器裁剪会截不全甚至失败
static Future<bool> saveWidgetToAlbum(GlobalKey boundaryKey) async {
await WidgetsBinding.instance.endOfFrame;
final boundary = boundaryKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
final boundary = boundaryKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
if (boundary == null) return false;
Image? image;
@@ -112,7 +118,8 @@ class ImageUtil {
if (decoder == null) return null;
decoder.startDecode(bytes); //只解头/块信息,不解像素,很便宜
final total = decoder.numFrames();
debugLog('saveToAlbum', '${decoder.runtimeType} frames=$total size=${bytes.length}');
debugLog('saveToAlbum',
'${decoder.runtimeType} frames=$total size=${bytes.length}');
if (total <= 1) return null;
// 必须整体 decode:animated webp 的每帧只是个局部矩形,靠 dispose/blend 往画布上叠,
@@ -133,11 +140,16 @@ class ImageUtil {
var delay = 0;
for (var i = 0; i < src.numFrames; i += step) {
//frames[0] 就是 src 本身、挂着整条动画,copyResize 会连整条一起缩放,得先取单帧副本
final f = i == 0 ? img.Image.from(src.frames[0], noAnimation: true) : src.frames[i];
final f = i == 0
? img.Image.from(src.frames[0], noAnimation: true)
: src.frames[i];
//必须指定 average:copyResize 默认是 nearest,降采样直接丢像素,出来全是锯齿
final out = (f.width == outW && f.height == outH)
? f
: img.copyResize(f, width: outW, height: outH, interpolation: img.Interpolation.average);
: img.copyResize(f,
width: outW,
height: outH,
interpolation: img.Interpolation.average);
sink.writeFromSync(out.getBytes(order: img.ChannelOrder.rgba));
if (delay == 0 && f.frameDuration > 0) delay = f.frameDuration;
count++;
@@ -145,9 +157,15 @@ class ImageUtil {
sink.closeSync();
if (count < 2) return null;
debugLog('saveToAlbum', 'raw ${outW}x$outH frames=$count step=$step delay=$delay');
debugLog('saveToAlbum',
'raw ${outW}x$outH frames=$count step=$step delay=$delay');
//抽帧后帧间隔要乘上 step,整体播放速度才不变;源没写时长就兜 80ms
return {'w': outW, 'h': outH, 'n': count, 'delay': (delay > 0 ? delay : 80) * step};
return {
'w': outW,
'h': outH,
'n': count,
'delay': (delay > 0 ? delay : 80) * step
};
} catch (e) {
debugLog('saveToAlbum', 'decodeToRaw failed: $e');
return null;
@@ -155,17 +173,20 @@ class ImageUtil {
}
/// RGBA 裸流 → H.264 mp4(x264 在 -gpl 变体里才有)
static Future<Uint8List?> _rawToMp4(String rawPath, Map<String, int> meta) async {
static Future<Uint8List?> _rawToMp4(
String rawPath, Map<String, int> meta) async {
final outPath = rawPath.replaceFirst(RegExp(r'\.rgba$'), '.mp4');
final fps = (1000 / (meta['delay'] ?? 80)).clamp(1.0, 60.0);
final cmd = '-y -f rawvideo -pixel_format rgba -video_size ${meta['w']}x${meta['h']} '
final cmd =
'-y -f rawvideo -pixel_format rgba -video_size ${meta['w']}x${meta['h']} '
'-framerate ${fps.toStringAsFixed(2)} -i "$rawPath" '
'-c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -movflags +faststart "$outPath"';
final out = File(outPath);
try {
final session = await FFmpegKit.execute(cmd);
if (!ReturnCode.isSuccess(await session.getReturnCode())) {
debugLog('saveToAlbum', 'ffmpeg failed: ${await session.getAllLogsAsString()}');
debugLog('saveToAlbum',
'ffmpeg failed: ${await session.getAllLogsAsString()}');
return null;
}
return await out.readAsBytes();
@@ -196,10 +217,13 @@ class ImageUtil {
// 而那条分支用 URL(string:) 解析,裸路径没 scheme 会被 PhotoKit 拒,得传 file:// 形式
final isVideo = ext == 'mp4';
final result = await ImageGallerySaver.saveFile(
(Platform.isIOS && !isVideo) ? Uri.file(file.path).toString() : file.path,
(Platform.isIOS && !isVideo)
? Uri.file(file.path).toString()
: file.path,
name: fileName,
isReturnPathOfIOS: !isVideo,
).timeout(const Duration(seconds: 20), onTimeout: () => null); //插件在拿不到 fullSizeImageURL 时不回调,兜底防卡死
).timeout(const Duration(seconds: 20),
onTimeout: () => null); //插件在拿不到 fullSizeImageURL 时不回调,兜底防卡死
return result is Map && result["isSuccess"] == true;
} catch (e) {
debugLog('saveRawToAlbum 写入相册失败', e);