import 'dart:convert'; import 'dart:io'; import 'package:carddex_demo/native/phash_loader.dart'; import 'package:carddex_demo/services/hash_library.dart'; import 'package:image/image.dart' as img; import 'package:path/path.dart' as p; const _imageExts = {'.jpg', '.jpeg', '.png', '.webp', '.bmp'}; /// Same pipeline as CardCropper.cropAndCorrect so library hashes match /// the same conditions that query hashes are computed under. const _cropW = 280; const _cropH = 390; const _borderTrim = 10; Future main(List args) async { final root = Directory.current.path; final assetsDir = Directory(p.join(root, 'test_assets')); final outHashes = p.join(root, 'data', 'hashes.json'); final outCatalog = p.join(root, 'data', 'catalog.json'); final libPath = _resolveLib(root, args); if (!assetsDir.existsSync()) { stderr.writeln('test_assets/ not found at ${assetsDir.path}'); exit(1); } final native = PhashNative(hostLibraryPath: libPath); final files = assetsDir .listSync() .whereType() .where((f) => _imageExts.contains(p.extension(f.path).toLowerCase())) .toList() ..sort((a, b) => p.basename(a.path).compareTo(p.basename(b.path))); stdout.writeln('hashgen — ${files.length} images from ${assetsDir.path}'); stdout.writeln('libphash — $libPath'); final catalogItems = >[]; final hashItems = []; for (final file in files) { final filename = p.basename(file.path); final cardId = p.basenameWithoutExtension(filename); try { final raw = await file.readAsBytes(); var decoded = img.decodeImage(raw); if (decoded == null) { stderr.writeln('skip $cardId: decode failed'); continue; } decoded = img.bakeOrientation(decoded); // Match the App pipeline: resize → trim border → re-encode JPEG q=98. var resized = img.copyResize(decoded, width: _cropW, height: _cropH, interpolation: img.Interpolation.cubic); resized = img.copyCrop(resized, x: _borderTrim, y: _borderTrim, width: _cropW - _borderTrim * 2, height: _cropH - _borderTrim * 2); final hashedBytes = img.encodeJpg(resized, quality: 98); final hash = native.hashFromBytes(hashedBytes); hashItems.add(HashEntry(cardId: cardId, hash: hash, filename: filename)); catalogItems.add({ 'card_id': cardId, 'name': cardId, 'image': 'test_assets/$filename', 'filename': filename, }); stdout.writeln(' $cardId'); } catch (e) { stderr.writeln('skip $cardId: $e'); } } final library = HashLibrary( version: 'assets-v1', algorithm: 'rgb-phash-v1', hashBits: 105, items: hashItems, ); await library.saveFile(outHashes); final catalog = { 'version': 'assets-v1', 'algorithm': 'rgb-phash-v1', 'items': catalogItems, }; final catalogFile = File(outCatalog); await catalogFile.parent.create(recursive: true); await catalogFile.writeAsString( '${const JsonEncoder.withIndent(' ').convert(catalog)}\n', ); stdout.writeln('saved ${hashItems.length} hashes → $outHashes'); stdout.writeln('catalog → $outCatalog'); } String _resolveLib(String root, List args) { for (var i = 0; i < args.length; i++) { if (args[i] == '--lib' && i + 1 < args.length) { return p.normalize(p.absolute(args[i + 1])); } } final dylib = p.join(root, 'native', 'phash', 'build', 'libphash.dylib'); final so = p.join(root, 'native', 'phash', 'build', 'libphash.so'); if (File(dylib).existsSync()) return dylib; if (File(so).existsSync()) return so; stderr.writeln( 'libphash not found. Build first:\n' ' cmake -S native/phash -B native/phash/build -DPHASH_BUILD_SHARED=ON\n' ' cmake --build native/phash/build', ); exit(1); }