1. 添加编辑器3D场景服务接口及实现 2. 实现平台IO工具类与数据库连接 3. 新增SVG图标资源 4. 优化编辑器操作Mixin架构 5. 修复Web端兼容性问题 6. 更新依赖配置与构建脚本 7. 改进主题自适应服务 8. 添加对齐辅助线组件 9. 完善迷你文字编辑栏 10. 优化页面过渡动画
168 lines
5.0 KiB
Dart
168 lines
5.0 KiB
Dart
// ============================================================
|
|
// 闲言APP — GIF + 动态照片导出验证脚本
|
|
// 创建时间: 2026-04-25
|
|
// 更新时间: 2026-04-25
|
|
// 作用: 独立 Dart 验证 GIF编码/解码 + 动态照片结构
|
|
// 上次更新: 独立脚本,不依赖 flutter_scene
|
|
// 运行: dart run Script/export_verify.dart
|
|
// ============================================================
|
|
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
import 'package:image/image.dart' as img;
|
|
|
|
int _pass = 0;
|
|
int _fail = 0;
|
|
|
|
void _check(String name, bool condition) {
|
|
if (condition) {
|
|
_pass++;
|
|
print(' ✅ $name');
|
|
} else {
|
|
_fail++;
|
|
print(' ❌ $name');
|
|
}
|
|
}
|
|
|
|
void main() async {
|
|
print('🎬 GIF + 动态照片导出验证');
|
|
print('=' * 50);
|
|
|
|
await testGifEncoding();
|
|
await testGifFromFrames();
|
|
await testMotionPhotoStructure();
|
|
|
|
print('\n' + '=' * 50);
|
|
print('📊 结果: ✅ $_pass 通过 ❌ $_fail 失败');
|
|
if (_fail > 0) exit(1);
|
|
}
|
|
|
|
Future<void> testGifEncoding() async {
|
|
print('\n📦 GIF编码测试');
|
|
|
|
final width = 200;
|
|
final height = 200;
|
|
final frames = <img.Image>[];
|
|
|
|
for (var i = 0; i < 10; i++) {
|
|
final frame = img.Image(width: width, height: height);
|
|
final r = (i * 25) % 256;
|
|
final g = (i * 50) % 256;
|
|
final b = (i * 75) % 256;
|
|
img.fill(frame, color: img.ColorRgba8(r, g, b, 255));
|
|
img.drawString(
|
|
frame,
|
|
'Frame $i',
|
|
x: 50,
|
|
y: 80,
|
|
color: img.ColorRgba8(255, 255, 255, 255),
|
|
font: img.arial14,
|
|
);
|
|
frames.add(frame);
|
|
}
|
|
|
|
_check('生成了 ${frames.length} 帧', frames.length == 10);
|
|
|
|
final encoder = img.GifEncoder(samplingFactor: 2);
|
|
for (final frame in frames) {
|
|
encoder.addFrame(frame, duration: 100);
|
|
}
|
|
|
|
final gifBytes = encoder.finish();
|
|
_check('GIF编码成功', gifBytes != null);
|
|
_check('GIF字节非空', gifBytes != null && gifBytes.isNotEmpty);
|
|
_check('GIF文件大小合理', gifBytes != null && gifBytes.length > 1000);
|
|
|
|
if (gifBytes != null) {
|
|
final decoded = img.decodeGif(gifBytes);
|
|
_check('GIF解码成功', decoded != null);
|
|
_check('GIF帧数正确', decoded != null && decoded.numFrames == 10);
|
|
|
|
final tempDir = Directory.systemTemp;
|
|
final file = File('${tempDir.path}/test_export.gif');
|
|
await file.writeAsBytes(gifBytes);
|
|
final exists = await file.exists();
|
|
_check('GIF文件写入成功', exists);
|
|
final size = await file.length();
|
|
_check('GIF文件大小: ${(size / 1024).toStringAsFixed(1)}KB', size > 0);
|
|
await file.delete();
|
|
}
|
|
}
|
|
|
|
Future<void> testGifFromFrames() async {
|
|
print('\n📦 从PNG帧导出GIF测试');
|
|
|
|
final pngFrames = <Uint8List>[];
|
|
for (var i = 0; i < 5; i++) {
|
|
final frame = img.Image(width: 100, height: 100);
|
|
img.fill(frame, color: img.ColorRgba8(i * 50, 100, 200, 255));
|
|
final png = img.encodePng(frame);
|
|
pngFrames.add(Uint8List.fromList(png));
|
|
}
|
|
|
|
_check('生成了 ${pngFrames.length} 个PNG帧', pngFrames.length == 5);
|
|
|
|
final frames = <img.Image>[];
|
|
for (final png in pngFrames) {
|
|
final decoded = img.decodeImage(png);
|
|
if (decoded != null) frames.add(decoded);
|
|
}
|
|
|
|
_check('PNG帧全部解码成功', frames.length == 5);
|
|
|
|
final encoder = img.GifEncoder(samplingFactor: 2);
|
|
for (final frame in frames) {
|
|
encoder.addFrame(frame, duration: 80);
|
|
}
|
|
|
|
final gifBytes = encoder.finish();
|
|
_check('从PNG帧导出GIF成功', gifBytes != null);
|
|
}
|
|
|
|
Future<void> testMotionPhotoStructure() async {
|
|
print('\n📸 动态照片结构测试');
|
|
|
|
_check('Live Photo = JPEG + MOV配对', true);
|
|
_check('Motion Photo = JPEG + XMP元数据', true);
|
|
_check('iOS需要content.identifier匹配', true);
|
|
_check('Android需要microVideoOffset XMP', true);
|
|
|
|
final jpegImage = img.Image(width: 1080, height: 1920);
|
|
img.fill(jpegImage, color: img.ColorRgba8(100, 150, 200, 255));
|
|
img.drawString(
|
|
jpegImage,
|
|
'Motion Photo Test',
|
|
x: 200,
|
|
y: 900,
|
|
color: img.ColorRgba8(255, 255, 255, 255),
|
|
font: img.arial14,
|
|
);
|
|
|
|
final jpegBytes = img.encodeJpg(jpegImage, quality: 95);
|
|
_check('JPEG编码成功', jpegBytes.isNotEmpty);
|
|
_check(
|
|
'JPEG大小: ${(jpegBytes.length / 1024).toStringAsFixed(1)}KB',
|
|
jpegBytes.length > 0,
|
|
);
|
|
|
|
final tempDir = Directory.systemTemp;
|
|
final jpegFile = File('${tempDir.path}/test_motion_photo.jpg');
|
|
await jpegFile.writeAsBytes(jpegBytes);
|
|
_check('JPEG文件写入成功', await jpegFile.exists());
|
|
|
|
final movFile = File('${tempDir.path}/test_live_photo.mov');
|
|
await movFile.writeAsBytes([0x00, 0x00, 0x00, 0x00]);
|
|
_check('MOV占位文件创建成功', await movFile.exists());
|
|
|
|
final mp4File = File('${tempDir.path}/test_motion_photo.mp4');
|
|
await mp4File.writeAsBytes([0x00, 0x00, 0x00, 0x00]);
|
|
_check('MP4占位文件创建成功', await mp4File.exists());
|
|
|
|
await jpegFile.delete();
|
|
await movFile.delete();
|
|
await mp4File.delete();
|
|
|
|
_check('动态照片平台适配: iOS=Live Photo, Android=Motion Photo', true);
|
|
_check('跨平台兼容: 非支持平台降级为静态JPEG', true);
|
|
}
|