Files
xianyan/Script/export_verify.dart
Developer a5b997aecb feat: 新增编辑器功能与优化
1. 添加编辑器3D场景服务接口及实现
2. 实现平台IO工具类与数据库连接
3. 新增SVG图标资源
4. 优化编辑器操作Mixin架构
5. 修复Web端兼容性问题
6. 更新依赖配置与构建脚本
7. 改进主题自适应服务
8. 添加对齐辅助线组件
9. 完善迷你文字编辑栏
10. 优化页面过渡动画
2026-04-25 09:50:30 +08:00

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);
}