import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/core/photo_info.dart'; import 'package:jj_mkvmaker/core/reader_sources.dart'; import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/reader_page.dart'; import 'package:path/path.dart' as p; /// 도 · 분 · 초 (image 패키지가 Rational 을 내보내지 않아 IfdValueRational 하나씩 만들어 잇는다) img.IfdValueRational _dms(int d, int m, int s) => img.IfdValueRational(d, 1)..value.addAll([...img.IfdValueRational(m, 1).value, ...img.IfdValueRational(s, 1).value]); /// Exif 가 든 JPEG (찍은 날 · 기기 · 위치) Uint8List _jpeg({bool gps = true, bool south = false}) { final im = img.Image(width: 30, height: 20); im.exif.imageIfd['Make'] = 'Canon'; im.exif.imageIfd['Model'] = 'Canon EOS 5D'; im.exif.exifIfd['DateTimeOriginal'] = '2024:05:01 12:30:00'; if (gps) { im.exif.gpsIfd[1] = img.IfdValueAscii(south ? 'S' : 'N'); im.exif.gpsIfd[2] = _dms(37, 30, 0); im.exif.gpsIfd[3] = img.IfdValueAscii('E'); im.exif.gpsIfd[4] = _dms(127, 0, 36); } return Uint8List.fromList(img.encodeJpg(im)); } /// X-plore 맞추기 (관리자 지시 10/20): 보기 화면 사진 정보 (Exif) - 찍은 날 · 기기 · 크기 · 위치 (누를 때만, 보내지 않음) void main() { test('Exif 읽기: 찍은 날 (2024-05-01) · 기기 (Make 가 Model 앞에 있으면 한 번만) · 크기 · 위치 (도 · 분 · 초 → 도, 남은 음수)', () { final i = readPhotoInfo(_jpeg()); expect(i.width, 30); expect(i.height, 20); expect(i.taken, '2024-05-01 12:30:00'); expect(i.device, 'Canon EOS 5D'); expect(i.latitude, closeTo(37.5, 1e-6)); expect(i.longitude, closeTo(127.01, 1e-6)); expect(i.locationText, '37.500000, 127.010000'); expect(i.hasExif, isTrue); expect(readPhotoInfo(_jpeg(south: true)).latitude, closeTo(-37.5, 1e-6)); expect(readPhotoInfo(_jpeg(gps: false)).hasLocation, isFalse); }); test('Exif 가 없는 그림 (PNG) 은 크기만, 깨진 바이트는 아무것도 없이 (예외 없음)', () { final png = readPhotoInfo(Uint8List.fromList(img.encodePng(img.Image(width: 7, height: 9)))); expect((png.width, png.height), (7, 9)); expect(png.hasExif, isFalse); final bad = readPhotoInfo(Uint8List.fromList([1, 2, 3])); expect(bad.width, isNull); expect(bad.hasExif, isFalse); }); testWidgets('보기 화면 [사진 정보 (Exif)]: 찍은 날 · 기기 · 크기, 위치는 [위치 보기] 를 눌러야 보임 (링크 없음)', (tester) async { tester.view.physicalSize = const Size(1400, 1200); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final tmp = Directory.systemTemp.createTempSync('jj_photoinfo_'); addTearDown(() => tmp.deleteSync(recursive: true)); final f = File(p.join(tmp.path, 'trip.jpg'))..writeAsBytesSync(_jpeg()); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); await tester.pumpWidget(MaterialApp(home: ReaderPage(c: c, source: ImageFilesSource([f.path], tempDir: tmp.path, title: 'x')))); await tester.pump(); await tester.tap(find.byTooltip('사진 정보 (Exif)')); for (var k = 0; k < 10; k++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); await tester.pump(); } expect(find.text('2024-05-01 12:30:00'), findsOneWidget); expect(find.text('Canon EOS 5D'), findsOneWidget); expect(find.textContaining('30 × 20 픽셀'), findsOneWidget); expect(find.textContaining('37.5'), findsNothing, reason: '위치는 누를 때만'); await tester.tap(find.text('위치 보기')); await tester.pump(); expect(find.text('37.500000, 127.010000'), findsOneWidget); expect(find.textContaining('http'), findsNothing, reason: '지도 · 인터넷 링크 없음'); await tester.tap(find.text('닫기')); await tester.pumpAndSettle(); await tester.pumpWidget(const SizedBox()); }); }