109 lines
2.9 KiB
Dart
109 lines
2.9 KiB
Dart
|
|
import 'dart:ffi';
|
||
|
|
import 'dart:io';
|
||
|
|
|
||
|
|
import 'package:ffi/ffi.dart';
|
||
|
|
|
||
|
|
import 'phash_bindings.dart';
|
||
|
|
|
||
|
|
/// Loads the vendored C++ `libphash` for the current platform.
|
||
|
|
DynamicLibrary loadPhashLibrary({String? hostLibraryPath}) {
|
||
|
|
if (hostLibraryPath != null && hostLibraryPath.isNotEmpty) {
|
||
|
|
return DynamicLibrary.open(hostLibraryPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Platform.isAndroid) {
|
||
|
|
return DynamicLibrary.open('libphash.so');
|
||
|
|
}
|
||
|
|
if (Platform.isIOS) {
|
||
|
|
try {
|
||
|
|
return DynamicLibrary.process();
|
||
|
|
} catch (_) {
|
||
|
|
return DynamicLibrary.open('phash.framework/phash');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (Platform.isMacOS) {
|
||
|
|
final candidates = <String>[
|
||
|
|
'native/phash/build/libphash.dylib',
|
||
|
|
'native/phash/build/libphash.so',
|
||
|
|
'libphash.dylib',
|
||
|
|
'libphash.so',
|
||
|
|
];
|
||
|
|
for (final path in candidates) {
|
||
|
|
final file = File(path);
|
||
|
|
if (file.existsSync()) {
|
||
|
|
return DynamicLibrary.open(file.absolute.path);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return DynamicLibrary.open('libphash.dylib');
|
||
|
|
}
|
||
|
|
if (Platform.isLinux) {
|
||
|
|
final candidates = <String>[
|
||
|
|
'native/phash/build/libphash.so',
|
||
|
|
'libphash.so',
|
||
|
|
];
|
||
|
|
for (final path in candidates) {
|
||
|
|
final file = File(path);
|
||
|
|
if (file.existsSync()) {
|
||
|
|
return DynamicLibrary.open(file.absolute.path);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return DynamicLibrary.open('libphash.so');
|
||
|
|
}
|
||
|
|
throw UnsupportedError('phash FFI is not supported on ${Platform.operatingSystem}');
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Thin FFI wrapper around rgb-phash-v1 C API.
|
||
|
|
class PhashNative {
|
||
|
|
PhashNative._(this._fromBytes, this._hamming);
|
||
|
|
|
||
|
|
factory PhashNative({String? hostLibraryPath}) {
|
||
|
|
final lib = loadPhashLibrary(hostLibraryPath: hostLibraryPath);
|
||
|
|
return PhashNative._(
|
||
|
|
lib.lookupFunction<PhashFromBytesNative, PhashFromBytesDart>(
|
||
|
|
'phash_from_bytes',
|
||
|
|
),
|
||
|
|
lib.lookupFunction<PhashHammingNative, PhashHammingDart>(
|
||
|
|
'phash_hamming',
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
final PhashFromBytesDart _fromBytes;
|
||
|
|
final PhashHammingDart _hamming;
|
||
|
|
|
||
|
|
/// Compute 105-bit ASCII hash from encoded image bytes (JPEG/PNG/…).
|
||
|
|
String hashFromBytes(List<int> bytes) {
|
||
|
|
if (bytes.isEmpty) {
|
||
|
|
throw ArgumentError('image bytes are empty');
|
||
|
|
}
|
||
|
|
final data = malloc<Uint8>(bytes.length);
|
||
|
|
final out = malloc<Uint8>(kPhashBits + 1);
|
||
|
|
try {
|
||
|
|
data.asTypedList(bytes.length).setAll(0, bytes);
|
||
|
|
final rc = _fromBytes(data, bytes.length, out.cast<Char>(), kPhashBits + 1);
|
||
|
|
if (rc != 0) {
|
||
|
|
throw StateError('phash_from_bytes failed with code $rc');
|
||
|
|
}
|
||
|
|
return out.cast<Utf8>().toDartString();
|
||
|
|
} finally {
|
||
|
|
malloc.free(data);
|
||
|
|
malloc.free(out);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int hamming(String a, String b) {
|
||
|
|
final pa = a.toNativeUtf8();
|
||
|
|
final pb = b.toNativeUtf8();
|
||
|
|
try {
|
||
|
|
final d = _hamming(pa.cast<Char>(), pb.cast<Char>());
|
||
|
|
if (d < 0) {
|
||
|
|
throw StateError('phash_hamming failed');
|
||
|
|
}
|
||
|
|
return d;
|
||
|
|
} finally {
|
||
|
|
malloc.free(pa);
|
||
|
|
malloc.free(pb);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|