photodna_sys/
lib.rs

1//! # photodna-sys
2//!
3//! Low-level, unsafe FFI bindings to the Microsoft PhotoDNA Edge Hash Generator library.
4//!
5//! This crate provides raw bindings to the proprietary Microsoft PhotoDNA library,
6//! which computes perceptual hashes of images for content identification purposes.
7//!
8//! ## Library Loading Model
9//!
10//! The PhotoDNA library is designed for **runtime dynamic loading** via `dlopen`/`LoadLibrary`,
11//! not compile-time linking. This crate provides:
12//!
13//! 1. Type definitions and constants compatible with the C API
14//! 2. Function pointer types for all library functions
15//! 3. The [`EdgeHashGenerator`] struct that handles library loading and provides safe function access
16//!
17//! ## Requirements
18//!
19//! This crate requires the proprietary Microsoft PhotoDNA SDK. You must set the
20//! `PHOTODNA_SDK_ROOT` environment variable to point to the SDK installation directory
21//! before building.
22//!
23//! ```bash
24//! export PHOTODNA_SDK_ROOT=/path/to/PhotoDNA.EdgeHashGeneration-1.05.001
25//! ```
26//!
27//! ## Platform Support
28//!
29//! | Platform | Architecture | Support |
30//! |----------|--------------|---------|
31//! | Windows  | x86_64, x86, arm64 | Native library |
32//! | Linux    | x86_64, x86, arm64 | Native library |
33//! | macOS    | x86_64, arm64 | Native library |
34//! | BSD      | any | WebAssembly module (requires `wasm` feature) |
35//!
36//! ## Features
37//!
38//! - `native` (default): Enables runtime loading of native dynamic libraries (`.dll`/`.so`).
39//! - `wasm`: Embeds the WebAssembly module for platforms without native library support.
40//! - `bindgen`: Regenerates bindings from C headers at build time (requires clang).
41//!
42//! ## Safety
43//!
44//! All FFI functions in this crate are `unsafe`. Callers must ensure:
45//!
46//! - The library has been properly initialized via [`EdgeHashGenerator::new`].
47//! - All pointers passed to functions are valid and point to sufficient memory.
48//! - Image data buffers match the specified dimensions, stride, and pixel format.
49//! - The library instance is not used after being dropped.
50//!
51//! ## Example
52//!
53//! ```rust,ignore
54//! use photodna_sys::*;
55//!
56//! // Initialize the library
57//! let lib = EdgeHashGenerator::new(None, 4)?; // Use default path, 4 threads
58//!
59//! // Prepare image data (RGB format)
60//! let image_data: &[u8] = /* your image pixels */;
61//! let width = 100;
62//! let height = 100;
63//!
64//! // Compute the hash
65//! let mut hash = [0u8; PHOTODNA_HASH_SIZE_MAX];
66//! unsafe {
67//!     let result = lib.photo_dna_edge_hash(
68//!         image_data.as_ptr(),
69//!         hash.as_mut_ptr(),
70//!         width,
71//!         height,
72//!         0, // auto-calculate stride
73//!         PhotoDna_Default,
74//!     );
75//!     
76//!     if result < 0 {
77//!         eprintln!("Error: {}", error_code_description(result));
78//!     }
79//! }
80//! ```
81
82#![allow(non_upper_case_globals)]
83#![allow(non_camel_case_types)]
84#![allow(non_snake_case)]
85// Allow dead code for constants that may not be used by all consumers
86#![allow(dead_code)]
87// FFI functions must match the C API signature exactly
88#![allow(clippy::too_many_arguments)]
89
90use std::ffi::{c_char, c_void, CStr};
91
92#[cfg(not(photodna_no_sdk))]
93use std::ffi::CString;
94
95// ============================================================================
96// Constants
97// ============================================================================
98
99/// Size of PhotoDNA Edge V2 hash in bytes (binary format).
100pub const PHOTODNA_HASH_SIZE_EDGE_V2: usize = 0x39c; // 924 bytes
101
102/// Size of PhotoDNA Edge V2 hash in bytes (Base64 format).
103pub const PHOTODNA_HASH_SIZE_EDGE_V2_BASE64: usize = 0x4d0; // 1232 bytes
104
105/// Maximum hash buffer size required.
106pub const PHOTODNA_HASH_SIZE_MAX: usize = 0x4d0; // 1232 bytes
107
108/// Library version string.
109pub const PHOTODNA_LIBRARY_VERSION: &str = "1.05";
110
111/// The SDK root path (set at compile time from PHOTODNA_SDK_ROOT environment variable).
112/// Only available on native platforms (Windows, Linux, macOS) with SDK configured at build time.
113#[cfg(all(
114    any(target_os = "windows", target_os = "linux", target_os = "macos"),
115    not(photodna_no_sdk)
116))]
117pub const PHOTODNA_SDK_ROOT: &str = env!("PHOTODNA_SDK_ROOT");
118
119/// The client library directory path.
120/// Only available on native platforms (Windows, Linux, macOS) with SDK configured at build time.
121#[cfg(all(
122    any(target_os = "windows", target_os = "linux", target_os = "macos"),
123    not(photodna_no_sdk)
124))]
125pub const PHOTODNA_LIB_DIR: &str = env!("PHOTODNA_LIB_DIR");
126
127// ============================================================================
128// Error Codes
129// ============================================================================
130
131/// Type alias for PhotoDNA error codes.
132pub type ErrorCode = u32;
133
134/// An undetermined error occurred.
135pub const PhotoDna_ErrorUnknown: i32 = -7000;
136
137/// Failed to allocate memory.
138pub const PhotoDna_ErrorMemoryAllocationFailed: i32 = -7001;
139
140/// Alias for memory allocation failure (host-side).
141pub const PhotoDna_ErrorHostMemoryAllocationFailed: i32 = -7001;
142
143/// General failure within the library.
144pub const PhotoDna_ErrorLibraryFailure: i32 = -7002;
145
146/// System memory exception occurred.
147pub const PhotoDna_ErrorMemoryAccess: i32 = -7003;
148
149/// Hash that does not conform to PhotoDNA specifications.
150pub const PhotoDna_ErrorInvalidHash: i32 = -7004;
151
152/// An invalid character was contained in a Base64 or Hex hash.
153pub const PhotoDna_ErrorHashFormatInvalidCharacters: i32 = -7005;
154
155/// Provided image had a dimension less than 50 pixels.
156pub const PhotoDna_ErrorImageTooSmall: i32 = -7006;
157
158/// A border was not detected for the image.
159pub const PhotoDna_ErrorNoBorder: i32 = -7007;
160
161/// An invalid argument was passed to the function.
162pub const PhotoDna_ErrorBadArgument: i32 = -7008;
163
164/// The image has few or no gradients.
165pub const PhotoDna_ErrorImageIsFlat: i32 = -7009;
166
167/// Provided image had a dimension less than 50 pixels (no border variant).
168pub const PhotoDna_ErrorNoBorderImageTooSmall: i32 = -7010;
169
170/// Not a known source image format.
171pub const PhotoDna_ErrorSourceFormatUnknown: i32 = -7011;
172
173/// Stride should be 0, or greater than or equal to width in bytes.
174pub const PhotoDna_ErrorInvalidStride: i32 = -7012;
175
176/// The sub region area is not within the boundaries of the image.
177pub const PhotoDna_ErrorInvalidSubImage: i32 = -7013;
178
179// ============================================================================
180// Hash Size Constants
181// ============================================================================
182
183/// Type alias for hash size identifiers.
184pub type HashSize = u32;
185
186/// Edge V2 format hash size.
187pub const PhotoDna_EdgeV2: HashSize = 0x0000039c;
188
189/// Edge V2 format hash size (Base64 encoded).
190pub const PhotoDna_EdgeV2Base64: HashSize = 0x000004d0;
191
192/// Maximum hash size.
193pub const PhotoDna_MaxSize: HashSize = 0x000004d0;
194
195// ============================================================================
196// PhotoDNA Options (Flags)
197// ============================================================================
198
199/// Type alias for PhotoDNA option flags.
200pub type PhotoDnaOptions = u32;
201
202/// No options specified. See description for default behavior.
203pub const PhotoDna_OptionNone: PhotoDnaOptions = 0x00000000;
204
205/// Default options. The matcher will return all results found.
206pub const PhotoDna_Default: PhotoDnaOptions = 0x00000000;
207
208/// Mask to isolate the hash format bits.
209pub const PhotoDna_HashFormatMask: PhotoDnaOptions = 0x000000f0;
210
211/// Hash output format: PhotoDNA Edge Hash V2.
212pub const PhotoDna_HashFormatEdgeV2: PhotoDnaOptions = 0x00000080;
213
214/// Hash output format: PhotoDNA Edge Hash V2 Base64.
215pub const PhotoDna_HashFormatEdgeV2Base64: PhotoDnaOptions = 0x00000090;
216
217/// Mask to isolate the pixel layout bits.
218pub const PhotoDna_PixelLayoutMask: PhotoDnaOptions = 0x00001f00;
219
220/// Pixel layout: RGB, 3 bytes per pixel.
221pub const PhotoDna_Rgb: PhotoDnaOptions = 0x00000000;
222
223/// Pixel layout: BGR, 3 bytes per pixel.
224pub const PhotoDna_Bgr: PhotoDnaOptions = 0x00000000;
225
226/// Pixel layout: RGBA, 4 bytes per pixel.
227pub const PhotoDna_Rgba: PhotoDnaOptions = 0x00000100;
228
229/// Pixel layout: RGBA with pre-multiplied alpha, 4 bytes per pixel.
230pub const PhotoDna_RgbaPm: PhotoDnaOptions = 0x00000700;
231
232/// Pixel layout: BGRA, 4 bytes per pixel.
233pub const PhotoDna_Bgra: PhotoDnaOptions = 0x00000100;
234
235/// Pixel layout: ARGB, 4 bytes per pixel.
236pub const PhotoDna_Argb: PhotoDnaOptions = 0x00000200;
237
238/// Pixel layout: ABGR, 4 bytes per pixel.
239pub const PhotoDna_Abgr: PhotoDnaOptions = 0x00000200;
240
241/// Pixel layout: CMYK, 4 bytes per pixel.
242pub const PhotoDna_Cmyk: PhotoDnaOptions = 0x00000300;
243
244/// Pixel layout: Grayscale 8-bit, 1 byte per pixel.
245pub const PhotoDna_Grey8: PhotoDnaOptions = 0x00000400;
246
247/// Pixel layout: Grayscale 32-bit, 4 bytes per pixel.
248pub const PhotoDna_Grey32: PhotoDnaOptions = 0x00000500;
249
250/// Pixel layout: YCbCr, 3 bytes per pixel.
251pub const PhotoDna_YCbCr: PhotoDnaOptions = 0x00000600;
252
253/// Pixel layout: YUV420P planar format.
254/// Y = 1 byte per pixel, U = 1 byte per 4 pixels, V = 1 byte per 4 pixels.
255pub const PhotoDna_Yuv420p: PhotoDnaOptions = 0x00000800;
256
257/// Check for and remove borders from the image.
258pub const PhotoDna_RemoveBorder: PhotoDnaOptions = 0x00200000;
259
260/// Prevent checks for rotated and/or flipped orientations.
261pub const PhotoDna_NoRotateFlip: PhotoDnaOptions = 0x01000000;
262
263/// Check data pointers for valid allocated memory.
264/// Note: This may negatively impact performance.
265pub const PhotoDna_CheckMemory: PhotoDnaOptions = 0x20000000;
266
267/// Enable debug output to stderr.
268pub const PhotoDna_Verbose: PhotoDnaOptions = 0x40000000;
269
270/// Equivalent to Verbose + CheckMemory.
271pub const PhotoDna_Test: PhotoDnaOptions = 0x60000000;
272
273/// Use same options as specified for primary parameter.
274pub const PhotoDna_Other: PhotoDnaOptions = 0xffffffff; // -1 as u32
275
276// ============================================================================
277// Structures
278// ============================================================================
279
280/// Result structure returned by border detection hash functions.
281///
282/// When a border is found, the hash with the border removed will be in the
283/// second instance of the results array.
284///
285/// # Result Values
286/// - `< 0`: An error occurred (see error codes)
287/// - `1`: No border was found
288/// - `2`: A border was found
289#[repr(C, packed)]
290#[derive(Copy, Clone)]
291pub struct HashResult {
292    /// Error code if less than 0, otherwise indicates border detection result.
293    pub result: i32,
294    /// Hash format used for this result.
295    pub hash_format: i32,
296    /// Left position (X) within the provided image.
297    pub header_dimensions_image_x: i32,
298    /// Top position (Y) within the provided image.
299    pub header_dimensions_image_y: i32,
300    /// Width within the provided image.
301    pub header_dimensions_image_w: i32,
302    /// Height within the provided image.
303    pub header_dimensions_image_h: i32,
304    /// The computed hash in the requested format.
305    pub hash: [u8; PHOTODNA_HASH_SIZE_MAX],
306    /// Reserved for future use.
307    pub reserved0: i32,
308    /// Reserved for future use.
309    pub reserved1: i32,
310    /// Reserved for future use.
311    pub reserved2: i32,
312    /// Reserved for future use.
313    pub reserved3: i32,
314    /// Reserved for future use.
315    pub reserved4: i32,
316    /// Reserved for future use.
317    pub reserved5: i32,
318}
319
320impl Default for HashResult {
321    fn default() -> Self {
322        Self {
323            result: 0,
324            hash_format: 0,
325            header_dimensions_image_x: 0,
326            header_dimensions_image_y: 0,
327            header_dimensions_image_w: 0,
328            header_dimensions_image_h: 0,
329            hash: [0u8; PHOTODNA_HASH_SIZE_MAX],
330            reserved0: 0,
331            reserved1: 0,
332            reserved2: 0,
333            reserved3: 0,
334            reserved4: 0,
335            reserved5: 0,
336        }
337    }
338}
339
340impl core::fmt::Debug for HashResult {
341    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
342        // Copy packed fields to avoid unaligned references
343        let result = self.result;
344        let hash_format = self.hash_format;
345        let x = self.header_dimensions_image_x;
346        let y = self.header_dimensions_image_y;
347        let w = self.header_dimensions_image_w;
348        let h = self.header_dimensions_image_h;
349
350        f.debug_struct("HashResult")
351            .field("result", &result)
352            .field("hash_format", &hash_format)
353            .field("x", &x)
354            .field("y", &y)
355            .field("w", &w)
356            .field("h", &h)
357            .field("hash", &"[...]")
358            .finish()
359    }
360}
361
362// ============================================================================
363// Function Pointer Types
364// ============================================================================
365
366/// Function pointer type for EdgeHashGeneratorInit.
367pub type FnEdgeHashGeneratorInit =
368    unsafe extern "C" fn(library_path: *const c_char, max_threads: i32) -> *mut c_void;
369
370/// Function pointer type for EdgeHashGeneratorRelease.
371pub type FnEdgeHashGeneratorRelease = unsafe extern "C" fn(library_instance: *mut c_void);
372
373/// Function pointer type for GetErrorNumber.
374pub type FnGetErrorNumber = unsafe extern "C" fn(library_instance: *mut c_void) -> i32;
375
376/// Function pointer type for GetErrorString.
377pub type FnGetErrorString =
378    unsafe extern "C" fn(library_instance: *mut c_void, error: i32) -> *const c_char;
379
380/// Function pointer type for LibraryVersion.
381pub type FnLibraryVersion = unsafe extern "C" fn(library_instance: *mut c_void) -> i32;
382
383/// Function pointer type for LibraryVersionMajor.
384pub type FnLibraryVersionMajor = unsafe extern "C" fn(library_instance: *mut c_void) -> i32;
385
386/// Function pointer type for LibraryVersionMinor.
387pub type FnLibraryVersionMinor = unsafe extern "C" fn(library_instance: *mut c_void) -> i32;
388
389/// Function pointer type for LibraryVersionPatch.
390pub type FnLibraryVersionPatch = unsafe extern "C" fn(library_instance: *mut c_void) -> i32;
391
392/// Function pointer type for LibraryVersionText.
393pub type FnLibraryVersionText =
394    unsafe extern "C" fn(library_instance: *mut c_void) -> *const c_char;
395
396/// Function pointer type for PhotoDnaEdgeHash.
397pub type FnPhotoDnaEdgeHash = unsafe extern "C" fn(
398    library_instance: *mut c_void,
399    image_data: *const u8,
400    hash_value: *mut u8,
401    width: i32,
402    height: i32,
403    stride: i32,
404    options: PhotoDnaOptions,
405) -> i32;
406
407/// Function pointer type for PhotoDnaEdgeHashBorder.
408pub type FnPhotoDnaEdgeHashBorder = unsafe extern "C" fn(
409    library_instance: *mut c_void,
410    image_data: *const u8,
411    hash_results: *mut HashResult,
412    max_hash_count: i32,
413    width: i32,
414    height: i32,
415    stride: i32,
416    options: PhotoDnaOptions,
417) -> i32;
418
419/// Function pointer type for PhotoDnaEdgeHashBorderSub.
420pub type FnPhotoDnaEdgeHashBorderSub = unsafe extern "C" fn(
421    library_instance: *mut c_void,
422    image_data: *const u8,
423    hash_results: *mut HashResult,
424    max_hash_count: i32,
425    width: i32,
426    height: i32,
427    stride: i32,
428    x: i32,
429    y: i32,
430    w: i32,
431    h: i32,
432    options: PhotoDnaOptions,
433) -> i32;
434
435/// Function pointer type for PhotoDnaEdgeHashSub.
436pub type FnPhotoDnaEdgeHashSub = unsafe extern "C" fn(
437    library_instance: *mut c_void,
438    image_data: *const u8,
439    hash_value: *mut u8,
440    width: i32,
441    height: i32,
442    stride: i32,
443    x: i32,
444    y: i32,
445    w: i32,
446    h: i32,
447    options: PhotoDnaOptions,
448) -> i32;
449
450// ============================================================================
451// Native Library Loading (Windows, Linux, macOS)
452// ============================================================================
453
454#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
455mod native {
456    use super::*;
457
458    /// Returns the platform-specific library filename.
459    pub fn get_library_filename() -> String {
460        #[cfg(target_os = "windows")]
461        {
462            #[cfg(target_arch = "x86_64")]
463            {
464                format!("libEdgeHashGenerator.{}.dll", PHOTODNA_LIBRARY_VERSION)
465            }
466            #[cfg(target_arch = "aarch64")]
467            {
468                format!(
469                    "libEdgeHashGenerator-arm64.{}.dll",
470                    PHOTODNA_LIBRARY_VERSION
471                )
472            }
473            #[cfg(target_arch = "x86")]
474            {
475                format!("libEdgeHashGenerator-x86.{}.dll", PHOTODNA_LIBRARY_VERSION)
476            }
477            #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "x86")))]
478            {
479                format!("libEdgeHashGenerator.{}.dll", PHOTODNA_LIBRARY_VERSION)
480            }
481        }
482        #[cfg(target_os = "linux")]
483        {
484            #[cfg(target_arch = "x86_64")]
485            {
486                format!("libEdgeHashGenerator.so.{}", PHOTODNA_LIBRARY_VERSION)
487            }
488            #[cfg(target_arch = "aarch64")]
489            {
490                format!("libEdgeHashGenerator-arm64.so.{}", PHOTODNA_LIBRARY_VERSION)
491            }
492            #[cfg(target_arch = "x86")]
493            {
494                format!("libEdgeHashGenerator-x86.so.{}", PHOTODNA_LIBRARY_VERSION)
495            }
496            #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "x86")))]
497            {
498                format!("libEdgeHashGenerator.so.{}", PHOTODNA_LIBRARY_VERSION)
499            }
500        }
501        #[cfg(target_os = "macos")]
502        {
503            #[cfg(target_arch = "aarch64")]
504            {
505                format!(
506                    "libEdgeHashGenerator-arm64-macos.so.{}",
507                    PHOTODNA_LIBRARY_VERSION
508                )
509            }
510            #[cfg(not(target_arch = "aarch64"))]
511            {
512                format!("libEdgeHashGenerator-macos.so.{}", PHOTODNA_LIBRARY_VERSION)
513            }
514        }
515    }
516}
517
518#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
519pub use native::*;
520
521// ============================================================================
522// Edge Hash Generator
523// ============================================================================
524
525/// The PhotoDNA Edge Hash Generator library wrapper.
526///
527/// This struct handles loading the native library and provides access to all
528/// library functions through type-safe function pointers.
529///
530/// # Example
531///
532/// ```rust,ignore
533/// use photodna_sys::*;
534///
535/// let lib = EdgeHashGenerator::new(None, 4)?;
536/// println!("Library version: {}", lib.library_version_text());
537/// ```
538#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
539pub struct EdgeHashGenerator {
540    /// Handle to the loaded dynamic library.
541    _library: libloading::Library,
542    /// Handle to the PhotoDNA library instance.
543    library_instance: *mut c_void,
544    /// Function pointer: EdgeHashGeneratorRelease
545    fn_release: libloading::Symbol<'static, FnEdgeHashGeneratorRelease>,
546    /// Function pointer: GetErrorNumber
547    fn_get_error_number: libloading::Symbol<'static, FnGetErrorNumber>,
548    /// Function pointer: GetErrorString
549    fn_get_error_string: libloading::Symbol<'static, FnGetErrorString>,
550    /// Function pointer: LibraryVersion
551    fn_library_version: libloading::Symbol<'static, FnLibraryVersion>,
552    /// Function pointer: LibraryVersionMajor
553    fn_library_version_major: libloading::Symbol<'static, FnLibraryVersionMajor>,
554    /// Function pointer: LibraryVersionMinor
555    fn_library_version_minor: libloading::Symbol<'static, FnLibraryVersionMinor>,
556    /// Function pointer: LibraryVersionPatch
557    fn_library_version_patch: libloading::Symbol<'static, FnLibraryVersionPatch>,
558    /// Function pointer: LibraryVersionText
559    fn_library_version_text: libloading::Symbol<'static, FnLibraryVersionText>,
560    /// Function pointer: PhotoDnaEdgeHash
561    fn_photo_dna_edge_hash: libloading::Symbol<'static, FnPhotoDnaEdgeHash>,
562    /// Function pointer: PhotoDnaEdgeHashBorder
563    fn_photo_dna_edge_hash_border: libloading::Symbol<'static, FnPhotoDnaEdgeHashBorder>,
564    /// Function pointer: PhotoDnaEdgeHashBorderSub
565    fn_photo_dna_edge_hash_border_sub: libloading::Symbol<'static, FnPhotoDnaEdgeHashBorderSub>,
566    /// Function pointer: PhotoDnaEdgeHashSub
567    fn_photo_dna_edge_hash_sub: libloading::Symbol<'static, FnPhotoDnaEdgeHashSub>,
568}
569
570#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
571impl EdgeHashGenerator {
572    /// Creates a new EdgeHashGenerator by loading the native library.
573    ///
574    /// # Parameters
575    ///
576    /// - `library_dir`: Directory containing the library. If `None`, uses [`PHOTODNA_LIB_DIR`].
577    /// - `max_threads`: Maximum number of concurrent threads. Calls exceeding this
578    ///   will block until a previous call completes.
579    ///
580    /// # Returns
581    ///
582    /// A Result containing the EdgeHashGenerator or an error message.
583    ///
584    /// # Example
585    ///
586    /// ```rust,ignore
587    /// // Use default library path
588    /// let lib = EdgeHashGenerator::new(None, 4)?;
589    ///
590    /// // Use custom library path
591    /// let lib = EdgeHashGenerator::new(Some("/path/to/libs"), 4)?;
592    /// ```
593    pub fn new(library_dir: Option<&str>, max_threads: i32) -> Result<Self, String> {
594        #[cfg(photodna_no_sdk)]
595        {
596            let _ = (library_dir, max_threads); // Suppress unused warnings
597            Err(
598                "PhotoDNA SDK not available: PHOTODNA_SDK_ROOT was not set at build time. \
599                 Please rebuild with PHOTODNA_SDK_ROOT environment variable set to the SDK directory."
600                    .to_string(),
601            )
602        }
603
604        #[cfg(not(photodna_no_sdk))]
605        {
606            let lib_dir = library_dir.unwrap_or(PHOTODNA_LIB_DIR);
607            let lib_filename = get_library_filename();
608            let lib_path = format!("{}/{}", lib_dir, lib_filename);
609
610            unsafe {
611                // Load the dynamic library using libloading
612                let library = libloading::Library::new(&lib_path)
613                    .map_err(|e| format!("Failed to load library '{}': {}", lib_path, e))?;
614
615                // Get function pointers using libloading
616                let fn_init: libloading::Symbol<FnEdgeHashGeneratorInit> = library
617                    .get(b"EdgeHashGeneratorInit\0")
618                    .map_err(|e| format!("Failed to find symbol 'EdgeHashGeneratorInit': {}", e))?;
619                let fn_release: libloading::Symbol<FnEdgeHashGeneratorRelease> = library
620                    .get(b"EdgeHashGeneratorRelease\0")
621                    .map_err(|e| {
622                        format!("Failed to find symbol 'EdgeHashGeneratorRelease': {}", e)
623                    })?;
624                let fn_get_error_number: libloading::Symbol<FnGetErrorNumber> = library
625                    .get(b"GetErrorNumber\0")
626                    .map_err(|e| format!("Failed to find symbol 'GetErrorNumber': {}", e))?;
627                let fn_get_error_string: libloading::Symbol<FnGetErrorString> = library
628                    .get(b"GetErrorString\0")
629                    .map_err(|e| format!("Failed to find symbol 'GetErrorString': {}", e))?;
630                let fn_library_version: libloading::Symbol<FnLibraryVersion> = library
631                    .get(b"LibraryVersion\0")
632                    .map_err(|e| format!("Failed to find symbol 'LibraryVersion': {}", e))?;
633                let fn_library_version_major: libloading::Symbol<FnLibraryVersionMajor> = library
634                    .get(b"LibraryVersionMajor\0")
635                    .map_err(|e| format!("Failed to find symbol 'LibraryVersionMajor': {}", e))?;
636                let fn_library_version_minor: libloading::Symbol<FnLibraryVersionMinor> = library
637                    .get(b"LibraryVersionMinor\0")
638                    .map_err(|e| format!("Failed to find symbol 'LibraryVersionMinor': {}", e))?;
639                let fn_library_version_patch: libloading::Symbol<FnLibraryVersionPatch> = library
640                    .get(b"LibraryVersionPatch\0")
641                    .map_err(|e| format!("Failed to find symbol 'LibraryVersionPatch': {}", e))?;
642                let fn_library_version_text: libloading::Symbol<FnLibraryVersionText> = library
643                    .get(b"LibraryVersionText\0")
644                    .map_err(|e| format!("Failed to find symbol 'LibraryVersionText': {}", e))?;
645                let fn_photo_dna_edge_hash: libloading::Symbol<FnPhotoDnaEdgeHash> = library
646                    .get(b"PhotoDnaEdgeHash\0")
647                    .map_err(|e| format!("Failed to find symbol 'PhotoDnaEdgeHash': {}", e))?;
648                let fn_photo_dna_edge_hash_border: libloading::Symbol<FnPhotoDnaEdgeHashBorder> =
649                    library.get(b"PhotoDnaEdgeHashBorder\0").map_err(|e| {
650                        format!("Failed to find symbol 'PhotoDnaEdgeHashBorder': {}", e)
651                    })?;
652                let fn_photo_dna_edge_hash_border_sub: libloading::Symbol<
653                    FnPhotoDnaEdgeHashBorderSub,
654                > = library
655                    .get(b"PhotoDnaEdgeHashBorderSub\0")
656                    .map_err(|e| {
657                        format!("Failed to find symbol 'PhotoDnaEdgeHashBorderSub': {}", e)
658                    })?;
659                let fn_photo_dna_edge_hash_sub: libloading::Symbol<FnPhotoDnaEdgeHashSub> =
660                    library.get(b"PhotoDnaEdgeHashSub\0").map_err(|e| {
661                        format!("Failed to find symbol 'PhotoDnaEdgeHashSub': {}", e)
662                    })?;
663
664                // Initialize the library
665                let c_lib_dir = CString::new(lib_dir).map_err(|e| e.to_string())?;
666                let library_instance = fn_init(c_lib_dir.as_ptr(), max_threads);
667
668                if library_instance.is_null() {
669                    return Err("Failed to initialize PhotoDNA library".to_string());
670                }
671
672                // Convert symbols to 'static lifetime for storage
673                // This is safe because the library will remain loaded for the lifetime of Self
674                #[allow(clippy::missing_transmute_annotations)]
675                let fn_release = std::mem::transmute(fn_release);
676                #[allow(clippy::missing_transmute_annotations)]
677                let fn_get_error_number = std::mem::transmute(fn_get_error_number);
678                #[allow(clippy::missing_transmute_annotations)]
679                let fn_get_error_string = std::mem::transmute(fn_get_error_string);
680                #[allow(clippy::missing_transmute_annotations)]
681                let fn_library_version = std::mem::transmute(fn_library_version);
682                #[allow(clippy::missing_transmute_annotations)]
683                let fn_library_version_major = std::mem::transmute(fn_library_version_major);
684                #[allow(clippy::missing_transmute_annotations)]
685                let fn_library_version_minor = std::mem::transmute(fn_library_version_minor);
686                #[allow(clippy::missing_transmute_annotations)]
687                let fn_library_version_patch = std::mem::transmute(fn_library_version_patch);
688                #[allow(clippy::missing_transmute_annotations)]
689                let fn_library_version_text = std::mem::transmute(fn_library_version_text);
690                #[allow(clippy::missing_transmute_annotations)]
691                let fn_photo_dna_edge_hash = std::mem::transmute(fn_photo_dna_edge_hash);
692                #[allow(clippy::missing_transmute_annotations)]
693                let fn_photo_dna_edge_hash_border =
694                    std::mem::transmute(fn_photo_dna_edge_hash_border);
695                #[allow(clippy::missing_transmute_annotations)]
696                let fn_photo_dna_edge_hash_border_sub =
697                    std::mem::transmute(fn_photo_dna_edge_hash_border_sub);
698                #[allow(clippy::missing_transmute_annotations)]
699                let fn_photo_dna_edge_hash_sub = std::mem::transmute(fn_photo_dna_edge_hash_sub);
700
701                Ok(Self {
702                    _library: library,
703                    library_instance,
704                    fn_release,
705                    fn_get_error_number,
706                    fn_get_error_string,
707                    fn_library_version,
708                    fn_library_version_major,
709                    fn_library_version_minor,
710                    fn_library_version_patch,
711                    fn_library_version_text,
712                    fn_photo_dna_edge_hash,
713                    fn_photo_dna_edge_hash_border,
714                    fn_photo_dna_edge_hash_border_sub,
715                    fn_photo_dna_edge_hash_sub,
716                })
717            }
718        }
719    }
720
721    /// Returns the raw library instance handle.
722    ///
723    /// # Safety
724    ///
725    /// The returned pointer is only valid while this EdgeHashGenerator is alive.
726    pub fn raw_instance(&self) -> *mut c_void {
727        self.library_instance
728    }
729
730    /// Retrieves the last error number from the library.
731    pub fn get_error_number(&self) -> i32 {
732        unsafe { (self.fn_get_error_number)(self.library_instance) }
733    }
734
735    /// Returns a human-readable description for an error code.
736    ///
737    /// Returns `None` if the error code is unknown.
738    pub fn get_error_string(&self, error: i32) -> Option<&str> {
739        unsafe {
740            let ptr = (self.fn_get_error_string)(self.library_instance, error);
741            if ptr.is_null() {
742                None
743            } else {
744                CStr::from_ptr(ptr).to_str().ok()
745            }
746        }
747    }
748
749    /// Returns the library version as a packed integer.
750    ///
751    /// High 16 bits = major, low 16 bits = minor.
752    pub fn library_version(&self) -> i32 {
753        unsafe { (self.fn_library_version)(self.library_instance) }
754    }
755
756    /// Returns the major version number.
757    pub fn library_version_major(&self) -> i32 {
758        unsafe { (self.fn_library_version_major)(self.library_instance) }
759    }
760
761    /// Returns the minor version number.
762    pub fn library_version_minor(&self) -> i32 {
763        unsafe { (self.fn_library_version_minor)(self.library_instance) }
764    }
765
766    /// Returns the patch version number.
767    pub fn library_version_patch(&self) -> i32 {
768        unsafe { (self.fn_library_version_patch)(self.library_instance) }
769    }
770
771    /// Returns the library version as a human-readable string.
772    pub fn library_version_text(&self) -> Option<&str> {
773        unsafe {
774            let ptr = (self.fn_library_version_text)(self.library_instance);
775            if ptr.is_null() {
776                None
777            } else {
778                CStr::from_ptr(ptr).to_str().ok()
779            }
780        }
781    }
782
783    /// Computes the PhotoDNA Edge Hash of an image.
784    ///
785    /// # Parameters
786    ///
787    /// - `image_data`: Pointer to pixel data in the format specified by `options`.
788    /// - `hash_value`: Output buffer for the computed hash. Must be at least
789    ///   [`PHOTODNA_HASH_SIZE_MAX`] bytes.
790    /// - `width`: Image width in pixels (minimum 50).
791    /// - `height`: Image height in pixels (minimum 50).
792    /// - `stride`: Row stride in bytes, or 0 to calculate from dimensions.
793    /// - `options`: Combination of [`PhotoDnaOptions`] flags.
794    ///
795    /// # Returns
796    ///
797    /// 0 on success, or a negative error code.
798    ///
799    /// # Safety
800    ///
801    /// - `image_data` must point to valid pixel data of size `height * stride` bytes.
802    /// - `hash_value` must point to a buffer of at least `PHOTODNA_HASH_SIZE_MAX` bytes.
803    pub unsafe fn photo_dna_edge_hash(
804        &self,
805        image_data: *const u8,
806        hash_value: *mut u8,
807        width: i32,
808        height: i32,
809        stride: i32,
810        options: PhotoDnaOptions,
811    ) -> i32 {
812        (self.fn_photo_dna_edge_hash)(
813            self.library_instance,
814            image_data,
815            hash_value,
816            width,
817            height,
818            stride,
819            options,
820        )
821    }
822
823    /// Computes the PhotoDNA Edge Hash with border detection.
824    ///
825    /// Returns hashes for both the original image and the image with
826    /// borders removed (if detected).
827    ///
828    /// # Parameters
829    ///
830    /// - `image_data`: Pointer to pixel data in the format specified by `options`.
831    /// - `hash_results`: Output array for computed hashes (at least 2 entries).
832    /// - `max_hash_count`: Size of the `hash_results` array.
833    /// - `width`: Image width in pixels (minimum 50).
834    /// - `height`: Image height in pixels (minimum 50).
835    /// - `stride`: Row stride in bytes, or 0 to calculate from dimensions.
836    /// - `options`: Combination of [`PhotoDnaOptions`] flags.
837    ///
838    /// # Returns
839    ///
840    /// Number of hashes returned (1 or 2), or a negative error code.
841    ///
842    /// # Safety
843    ///
844    /// - `image_data` must point to valid pixel data.
845    /// - `hash_results` must point to an array of at least `max_hash_count` elements.
846    pub unsafe fn photo_dna_edge_hash_border(
847        &self,
848        image_data: *const u8,
849        hash_results: *mut HashResult,
850        max_hash_count: i32,
851        width: i32,
852        height: i32,
853        stride: i32,
854        options: PhotoDnaOptions,
855    ) -> i32 {
856        (self.fn_photo_dna_edge_hash_border)(
857            self.library_instance,
858            image_data,
859            hash_results,
860            max_hash_count,
861            width,
862            height,
863            stride,
864            options,
865        )
866    }
867
868    /// Computes the PhotoDNA Edge Hash for a sub-region with border detection.
869    ///
870    /// # Safety
871    ///
872    /// - All pointer parameters must be valid.
873    /// - The sub-region must be within the image bounds.
874    pub unsafe fn photo_dna_edge_hash_border_sub(
875        &self,
876        image_data: *const u8,
877        hash_results: *mut HashResult,
878        max_hash_count: i32,
879        width: i32,
880        height: i32,
881        stride: i32,
882        x: i32,
883        y: i32,
884        w: i32,
885        h: i32,
886        options: PhotoDnaOptions,
887    ) -> i32 {
888        (self.fn_photo_dna_edge_hash_border_sub)(
889            self.library_instance,
890            image_data,
891            hash_results,
892            max_hash_count,
893            width,
894            height,
895            stride,
896            x,
897            y,
898            w,
899            h,
900            options,
901        )
902    }
903
904    /// Computes the PhotoDNA Edge Hash for a sub-region of an image.
905    ///
906    /// # Safety
907    ///
908    /// - All pointer parameters must be valid.
909    /// - The sub-region must be within the image bounds.
910    pub unsafe fn photo_dna_edge_hash_sub(
911        &self,
912        image_data: *const u8,
913        hash_value: *mut u8,
914        width: i32,
915        height: i32,
916        stride: i32,
917        x: i32,
918        y: i32,
919        w: i32,
920        h: i32,
921        options: PhotoDnaOptions,
922    ) -> i32 {
923        (self.fn_photo_dna_edge_hash_sub)(
924            self.library_instance,
925            image_data,
926            hash_value,
927            width,
928            height,
929            stride,
930            x,
931            y,
932            w,
933            h,
934            options,
935        )
936    }
937}
938
939#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
940impl Drop for EdgeHashGenerator {
941    fn drop(&mut self) {
942        unsafe {
943            // Release the library instance
944            (self.fn_release)(self.library_instance);
945            // The library is automatically unloaded when _library is dropped
946        }
947    }
948}
949
950// EdgeHashGenerator is not Send/Sync by default due to raw pointers.
951// The library may or may not be thread-safe internally.
952// Users should wrap in appropriate synchronization primitives if needed.
953
954// ============================================================================
955// WebAssembly Module (BSD and other platforms)
956// ============================================================================
957
958/// WebAssembly module support for platforms without native library binaries.
959///
960/// This module provides the embedded WASM binary for use with a WASM runtime
961/// like `wasmtime`. The consuming crate is responsible for instantiating and
962/// calling the WASM module.
963///
964/// Note: This module is only available when the SDK was present at build time
965/// and the `wasm` feature is enabled, or when building for BSD targets with SDK.
966/// If building without SDK for BSD targets, the WASM module must be loaded at runtime.
967#[cfg(any(
968    all(
969        feature = "wasm",
970        not(any(target_os = "windows", target_os = "linux", target_os = "macos"))
971    ),
972    target_os = "openbsd",
973    target_os = "freebsd",
974    target_os = "netbsd",
975    target_os = "dragonfly",
976))]
977pub mod wasm {
978    /// The PhotoDNA Edge Hash Generator WebAssembly module bytes.
979    ///
980    /// This constant contains the complete WASM module that can be instantiated
981    /// with a WASM runtime (e.g., `wasmtime`, `wasmer`).
982    ///
983    /// # Example
984    ///
985    /// ```rust,ignore
986    /// use wasmtime::*;
987    ///
988    /// let engine = Engine::default();
989    /// let module = Module::new(&engine, photodna_sys::wasm::PHOTODNA_WASM_BYTES)?;
990    /// // ... instantiate and call functions
991    /// ```
992    ///
993    /// # Note
994    ///
995    /// The WASM module exports the same functions as the native library.
996    /// Consult the PhotoDNA documentation for the expected calling conventions.
997    pub const PHOTODNA_WASM_BYTES: &[u8] = include_bytes!(env!("PHOTODNA_WASM_PATH"));
998
999    /// Size of the embedded WASM module in bytes.
1000    pub const PHOTODNA_WASM_SIZE: usize = PHOTODNA_WASM_BYTES.len();
1001}
1002
1003#[cfg(any(
1004    all(
1005        feature = "wasm",
1006        not(any(target_os = "windows", target_os = "linux", target_os = "macos"))
1007    ),
1008    target_os = "openbsd",
1009    target_os = "freebsd",
1010    target_os = "netbsd",
1011    target_os = "dragonfly",
1012))]
1013pub use wasm::*;
1014
1015// ============================================================================
1016// Utility Functions
1017// ============================================================================
1018
1019/// Returns a human-readable description of a PhotoDNA error code.
1020///
1021/// This is a compile-time lookup that doesn't require a library instance.
1022pub const fn error_code_description(code: i32) -> &'static str {
1023    match code {
1024        0 => "Success",
1025        PhotoDna_ErrorUnknown => "An undetermined error occurred",
1026        PhotoDna_ErrorMemoryAllocationFailed => "Failed to allocate memory",
1027        PhotoDna_ErrorLibraryFailure => "General failure within the library",
1028        PhotoDna_ErrorMemoryAccess => "System memory exception occurred",
1029        PhotoDna_ErrorInvalidHash => "Hash does not conform to PhotoDNA specifications",
1030        PhotoDna_ErrorHashFormatInvalidCharacters => "Invalid character in Base64 or Hex hash",
1031        PhotoDna_ErrorImageTooSmall => "Image dimension is less than 50 pixels",
1032        PhotoDna_ErrorNoBorder => "No border was detected for the image",
1033        PhotoDna_ErrorBadArgument => "An invalid argument was passed to the function",
1034        PhotoDna_ErrorImageIsFlat => "Image has few or no gradients",
1035        PhotoDna_ErrorNoBorderImageTooSmall => "No border; image too small after border removal",
1036        PhotoDna_ErrorSourceFormatUnknown => "Not a known source image format",
1037        PhotoDna_ErrorInvalidStride => "Stride should be 0 or >= width in bytes",
1038        PhotoDna_ErrorInvalidSubImage => "Sub region is not within image boundaries",
1039        _ => "Unknown error code",
1040    }
1041}
1042
1043/// Returns the expected hash size for the given options.
1044///
1045/// # Parameters
1046///
1047/// - `options`: The PhotoDNA options flags.
1048///
1049/// # Returns
1050///
1051/// The hash size in bytes based on the format specified in options.
1052pub const fn hash_size_for_options(options: PhotoDnaOptions) -> usize {
1053    let format = options & PhotoDna_HashFormatMask;
1054    if format == PhotoDna_HashFormatEdgeV2Base64 {
1055        PHOTODNA_HASH_SIZE_EDGE_V2_BASE64
1056    } else {
1057        PHOTODNA_HASH_SIZE_EDGE_V2
1058    }
1059}
1060
1061#[cfg(test)]
1062mod tests {
1063    use super::*;
1064
1065    #[test]
1066    fn test_hash_result_size() {
1067        // Verify the struct is packed correctly
1068        assert_eq!(
1069            core::mem::size_of::<HashResult>(),
1070            4 + 4 + 4 + 4 + 4 + 4 + PHOTODNA_HASH_SIZE_MAX + 4 * 6
1071        );
1072    }
1073
1074    #[test]
1075    fn test_error_code_descriptions() {
1076        assert_eq!(error_code_description(0), "Success");
1077        assert_eq!(
1078            error_code_description(PhotoDna_ErrorImageTooSmall),
1079            "Image dimension is less than 50 pixels"
1080        );
1081    }
1082
1083    #[test]
1084    fn test_hash_size_for_options() {
1085        assert_eq!(
1086            hash_size_for_options(PhotoDna_Default),
1087            PHOTODNA_HASH_SIZE_EDGE_V2
1088        );
1089        assert_eq!(
1090            hash_size_for_options(PhotoDna_HashFormatEdgeV2Base64),
1091            PHOTODNA_HASH_SIZE_EDGE_V2_BASE64
1092        );
1093    }
1094
1095    #[test]
1096    fn test_constants() {
1097        assert_eq!(PHOTODNA_HASH_SIZE_EDGE_V2, 924);
1098        assert_eq!(PHOTODNA_HASH_SIZE_EDGE_V2_BASE64, 1232);
1099        assert_eq!(PhotoDna_EdgeV2 as usize, PHOTODNA_HASH_SIZE_EDGE_V2);
1100    }
1101
1102    #[test]
1103    #[cfg(all(
1104        any(target_os = "windows", target_os = "linux", target_os = "macos"),
1105        not(photodna_no_sdk)
1106    ))]
1107    fn test_sdk_paths() {
1108        // Verify SDK paths are set at compile time (only for native targets with SDK)
1109        assert!(!PHOTODNA_SDK_ROOT.is_empty());
1110        assert!(!PHOTODNA_LIB_DIR.is_empty());
1111    }
1112}