feat: add acquisition pairing evidence
This commit is contained in:
parent
e6e7271094
commit
961d92fff3
5 changed files with 877 additions and 0 deletions
|
|
@ -25,6 +25,29 @@ chaque image v18, relie son asset comme source et rend cette même image
|
||||||
sélectionnée. Aucun nom de fichier, EXIF ou rapprochement de siblings n'est
|
sélectionnée. Aucun nom de fichier, EXIF ou rapprochement de siblings n'est
|
||||||
interprété ; les IDs et la sémantique v18 restent inchangés.
|
interprété ; les IDs et la sémantique v18 restent inchangés.
|
||||||
|
|
||||||
|
### S3-D — Acquisition Pairing Evidence v1
|
||||||
|
|
||||||
|
**IMPLEMENTED / VALIDATION PENDING — non FROZEN.** S3-D extrait, depuis des
|
||||||
|
assets source immuables, un ensemble fixe et borné de métadonnées d'évidence
|
||||||
|
d'appariement. Pour les RAW, l'extraction est metadata-only avec LibRaw ; pour
|
||||||
|
les JPEG, elle utilise libexif. Cette évidence n'est pas une identité
|
||||||
|
scientifique et ne modifie ni les identités existantes ni leur sémantique.
|
||||||
|
|
||||||
|
La politique d'appariement v1 est la suivante : entre deux assets distincts,
|
||||||
|
seul un `ImageUniqueID` caméra connu, non vide et exactement partagé est une
|
||||||
|
évidence forte, éligible à une association automatique dans une future
|
||||||
|
intégration. Les timestamp, make/model, numéro de série du boîtier concordant,
|
||||||
|
dimensions/exposition et basename sont faibles ou corroboratifs : ils ne
|
||||||
|
constituent jamais une identité. Des `ImageUniqueID` connus contradictoires, ou
|
||||||
|
des numéros de série de boîtier connus contradictoires, signifient que les
|
||||||
|
assets sont différents. Une valeur absente n'est pas un conflit. Une ambiguïté
|
||||||
|
n'est jamais résolue par l'ordre des assets.
|
||||||
|
|
||||||
|
S3-D n'effectue aucune écriture en base ni changement de schéma : Project DB
|
||||||
|
reste v19 et S3-C attach demeure l'unique primitive de persistance. S3-E, la
|
||||||
|
future intégration d'ingestion multi-source avant `Capture`, n'est pas
|
||||||
|
implémentée.
|
||||||
|
|
||||||
## Phase H — décision Project DB v18
|
## Phase H — décision Project DB v18
|
||||||
|
|
||||||
**PASS / FROZEN.** La migration transactionnelle v17→v18
|
**PASS / FROZEN.** La migration transactionnelle v17→v18
|
||||||
|
|
|
||||||
126
include/lardon3d/acquisition_pairing.h
Normal file
126
include/lardon3d/acquisition_pairing.h
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
#ifndef LARDON3D_ACQUISITION_PAIRING_H
|
||||||
|
#define LARDON3D_ACQUISITION_PAIRING_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define LARDON3D_ACQUISITION_PAIRING_POLICY_VERSION 1u
|
||||||
|
#define LARDON3D_ACQUISITION_TEXT_CAPACITY 128u
|
||||||
|
#define LARDON3D_ACQUISITION_DATETIME_CAPACITY 32u
|
||||||
|
#define LARDON3D_ACQUISITION_MAX_CANDIDATES 64u
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_ACQUISITION_SOURCE_UNSUPPORTED = 0,
|
||||||
|
LARDON3D_ACQUISITION_SOURCE_RAW = 1,
|
||||||
|
LARDON3D_ACQUISITION_SOURCE_JPEG = 2
|
||||||
|
} Lardon3DAcquisitionSourceKind;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_ACQUISITION_OK = 0,
|
||||||
|
LARDON3D_ACQUISITION_INVALID_ARGUMENT,
|
||||||
|
LARDON3D_ACQUISITION_UNSUPPORTED_FORMAT,
|
||||||
|
LARDON3D_ACQUISITION_CORRUPT_SOURCE,
|
||||||
|
LARDON3D_ACQUISITION_IO_ERROR,
|
||||||
|
LARDON3D_ACQUISITION_METADATA_UNAVAILABLE,
|
||||||
|
LARDON3D_ACQUISITION_LIMIT_EXCEEDED,
|
||||||
|
LARDON3D_ACQUISITION_INTERNAL_ERROR
|
||||||
|
} Lardon3DAcquisitionResult;
|
||||||
|
|
||||||
|
enum {
|
||||||
|
LARDON3D_ACQUISITION_FIELD_MAKE = UINT32_C(1) << 0,
|
||||||
|
LARDON3D_ACQUISITION_FIELD_MODEL = UINT32_C(1) << 1,
|
||||||
|
LARDON3D_ACQUISITION_FIELD_BODY_SERIAL = UINT32_C(1) << 2,
|
||||||
|
LARDON3D_ACQUISITION_FIELD_DATETIME_ORIGINAL = UINT32_C(1) << 3,
|
||||||
|
LARDON3D_ACQUISITION_FIELD_SUBSEC_ORIGINAL = UINT32_C(1) << 4,
|
||||||
|
LARDON3D_ACQUISITION_FIELD_OFFSET_ORIGINAL = UINT32_C(1) << 5,
|
||||||
|
LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID = UINT32_C(1) << 6,
|
||||||
|
LARDON3D_ACQUISITION_FIELD_DIMENSIONS = UINT32_C(1) << 7,
|
||||||
|
LARDON3D_ACQUISITION_FIELD_ORIENTATION = UINT32_C(1) << 8
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t policy_version;
|
||||||
|
Lardon3DAcquisitionSourceKind source_kind;
|
||||||
|
uint32_t present_fields;
|
||||||
|
char make[LARDON3D_ACQUISITION_TEXT_CAPACITY];
|
||||||
|
char model[LARDON3D_ACQUISITION_TEXT_CAPACITY];
|
||||||
|
char body_serial[LARDON3D_ACQUISITION_TEXT_CAPACITY];
|
||||||
|
char datetime_original[LARDON3D_ACQUISITION_DATETIME_CAPACITY];
|
||||||
|
char subsec_original[LARDON3D_ACQUISITION_DATETIME_CAPACITY];
|
||||||
|
char offset_original[LARDON3D_ACQUISITION_DATETIME_CAPACITY];
|
||||||
|
char image_unique_id[LARDON3D_ACQUISITION_TEXT_CAPACITY];
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
uint16_t orientation;
|
||||||
|
uint16_t reserved;
|
||||||
|
} Lardon3DAcquisitionMetadata;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_ACQUISITION_INSUFFICIENT = 0,
|
||||||
|
LARDON3D_ACQUISITION_SAME_ACQUISITION_CANDIDATE = 1,
|
||||||
|
LARDON3D_ACQUISITION_SAME_ACQUISITION_STRONG = 2,
|
||||||
|
LARDON3D_ACQUISITION_DIFFERENT = 3,
|
||||||
|
LARDON3D_ACQUISITION_AMBIGUOUS = 4
|
||||||
|
} Lardon3DAcquisitionDecision;
|
||||||
|
|
||||||
|
enum {
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_IMAGE_UNIQUE_ID = UINT32_C(1) << 0,
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_BODY_SERIAL = UINT32_C(1) << 1,
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_MAKE = UINT32_C(1) << 2,
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_MODEL = UINT32_C(1) << 3,
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_DATETIME_SECOND = UINT32_C(1) << 4,
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_DIMENSIONS = UINT32_C(1) << 5,
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_BASENAME_HINT = UINT32_C(1) << 6
|
||||||
|
};
|
||||||
|
|
||||||
|
enum {
|
||||||
|
LARDON3D_ACQUISITION_CONTRADICTION_IMAGE_UNIQUE_ID = UINT32_C(1) << 0,
|
||||||
|
LARDON3D_ACQUISITION_CONTRADICTION_BODY_SERIAL = UINT32_C(1) << 1
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Lardon3DAcquisitionDecision decision;
|
||||||
|
uint32_t evidence;
|
||||||
|
uint32_t contradictions;
|
||||||
|
uint32_t strength;
|
||||||
|
} Lardon3DAcquisitionPairResult;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint64_t candidate_id;
|
||||||
|
const Lardon3DAcquisitionMetadata *metadata;
|
||||||
|
uint8_t same_asset;
|
||||||
|
uint8_t basename_hint;
|
||||||
|
uint8_t reserved[6];
|
||||||
|
} Lardon3DAcquisitionCandidate;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Lardon3DAcquisitionDecision decision;
|
||||||
|
uint64_t selected_candidate_id;
|
||||||
|
size_t top_candidate_count;
|
||||||
|
Lardon3DAcquisitionPairResult selected_pair;
|
||||||
|
} Lardon3DAcquisitionSelection;
|
||||||
|
|
||||||
|
Lardon3DAcquisitionResult lardon3d_acquisition_extract_metadata(
|
||||||
|
const char *path, Lardon3DAcquisitionMetadata *metadata
|
||||||
|
);
|
||||||
|
|
||||||
|
Lardon3DAcquisitionResult lardon3d_acquisition_compare(
|
||||||
|
const Lardon3DAcquisitionMetadata *left, const Lardon3DAcquisitionMetadata *right,
|
||||||
|
int same_asset, int basename_hint, Lardon3DAcquisitionPairResult *result
|
||||||
|
);
|
||||||
|
|
||||||
|
Lardon3DAcquisitionResult lardon3d_acquisition_select_candidate(
|
||||||
|
const Lardon3DAcquisitionMetadata *source,
|
||||||
|
const Lardon3DAcquisitionCandidate *candidates, size_t candidate_count,
|
||||||
|
Lardon3DAcquisitionSelection *selection
|
||||||
|
);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
13
meson.build
13
meson.build
|
|
@ -37,6 +37,7 @@ opencv = dependency(
|
||||||
modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_features2d', 'opencv_geometry'],
|
modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_features2d', 'opencv_geometry'],
|
||||||
)
|
)
|
||||||
libraw = dependency('libraw', required: true)
|
libraw = dependency('libraw', required: true)
|
||||||
|
libexif = dependency('libexif', required: true)
|
||||||
libpng = dependency('libpng', required: true)
|
libpng = dependency('libpng', required: true)
|
||||||
libdeflate = dependency('libdeflate', required: true)
|
libdeflate = dependency('libdeflate', required: true)
|
||||||
opencv_benchmark = dependency(
|
opencv_benchmark = dependency(
|
||||||
|
|
@ -148,6 +149,7 @@ executable(
|
||||||
'src/image_catalog.c',
|
'src/image_catalog.c',
|
||||||
'src/image_catalog_persistent.c',
|
'src/image_catalog_persistent.c',
|
||||||
'src/raw_development.cpp',
|
'src/raw_development.cpp',
|
||||||
|
'src/acquisition_pairing.cpp',
|
||||||
'src/feature_extractor_opencv.cpp',
|
'src/feature_extractor_opencv.cpp',
|
||||||
'src/feature_store.c',
|
'src/feature_store.c',
|
||||||
'src/feature_extractor_opencv.cpp',
|
'src/feature_extractor_opencv.cpp',
|
||||||
|
|
@ -198,6 +200,7 @@ executable(
|
||||||
sqlite3,
|
sqlite3,
|
||||||
openssl,
|
openssl,
|
||||||
libraw,
|
libraw,
|
||||||
|
libexif,
|
||||||
libpng,
|
libpng,
|
||||||
libdeflate,
|
libdeflate,
|
||||||
opencv,
|
opencv,
|
||||||
|
|
@ -320,6 +323,16 @@ raw_development_test = executable(
|
||||||
|
|
||||||
test('raw-development', raw_development_test, timeout: 30)
|
test('raw-development', raw_development_test, timeout: 30)
|
||||||
|
|
||||||
|
acquisition_pairing_test = executable(
|
||||||
|
'test-acquisition-pairing',
|
||||||
|
sources: ['tests/test_acquisition_pairing.cpp', 'src/acquisition_pairing.cpp'],
|
||||||
|
include_directories: include_directories('include'),
|
||||||
|
cpp_args: ['-DLARDON3D_ACQUISITION_PAIRING_TESTING'],
|
||||||
|
dependencies: [libraw, libexif],
|
||||||
|
)
|
||||||
|
|
||||||
|
test('acquisition-pairing', acquisition_pairing_test, timeout: 30)
|
||||||
|
|
||||||
feature_store_test = executable(
|
feature_store_test = executable(
|
||||||
'test-feature-store',
|
'test-feature-store',
|
||||||
sources: [
|
sources: [
|
||||||
|
|
|
||||||
425
src/acquisition_pairing.cpp
Normal file
425
src/acquisition_pairing.cpp
Normal file
|
|
@ -0,0 +1,425 @@
|
||||||
|
#include <lardon3d/acquisition_pairing.h>
|
||||||
|
|
||||||
|
#include <libexif/exif-data.h>
|
||||||
|
#include <libexif/exif-utils.h>
|
||||||
|
#include <libraw/libraw.h>
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <ctime>
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool has_field(const Lardon3DAcquisitionMetadata &metadata, uint32_t field) {
|
||||||
|
return (metadata.present_fields & field) != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BoundedText {
|
||||||
|
const char *data;
|
||||||
|
size_t length;
|
||||||
|
bool available;
|
||||||
|
};
|
||||||
|
|
||||||
|
BoundedText bounded_text(const char *text, size_t capacity) {
|
||||||
|
const void *terminator = std::memchr(text, '\0', capacity);
|
||||||
|
if (terminator == nullptr || terminator == text) {
|
||||||
|
return {text, 0u, false};
|
||||||
|
}
|
||||||
|
const size_t length = static_cast<size_t>(static_cast<const char *>(terminator) - text);
|
||||||
|
return {text, length, true};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool same_text(const char *left, size_t left_capacity, const char *right,
|
||||||
|
size_t right_capacity) {
|
||||||
|
const BoundedText left_text = bounded_text(left, left_capacity);
|
||||||
|
const BoundedText right_text = bounded_text(right, right_capacity);
|
||||||
|
return left_text.available && right_text.available &&
|
||||||
|
left_text.length == right_text.length &&
|
||||||
|
std::memcmp(left_text.data, right_text.data, left_text.length) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool different_text(const char *left, size_t left_capacity, const char *right,
|
||||||
|
size_t right_capacity) {
|
||||||
|
const BoundedText left_text = bounded_text(left, left_capacity);
|
||||||
|
const BoundedText right_text = bounded_text(right, right_capacity);
|
||||||
|
return left_text.available && right_text.available &&
|
||||||
|
(left_text.length != right_text.length ||
|
||||||
|
std::memcmp(left_text.data, right_text.data, left_text.length) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool copy_padded_text(char *destination, size_t capacity, const unsigned char *source,
|
||||||
|
size_t source_size) {
|
||||||
|
size_t length = source_size;
|
||||||
|
while (length > 0u && (source[length - 1u] == '\0' || source[length - 1u] == ' ')) {
|
||||||
|
--length;
|
||||||
|
}
|
||||||
|
if (length >= capacity) {
|
||||||
|
destination[0] = '\0';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (length > 0u) {
|
||||||
|
std::memcpy(destination, source, length);
|
||||||
|
}
|
||||||
|
destination[length] = '\0';
|
||||||
|
return length != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
void extract_raw_fixed_text(const libraw_data_t &raw,
|
||||||
|
Lardon3DAcquisitionMetadata &metadata) {
|
||||||
|
if (copy_padded_text(metadata.make, sizeof(metadata.make),
|
||||||
|
reinterpret_cast<const unsigned char *>(raw.idata.make),
|
||||||
|
sizeof(raw.idata.make))) {
|
||||||
|
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_MAKE;
|
||||||
|
}
|
||||||
|
if (copy_padded_text(metadata.model, sizeof(metadata.model),
|
||||||
|
reinterpret_cast<const unsigned char *>(raw.idata.model),
|
||||||
|
sizeof(raw.idata.model))) {
|
||||||
|
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_MODEL;
|
||||||
|
}
|
||||||
|
if (copy_padded_text(metadata.body_serial, sizeof(metadata.body_serial),
|
||||||
|
reinterpret_cast<const unsigned char *>(raw.shootinginfo.BodySerial),
|
||||||
|
sizeof(raw.shootinginfo.BodySerial))) {
|
||||||
|
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_BODY_SERIAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void extract_raw_image_unique_id(const libraw_data_t &raw,
|
||||||
|
Lardon3DAcquisitionMetadata &metadata) {
|
||||||
|
if (copy_padded_text(metadata.image_unique_id, sizeof(metadata.image_unique_id),
|
||||||
|
reinterpret_cast<const unsigned char *>(raw.color.ImageUniqueID),
|
||||||
|
sizeof(raw.color.ImageUniqueID))) {
|
||||||
|
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExifEntry *find_entry(ExifData *data, ExifTag tag) {
|
||||||
|
for (unsigned int index = 0u; index < EXIF_IFD_COUNT; ++index) {
|
||||||
|
ExifEntry *entry = exif_content_get_entry(data->ifd[index], tag);
|
||||||
|
if (entry != nullptr) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void extract_ascii(ExifData *data, ExifTag tag, char *destination, size_t capacity,
|
||||||
|
uint32_t field, Lardon3DAcquisitionMetadata &metadata) {
|
||||||
|
ExifEntry *entry = find_entry(data, tag);
|
||||||
|
if (entry == nullptr || entry->data == nullptr || entry->size == 0u ||
|
||||||
|
entry->format != EXIF_FORMAT_ASCII) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (copy_padded_text(destination, capacity, entry->data, entry->size)) {
|
||||||
|
metadata.present_fields |= field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool extract_unsigned(ExifData *data, ExifTag tag, uint32_t &value) {
|
||||||
|
ExifEntry *entry = find_entry(data, tag);
|
||||||
|
if (entry == nullptr || entry->data == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ExifByteOrder order = exif_data_get_byte_order(data);
|
||||||
|
if (entry->format == EXIF_FORMAT_SHORT && entry->size >= 2u) {
|
||||||
|
value = exif_get_short(entry->data, order);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (entry->format == EXIF_FORMAT_LONG && entry->size >= 4u) {
|
||||||
|
value = exif_get_long(entry->data, order);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DAcquisitionResult extract_jpeg(const char *path,
|
||||||
|
Lardon3DAcquisitionMetadata &metadata) {
|
||||||
|
FILE *file = std::fopen(path, "rb");
|
||||||
|
if (file == nullptr) {
|
||||||
|
return errno == ENOENT || errno == EACCES ? LARDON3D_ACQUISITION_IO_ERROR
|
||||||
|
: LARDON3D_ACQUISITION_IO_ERROR;
|
||||||
|
}
|
||||||
|
bool has_end_marker = false;
|
||||||
|
if (std::fseek(file, -2L, SEEK_END) == 0) {
|
||||||
|
unsigned char tail[2] = {};
|
||||||
|
has_end_marker = std::fread(tail, 1u, sizeof(tail), file) == sizeof(tail) &&
|
||||||
|
tail[0] == 0xffu && tail[1] == 0xd9u;
|
||||||
|
}
|
||||||
|
std::fclose(file);
|
||||||
|
if (!has_end_marker) {
|
||||||
|
return LARDON3D_ACQUISITION_CORRUPT_SOURCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExifData *data = exif_data_new_from_file(path);
|
||||||
|
if (data == nullptr) {
|
||||||
|
return LARDON3D_ACQUISITION_METADATA_UNAVAILABLE;
|
||||||
|
}
|
||||||
|
metadata.source_kind = LARDON3D_ACQUISITION_SOURCE_JPEG;
|
||||||
|
extract_ascii(data, EXIF_TAG_MAKE, metadata.make, sizeof(metadata.make),
|
||||||
|
LARDON3D_ACQUISITION_FIELD_MAKE, metadata);
|
||||||
|
extract_ascii(data, EXIF_TAG_MODEL, metadata.model, sizeof(metadata.model),
|
||||||
|
LARDON3D_ACQUISITION_FIELD_MODEL, metadata);
|
||||||
|
extract_ascii(data, EXIF_TAG_BODY_SERIAL_NUMBER, metadata.body_serial,
|
||||||
|
sizeof(metadata.body_serial), LARDON3D_ACQUISITION_FIELD_BODY_SERIAL,
|
||||||
|
metadata);
|
||||||
|
extract_ascii(data, EXIF_TAG_DATE_TIME_ORIGINAL, metadata.datetime_original,
|
||||||
|
sizeof(metadata.datetime_original),
|
||||||
|
LARDON3D_ACQUISITION_FIELD_DATETIME_ORIGINAL, metadata);
|
||||||
|
extract_ascii(data, EXIF_TAG_SUB_SEC_TIME_ORIGINAL, metadata.subsec_original,
|
||||||
|
sizeof(metadata.subsec_original), LARDON3D_ACQUISITION_FIELD_SUBSEC_ORIGINAL,
|
||||||
|
metadata);
|
||||||
|
extract_ascii(data, EXIF_TAG_OFFSET_TIME_ORIGINAL, metadata.offset_original,
|
||||||
|
sizeof(metadata.offset_original), LARDON3D_ACQUISITION_FIELD_OFFSET_ORIGINAL,
|
||||||
|
metadata);
|
||||||
|
extract_ascii(data, EXIF_TAG_IMAGE_UNIQUE_ID, metadata.image_unique_id,
|
||||||
|
sizeof(metadata.image_unique_id), LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID,
|
||||||
|
metadata);
|
||||||
|
|
||||||
|
uint32_t width = 0u;
|
||||||
|
uint32_t height = 0u;
|
||||||
|
if (extract_unsigned(data, EXIF_TAG_PIXEL_X_DIMENSION, width) &&
|
||||||
|
extract_unsigned(data, EXIF_TAG_PIXEL_Y_DIMENSION, height) && width != 0u &&
|
||||||
|
height != 0u) {
|
||||||
|
metadata.width = width;
|
||||||
|
metadata.height = height;
|
||||||
|
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_DIMENSIONS;
|
||||||
|
}
|
||||||
|
uint32_t orientation = 0u;
|
||||||
|
if (extract_unsigned(data, EXIF_TAG_ORIENTATION, orientation) && orientation <= UINT16_MAX) {
|
||||||
|
metadata.orientation = static_cast<uint16_t>(orientation);
|
||||||
|
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_ORIENTATION;
|
||||||
|
}
|
||||||
|
exif_data_unref(data);
|
||||||
|
return LARDON3D_ACQUISITION_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DAcquisitionResult extract_raw(const char *path,
|
||||||
|
Lardon3DAcquisitionMetadata &metadata) {
|
||||||
|
libraw_data_t *raw = libraw_init(0u);
|
||||||
|
if (raw == nullptr) {
|
||||||
|
return LARDON3D_ACQUISITION_INTERNAL_ERROR;
|
||||||
|
}
|
||||||
|
const int status = libraw_open_file(raw, path);
|
||||||
|
if (status != LIBRAW_SUCCESS) {
|
||||||
|
libraw_close(raw);
|
||||||
|
if (status == LIBRAW_FILE_UNSUPPORTED || status == LIBRAW_DATA_ERROR ||
|
||||||
|
status == LIBRAW_IO_ERROR) {
|
||||||
|
return LARDON3D_ACQUISITION_UNSUPPORTED_FORMAT;
|
||||||
|
}
|
||||||
|
return LARDON3D_ACQUISITION_CORRUPT_SOURCE;
|
||||||
|
}
|
||||||
|
metadata.source_kind = LARDON3D_ACQUISITION_SOURCE_RAW;
|
||||||
|
extract_raw_fixed_text(*raw, metadata);
|
||||||
|
extract_raw_image_unique_id(*raw, metadata);
|
||||||
|
if (raw->other.timestamp > 0) {
|
||||||
|
std::tm time_parts = {};
|
||||||
|
if (gmtime_r(&raw->other.timestamp, &time_parts) != nullptr &&
|
||||||
|
std::strftime(metadata.datetime_original, sizeof(metadata.datetime_original),
|
||||||
|
"%Y:%m:%d %H:%M:%S", &time_parts) != 0u) {
|
||||||
|
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_DATETIME_ORIGINAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (raw->sizes.width != 0u && raw->sizes.height != 0u) {
|
||||||
|
metadata.width = raw->sizes.width;
|
||||||
|
metadata.height = raw->sizes.height;
|
||||||
|
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_DIMENSIONS;
|
||||||
|
}
|
||||||
|
if (raw->sizes.flip >= 0 && raw->sizes.flip <= UINT16_MAX) {
|
||||||
|
metadata.orientation = static_cast<uint16_t>(raw->sizes.flip);
|
||||||
|
metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_ORIENTATION;
|
||||||
|
}
|
||||||
|
libraw_close(raw);
|
||||||
|
return LARDON3D_ACQUISITION_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void record_matching_text(const Lardon3DAcquisitionMetadata &left,
|
||||||
|
const Lardon3DAcquisitionMetadata &right, uint32_t field,
|
||||||
|
const char *left_text, size_t left_capacity,
|
||||||
|
const char *right_text, size_t right_capacity, uint32_t evidence,
|
||||||
|
Lardon3DAcquisitionPairResult &result) {
|
||||||
|
if (has_field(left, field) && has_field(right, field) &&
|
||||||
|
same_text(left_text, left_capacity, right_text, right_capacity)) {
|
||||||
|
result.evidence |= evidence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned evidence_count(uint32_t value) {
|
||||||
|
unsigned count = 0u;
|
||||||
|
while (value != 0u) {
|
||||||
|
count += value & 1u;
|
||||||
|
value >>= 1u;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#ifdef LARDON3D_ACQUISITION_PAIRING_TESTING
|
||||||
|
extern "C" void lardon3d_acquisition_test_extract_raw_image_unique_id(
|
||||||
|
const libraw_data_t *raw, Lardon3DAcquisitionMetadata *metadata) {
|
||||||
|
if (raw != nullptr && metadata != nullptr) {
|
||||||
|
extract_raw_image_unique_id(*raw, *metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void lardon3d_acquisition_test_extract_raw_fixed_text(
|
||||||
|
const libraw_data_t *raw, Lardon3DAcquisitionMetadata *metadata) {
|
||||||
|
if (raw != nullptr && metadata != nullptr) {
|
||||||
|
extract_raw_fixed_text(*raw, *metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern "C" Lardon3DAcquisitionResult lardon3d_acquisition_extract_metadata(
|
||||||
|
const char *path, Lardon3DAcquisitionMetadata *metadata) {
|
||||||
|
if (path == nullptr || path[0] == '\0' || metadata == nullptr) {
|
||||||
|
return LARDON3D_ACQUISITION_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
*metadata = {};
|
||||||
|
metadata->policy_version = LARDON3D_ACQUISITION_PAIRING_POLICY_VERSION;
|
||||||
|
FILE *file = std::fopen(path, "rb");
|
||||||
|
if (file == nullptr) {
|
||||||
|
return LARDON3D_ACQUISITION_IO_ERROR;
|
||||||
|
}
|
||||||
|
unsigned char signature[2] = {};
|
||||||
|
const size_t bytes_read = std::fread(signature, 1u, sizeof(signature), file);
|
||||||
|
const bool read_error = std::ferror(file) != 0;
|
||||||
|
std::fclose(file);
|
||||||
|
if (read_error) {
|
||||||
|
return LARDON3D_ACQUISITION_IO_ERROR;
|
||||||
|
}
|
||||||
|
if (bytes_read == sizeof(signature) && signature[0] == 0xffu && signature[1] == 0xd8u) {
|
||||||
|
return extract_jpeg(path, *metadata);
|
||||||
|
}
|
||||||
|
return extract_raw(path, *metadata);
|
||||||
|
} catch (const std::bad_alloc &) {
|
||||||
|
return LARDON3D_ACQUISITION_INTERNAL_ERROR;
|
||||||
|
} catch (...) {
|
||||||
|
return LARDON3D_ACQUISITION_INTERNAL_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Lardon3DAcquisitionResult lardon3d_acquisition_compare(
|
||||||
|
const Lardon3DAcquisitionMetadata *left, const Lardon3DAcquisitionMetadata *right,
|
||||||
|
int same_asset, int basename_hint, Lardon3DAcquisitionPairResult *result) {
|
||||||
|
if (left == nullptr || right == nullptr || result == nullptr ||
|
||||||
|
left->policy_version != LARDON3D_ACQUISITION_PAIRING_POLICY_VERSION ||
|
||||||
|
right->policy_version != LARDON3D_ACQUISITION_PAIRING_POLICY_VERSION) {
|
||||||
|
return LARDON3D_ACQUISITION_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
*result = {};
|
||||||
|
result->decision = LARDON3D_ACQUISITION_INSUFFICIENT;
|
||||||
|
if (basename_hint != 0) {
|
||||||
|
result->evidence |= LARDON3D_ACQUISITION_EVIDENCE_BASENAME_HINT;
|
||||||
|
}
|
||||||
|
record_matching_text(*left, *right, LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID,
|
||||||
|
left->image_unique_id, sizeof(left->image_unique_id),
|
||||||
|
right->image_unique_id, sizeof(right->image_unique_id),
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_IMAGE_UNIQUE_ID, *result);
|
||||||
|
record_matching_text(*left, *right, LARDON3D_ACQUISITION_FIELD_BODY_SERIAL,
|
||||||
|
left->body_serial, sizeof(left->body_serial), right->body_serial,
|
||||||
|
sizeof(right->body_serial),
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_BODY_SERIAL, *result);
|
||||||
|
record_matching_text(*left, *right, LARDON3D_ACQUISITION_FIELD_MAKE, left->make,
|
||||||
|
sizeof(left->make), right->make, sizeof(right->make),
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_MAKE, *result);
|
||||||
|
record_matching_text(*left, *right, LARDON3D_ACQUISITION_FIELD_MODEL, left->model,
|
||||||
|
sizeof(left->model), right->model, sizeof(right->model),
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_MODEL, *result);
|
||||||
|
record_matching_text(*left, *right, LARDON3D_ACQUISITION_FIELD_DATETIME_ORIGINAL,
|
||||||
|
left->datetime_original, sizeof(left->datetime_original),
|
||||||
|
right->datetime_original, sizeof(right->datetime_original),
|
||||||
|
LARDON3D_ACQUISITION_EVIDENCE_DATETIME_SECOND, *result);
|
||||||
|
if (has_field(*left, LARDON3D_ACQUISITION_FIELD_DIMENSIONS) &&
|
||||||
|
has_field(*right, LARDON3D_ACQUISITION_FIELD_DIMENSIONS) &&
|
||||||
|
left->width == right->width && left->height == right->height) {
|
||||||
|
result->evidence |= LARDON3D_ACQUISITION_EVIDENCE_DIMENSIONS;
|
||||||
|
}
|
||||||
|
if (has_field(*left, LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID) &&
|
||||||
|
has_field(*right, LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID) &&
|
||||||
|
different_text(left->image_unique_id, sizeof(left->image_unique_id),
|
||||||
|
right->image_unique_id, sizeof(right->image_unique_id))) {
|
||||||
|
result->contradictions |= LARDON3D_ACQUISITION_CONTRADICTION_IMAGE_UNIQUE_ID;
|
||||||
|
}
|
||||||
|
if (has_field(*left, LARDON3D_ACQUISITION_FIELD_BODY_SERIAL) &&
|
||||||
|
has_field(*right, LARDON3D_ACQUISITION_FIELD_BODY_SERIAL) &&
|
||||||
|
different_text(left->body_serial, sizeof(left->body_serial), right->body_serial,
|
||||||
|
sizeof(right->body_serial))) {
|
||||||
|
result->contradictions |= LARDON3D_ACQUISITION_CONTRADICTION_BODY_SERIAL;
|
||||||
|
}
|
||||||
|
if (same_asset != 0) {
|
||||||
|
result->strength = 0u;
|
||||||
|
return LARDON3D_ACQUISITION_OK;
|
||||||
|
}
|
||||||
|
if (result->contradictions != 0u) {
|
||||||
|
result->decision = LARDON3D_ACQUISITION_DIFFERENT;
|
||||||
|
return LARDON3D_ACQUISITION_OK;
|
||||||
|
}
|
||||||
|
if ((result->evidence & LARDON3D_ACQUISITION_EVIDENCE_IMAGE_UNIQUE_ID) != 0u) {
|
||||||
|
result->decision = LARDON3D_ACQUISITION_SAME_ACQUISITION_STRONG;
|
||||||
|
result->strength = 100u;
|
||||||
|
return LARDON3D_ACQUISITION_OK;
|
||||||
|
}
|
||||||
|
const uint32_t metadata_evidence =
|
||||||
|
result->evidence & ~LARDON3D_ACQUISITION_EVIDENCE_BASENAME_HINT;
|
||||||
|
if (metadata_evidence != 0u) {
|
||||||
|
result->decision = LARDON3D_ACQUISITION_SAME_ACQUISITION_CANDIDATE;
|
||||||
|
result->strength = evidence_count(metadata_evidence) +
|
||||||
|
((result->evidence & LARDON3D_ACQUISITION_EVIDENCE_BASENAME_HINT) != 0u
|
||||||
|
? 1u
|
||||||
|
: 0u);
|
||||||
|
}
|
||||||
|
return LARDON3D_ACQUISITION_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Lardon3DAcquisitionResult lardon3d_acquisition_select_candidate(
|
||||||
|
const Lardon3DAcquisitionMetadata *source,
|
||||||
|
const Lardon3DAcquisitionCandidate *candidates, size_t candidate_count,
|
||||||
|
Lardon3DAcquisitionSelection *selection) {
|
||||||
|
if (source == nullptr || selection == nullptr ||
|
||||||
|
(candidate_count != 0u && candidates == nullptr)) {
|
||||||
|
return LARDON3D_ACQUISITION_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
if (candidate_count > LARDON3D_ACQUISITION_MAX_CANDIDATES) {
|
||||||
|
return LARDON3D_ACQUISITION_LIMIT_EXCEEDED;
|
||||||
|
}
|
||||||
|
*selection = {};
|
||||||
|
selection->decision = LARDON3D_ACQUISITION_INSUFFICIENT;
|
||||||
|
uint32_t top_strength = 0u;
|
||||||
|
for (size_t index = 0u; index < candidate_count; ++index) {
|
||||||
|
if (candidates[index].metadata == nullptr) {
|
||||||
|
return LARDON3D_ACQUISITION_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
Lardon3DAcquisitionPairResult pair = {};
|
||||||
|
const Lardon3DAcquisitionResult status = lardon3d_acquisition_compare(
|
||||||
|
source, candidates[index].metadata, candidates[index].same_asset,
|
||||||
|
candidates[index].basename_hint, &pair);
|
||||||
|
if (status != LARDON3D_ACQUISITION_OK) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
if (pair.decision != LARDON3D_ACQUISITION_SAME_ACQUISITION_STRONG &&
|
||||||
|
pair.decision != LARDON3D_ACQUISITION_SAME_ACQUISITION_CANDIDATE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pair.strength > top_strength) {
|
||||||
|
top_strength = pair.strength;
|
||||||
|
selection->selected_candidate_id = candidates[index].candidate_id;
|
||||||
|
selection->selected_pair = pair;
|
||||||
|
selection->top_candidate_count = 1u;
|
||||||
|
} else if (pair.strength == top_strength) {
|
||||||
|
++selection->top_candidate_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selection->top_candidate_count == 0u) {
|
||||||
|
return LARDON3D_ACQUISITION_OK;
|
||||||
|
}
|
||||||
|
if (selection->top_candidate_count > 1u) {
|
||||||
|
selection->decision = LARDON3D_ACQUISITION_AMBIGUOUS;
|
||||||
|
selection->selected_candidate_id = 0u;
|
||||||
|
return LARDON3D_ACQUISITION_OK;
|
||||||
|
}
|
||||||
|
selection->decision = selection->selected_pair.decision;
|
||||||
|
return LARDON3D_ACQUISITION_OK;
|
||||||
|
}
|
||||||
290
tests/test_acquisition_pairing.cpp
Normal file
290
tests/test_acquisition_pairing.cpp
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
#include <lardon3d/acquisition_pairing.h>
|
||||||
|
|
||||||
|
#include <libexif/exif-data.h>
|
||||||
|
#include <libraw/libraw.h>
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
extern "C" void lardon3d_acquisition_test_extract_raw_image_unique_id(
|
||||||
|
const libraw_data_t *raw, Lardon3DAcquisitionMetadata *metadata);
|
||||||
|
extern "C" void lardon3d_acquisition_test_extract_raw_fixed_text(
|
||||||
|
const libraw_data_t *raw, Lardon3DAcquisitionMetadata *metadata);
|
||||||
|
|
||||||
|
Lardon3DAcquisitionMetadata metadata() {
|
||||||
|
Lardon3DAcquisitionMetadata value = {};
|
||||||
|
value.policy_version = LARDON3D_ACQUISITION_PAIRING_POLICY_VERSION;
|
||||||
|
value.source_kind = LARDON3D_ACQUISITION_SOURCE_RAW;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_text(Lardon3DAcquisitionMetadata &value, uint32_t field, char *destination,
|
||||||
|
size_t capacity, const char *text) {
|
||||||
|
assert(std::strlen(text) < capacity);
|
||||||
|
std::strcpy(destination, text);
|
||||||
|
value.present_fields |= field;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DAcquisitionPairResult compare(const Lardon3DAcquisitionMetadata &left,
|
||||||
|
const Lardon3DAcquisitionMetadata &right,
|
||||||
|
bool same_asset = false, bool basename_hint = false) {
|
||||||
|
Lardon3DAcquisitionPairResult result = {};
|
||||||
|
assert(lardon3d_acquisition_compare(&left, &right, same_asset, basename_hint, &result) ==
|
||||||
|
LARDON3D_ACQUISITION_OK);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_pairing_policy() {
|
||||||
|
auto left = metadata();
|
||||||
|
auto right = metadata();
|
||||||
|
set_text(left, LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID, left.image_unique_id,
|
||||||
|
sizeof(left.image_unique_id), "UID-1");
|
||||||
|
set_text(right, LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID, right.image_unique_id,
|
||||||
|
sizeof(right.image_unique_id), "UID-1");
|
||||||
|
assert(compare(left, right).decision == LARDON3D_ACQUISITION_SAME_ACQUISITION_STRONG);
|
||||||
|
|
||||||
|
auto result = compare(left, right, false, true);
|
||||||
|
assert(result.decision == LARDON3D_ACQUISITION_SAME_ACQUISITION_STRONG);
|
||||||
|
assert((result.evidence & LARDON3D_ACQUISITION_EVIDENCE_BASENAME_HINT) != 0u);
|
||||||
|
|
||||||
|
auto empty_left = metadata();
|
||||||
|
auto empty_right = metadata();
|
||||||
|
result = compare(empty_left, empty_right, false, true); /* DSC03350.ARW / DSC03350.JPG */
|
||||||
|
assert(result.decision == LARDON3D_ACQUISITION_INSUFFICIENT);
|
||||||
|
assert(result.evidence == LARDON3D_ACQUISITION_EVIDENCE_BASENAME_HINT);
|
||||||
|
|
||||||
|
set_text(empty_left, LARDON3D_ACQUISITION_FIELD_DATETIME_ORIGINAL,
|
||||||
|
empty_left.datetime_original, sizeof(empty_left.datetime_original),
|
||||||
|
"2026:08:27 12:34:56");
|
||||||
|
set_text(empty_right, LARDON3D_ACQUISITION_FIELD_DATETIME_ORIGINAL,
|
||||||
|
empty_right.datetime_original, sizeof(empty_right.datetime_original),
|
||||||
|
"2026:08:27 12:34:56");
|
||||||
|
assert(compare(empty_left, empty_right).decision ==
|
||||||
|
LARDON3D_ACQUISITION_SAME_ACQUISITION_CANDIDATE);
|
||||||
|
set_text(empty_left, LARDON3D_ACQUISITION_FIELD_MODEL, empty_left.model,
|
||||||
|
sizeof(empty_left.model), "ILCE-7M4");
|
||||||
|
set_text(empty_right, LARDON3D_ACQUISITION_FIELD_MODEL, empty_right.model,
|
||||||
|
sizeof(empty_right.model), "ILCE-7M4");
|
||||||
|
assert(compare(empty_left, empty_right).decision ==
|
||||||
|
LARDON3D_ACQUISITION_SAME_ACQUISITION_CANDIDATE);
|
||||||
|
|
||||||
|
std::strcpy(right.image_unique_id, "UID-2");
|
||||||
|
result = compare(left, right);
|
||||||
|
assert(result.decision == LARDON3D_ACQUISITION_DIFFERENT);
|
||||||
|
assert(result.contradictions ==
|
||||||
|
LARDON3D_ACQUISITION_CONTRADICTION_IMAGE_UNIQUE_ID);
|
||||||
|
|
||||||
|
auto serial_left = metadata();
|
||||||
|
auto serial_right = metadata();
|
||||||
|
set_text(serial_left, LARDON3D_ACQUISITION_FIELD_BODY_SERIAL, serial_left.body_serial,
|
||||||
|
sizeof(serial_left.body_serial), "BODY-A");
|
||||||
|
set_text(serial_right, LARDON3D_ACQUISITION_FIELD_BODY_SERIAL, serial_right.body_serial,
|
||||||
|
sizeof(serial_right.body_serial), "BODY-B");
|
||||||
|
assert(compare(serial_left, serial_right).decision == LARDON3D_ACQUISITION_DIFFERENT);
|
||||||
|
serial_right = metadata();
|
||||||
|
assert(compare(serial_left, serial_right).decision == LARDON3D_ACQUISITION_INSUFFICIENT);
|
||||||
|
|
||||||
|
auto other_device = metadata();
|
||||||
|
set_text(other_device, LARDON3D_ACQUISITION_FIELD_BODY_SERIAL, other_device.body_serial,
|
||||||
|
sizeof(other_device.body_serial), "BODY-B");
|
||||||
|
set_text(other_device, LARDON3D_ACQUISITION_FIELD_DATETIME_ORIGINAL,
|
||||||
|
other_device.datetime_original, sizeof(other_device.datetime_original),
|
||||||
|
"2026:08:27 12:34:56");
|
||||||
|
set_text(serial_left, LARDON3D_ACQUISITION_FIELD_DATETIME_ORIGINAL,
|
||||||
|
serial_left.datetime_original, sizeof(serial_left.datetime_original),
|
||||||
|
"2026:08:27 12:34:56");
|
||||||
|
assert(compare(serial_left, other_device, false, true).decision ==
|
||||||
|
LARDON3D_ACQUISITION_DIFFERENT);
|
||||||
|
assert(compare(left, left, true, true).decision == LARDON3D_ACQUISITION_INSUFFICIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_selector() {
|
||||||
|
auto source = metadata();
|
||||||
|
auto candidate_b = metadata();
|
||||||
|
auto candidate_c = metadata();
|
||||||
|
set_text(source, LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID, source.image_unique_id,
|
||||||
|
sizeof(source.image_unique_id), "SHARED");
|
||||||
|
set_text(candidate_b, LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID,
|
||||||
|
candidate_b.image_unique_id, sizeof(candidate_b.image_unique_id), "SHARED");
|
||||||
|
set_text(candidate_c, LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID,
|
||||||
|
candidate_c.image_unique_id, sizeof(candidate_c.image_unique_id), "SHARED");
|
||||||
|
Lardon3DAcquisitionCandidate candidates[2] = {
|
||||||
|
{20u, &candidate_b, 0u, 0u, {}},
|
||||||
|
{30u, &candidate_c, 0u, 0u, {}},
|
||||||
|
};
|
||||||
|
Lardon3DAcquisitionSelection selection = {};
|
||||||
|
assert(lardon3d_acquisition_select_candidate(&source, candidates, 2u, &selection) ==
|
||||||
|
LARDON3D_ACQUISITION_OK);
|
||||||
|
assert(selection.decision == LARDON3D_ACQUISITION_AMBIGUOUS);
|
||||||
|
assert(selection.top_candidate_count == 2u);
|
||||||
|
assert(selection.selected_candidate_id == 0u);
|
||||||
|
const auto temporary = candidates[0];
|
||||||
|
candidates[0] = candidates[1];
|
||||||
|
candidates[1] = temporary;
|
||||||
|
assert(lardon3d_acquisition_select_candidate(&source, candidates, 2u, &selection) ==
|
||||||
|
LARDON3D_ACQUISITION_OK);
|
||||||
|
assert(selection.decision == LARDON3D_ACQUISITION_AMBIGUOUS);
|
||||||
|
assert(selection.top_candidate_count == 2u);
|
||||||
|
assert(selection.selected_candidate_id == 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_non_terminated_public_text_is_unavailable() {
|
||||||
|
auto left = metadata();
|
||||||
|
auto right = metadata();
|
||||||
|
std::memset(left.image_unique_id, 'X', sizeof(left.image_unique_id));
|
||||||
|
std::memset(right.image_unique_id, 'X', sizeof(right.image_unique_id));
|
||||||
|
left.present_fields |= LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID;
|
||||||
|
right.present_fields |= LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID;
|
||||||
|
|
||||||
|
auto result = compare(left, right);
|
||||||
|
assert(result.decision == LARDON3D_ACQUISITION_INSUFFICIENT);
|
||||||
|
assert(result.evidence == 0u);
|
||||||
|
assert(result.contradictions == 0u);
|
||||||
|
|
||||||
|
std::memcpy(right.image_unique_id, "VALID", 6u);
|
||||||
|
result = compare(left, right);
|
||||||
|
assert(result.decision == LARDON3D_ACQUISITION_INSUFFICIENT);
|
||||||
|
assert(result.evidence == 0u);
|
||||||
|
assert(result.contradictions == 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
void add_ascii(ExifData *data, ExifIfd ifd, ExifTag tag, const char *value) {
|
||||||
|
ExifEntry *entry = exif_entry_new();
|
||||||
|
assert(entry != nullptr);
|
||||||
|
entry->tag = tag;
|
||||||
|
entry->format = EXIF_FORMAT_ASCII;
|
||||||
|
const size_t value_size = std::strlen(value) + 2u;
|
||||||
|
assert(value_size <= UINT32_MAX);
|
||||||
|
entry->components = static_cast<unsigned long>(value_size);
|
||||||
|
entry->size = static_cast<unsigned int>(value_size);
|
||||||
|
entry->data = static_cast<unsigned char *>(std::malloc(entry->size));
|
||||||
|
assert(entry->data != nullptr);
|
||||||
|
std::memcpy(entry->data, value, std::strlen(value));
|
||||||
|
entry->data[entry->size - 2u] = ' ';
|
||||||
|
entry->data[entry->size - 1u] = '\0';
|
||||||
|
exif_content_add_entry(data->ifd[ifd], entry);
|
||||||
|
exif_entry_unref(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string temporary_path(const char *suffix) {
|
||||||
|
char path[] = "/tmp/lardon3d-acquisition-XXXXXX";
|
||||||
|
const int descriptor = mkstemp(path);
|
||||||
|
assert(descriptor >= 0);
|
||||||
|
close(descriptor);
|
||||||
|
std::string result(path);
|
||||||
|
result += suffix;
|
||||||
|
assert(std::rename(path, result.c_str()) == 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_exif_jpeg(const std::string &path) {
|
||||||
|
ExifData *data = exif_data_new();
|
||||||
|
assert(data != nullptr);
|
||||||
|
exif_data_set_byte_order(data, EXIF_BYTE_ORDER_INTEL);
|
||||||
|
add_ascii(data, EXIF_IFD_0, EXIF_TAG_MAKE, "Sony");
|
||||||
|
add_ascii(data, EXIF_IFD_0, EXIF_TAG_MODEL, "ILCE-7M4");
|
||||||
|
add_ascii(data, EXIF_IFD_EXIF, EXIF_TAG_DATE_TIME_ORIGINAL, "2026:08:27 12:34:56");
|
||||||
|
add_ascii(data, EXIF_IFD_EXIF, EXIF_TAG_SUB_SEC_TIME_ORIGINAL, "123");
|
||||||
|
add_ascii(data, EXIF_IFD_EXIF, EXIF_TAG_OFFSET_TIME_ORIGINAL, "+02:00");
|
||||||
|
add_ascii(data, EXIF_IFD_EXIF, EXIF_TAG_BODY_SERIAL_NUMBER, "BODY-42");
|
||||||
|
add_ascii(data, EXIF_IFD_EXIF, EXIF_TAG_IMAGE_UNIQUE_ID, "IMAGE-99");
|
||||||
|
unsigned char *encoded = nullptr;
|
||||||
|
unsigned int encoded_size = 0u;
|
||||||
|
exif_data_save_data(data, &encoded, &encoded_size);
|
||||||
|
assert(encoded != nullptr && encoded_size > 0u && encoded_size <= UINT16_MAX - 2u);
|
||||||
|
FILE *file = std::fopen(path.c_str(), "wb");
|
||||||
|
assert(file != nullptr);
|
||||||
|
const unsigned char soi[] = {0xffu, 0xd8u, 0xffu, 0xe1u};
|
||||||
|
const unsigned int segment_size = encoded_size + 2u;
|
||||||
|
const unsigned char length[] = {static_cast<unsigned char>(segment_size >> 8u),
|
||||||
|
static_cast<unsigned char>(segment_size & 0xffu)};
|
||||||
|
const unsigned char eoi[] = {0xffu, 0xd9u};
|
||||||
|
assert(std::fwrite(soi, 1u, sizeof(soi), file) == sizeof(soi));
|
||||||
|
assert(std::fwrite(length, 1u, sizeof(length), file) == sizeof(length));
|
||||||
|
assert(std::fwrite(encoded, 1u, encoded_size, file) == encoded_size);
|
||||||
|
assert(std::fwrite(eoi, 1u, sizeof(eoi), file) == sizeof(eoi));
|
||||||
|
assert(std::fclose(file) == 0);
|
||||||
|
std::free(encoded);
|
||||||
|
exif_data_unref(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_extraction() {
|
||||||
|
const std::string jpeg = temporary_path(".jpg");
|
||||||
|
write_exif_jpeg(jpeg);
|
||||||
|
Lardon3DAcquisitionMetadata value = {};
|
||||||
|
assert(lardon3d_acquisition_extract_metadata(jpeg.c_str(), &value) ==
|
||||||
|
LARDON3D_ACQUISITION_OK);
|
||||||
|
assert(value.source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG);
|
||||||
|
assert(std::strcmp(value.make, "Sony") == 0);
|
||||||
|
assert(std::strcmp(value.model, "ILCE-7M4") == 0);
|
||||||
|
assert(std::strcmp(value.datetime_original, "2026:08:27 12:34:56") == 0);
|
||||||
|
assert(std::strcmp(value.subsec_original, "123") == 0);
|
||||||
|
assert(std::strcmp(value.offset_original, "+02:00") == 0);
|
||||||
|
assert(std::strcmp(value.body_serial, "BODY-42") == 0);
|
||||||
|
assert(std::strcmp(value.image_unique_id, "IMAGE-99") == 0);
|
||||||
|
assert(unlink(jpeg.c_str()) == 0);
|
||||||
|
|
||||||
|
const std::string malformed = temporary_path(".jpg");
|
||||||
|
FILE *file = std::fopen(malformed.c_str(), "wb");
|
||||||
|
assert(file != nullptr);
|
||||||
|
const unsigned char bad[] = {0xffu, 0xd8u, 0x00u};
|
||||||
|
assert(std::fwrite(bad, 1u, sizeof(bad), file) == sizeof(bad));
|
||||||
|
assert(std::fclose(file) == 0);
|
||||||
|
assert(lardon3d_acquisition_extract_metadata(malformed.c_str(), &value) ==
|
||||||
|
LARDON3D_ACQUISITION_CORRUPT_SOURCE);
|
||||||
|
assert(unlink(malformed.c_str()) == 0);
|
||||||
|
|
||||||
|
const std::string unsupported = temporary_path(".bin");
|
||||||
|
file = std::fopen(unsupported.c_str(), "wb");
|
||||||
|
assert(file != nullptr);
|
||||||
|
assert(std::fwrite("not an image", 1u, 12u, file) == 12u);
|
||||||
|
assert(std::fclose(file) == 0);
|
||||||
|
assert(lardon3d_acquisition_extract_metadata(unsupported.c_str(), &value) ==
|
||||||
|
LARDON3D_ACQUISITION_UNSUPPORTED_FORMAT);
|
||||||
|
assert(unlink(unsupported.c_str()) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_raw_image_unique_id_mapping() {
|
||||||
|
libraw_data_t raw = {};
|
||||||
|
Lardon3DAcquisitionMetadata value = {};
|
||||||
|
std::memcpy(raw.color.ImageUniqueID, "RAW-UID-42 ", 11u);
|
||||||
|
|
||||||
|
lardon3d_acquisition_test_extract_raw_image_unique_id(&raw, &value);
|
||||||
|
|
||||||
|
assert(std::strcmp(value.image_unique_id, "RAW-UID-42") == 0);
|
||||||
|
assert((value.present_fields & LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID) != 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_non_terminated_raw_fixed_text_mapping() {
|
||||||
|
libraw_data_t raw = {};
|
||||||
|
Lardon3DAcquisitionMetadata value = {};
|
||||||
|
std::memset(raw.idata.make, 'M', sizeof(raw.idata.make));
|
||||||
|
std::memset(raw.idata.model, 'D', sizeof(raw.idata.model));
|
||||||
|
std::memset(raw.shootinginfo.BodySerial, 'S', sizeof(raw.shootinginfo.BodySerial));
|
||||||
|
|
||||||
|
lardon3d_acquisition_test_extract_raw_fixed_text(&raw, &value);
|
||||||
|
|
||||||
|
assert(std::strlen(value.make) == sizeof(raw.idata.make));
|
||||||
|
assert(std::strlen(value.model) == sizeof(raw.idata.model));
|
||||||
|
assert(std::strlen(value.body_serial) == sizeof(raw.shootinginfo.BodySerial));
|
||||||
|
assert((value.present_fields & LARDON3D_ACQUISITION_FIELD_MAKE) != 0u);
|
||||||
|
assert((value.present_fields & LARDON3D_ACQUISITION_FIELD_MODEL) != 0u);
|
||||||
|
assert((value.present_fields & LARDON3D_ACQUISITION_FIELD_BODY_SERIAL) != 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
test_pairing_policy();
|
||||||
|
test_selector();
|
||||||
|
test_non_terminated_public_text_is_unavailable();
|
||||||
|
test_extraction();
|
||||||
|
test_raw_image_unique_id_mapping();
|
||||||
|
test_non_terminated_raw_fixed_text_mapping();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue