labfy-investigation/src/views/create_person_dialog.c

1957 lines
88 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/******************************************************************************
* @file create_person_dialog.c
* @brief Formulaire GTK de création d'une personne observée.
******************************************************************************/
#include "views/create_person_dialog.h"
#include "core/evidence_staging.h"
#include "core/evidence_staging_task.h"
#include "core/person_confirmation_summary.h"
#include "core/person_dialog_lifecycle.h"
#include "core/identity_ocr_preprocessor.h"
#include "core/identity_ocr_workflow.h"
#include "core/identity_field_extractor.h"
#include "core/ocr_analysis.h"
#include "models/evidence_selection_model.h"
#include "models/person_evidence_selection.h"
#include "models/identity_ocr.h"
#include "widgets/evidence_preview_widget.h"
#include "widgets/ocr_provenance_overlay.h"
struct CreatePersonDialogResult
{
PersonEntityInput input;
char *designation;
char *declared_name;
char *pseudonym;
char *status;
char *notes;
char *evidence_identifier;
GPtrArray *role_assignments;
PersonEvidenceSelection *evidence_selection;
EvidenceStaging *staging;
GPtrArray *ocr_runs;
};
typedef struct
{
GtkWindow *window;
GtkEntry *designation;
GtkEntry *name;
GtkEntry *pseudonym;
GtkDropDown *status;
GtkSpinButton *confidence;
GtkDropDown *evidence;
GtkDropDown *retained;
GtkDropDown *retained_type;
GtkDropDown *type_filter;
GtkEntry *search;
GtkTextView *notes;
GtkLabel *error;
EvidencePreviewWidget *preview;
GtkLabel *progress;
GtkLabel *summary;
GtkStack *stack;
GtkButton *previous;
GtkButton *next;
GtkButton *create;
GtkButton *add_existing;
GtkButton *remove_retained;
GtkButton *import_files;
GtkButton *ocr_start;
GtkButton *ocr_cancel;
GtkDropDown *ocr_document_type;
GtkDropDown *ocr_document_side;
GtkDropDown *ocr_languages;
GtkDropDown *ocr_profile;
GtkSpinButton *ocr_page;
GtkTextView *ocr_text;
GtkTextView *ocr_corrected_text;
GtkStack *ocr_transcription_stack;
GtkBox *ocr_fields;
GtkDropDown *ocr_manual_code;
GtkEntry *ocr_manual_value;
GtkTextView *ocr_factual_notes;
GtkLabel *ocr_status;
OcrProvenanceOverlay *ocr_overlay;
GPtrArray *ocr_language_codes;
GtkStringList *evidence_labels;
GtkStringList *retained_labels;
GtkStringList *type_filter_labels;
GtkCheckButton *roles[9];
guint step;
GPtrArray *evidence_identifiers;
GPtrArray *visible_records;
GPtrArray *type_codes;
EvidenceSelectionModel *selection_model;
PersonEvidenceSelection *person_evidence_selection;
EvidenceStaging *staging;
char *investigation_root_path;
TaskManager *task_manager;
BackgroundTask *staging_task;
GTask *ocr_task;
GCancellable *ocr_cancellable;
GPtrArray *ocr_runs;
char *tesseract_path;
char *tesseract_version;
guint64 ocr_generation;
PersonDialogLifecycle *lifecycle;
CreatePersonDialogCallback callback;
CreatePersonDialogSessionCheck session_check;
gpointer user_data;
GDestroyNotify user_data_destroy;
gulong evidence_selected_handler;
gulong retained_selected_handler;
gboolean updating_evidence;
gboolean updating_retained;
} CreatePersonDialogState;
typedef struct {
char *root, *identifier, *relative, *sha256, *mime;
char *document_type, *document_side, *languages, *profile;
char *executable, *version;
guint page;
guint profile_index;
guint64 generation;
GWeakRef window;
} IdentityOcrJob;
typedef struct {
char *executable;
GWeakRef window;
} IdentityLanguageJob;
typedef struct {
GWeakRef window;
guint64 generation;
} CreatePersonStagingContext;
static const char *const status_codes[] = {"unknown", "suspected", "confirmed"};
static void create_person_dialog_clear_preview(CreatePersonDialogState *state);
static void create_person_dialog_select_record(CreatePersonDialogState *state,
const EvidenceRecord *record, const char *business_type);
static void create_person_dialog_on_retained_changed(
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data);
static void create_person_dialog_render_ocr_fields(
CreatePersonDialogState *state, IdentityOcrRun *run);
static gboolean create_person_dialog_set_initial_ocr_paned_position(
gpointer data)
{
GtkPaned *paned = GTK_PANED(data);
int width;
int position;
if (g_object_get_data(
G_OBJECT(paned), "create-person-ocr-position-set") != NULL)
return G_SOURCE_REMOVE;
width = gtk_widget_get_width(GTK_WIDGET(paned));
if (width <= 0) return G_SOURCE_CONTINUE;
position = MAX(500, (width * 2) / 3);
position = MIN(position, MAX(0, width - 180));
gtk_paned_set_position(paned, position);
g_object_set_data(G_OBJECT(paned),
"create-person-ocr-position-set", GINT_TO_POINTER(1));
return G_SOURCE_REMOVE;
}
static void identity_language_job_free(gpointer data)
{
IdentityLanguageJob *job = data;
if (job == NULL) return;
g_free(job->executable);
g_weak_ref_clear(&job->window);
g_free(job);
}
static void identity_languages_worker(GTask *task, gpointer source,
gpointer task_data, GCancellable *cancellable)
{
IdentityLanguageJob *job = task_data;
GError *error = NULL;
char *raw = ocr_analysis_list_languages(job->executable, cancellable,
&error);
GPtrArray *parsed = raw != NULL
? ocr_analysis_parse_languages(raw) : NULL;
GPtrArray *choices = parsed != NULL
? ocr_analysis_build_language_choices(parsed) : NULL;
g_free(raw);
g_clear_pointer(&parsed, g_ptr_array_unref);
(void) source;
if (choices != NULL) g_task_return_pointer(task, choices,
(GDestroyNotify) g_ptr_array_unref);
else g_task_return_error(task, error);
}
static const char *identity_language_label(const char *code)
{
if (g_str_equal(code, "fra")) return "Français (fra)";
if (g_str_equal(code, "eng")) return "Anglais (eng)";
if (g_str_equal(code, "fra+eng")) return "Français + anglais (fra+eng)";
return code;
}
static void identity_languages_completed(GObject *source,
GAsyncResult *result, gpointer data)
{
IdentityLanguageJob *job = data;
GtkWindow *window = g_weak_ref_get(&job->window);
CreatePersonDialogState *state = window != NULL
? g_object_get_data(G_OBJECT(window), "person-dialog-state") : NULL;
GError *error = NULL;
GPtrArray *choices = g_task_propagate_pointer(G_TASK(result), &error);
(void) source;
if (state != NULL) {
g_clear_pointer(&state->ocr_language_codes, g_ptr_array_unref);
state->ocr_language_codes = choices;
choices = NULL;
GtkStringList *labels = gtk_string_list_new(NULL);
for (guint i = 0; state->ocr_language_codes != NULL &&
i < state->ocr_language_codes->len; i++) {
const char *code = g_ptr_array_index(state->ocr_language_codes, i);
gtk_string_list_append(labels, identity_language_label(code));
}
gtk_drop_down_set_model(state->ocr_languages, G_LIST_MODEL(labels));
gtk_drop_down_set_selected(state->ocr_languages,
state->ocr_language_codes != NULL &&
state->ocr_language_codes->len > 0 ? 0 :
GTK_INVALID_LIST_POSITION);
gtk_widget_set_sensitive(GTK_WIDGET(state->ocr_start),
state->ocr_language_codes != NULL &&
state->ocr_language_codes->len > 0);
if (state->ocr_language_codes == NULL ||
state->ocr_language_codes->len == 0)
gtk_label_set_text(state->ocr_status,
"Aucune langue Tesseract installée.");
g_object_unref(labels);
}
g_clear_pointer(&choices, g_ptr_array_unref);
g_clear_error(&error);
g_clear_object(&window);
}
/** @brief Copie et nettoie un texte facultatif. */
static char *create_person_dialog_copy(const char *text)
{
char *copy = text != NULL ? g_strdup(text) : NULL;
if (copy == NULL) return NULL;
g_strstrip(copy);
if (copy[0] == '\0') { g_free(copy); return NULL; }
return copy;
}
/** @brief Extrait les notes de la zone de texte. */
static char *create_person_dialog_get_notes(CreatePersonDialogState *state)
{
GtkTextBuffer *buffer = gtk_text_view_get_buffer(state->notes);
GtkTextIter start, end;
gtk_text_buffer_get_bounds(buffer, &start, &end);
return gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
}
static void identity_ocr_job_free(gpointer data)
{
IdentityOcrJob *job = data;
if (job == NULL) return;
g_free(job->root); g_free(job->identifier); g_free(job->relative);
g_free(job->sha256); g_free(job->mime); g_free(job->document_type);
g_free(job->document_side); g_free(job->languages); g_free(job->profile);
g_free(job->executable); g_free(job->version);
g_weak_ref_clear(&job->window); g_free(job);
}
static void create_person_dialog_ocr_worker(GTask *task, gpointer source,
gpointer task_data, GCancellable *cancellable)
{
IdentityOcrJob *job = task_data;
IdentityOcrWorkflowRequest request = {
.root_path = job->root,
.evidence_identifier = job->identifier,
.relative_path = job->relative,
.expected_sha256 = job->sha256,
.mime_type = job->mime,
.document_type = job->document_type,
.document_side = job->document_side,
.languages = job->languages,
.preprocessing_profile = job->profile,
.tesseract_executable = job->executable,
.tesseract_version = job->version,
.page_number = job->page,
.profile = (IdentityOcrPreprocessProfile) job->profile_index,
.generation = job->generation
};
GError *error = NULL;
(void) source;
IdentityOcrRun *run = identity_ocr_workflow_execute(
&request, cancellable, &error);
if (run != NULL)
g_task_return_pointer(task, run,
(GDestroyNotify) identity_ocr_run_free);
else
g_task_return_error(task, error != NULL ? error :
g_error_new_literal(G_IO_ERROR, G_IO_ERROR_FAILED,
"LOCR contrôlé a échoué."));
}
static void create_person_dialog_on_field_accept(GtkButton *button,
gpointer data)
{
IdentityFieldObservation *field = g_object_get_data(
G_OBJECT(button), "identity-field");
(void) data;
identity_field_observation_accept(field);
gtk_button_set_label(button, "Acceptée");
gtk_widget_set_sensitive(GTK_WIDGET(button), FALSE);
GtkEntry *entry = g_object_get_data(
G_OBJECT(button), "identity-entry");
if (entry != NULL)
gtk_editable_set_editable(GTK_EDITABLE(entry), FALSE);
}
static void create_person_dialog_on_field_reedit(GtkButton *button,
gpointer data)
{
GtkEntry *entry = g_object_get_data(
G_OBJECT(button), "identity-entry");
GtkButton *save = g_object_get_data(
G_OBJECT(button), "identity-save");
(void) data;
if (entry == NULL || save == NULL) return;
gtk_editable_set_editable(GTK_EDITABLE(entry), TRUE);
gtk_widget_set_sensitive(GTK_WIDGET(save), TRUE);
gtk_widget_grab_focus(GTK_WIDGET(entry));
gtk_editable_set_position(GTK_EDITABLE(entry), -1);
}
static void create_person_dialog_on_field_restore(GtkButton *button,
gpointer data)
{
IdentityFieldObservation *field = g_object_get_data(
G_OBJECT(button), "identity-field");
GtkEntry *entry = g_object_get_data(
G_OBJECT(button), "identity-entry");
GtkButton *save = g_object_get_data(
G_OBJECT(button), "identity-save");
(void) data;
if (!identity_field_observation_restore_raw(field) || entry == NULL)
return;
gtk_editable_set_text(GTK_EDITABLE(entry),
identity_field_observation_get_raw_value(field));
gtk_editable_set_editable(GTK_EDITABLE(entry), TRUE);
if (save != NULL) {
gtk_button_set_label(save, "Enregistrer la correction");
gtk_widget_set_sensitive(GTK_WIDGET(save), TRUE);
}
}
static void create_person_dialog_on_field_modify(GtkButton *button,
gpointer data)
{
IdentityFieldObservation *field = g_object_get_data(
G_OBJECT(button), "identity-field");
(void) data;
GtkEntry *entry = g_object_get_data(
G_OBJECT(button), "identity-entry");
const char *value = entry != NULL
? gtk_editable_get_text(GTK_EDITABLE(entry))
: identity_field_observation_get_raw_value(field);
if (!identity_field_observation_modify(field, value,
"Valeur revue individuellement dans lassistant.")) {
if (entry != NULL)
gtk_widget_add_css_class(GTK_WIDGET(entry), "error");
return;
}
if (entry != NULL) {
gtk_widget_remove_css_class(GTK_WIDGET(entry), "error");
gtk_editable_set_editable(GTK_EDITABLE(entry), FALSE);
}
gtk_button_set_label(button, "Modifiée");
gtk_widget_set_sensitive(GTK_WIDGET(button), FALSE);
}
static void create_person_dialog_on_field_reject(GtkButton *button,
gpointer data)
{
IdentityFieldObservation *field = g_object_get_data(
G_OBJECT(button), "identity-field");
(void) data;
identity_field_observation_reject(field);
gtk_button_set_label(button, "Rejetée");
}
static void create_person_dialog_on_field_show(GtkButton *button,
gpointer data)
{
CreatePersonDialogState *state = data;
IdentityFieldObservation *field = g_object_get_data(
G_OBJECT(button), "identity-field");
ocr_provenance_overlay_set_field(state->ocr_overlay, field,
state->ocr_generation);
}
static void create_person_dialog_on_add_manual_field(GtkButton *button,
gpointer data)
{
static const char *const codes[] = {
"document_type","issuing_country","issuing_authority",
"document_number","surname","birth_name","given_names",
"sex_as_printed","nationality","birth_date","birth_place",
"issue_date","expiry_date","address_as_printed",
"mrz_line_1","mrz_line_2","mrz_line_3"};
CreatePersonDialogState *state = data;
guint selected = gtk_drop_down_get_selected(state->ocr_manual_code);
const char *value =
gtk_editable_get_text(GTK_EDITABLE(state->ocr_manual_value));
(void) button;
if (state->ocr_runs == NULL || state->ocr_runs->len == 0 ||
selected >= G_N_ELEMENTS(codes) || value[0] == '\0') return;
IdentityOcrRun *run = g_ptr_array_index(
state->ocr_runs, state->ocr_runs->len - 1);
IdentityFieldObservation *field =
identity_field_observation_new_manual(
codes[selected], value,
identity_ocr_run_get_fields(run)->len);
if (field == NULL) return;
identity_ocr_run_add_field(run, field);
gtk_editable_set_text(GTK_EDITABLE(state->ocr_manual_value), "");
create_person_dialog_render_ocr_fields(state, run);
}
static void create_person_dialog_capture_factual_notes(
CreatePersonDialogState *state)
{
if (state == NULL || state->ocr_runs == NULL ||
state->ocr_runs->len == 0) return;
GtkTextBuffer *buffer =
gtk_text_view_get_buffer(state->ocr_factual_notes);
GtkTextIter start, end;
gtk_text_buffer_get_bounds(buffer, &start, &end);
char *notes = gtk_text_buffer_get_text(
buffer, &start, &end, FALSE);
IdentityOcrRun *run = g_ptr_array_index(
state->ocr_runs, state->ocr_runs->len - 1);
identity_ocr_run_set_factual_notes(run,
notes[0] != '\0' ? notes : NULL);
g_free(notes);
}
static char *create_person_dialog_text_view_contents(GtkTextView *view)
{
GtkTextIter start;
GtkTextIter end;
GtkTextBuffer *buffer = gtk_text_view_get_buffer(view);
gtk_text_buffer_get_bounds(buffer, &start, &end);
return gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
}
static void create_person_dialog_capture_corrected_transcription(
CreatePersonDialogState *state)
{
if (state == NULL || state->ocr_corrected_text == NULL ||
state->ocr_runs == NULL || state->ocr_runs->len == 0) return;
IdentityOcrRun *run = g_ptr_array_index(
state->ocr_runs, state->ocr_runs->len - 1);
char *text = create_person_dialog_text_view_contents(
state->ocr_corrected_text);
const char *raw = identity_ocr_run_get_raw_text(run);
if (g_strcmp0(text, raw) == 0)
identity_ocr_run_reset_corrected_transcription(run);
else {
GDateTime *now = g_date_time_new_now_utc();
char *timestamp = g_date_time_format(
now, "%Y-%m-%dT%H:%M:%SZ");
identity_ocr_run_set_corrected_transcription(
run, text, timestamp);
g_free(timestamp);
g_date_time_unref(now);
}
g_free(text);
}
static void create_person_dialog_on_reset_transcription(
GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data;
(void) button;
if (state->ocr_runs == NULL || state->ocr_runs->len == 0) return;
IdentityOcrRun *run = g_ptr_array_index(
state->ocr_runs, state->ocr_runs->len - 1);
identity_ocr_run_reset_corrected_transcription(run);
gtk_text_buffer_set_text(gtk_text_view_get_buffer(
state->ocr_corrected_text),
identity_ocr_run_get_raw_text(run), -1);
gtk_text_view_set_editable(state->ocr_corrected_text, TRUE);
}
static void create_person_dialog_render_ocr_fields(
CreatePersonDialogState *state, IdentityOcrRun *run)
{
GtkWidget *child;
while ((child = gtk_widget_get_first_child(
GTK_WIDGET(state->ocr_fields))) != NULL)
gtk_box_remove(state->ocr_fields, child);
const GPtrArray *fields = identity_ocr_run_get_fields(run);
for (guint i = 0; fields != NULL && i < fields->len; i++) {
IdentityFieldObservation *field = g_ptr_array_index(
(GPtrArray *) fields, i);
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
const IdentitySourceBox *box =
identity_field_observation_get_box(field);
char *source = box != NULL && box->available
? g_strdup_printf("page %d, zone %d,%d %dx%d, confiance %.1f",
box->page, box->x, box->y, box->width, box->height,
identity_field_observation_get_confidence(field))
: g_strdup("Zone source indisponible");
char *label = g_strdup_printf(
"%s\nValeur OCR brute : %s\nOrigine : %s — %s",
identity_field_observation_get_code(field),
identity_field_observation_get_raw_value(field) != NULL
? identity_field_observation_get_raw_value(field)
: "absente",
identity_field_observation_get_origin(field), source);
GtkWidget *field_label = gtk_label_new(label);
gtk_label_set_wrap(GTK_LABEL(field_label), TRUE);
gtk_label_set_xalign(GTK_LABEL(field_label), 0.0f);
gtk_box_append(GTK_BOX(row), field_label);
GtkWidget *entry = gtk_entry_new();
gtk_widget_set_hexpand(entry, TRUE);
gtk_widget_set_tooltip_text(entry,
"Modifiez la valeur puis choisissez « Modifier ».");
gtk_editable_set_text(GTK_EDITABLE(entry),
identity_field_observation_get_corrected_value(field) != NULL
? identity_field_observation_get_corrected_value(field)
: identity_field_observation_get_raw_value(field));
gtk_box_append(GTK_BOX(row), entry);
GtkWidget *actions =
gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget *accept = gtk_button_new_with_label("Accepter");
GtkWidget *modify = gtk_button_new_with_label("Modifier");
GtkWidget *reedit = gtk_button_new_with_label("Modifier à nouveau");
GtkWidget *restore =
gtk_button_new_with_label("Revenir à la valeur OCR");
GtkWidget *reject = gtk_button_new_with_label("Rejeter");
GtkWidget *show = gtk_button_new_with_label("Voir la zone");
g_object_set_data(G_OBJECT(row), "identity-field-code",
(gpointer) identity_field_observation_get_code(field));
g_object_set_data(G_OBJECT(row), "identity-field-order",
GUINT_TO_POINTER(identity_field_observation_get_order(field) + 1));
g_object_set_data(G_OBJECT(row), "identity-value-entry", entry);
g_object_set_data(G_OBJECT(row), "identity-accept-button", accept);
g_object_set_data(G_OBJECT(row), "identity-modify-button", modify);
g_object_set_data(G_OBJECT(row), "identity-reedit-button", reedit);
g_object_set_data(G_OBJECT(row), "identity-restore-button", restore);
g_object_set_data(G_OBJECT(row), "identity-reject-button", reject);
g_object_set_data(G_OBJECT(accept), "identity-field", field);
g_object_set_data(G_OBJECT(accept), "identity-entry", entry);
g_object_set_data(G_OBJECT(modify), "identity-field", field);
g_object_set_data(G_OBJECT(modify), "identity-entry", entry);
g_object_set_data(G_OBJECT(reject), "identity-field", field);
g_object_set_data(G_OBJECT(reedit), "identity-entry", entry);
g_object_set_data(G_OBJECT(reedit), "identity-save", modify);
g_object_set_data(G_OBJECT(restore), "identity-field", field);
g_object_set_data(G_OBJECT(restore), "identity-entry", entry);
g_object_set_data(G_OBJECT(restore), "identity-save", modify);
g_object_set_data(G_OBJECT(show), "identity-field", field);
gtk_box_append(GTK_BOX(actions), show);
gtk_box_append(GTK_BOX(actions), accept);
gtk_box_append(GTK_BOX(actions), modify);
gtk_box_append(GTK_BOX(actions), reedit);
if (identity_field_observation_get_raw_value(field) != NULL)
gtk_box_append(GTK_BOX(actions), restore);
gtk_box_append(GTK_BOX(actions), reject);
gtk_box_append(GTK_BOX(row), actions);
g_signal_connect(accept, "clicked",
G_CALLBACK(create_person_dialog_on_field_accept), state);
g_signal_connect(modify, "clicked",
G_CALLBACK(create_person_dialog_on_field_modify), state);
g_signal_connect(reject, "clicked",
G_CALLBACK(create_person_dialog_on_field_reject), state);
g_signal_connect(reedit, "clicked",
G_CALLBACK(create_person_dialog_on_field_reedit), state);
g_signal_connect(restore, "clicked",
G_CALLBACK(create_person_dialog_on_field_restore), state);
g_signal_connect(show, "clicked",
G_CALLBACK(create_person_dialog_on_field_show), state);
gtk_box_append(state->ocr_fields, row);
g_free(source);
g_free(label);
}
}
static void create_person_dialog_ocr_completed(GObject *source,
GAsyncResult *result, gpointer data)
{
IdentityOcrJob *job = data;
GtkWindow *window = g_weak_ref_get(&job->window);
CreatePersonDialogState *state = window != NULL
? g_object_get_data(G_OBJECT(window), "person-dialog-state") : NULL;
GError *error = NULL;
IdentityOcrRun *run = g_task_propagate_pointer(G_TASK(result), &error);
(void) source;
if (state != NULL && state->ocr_generation == job->generation &&
(state->session_check == NULL ||
state->session_check(state->user_data))) {
if (run != NULL) {
gtk_text_buffer_set_text(gtk_text_view_get_buffer(state->ocr_text),
identity_ocr_run_get_raw_text(run), -1);
gtk_text_buffer_set_text(gtk_text_view_get_buffer(
state->ocr_corrected_text),
identity_ocr_run_get_raw_text(run), -1);
gtk_text_view_set_editable(state->ocr_corrected_text, TRUE);
gtk_stack_set_visible_child_name(
state->ocr_transcription_stack, "corrected");
create_person_dialog_render_ocr_fields(state, run);
ocr_provenance_overlay_set_image(state->ocr_overlay,
identity_ocr_run_get_preview(run), state->ocr_generation);
const GPtrArray *fields = identity_ocr_run_get_fields(run);
if (fields != NULL && fields->len > 0)
ocr_provenance_overlay_set_field(state->ocr_overlay,
g_ptr_array_index((GPtrArray *) fields, 0),
state->ocr_generation);
g_ptr_array_add(state->ocr_runs, run);
run = NULL;
gtk_label_set_text(state->ocr_status,
"OCR terminé. Chaque proposition reste À vérifier.");
} else
gtk_label_set_text(state->ocr_status,
error != NULL ? error->message : "OCR annulé.");
g_clear_object(&state->ocr_task);
g_clear_object(&state->ocr_cancellable);
}
identity_ocr_run_free(run);
g_clear_error(&error);
g_clear_object(&window);
}
static void create_person_dialog_on_ocr_cancel(GtkButton *button,
gpointer data)
{
CreatePersonDialogState *state = data;
(void) button;
state->ocr_generation++;
if (state->ocr_cancellable != NULL)
g_cancellable_cancel(state->ocr_cancellable);
gtk_label_set_text(state->ocr_status, "Annulation de lOCR…");
ocr_provenance_overlay_clear(state->ocr_overlay, state->ocr_generation);
}
static void create_person_dialog_on_ocr_start(GtkButton *button,
gpointer data)
{
static const char *const types[] = {"identity_card","passport",
"driving_licence","residence_permit","other"};
static const char *const sides[] = {"front","back","identity_page",
"other_page"};
CreatePersonDialogState *state = data;
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get_active(state->person_evidence_selection);
IdentityOcrJob *job;
const EvidenceRecord *record;
(void) button;
if (item == NULL || state->tesseract_path == NULL) {
gtk_label_set_text(state->ocr_status,
"Tesseract est absent ou aucune preuve compatible nest retenue.");
return;
}
job = g_new0(IdentityOcrJob, 1);
job->root = g_strdup(state->investigation_root_path);
job->identifier = g_strdup(
person_evidence_selection_item_get_identifier(item));
job->sha256 = g_strdup(person_evidence_selection_item_get_sha256(item));
job->mime = g_strdup(person_evidence_selection_item_get_mime_type(item));
if (person_evidence_selection_item_get_origin(item) ==
PERSON_EVIDENCE_ORIGIN_EXISTING) {
record = person_evidence_selection_item_get_record(item);
job->relative = g_strdup(evidence_record_get_relative_path(record));
} else {
g_free(job->root);
job->root = g_path_get_dirname(
person_evidence_selection_item_get_staging_path(item));
job->relative = g_path_get_basename(
person_evidence_selection_item_get_staging_path(item));
}
guint selected = gtk_drop_down_get_selected(state->ocr_document_type);
job->document_type = g_strdup(types[MIN(selected,
G_N_ELEMENTS(types)-1)]);
selected = gtk_drop_down_get_selected(state->ocr_document_side);
job->document_side = g_strdup(sides[MIN(selected,
G_N_ELEMENTS(sides)-1)]);
selected = gtk_drop_down_get_selected(state->ocr_languages);
if (selected == GTK_INVALID_LIST_POSITION ||
state->ocr_language_codes == NULL ||
selected >= state->ocr_language_codes->len) {
identity_ocr_job_free(job);
gtk_label_set_text(state->ocr_status,
"Aucune langue Tesseract disponible.");
return;
}
job->languages = g_strdup(g_ptr_array_index(
state->ocr_language_codes, selected));
static const char *const profiles[] = {
"none", "orientation", "grayscale", "upscale"};
job->profile_index = gtk_drop_down_get_selected(state->ocr_profile);
job->profile = g_strdup(profiles[MIN(job->profile_index,
G_N_ELEMENTS(profiles)-1)]);
job->page = (guint) gtk_spin_button_get_value_as_int(state->ocr_page);
job->executable = g_strdup(state->tesseract_path);
job->version = g_strdup(state->tesseract_version);
job->generation = ++state->ocr_generation;
g_weak_ref_init(&job->window, G_OBJECT(state->window));
g_clear_object(&state->ocr_cancellable);
state->ocr_cancellable = g_cancellable_new();
state->ocr_task = g_task_new(NULL, state->ocr_cancellable,
create_person_dialog_ocr_completed, job);
g_task_set_task_data(state->ocr_task, job, identity_ocr_job_free);
gtk_label_set_text(state->ocr_status,
"OCR contrôlé en cours… Aucune donnée nest encore persistée.");
g_task_run_in_thread(state->ocr_task, create_person_dialog_ocr_worker);
}
/** @brief Libère l'état attaché à la fenêtre. */
static void create_person_dialog_state_free(gpointer data)
{
CreatePersonDialogState *state = data;
if (state == NULL) return;
g_clear_pointer(&state->preview, evidence_preview_widget_free);
if (state->staging_task != NULL) {
background_task_cancel(state->staging_task);
background_task_unref(state->staging_task);
}
state->ocr_generation++;
if (state->ocr_cancellable != NULL)
g_cancellable_cancel(state->ocr_cancellable);
g_clear_object(&state->ocr_task);
g_clear_object(&state->ocr_cancellable);
g_clear_pointer(&state->ocr_runs, g_ptr_array_unref);
g_clear_pointer(&state->ocr_language_codes, g_ptr_array_unref);
g_clear_pointer(&state->ocr_overlay, ocr_provenance_overlay_free);
person_dialog_lifecycle_cancel(state->lifecycle);
g_clear_pointer(&state->evidence_identifiers, g_ptr_array_unref);
g_clear_pointer(&state->visible_records, g_ptr_array_unref);
g_clear_pointer(&state->type_codes, g_ptr_array_unref);
g_clear_pointer(&state->selection_model, evidence_selection_model_free);
g_clear_pointer(&state->person_evidence_selection,
person_evidence_selection_free);
g_clear_pointer(&state->staging, evidence_staging_free);
g_clear_pointer(&state->lifecycle, person_dialog_lifecycle_free);
g_clear_object(&state->evidence_labels);
g_clear_object(&state->retained_labels);
g_clear_object(&state->type_filter_labels);
g_free(state->investigation_root_path);
g_free(state->tesseract_path);
g_free(state->tesseract_version);
if (state->user_data_destroy != NULL)
state->user_data_destroy(state->user_data);
g_free(state);
}
static void create_person_dialog_rebuild_retained(
CreatePersonDialogState *state)
{
GPtrArray *labels = g_ptr_array_new_with_free_func(g_free);
const PersonEvidenceSelectionItem *active =
person_evidence_selection_get_active(
state->person_evidence_selection);
const char *active_identifier = active != NULL
? person_evidence_selection_item_get_identifier(active) : NULL;
guint selected = GTK_INVALID_LIST_POSITION;
for (guint i = 0; i < person_evidence_selection_get_count(
state->person_evidence_selection); i++) {
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get(state->person_evidence_selection, i);
char *label = g_strdup_printf("%s — %s — %s",
person_evidence_selection_item_get_original_name(item),
person_evidence_selection_item_get_origin(item) ==
PERSON_EVIDENCE_ORIGIN_STAGED ? "nouvelle" : "existante",
person_evidence_selection_item_get_type_identifier(item));
g_ptr_array_add(labels, label);
if (g_strcmp0(active_identifier,
person_evidence_selection_item_get_identifier(item)) == 0)
selected = i;
}
g_ptr_array_add(labels, NULL);
state->updating_retained = TRUE;
if (state->retained_selected_handler != 0)
g_signal_handler_block(state->retained,
state->retained_selected_handler);
gtk_string_list_splice(state->retained_labels, 0,
g_list_model_get_n_items(G_LIST_MODEL(state->retained_labels)),
(const char * const *) labels->pdata);
gtk_drop_down_set_selected(state->retained, selected);
if (state->retained_selected_handler != 0)
g_signal_handler_unblock(state->retained,
state->retained_selected_handler);
state->updating_retained = FALSE;
g_ptr_array_unref(labels);
if (selected != GTK_INVALID_LIST_POSITION)
create_person_dialog_on_retained_changed(
state->retained, NULL, state);
}
static void create_person_dialog_on_add_existing(
GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data;
const EvidenceRecord *record =
evidence_selection_model_get_selected(state->selection_model);
GError *error = NULL;
(void) button;
if (record == NULL && state->visible_records->len == 1)
record = g_ptr_array_index(state->visible_records, 0);
if (record == NULL) {
gtk_label_set_text(state->error,
"Sélectionnez dabord une preuve existante.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
return;
}
if (!person_evidence_selection_add_existing(
state->person_evidence_selection, record, &error)) {
gtk_label_set_text(state->error, error->message);
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
} else {
gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
create_person_dialog_rebuild_retained(state);
}
g_clear_error(&error);
}
static void create_person_dialog_on_retained_changed(
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data)
{
CreatePersonDialogState *state = data;
guint selected = gtk_drop_down_get_selected(dropdown);
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get(state->person_evidence_selection,
selected);
(void) pspec;
if (state->updating_retained ||
selected == GTK_INVALID_LIST_POSITION || item == NULL) return;
if (state->ocr_start != NULL) {
const char *mime =
person_evidence_selection_item_get_mime_type(item);
const char *name =
person_evidence_selection_item_get_original_name(item);
char *lower = name != NULL ? g_ascii_strdown(name, -1) : NULL;
gboolean compatible =
g_strcmp0(mime, "image/png") == 0 ||
g_strcmp0(mime, "image/jpeg") == 0 ||
g_strcmp0(mime, "image/heic") == 0 ||
g_strcmp0(mime, "image/heif") == 0 ||
g_strcmp0(mime, "application/pdf") == 0 ||
(lower != NULL && (g_str_has_suffix(lower, ".png") ||
g_str_has_suffix(lower, ".jpg") ||
g_str_has_suffix(lower, ".jpeg") ||
g_str_has_suffix(lower, ".heic") ||
g_str_has_suffix(lower, ".heif") ||
g_str_has_suffix(lower, ".pdf")));
gtk_widget_set_sensitive(GTK_WIDGET(state->ocr_start),
compatible && state->tesseract_path != NULL);
g_free(lower);
}
person_evidence_selection_set_active(state->person_evidence_selection,
person_evidence_selection_item_get_identifier(item));
if (state->ocr_overlay != NULL)
ocr_provenance_overlay_clear(state->ocr_overlay,
++state->ocr_generation);
static const char *const types[] = {
"screenshot", "photo", "video", "document", "email",
"archive", "audio", "text", "other"};
for (guint i = 0; i < G_N_ELEMENTS(types); i++)
if (g_strcmp0(types[i],
person_evidence_selection_item_get_type_identifier(item)) == 0)
gtk_drop_down_set_selected(state->retained_type, i);
if (person_evidence_selection_item_get_origin(item) ==
PERSON_EVIDENCE_ORIGIN_EXISTING)
create_person_dialog_select_record(state,
person_evidence_selection_item_get_record(item),
person_evidence_selection_item_get_type_identifier(item));
else {
char *size = g_format_size(
person_evidence_selection_item_get_size_bytes(item));
char *text = g_strdup_printf(
"Nom : %s\nOrigine : nouvelle (staging)\nMIME : %s\n"
"Type : %s\nTaille : %s\nSHA-256 : %.12s…",
person_evidence_selection_item_get_original_name(item),
person_evidence_selection_item_get_mime_type(item),
person_evidence_selection_item_get_type_identifier(item), size,
person_evidence_selection_item_get_sha256(item));
create_person_dialog_clear_preview(state);
{
char *staging_root = g_path_get_dirname(
person_evidence_selection_item_get_staging_path(item));
char *staging_name = g_path_get_basename(
person_evidence_selection_item_get_staging_path(item));
EvidencePreviewRequest *request = evidence_preview_request_new(
staging_root,
person_evidence_selection_item_get_identifier(item),
staging_name,
person_evidence_selection_item_get_sha256(item),
person_evidence_selection_item_get_mime_type(item),
person_dialog_lifecycle_get_generation(state->lifecycle));
evidence_preview_widget_show(state->preview, request, text);
evidence_preview_request_free(request);
g_free(staging_root); g_free(staging_name);
}
g_free(text); g_free(size);
}
}
static void create_person_dialog_on_retained_type_changed(
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data)
{
static const char *const types[] = {
"screenshot", "photo", "video", "document", "email",
"archive", "audio", "text", "other"};
CreatePersonDialogState *state = data;
guint selected = gtk_drop_down_get_selected(dropdown);
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get_active(
state->person_evidence_selection);
(void) pspec;
if (item != NULL && selected < G_N_ELEMENTS(types)) {
person_evidence_selection_set_type(state->person_evidence_selection,
person_evidence_selection_item_get_identifier(item),
types[selected]);
create_person_dialog_rebuild_retained(state);
}
}
static void create_person_dialog_on_remove_retained(
GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data;
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get_active(
state->person_evidence_selection);
char *identifier = item != NULL ? g_strdup(
person_evidence_selection_item_get_identifier(item)) : NULL;
char *staging_path = item != NULL &&
person_evidence_selection_item_get_origin(item) ==
PERSON_EVIDENCE_ORIGIN_STAGED
? g_strdup(person_evidence_selection_item_get_staging_path(item))
: NULL;
(void) button;
if (identifier != NULL) {
state->ocr_generation++;
if (state->ocr_cancellable != NULL)
g_cancellable_cancel(state->ocr_cancellable);
for (guint i = state->ocr_runs != NULL ? state->ocr_runs->len : 0;
i > 0; i--) {
IdentityOcrRun *run = g_ptr_array_index(state->ocr_runs, i - 1);
if (g_strcmp0(identity_ocr_run_get_evidence_id(run),
identifier) == 0)
g_ptr_array_remove_index(state->ocr_runs, i - 1);
}
while (gtk_widget_get_first_child(GTK_WIDGET(state->ocr_fields)) !=
NULL)
gtk_box_remove(state->ocr_fields,
gtk_widget_get_first_child(GTK_WIDGET(state->ocr_fields)));
gtk_text_buffer_set_text(gtk_text_view_get_buffer(state->ocr_text),
"", -1);
gtk_label_set_text(state->ocr_status,
"Aucune session OCR active pour la preuve retirée.");
ocr_provenance_overlay_clear(state->ocr_overlay,
state->ocr_generation);
person_evidence_selection_remove(
state->person_evidence_selection, identifier);
if (staging_path != NULL)
evidence_staging_remove(state->staging, staging_path, NULL);
create_person_dialog_rebuild_retained(state);
create_person_dialog_clear_preview(state);
}
g_free(identifier); g_free(staging_path);
}
static void staging_context_free(gpointer data)
{
CreatePersonStagingContext *context = data;
if (context == NULL) return;
g_weak_ref_clear(&context->window);
g_free(context);
}
static void create_person_dialog_staging_completed(
BackgroundTask *task, gpointer data)
{
CreatePersonStagingContext *context = data;
GtkWindow *window = g_weak_ref_get(&context->window);
CreatePersonDialogState *state = window != NULL
? g_object_get_data(G_OBJECT(window), "person-dialog-state") : NULL;
GPtrArray *prepared = background_task_get_result(task);
if (state != NULL && state->session_check != NULL &&
!state->session_check(state->user_data)) {
person_dialog_lifecycle_cancel(state->lifecycle);
evidence_staging_cleanup(state->staging, NULL);
gtk_label_set_text(state->error,
"Préparation annulée : lenquête active a changé.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
} else if (state != NULL &&
person_dialog_lifecycle_accepts_preview(
state->lifecycle, context->generation) && prepared != NULL) {
for (guint i = 0; i < prepared->len; i++) {
EvidenceStagingResult *item = g_ptr_array_index(prepared, i);
const EvidenceRecord *existing =
evidence_selection_model_find_by_sha256(
state->selection_model, item->sha256);
GError *error = NULL;
if (existing != NULL) {
if (!person_evidence_selection_add_existing(
state->person_evidence_selection,
existing, &error) && error != NULL &&
error->code != 2)
gtk_label_set_text(state->error, error->message);
evidence_staging_remove(state->staging,
item->staging_path, NULL);
gtk_label_set_text(state->error,
"Doublon détecté : la preuve existante a été réutilisée.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
} else if (!person_evidence_selection_add_staged(
state->person_evidence_selection,
item->source_path, item->staging_path,
item->original_name, item->mime_type,
item->suggested_type, item->size_bytes, item->sha256,
NULL, item->prepared_at, &error)) {
evidence_staging_remove(state->staging,
item->staging_path, NULL);
gtk_label_set_text(state->error,
error != NULL ? error->message :
"Doublon refusé dans la sélection.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
}
g_clear_error(&error);
}
create_person_dialog_rebuild_retained(state);
} else if (state != NULL) {
GError *error = background_task_dup_error(task);
gtk_label_set_text(state->error,
error != NULL ? error->message :
"La préparation des preuves a échoué.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
g_clear_error(&error);
}
if (state != NULL && state->staging_task == task) {
background_task_unref(state->staging_task);
state->staging_task = NULL;
}
g_clear_object(&window);
}
static void create_person_dialog_files_selected(
GObject *source, GAsyncResult *async_result, gpointer data)
{
CreatePersonStagingContext *selection_context = data;
GtkWindow *window = g_weak_ref_get(&selection_context->window);
CreatePersonDialogState *state = window != NULL
? g_object_get_data(G_OBJECT(window), "person-dialog-state") : NULL;
GListModel *files;
GPtrArray *paths;
GError *error = NULL;
CreatePersonStagingContext *context;
files = gtk_file_dialog_open_multiple_finish(
GTK_FILE_DIALOG(source), async_result, &error);
if (state == NULL) {
g_clear_object(&files);
g_clear_error(&error);
staging_context_free(selection_context);
g_clear_object(&window);
return;
}
if (files == NULL) {
if (!g_error_matches(error, GTK_DIALOG_ERROR,
GTK_DIALOG_ERROR_DISMISSED)) {
gtk_label_set_text(state->error, error->message);
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
}
g_clear_error(&error);
staging_context_free(selection_context);
g_clear_object(&window);
return;
}
paths = g_ptr_array_new_with_free_func(g_free);
for (guint i = 0; i < g_list_model_get_n_items(files); i++) {
GFile *file = g_list_model_get_item(files, i);
char *path = g_file_get_path(file);
if (path != NULL) g_ptr_array_add(paths, path);
g_object_unref(file);
}
g_object_unref(files);
if (paths->len == 0) {
g_ptr_array_unref(paths);
staging_context_free(selection_context);
g_clear_object(&window);
return;
}
context = g_new0(CreatePersonStagingContext, 1);
g_weak_ref_init(&context->window, G_OBJECT(state->window));
context->generation =
person_dialog_lifecycle_begin_preview(state->lifecycle);
state->staging_task = evidence_staging_task_start(state->task_manager,
state->staging, paths, create_person_dialog_staging_completed,
context, staging_context_free, &error);
if (state->staging_task == NULL) {
staging_context_free(context);
gtk_label_set_text(state->error,
error != NULL ? error->message :
"Impossible de démarrer le staging.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
}
g_clear_error(&error);
g_ptr_array_unref(paths);
staging_context_free(selection_context);
g_clear_object(&window);
}
static void create_person_dialog_on_import_files(
GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data;
GtkFileDialog *dialog;
CreatePersonStagingContext *context;
(void) button;
if (state->staging_task != NULL) return;
dialog = gtk_file_dialog_new();
gtk_file_dialog_set_title(dialog,
"Importer des fichiers dans le staging");
context = g_new0(CreatePersonStagingContext, 1);
g_weak_ref_init(&context->window, G_OBJECT(state->window));
gtk_file_dialog_open_multiple(dialog, state->window, NULL,
create_person_dialog_files_selected, context);
g_object_unref(dialog);
}
static void create_person_dialog_rebuild_evidence(CreatePersonDialogState *state)
{
GPtrArray *labels = g_ptr_array_new_with_free_func(g_free);
const EvidenceRecord *selected =
evidence_selection_model_get_selected(state->selection_model);
const char *selected_id = selected != NULL
? evidence_record_get_identifier(selected) : NULL;
guint selected_index = 0;
g_ptr_array_add(labels, g_strdup("Aucune preuve associée"));
g_ptr_array_set_size(state->evidence_identifiers, 0);
g_clear_pointer(&state->visible_records, g_ptr_array_unref);
state->visible_records =
evidence_selection_model_list_visible(state->selection_model);
for (guint i = 0; i < state->visible_records->len; i++) {
EvidenceRecord *record = g_ptr_array_index(state->visible_records, i);
g_ptr_array_add(labels,
g_strdup(evidence_record_get_original_name(record)));
g_ptr_array_add(state->evidence_identifiers,
g_strdup(evidence_record_get_identifier(record)));
if (g_strcmp0(selected_id,
evidence_record_get_identifier(record)) == 0)
selected_index = i + 1;
}
g_ptr_array_add(labels, NULL);
state->updating_evidence = TRUE;
if (state->evidence_selected_handler != 0)
g_signal_handler_block(state->evidence,
state->evidence_selected_handler);
gtk_string_list_splice(state->evidence_labels, 0,
g_list_model_get_n_items(G_LIST_MODEL(state->evidence_labels)),
(const char * const *) labels->pdata);
gtk_drop_down_set_selected(state->evidence, selected_index);
if (state->evidence_selected_handler != 0)
g_signal_handler_unblock(state->evidence,
state->evidence_selected_handler);
state->updating_evidence = FALSE;
g_ptr_array_unref(labels);
if (selected_index == 0) {
evidence_selection_model_select(state->selection_model, NULL);
create_person_dialog_clear_preview(state);
}
}
static void create_person_dialog_on_search_changed(
GtkEditable *editable, gpointer data)
{
CreatePersonDialogState *state = data;
evidence_selection_model_set_query(state->selection_model,
gtk_editable_get_text(editable));
create_person_dialog_rebuild_evidence(state);
}
static void create_person_dialog_on_type_changed(
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data)
{
CreatePersonDialogState *state = data;
guint selected = gtk_drop_down_get_selected(dropdown);
(void) pspec;
evidence_selection_model_set_type(state->selection_model,
selected > 0 && selected - 1 < state->type_codes->len
? g_ptr_array_index(state->type_codes, selected - 1) : NULL);
create_person_dialog_rebuild_evidence(state);
}
static void create_person_dialog_on_evidence_changed(
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data)
{
CreatePersonDialogState *state = data;
guint selected = gtk_drop_down_get_selected(dropdown);
const char *identifier = NULL;
const EvidenceRecord *record = NULL;
(void) pspec;
if (state->updating_evidence) return;
if (selected != GTK_INVALID_LIST_POSITION && selected > 0 &&
selected - 1 < state->evidence_identifiers->len)
identifier = g_ptr_array_index(
state->evidence_identifiers, selected - 1);
if (identifier != NULL)
record = evidence_selection_model_find_by_identifier(
state->selection_model, identifier);
create_person_dialog_select_record(state, record, NULL);
}
/** @brief Termine le dialogue comme une annulation. */
static void create_person_dialog_cancel(CreatePersonDialogState *state)
{
if (state == NULL ||
!person_dialog_lifecycle_cancel(state->lifecycle)) return;
evidence_preview_widget_cancel(state->preview);
if (state->callback != NULL) state->callback(NULL, state->user_data);
}
static const char *integrity_label(EvidenceIntegrityStatus status)
{
switch (status) {
case EVIDENCE_INTEGRITY_STATUS_VALID: return "Valide";
case EVIDENCE_INTEGRITY_STATUS_MISSING: return "Fichier absent";
case EVIDENCE_INTEGRITY_STATUS_MODIFIED: return "Invalide";
case EVIDENCE_INTEGRITY_STATUS_ERROR: return "Erreur de vérification";
default: return "Non vérifiée";
}
}
static void create_person_dialog_clear_preview(CreatePersonDialogState *state)
{
person_dialog_lifecycle_begin_preview(state->lifecycle);
evidence_preview_widget_clear(state->preview);
}
static void create_person_dialog_select_record(CreatePersonDialogState *state,
const EvidenceRecord *record, const char *business_type)
{
char *size = NULL, *sha = NULL, *text = NULL;
const char *mime;
const char *type_identifier;
EvidencePreviewRequest *request;
create_person_dialog_clear_preview(state);
if (record == NULL) return;
evidence_selection_model_select(state->selection_model,
evidence_record_get_identifier(record));
size = g_format_size(evidence_record_get_size_bytes(record));
sha = g_strndup(evidence_record_get_sha256(record), 12);
mime = evidence_record_get_mime_type(record);
type_identifier = business_type != NULL ? business_type :
evidence_record_get_type_identifier(record);
text = g_strdup_printf(
"Nom : %s\nType : %s (%s)\nMIME : %s\nTaille : %s\n"
"Import : %s\nSHA-256 : %s…\nIntégrité : %s\nDescription : %s",
evidence_record_get_original_name(record),
type_identifier, type_identifier,
mime != NULL ? mime : "Non renseigné", size,
evidence_record_get_imported_at(record), sha,
integrity_label(evidence_record_get_integrity_status(record)),
evidence_record_get_description(record) != NULL
? evidence_record_get_description(record) : "Aucune");
request = evidence_preview_request_new(state->investigation_root_path,
evidence_record_get_identifier(record),
evidence_record_get_relative_path(record),
evidence_record_get_sha256(record), mime,
person_dialog_lifecycle_get_generation(state->lifecycle));
evidence_preview_widget_show(state->preview, request, text);
evidence_preview_request_free(request);
g_free(size); g_free(sha); g_free(text);
}
/** @brief Traite la fermeture native. */
static gboolean create_person_dialog_on_close(GtkWindow *window, gpointer data)
{
(void) window; create_person_dialog_cancel(data); return FALSE;
}
/** @brief Traite le bouton Annuler. */
static void create_person_dialog_on_cancel(GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data;
(void) button; create_person_dialog_cancel(state); gtk_window_close(state->window);
}
/** @brief Valide puis transmet les valeurs du formulaire. */
static void create_person_dialog_on_create(GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data;
CreatePersonDialogResult *result = NULL;
char *notes = NULL;
guint status = gtk_drop_down_get_selected(state->status);
(void) button;
if (state->session_check != NULL &&
!state->session_check(state->user_data)) {
gtk_label_set_text(state->error,
"L'enquête active a changé : création refusée.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
return;
}
if (status >= G_N_ELEMENTS(status_codes) ||
gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] == '\0')
{
gtk_label_set_text(state->error, "La désignation de la personne est obligatoire.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE); return;
}
notes = create_person_dialog_get_notes(state);
result = g_new0(CreatePersonDialogResult, 1);
result->designation = create_person_dialog_copy(
gtk_editable_get_text(GTK_EDITABLE(state->designation)));
result->declared_name = create_person_dialog_copy(
gtk_editable_get_text(GTK_EDITABLE(state->name)));
result->pseudonym = create_person_dialog_copy(
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)));
result->status = g_strdup(status_codes[status]);
result->notes = create_person_dialog_copy(notes);
if (person_evidence_selection_get_count(
state->person_evidence_selection) > 0) {
const PersonEvidenceSelectionItem *first =
person_evidence_selection_get(
state->person_evidence_selection, 0);
result->evidence_identifier = g_strdup(
person_evidence_selection_item_get_evidence_identifier(first));
}
result->input.designation = result->designation;
result->input.declared_name = result->declared_name;
result->input.pseudonym = result->pseudonym;
result->input.identification_status = result->status;
result->input.notes = result->notes;
result->input.confidence = gtk_spin_button_get_value_as_int(state->confidence);
result->input.evidence_identifier = result->evidence_identifier;
result->role_assignments = g_ptr_array_new_with_free_func(
(GDestroyNotify) person_role_assignment_input_free);
static const char *const role_codes[] = {
"alleged_author", "presented_identity",
"potentially_impersonated_identity", "victim", "witness",
"declared_bank_holder", "intermediary", "mentioned_person", "other"};
for (guint i = 0; i < G_N_ELEMENTS(role_codes); i++)
if (gtk_check_button_get_active(state->roles[i])) {
PersonRoleAssignmentInput assignment = {
.role_code = (char *) role_codes[i],
.evidence_identifier = result->evidence_identifier,
.provenance_kind = "manual",
.has_confidence = FALSE
};
g_ptr_array_add(result->role_assignments,
person_role_assignment_input_copy(&assignment));
}
result->input.role_assignments = result->role_assignments;
result->evidence_selection = state->person_evidence_selection;
state->person_evidence_selection = NULL;
result->staging = state->staging;
state->staging = NULL;
result->ocr_runs = state->ocr_runs;
state->ocr_runs = NULL;
if (!person_dialog_lifecycle_complete(state->lifecycle)) {
create_person_dialog_result_free(result);
g_free(notes);
return;
}
if (state->callback != NULL) state->callback(result, state->user_data);
else create_person_dialog_result_free(result);
g_free(notes); gtk_window_close(state->window);
}
static void create_person_dialog_update_navigation(CreatePersonDialogState *state)
{
static const char *const pages[] = {
"person", "roles", "evidence", "identity-ocr", "summary"};
static const char *const steps[] = {
"1 Personne", "2 Rôles", "3 Preuves", "4 OCR identité",
"5 Confirmation"};
GString *progress = g_string_new(NULL);
gtk_stack_set_visible_child_name(state->stack, pages[state->step]);
for (guint i = 0; i < G_N_ELEMENTS(steps); i++) {
char *escaped = g_markup_escape_text(steps[i], -1);
if (i > 0) g_string_append(progress, "");
if (i == state->step)
g_string_append_printf(progress, "<b>%s</b>", escaped);
else
g_string_append(progress, escaped);
g_free(escaped);
}
gtk_label_set_markup(state->progress, progress->str);
g_string_free(progress, TRUE);
if (state->step == 4) {
static const char *const status_labels[] = {
"Inconnu", "Présumé", "Confirmé"};
GPtrArray *role_labels = g_ptr_array_new();
char *notes = create_person_dialog_get_notes(state);
char *text;
for (guint i = 0; i < G_N_ELEMENTS(state->roles); i++)
if (gtk_check_button_get_active(state->roles[i]))
g_ptr_array_add(role_labels, (gpointer)
gtk_check_button_get_label(state->roles[i]));
text = person_confirmation_summary_build_multiple(
gtk_editable_get_text(GTK_EDITABLE(state->designation)),
gtk_editable_get_text(GTK_EDITABLE(state->name)),
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)),
gtk_drop_down_get_selected(state->status) <
G_N_ELEMENTS(status_labels)
? status_labels[gtk_drop_down_get_selected(state->status)]
: "Non renseigné",
gtk_spin_button_get_value_as_int(state->confidence), notes,
role_labels, state->person_evidence_selection);
GString *confirmation = g_string_new(text);
g_string_append(confirmation,
"\n\nOCR identité\nLOCR peut contenir des erreurs. Ces "
"informations sont seulement présentées sur le document ; "
"lauthenticité nest pas établie et aucune fusion de personne "
"ne sera effectuée. Aucune écriture na encore eu lieu.");
for (guint run_index = 0;
state->ocr_runs != NULL && run_index < state->ocr_runs->len;
run_index++) {
IdentityOcrRun *run = g_ptr_array_index(
state->ocr_runs, run_index);
g_string_append_printf(confirmation,
"\n• %s — %s — page %u — Tesseract %s — %s",
identity_ocr_run_get_document_type(run),
identity_ocr_run_get_document_side(run),
identity_ocr_run_get_page(run),
identity_ocr_run_get_version(run) != NULL
? identity_ocr_run_get_version(run) : "version inconnue",
identity_ocr_run_get_languages(run));
if (identity_ocr_run_get_factual_notes(run) != NULL)
g_string_append_printf(confirmation,
"\n Notes factuelles : %s",
identity_ocr_run_get_factual_notes(run));
const GPtrArray *fields = identity_ocr_run_get_fields(run);
for (guint field_index = 0;
fields != NULL && field_index < fields->len; field_index++) {
IdentityFieldObservation *field =
g_ptr_array_index((GPtrArray *) fields, field_index);
IdentityReviewStatus review =
identity_field_observation_get_status(field);
if (review == IDENTITY_REVIEW_ACCEPTED ||
review == IDENTITY_REVIEW_MODIFIED)
g_string_append_printf(confirmation,
"\n - %s : %s (brut : %s ; origine : %s)",
identity_field_observation_get_code(field),
identity_field_observation_get_corrected_value(field)
!= NULL
? identity_field_observation_get_corrected_value(field)
: identity_field_observation_get_raw_value(field),
identity_field_observation_get_raw_value(field) != NULL
? identity_field_observation_get_raw_value(field)
: "absente",
identity_field_observation_get_origin(field));
}
}
gtk_label_set_text(state->summary, confirmation->str);
g_string_free(confirmation, TRUE);
g_free(text);
g_free(notes);
g_ptr_array_unref(role_labels);
}
gtk_widget_set_sensitive(GTK_WIDGET(state->previous), state->step > 0);
gtk_widget_set_visible(GTK_WIDGET(state->next), state->step < 4);
gtk_widget_set_visible(GTK_WIDGET(state->create), state->step == 4);
gtk_widget_set_sensitive(GTK_WIDGET(state->create),
state->step == 4 &&
person_evidence_selection_is_confirmable(
state->person_evidence_selection));
}
static void create_person_dialog_on_previous(GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data; (void) button;
if (state->step > 0) state->step--;
create_person_dialog_update_navigation(state);
}
static void create_person_dialog_on_next(GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data; (void) button;
if (state->step == 0 &&
gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] == '\0') {
gtk_label_set_text(state->error,
"La désignation de la personne est obligatoire.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
return;
}
if (state->step == 3)
create_person_dialog_capture_corrected_transcription(state);
if (state->step == 3)
create_person_dialog_capture_factual_notes(state);
gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
if (state->step < 4) state->step++;
create_person_dialog_update_navigation(state);
}
/** @brief Ajoute une ligne libellée au formulaire. */
static void create_person_dialog_add_row(GtkGrid *grid, int row,
const char *label, GtkWidget *widget)
{
GtkWidget *caption = gtk_label_new(label);
gtk_label_set_xalign(GTK_LABEL(caption), 0.0f);
gtk_widget_set_hexpand(widget, TRUE);
gtk_grid_attach(grid, caption, 0, row, 1, 1);
gtk_grid_attach(grid, widget, 1, row, 1, 1);
}
gboolean create_person_dialog_present(GtkWindow *parent,
const GPtrArray *records, const char *investigation_root_path,
TaskManager *task_manager, const ToolInfo *tesseract_tool,
CreatePersonDialogSessionCheck session_check,
CreatePersonDialogCallback callback, gpointer data,
GDestroyNotify data_destroy)
{
static const char *const statuses[] = {"Inconnu", "Présumé", "Confirmé", NULL};
CreatePersonDialogState *state = NULL;
GtkWidget *box = NULL, *grid = NULL, *actions = NULL, *cancel = NULL;
GtkWidget *roles_box = NULL, *evidence_box = NULL, *ocr_box = NULL;
GtkWidget *summary = NULL;
if (parent == NULL || investigation_root_path == NULL ||
task_manager == NULL) return FALSE;
state = g_new0(CreatePersonDialogState, 1);
state->callback = callback; state->session_check = session_check;
state->user_data = data; state->user_data_destroy = data_destroy;
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
state->selection_model = evidence_selection_model_new(records);
state->person_evidence_selection = person_evidence_selection_new();
state->staging = evidence_staging_new(NULL);
if (state->selection_model == NULL ||
state->person_evidence_selection == NULL || state->staging == NULL) {
create_person_dialog_state_free(state);
return FALSE;
}
state->visible_records =
evidence_selection_model_list_visible(state->selection_model);
state->type_codes =
evidence_selection_model_list_type_codes(state->selection_model);
state->investigation_root_path = g_strdup(investigation_root_path);
state->task_manager = task_manager;
state->ocr_runs = g_ptr_array_new_with_free_func(
(GDestroyNotify) identity_ocr_run_free);
if (tesseract_tool != NULL &&
tool_info_get_availability(tesseract_tool) ==
TOOL_AVAILABILITY_AVAILABLE) {
state->tesseract_path = g_strdup(
tool_info_get_resolved_path(tesseract_tool));
state->tesseract_version = g_strdup(
tool_info_get_detected_version(tesseract_tool));
}
state->lifecycle = person_dialog_lifecycle_new();
state->window = GTK_WINDOW(gtk_window_new());
gtk_window_set_application(state->window,
gtk_window_get_application(parent));
gtk_window_set_title(state->window, "Ajouter une personne");
gtk_window_set_transient_for(state->window, parent);
gtk_window_set_modal(state->window, TRUE);
gtk_window_set_default_size(state->window, 960, 700);
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start(box, 16); gtk_widget_set_margin_end(box, 16);
gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16);
grid = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
state->designation = GTK_ENTRY(gtk_entry_new());
gtk_entry_set_placeholder_text(state->designation, "Personne présumée liée aux comptes");
state->name = GTK_ENTRY(gtk_entry_new());
state->pseudonym = GTK_ENTRY(gtk_entry_new());
state->status = GTK_DROP_DOWN(gtk_drop_down_new_from_strings(statuses));
gtk_drop_down_set_selected(state->status, 1);
state->confidence = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(0, 100, 5));
gtk_spin_button_set_value(state->confidence, 30);
state->evidence_labels = gtk_string_list_new(NULL);
gtk_string_list_append(state->evidence_labels,
"Aucune preuve associée");
for (guint i = 0; i < state->visible_records->len; i++)
{
EvidenceRecord *record = g_ptr_array_index(state->visible_records, i);
const char *id = evidence_record_get_identifier(record);
const char *name = evidence_record_get_original_name(record);
if (id == NULL || name == NULL) continue;
gtk_string_list_append(state->evidence_labels, name);
g_ptr_array_add(state->evidence_identifiers, g_strdup(id));
}
state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(
G_LIST_MODEL(g_object_ref(state->evidence_labels)), NULL));
state->search = GTK_ENTRY(gtk_entry_new());
gtk_entry_set_placeholder_text(state->search,
"Rechercher par nom, description ou type");
state->type_filter_labels = gtk_string_list_new(NULL);
gtk_string_list_append(state->type_filter_labels, "Tous les types");
for (guint i = 0; i < state->type_codes->len; i++)
gtk_string_list_append(state->type_filter_labels,
g_ptr_array_index(state->type_codes, i));
state->type_filter = GTK_DROP_DOWN(
gtk_drop_down_new(G_LIST_MODEL(
g_object_ref(state->type_filter_labels)), NULL));
/*
* gtk_drop_down_new() prend la propriété complète du modèle. Une
* libération ici laisserait les vues internes du GtkDropDown avec un
* GListModel détruit et ferait échouer leur nettoyage à la fermeture.
*/
state->notes = GTK_TEXT_VIEW(gtk_text_view_new());
gtk_text_view_set_wrap_mode(state->notes, GTK_WRAP_WORD_CHAR);
gtk_widget_set_size_request(GTK_WIDGET(state->notes), -1, 90);
create_person_dialog_add_row(GTK_GRID(grid), 0, "Désignation", GTK_WIDGET(state->designation));
create_person_dialog_add_row(GTK_GRID(grid), 1, "Nom déclaré", GTK_WIDGET(state->name));
create_person_dialog_add_row(GTK_GRID(grid), 2, "Pseudonyme", GTK_WIDGET(state->pseudonym));
create_person_dialog_add_row(GTK_GRID(grid), 3, "Identification", GTK_WIDGET(state->status));
create_person_dialog_add_row(GTK_GRID(grid), 4, "Confiance (%)", GTK_WIDGET(state->confidence));
create_person_dialog_add_row(GTK_GRID(grid), 5, "Notes factuelles", GTK_WIDGET(state->notes));
state->stack = GTK_STACK(gtk_stack_new());
gtk_stack_set_transition_type(state->stack,
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
gtk_stack_add_titled(state->stack, grid, "person", "1 — Personne");
roles_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
static const char *const role_codes[] = {
"alleged_author", "presented_identity",
"potentially_impersonated_identity", "victim", "witness",
"declared_bank_holder", "intermediary", "mentioned_person", "other"};
for (guint i = 0; i < G_N_ELEMENTS(role_codes); i++) {
state->roles[i] = GTK_CHECK_BUTTON(gtk_check_button_new_with_label(
person_role_assignment_role_label(role_codes[i])));
gtk_box_append(GTK_BOX(roles_box), GTK_WIDGET(state->roles[i]));
}
gtk_stack_add_titled(state->stack, roles_box, "roles", "2 — Rôles");
evidence_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_box_append(GTK_BOX(evidence_box), gtk_label_new(
"Sélectionnez des preuves existantes et/ou préparez de nouveaux fichiers."));
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->search));
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->type_filter));
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->evidence));
{
GtkWidget *evidence_actions =
gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
state->add_existing = GTK_BUTTON(
gtk_button_new_with_label("Ajouter à la sélection"));
state->import_files = GTK_BUTTON(
gtk_button_new_with_label("Importer des fichiers"));
gtk_widget_set_tooltip_text(GTK_WIDGET(state->add_existing),
"Retenir la preuve existante affichée");
gtk_widget_set_tooltip_text(GTK_WIDGET(state->import_files),
"Créer des copies temporaires sans import définitif");
gtk_box_append(GTK_BOX(evidence_actions),
GTK_WIDGET(state->add_existing));
gtk_box_append(GTK_BOX(evidence_actions),
GTK_WIDGET(state->import_files));
gtk_box_append(GTK_BOX(evidence_box), evidence_actions);
}
gtk_box_append(GTK_BOX(evidence_box),
gtk_label_new("Preuves retenues"));
{
state->retained_labels = gtk_string_list_new(NULL);
state->retained = GTK_DROP_DOWN(gtk_drop_down_new(
G_LIST_MODEL(g_object_ref(state->retained_labels)), NULL));
}
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->retained));
{
static const char *const type_labels_fr[] = {
"Capture décran", "Photo", "Vidéo", "Document", "E-mail",
"Archive", "Audio", "Texte", "Autre", NULL};
GtkWidget *retained_actions =
gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
state->retained_type = GTK_DROP_DOWN(
gtk_drop_down_new_from_strings(type_labels_fr));
state->remove_retained = GTK_BUTTON(
gtk_button_new_with_label("Retirer"));
gtk_widget_set_tooltip_text(GTK_WIDGET(state->remove_retained),
"Retirer uniquement de la sélection");
gtk_box_append(GTK_BOX(retained_actions),
GTK_WIDGET(state->retained_type));
gtk_box_append(GTK_BOX(retained_actions),
GTK_WIDGET(state->remove_retained));
gtk_box_append(GTK_BOX(evidence_box), retained_actions);
}
state->preview = evidence_preview_widget_new(state->task_manager,
(EvidencePreviewWidgetSessionCheck) state->session_check,
state->user_data);
if (state->preview == NULL) {
create_person_dialog_state_free(state);
return FALSE;
}
{
GtkWidget *evidence_paned =
gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
gtk_widget_set_name(evidence_paned, "create-person-evidence-paned");
gtk_widget_set_size_request(evidence_box, 320, -1);
gtk_paned_set_start_child(GTK_PANED(evidence_paned), evidence_box);
gtk_paned_set_end_child(GTK_PANED(evidence_paned),
evidence_preview_widget_get_widget(state->preview));
gtk_paned_set_position(GTK_PANED(evidence_paned), 420);
gtk_paned_set_resize_start_child(
GTK_PANED(evidence_paned), FALSE);
gtk_paned_set_resize_end_child(GTK_PANED(evidence_paned), TRUE);
gtk_stack_add_titled(state->stack, evidence_paned,
"evidence", "3 — Preuves");
}
ocr_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_name(ocr_box, "create-person-ocr-review-content");
GtkWidget *ocr_warning = gtk_label_new(
"LOCR produit des propositions à vérifier. Il ne confirme ni "
"lidentité ni lauthenticité du document.");
gtk_label_set_wrap(GTK_LABEL(ocr_warning), TRUE);
static const char *const document_types[] = {
"Carte nationale didentité", "Passeport", "Permis de conduire",
"Titre de séjour", "Autre document didentité", NULL};
static const char *const document_sides[] = {
"Recto", "Verso", "Page didentité", "Autre page", NULL};
static const char *const profile_labels[] = {
"Aucun prétraitement", "Orientation normalisée",
"Niveaux de gris et contraste modéré",
"Agrandissement contrôlé", NULL};
state->ocr_document_type = GTK_DROP_DOWN(
gtk_drop_down_new_from_strings(document_types));
state->ocr_document_side = GTK_DROP_DOWN(
gtk_drop_down_new_from_strings(document_sides));
{
static const char *const pending_languages[] = {
"Détection des langues…", NULL};
state->ocr_languages = GTK_DROP_DOWN(
gtk_drop_down_new_from_strings(pending_languages));
}
state->ocr_profile = GTK_DROP_DOWN(
gtk_drop_down_new_from_strings(profile_labels));
state->ocr_page = GTK_SPIN_BUTTON(
gtk_spin_button_new_with_range(1, 999, 1));
state->ocr_start = GTK_BUTTON(
gtk_button_new_with_label("Analyser comme document didentité"));
state->ocr_cancel = GTK_BUTTON(
gtk_button_new_with_label("Annuler lOCR"));
gtk_widget_set_sensitive(GTK_WIDGET(state->ocr_start), FALSE);
gtk_widget_set_tooltip_text(GTK_WIDGET(state->ocr_start),
state->tesseract_path != NULL
? "Lancer explicitement Tesseract sur la copie contrôlée"
: "Tesseract est absent du registre doutils");
gtk_box_append(GTK_BOX(ocr_box), GTK_WIDGET(state->ocr_document_type));
gtk_box_append(GTK_BOX(ocr_box), GTK_WIDGET(state->ocr_document_side));
gtk_box_append(GTK_BOX(ocr_box), GTK_WIDGET(state->ocr_page));
gtk_box_append(GTK_BOX(ocr_box), GTK_WIDGET(state->ocr_languages));
gtk_box_append(GTK_BOX(ocr_box), GTK_WIDGET(state->ocr_profile));
GtkWidget *ocr_actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_box_append(GTK_BOX(ocr_actions), GTK_WIDGET(state->ocr_start));
gtk_box_append(GTK_BOX(ocr_actions), GTK_WIDGET(state->ocr_cancel));
gtk_box_append(GTK_BOX(ocr_box), ocr_actions);
state->ocr_status = GTK_LABEL(gtk_label_new(
state->tesseract_path != NULL ? "OCR facultatif, non lancé."
: "Tesseract absent : OCR indisponible."));
gtk_box_append(GTK_BOX(ocr_box), GTK_WIDGET(state->ocr_status));
state->ocr_overlay = ocr_provenance_overlay_new();
state->ocr_fields = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 6));
GtkWidget *field_scroll = gtk_scrolled_window_new();
gtk_widget_set_size_request(field_scroll, -1, 180);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(field_scroll),
GTK_WIDGET(state->ocr_fields));
gtk_box_append(GTK_BOX(ocr_box), field_scroll);
{
static const char *const field_codes[] = {
"Type de document", "Pays émetteur", "Autorité émettrice",
"Numéro du document", "Nom", "Nom de naissance", "Prénoms",
"Sexe imprimé", "Nationalité", "Date de naissance",
"Lieu de naissance", "Date de délivrance", "Date dexpiration",
"Adresse imprimée", "Ligne MRZ 1", "Ligne MRZ 2",
"Ligne MRZ 3", NULL};
GtkWidget *manual_box =
gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
state->ocr_manual_code = GTK_DROP_DOWN(
gtk_drop_down_new_from_strings(field_codes));
state->ocr_manual_value = GTK_ENTRY(gtk_entry_new());
gtk_entry_set_placeholder_text(state->ocr_manual_value,
"Valeur réellement visible dans la preuve");
gtk_widget_set_hexpand(GTK_WIDGET(state->ocr_manual_value), TRUE);
GtkWidget *add_manual =
gtk_button_new_with_label("Ajouter un champ manquant");
gtk_box_append(GTK_BOX(manual_box),
GTK_WIDGET(state->ocr_manual_code));
gtk_box_append(GTK_BOX(manual_box),
GTK_WIDGET(state->ocr_manual_value));
gtk_box_append(GTK_BOX(manual_box), add_manual);
gtk_box_append(GTK_BOX(ocr_box), manual_box);
g_signal_connect(add_manual, "clicked",
G_CALLBACK(create_person_dialog_on_add_manual_field), state);
}
gtk_box_append(GTK_BOX(ocr_box), gtk_label_new(
"Notes factuelles sur le document "
"(tronqué, flou, masqué ou incomplet)"));
state->ocr_factual_notes = GTK_TEXT_VIEW(gtk_text_view_new());
gtk_widget_set_name(GTK_WIDGET(state->ocr_factual_notes),
"create-person-ocr-factual-notes");
gtk_text_view_set_wrap_mode(
state->ocr_factual_notes, GTK_WRAP_WORD_CHAR);
gtk_widget_set_size_request(
GTK_WIDGET(state->ocr_factual_notes), -1, 90);
gtk_box_append(GTK_BOX(ocr_box),
GTK_WIDGET(state->ocr_factual_notes));
state->ocr_text = GTK_TEXT_VIEW(gtk_text_view_new());
gtk_widget_set_name(GTK_WIDGET(state->ocr_text),
"create-person-ocr-raw-text");
gtk_text_view_set_editable(state->ocr_text, FALSE);
gtk_text_view_set_monospace(state->ocr_text, TRUE);
GtkWidget *ocr_text_scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(ocr_text_scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(ocr_text_scroll),
GTK_WIDGET(state->ocr_text));
state->ocr_corrected_text = GTK_TEXT_VIEW(gtk_text_view_new());
gtk_widget_set_name(GTK_WIDGET(state->ocr_corrected_text),
"identity-corrected-transcription");
gtk_text_view_set_wrap_mode(
state->ocr_corrected_text, GTK_WRAP_WORD_CHAR);
GtkWidget *corrected_scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(corrected_scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(corrected_scroll),
GTK_WIDGET(state->ocr_corrected_text));
state->ocr_transcription_stack = GTK_STACK(gtk_stack_new());
gtk_widget_set_name(GTK_WIDGET(state->ocr_transcription_stack),
"create-person-ocr-transcription-stack");
gtk_widget_set_size_request(
GTK_WIDGET(state->ocr_transcription_stack), -1, 150);
gtk_stack_add_titled(state->ocr_transcription_stack, ocr_text_scroll,
"raw", "OCR brut");
gtk_stack_add_titled(
state->ocr_transcription_stack, corrected_scroll,
"corrected", "Transcription corrigée");
GtkWidget *transcription_switcher = gtk_stack_switcher_new();
gtk_widget_set_name(transcription_switcher,
"create-person-ocr-transcription-switcher");
gtk_stack_switcher_set_stack(
GTK_STACK_SWITCHER(transcription_switcher),
state->ocr_transcription_stack);
gtk_box_append(GTK_BOX(ocr_box), transcription_switcher);
gtk_box_append(GTK_BOX(ocr_box),
GTK_WIDGET(state->ocr_transcription_stack));
GtkWidget *reset_transcription = gtk_button_new_with_label(
"Réinitialiser depuis lOCR brut");
gtk_widget_set_halign(reset_transcription, GTK_ALIGN_START);
g_signal_connect(reset_transcription, "clicked",
G_CALLBACK(create_person_dialog_on_reset_transcription), state);
gtk_box_append(GTK_BOX(ocr_box), reset_transcription);
GtkWidget *ocr_left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_name(ocr_left, "create-person-ocr-left-panel");
gtk_widget_set_size_request(ocr_left, 500, -1);
gtk_box_append(GTK_BOX(ocr_left), ocr_warning);
GtkWidget *ocr_scroll = gtk_scrolled_window_new();
gtk_widget_set_name(ocr_scroll, "create-person-ocr-scroll");
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(ocr_scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_propagate_natural_height(
GTK_SCROLLED_WINDOW(ocr_scroll), FALSE);
gtk_widget_set_vexpand(ocr_scroll, TRUE);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(ocr_scroll), ocr_box);
gtk_box_append(GTK_BOX(ocr_left), ocr_scroll);
GtkWidget *ocr_paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
gtk_widget_set_name(ocr_paned, "create-person-ocr-paned");
gtk_paned_set_start_child(GTK_PANED(ocr_paned), ocr_left);
GtkWidget *ocr_preview =
ocr_provenance_overlay_get_widget(state->ocr_overlay);
gtk_widget_set_name(ocr_preview, "create-person-ocr-preview");
gtk_widget_set_size_request(ocr_preview, 180, -1);
gtk_paned_set_end_child(GTK_PANED(ocr_paned), ocr_preview);
gtk_paned_set_resize_start_child(GTK_PANED(ocr_paned), FALSE);
gtk_paned_set_resize_end_child(GTK_PANED(ocr_paned), TRUE);
gtk_stack_add_titled(state->stack, ocr_paned, "identity-ocr",
"4 — OCR identité");
state->summary = GTK_LABEL(gtk_label_new(""));
summary = GTK_WIDGET(state->summary);
gtk_label_set_wrap(state->summary, TRUE);
gtk_label_set_xalign(state->summary, 0.0f);
gtk_label_set_selectable(state->summary, TRUE);
gtk_stack_add_titled(state->stack, summary, "summary", "5 — Confirmation");
state->error = GTK_LABEL(gtk_label_new(NULL)); gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_name(actions, "create-person-actions");
gtk_widget_set_halign(actions, GTK_ALIGN_END);
cancel = gtk_button_new_with_label("Annuler");
state->previous = GTK_BUTTON(gtk_button_new_with_label("Précédent"));
state->next = GTK_BUTTON(gtk_button_new_with_label("Suivant"));
state->create = GTK_BUTTON(gtk_button_new_with_label("Créer la personne"));
gtk_widget_add_css_class(GTK_WIDGET(state->create), "suggested-action");
gtk_box_append(GTK_BOX(actions), cancel);
gtk_box_append(GTK_BOX(actions), GTK_WIDGET(state->previous));
gtk_box_append(GTK_BOX(actions), GTK_WIDGET(state->next));
gtk_box_append(GTK_BOX(actions), GTK_WIDGET(state->create));
state->progress = GTK_LABEL(gtk_label_new(NULL));
gtk_label_set_xalign(state->progress, 0.0f);
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->progress));
gtk_widget_set_vexpand(GTK_WIDGET(state->stack), TRUE);
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->stack));
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->error));
gtk_box_append(GTK_BOX(box), actions); gtk_window_set_child(state->window, box);
g_signal_connect(state->window, "close-request", G_CALLBACK(create_person_dialog_on_close), state);
g_signal_connect(cancel, "clicked", G_CALLBACK(create_person_dialog_on_cancel), state);
g_signal_connect(state->previous, "clicked",
G_CALLBACK(create_person_dialog_on_previous), state);
g_signal_connect(state->next, "clicked",
G_CALLBACK(create_person_dialog_on_next), state);
g_signal_connect(state->create, "clicked",
G_CALLBACK(create_person_dialog_on_create), state);
g_signal_connect(state->ocr_start, "clicked",
G_CALLBACK(create_person_dialog_on_ocr_start), state);
g_signal_connect(state->ocr_cancel, "clicked",
G_CALLBACK(create_person_dialog_on_ocr_cancel), state);
g_signal_connect(state->search, "changed",
G_CALLBACK(create_person_dialog_on_search_changed), state);
g_signal_connect(state->type_filter, "notify::selected",
G_CALLBACK(create_person_dialog_on_type_changed), state);
state->evidence_selected_handler = g_signal_connect(
state->evidence, "notify::selected",
G_CALLBACK(create_person_dialog_on_evidence_changed), state);
g_signal_connect(state->add_existing, "clicked",
G_CALLBACK(create_person_dialog_on_add_existing), state);
state->retained_selected_handler = g_signal_connect(
state->retained, "notify::selected",
G_CALLBACK(create_person_dialog_on_retained_changed), state);
g_signal_connect(state->retained_type, "notify::selected",
G_CALLBACK(create_person_dialog_on_retained_type_changed), state);
g_signal_connect(state->remove_retained, "clicked",
G_CALLBACK(create_person_dialog_on_remove_retained), state);
g_signal_connect(state->import_files, "clicked",
G_CALLBACK(create_person_dialog_on_import_files), state);
create_person_dialog_update_navigation(state);
g_object_set_data_full(G_OBJECT(state->window), "person-dialog-state", state,
create_person_dialog_state_free);
if (state->tesseract_path != NULL) {
IdentityLanguageJob *job = g_new0(IdentityLanguageJob, 1);
job->executable = g_strdup(state->tesseract_path);
g_weak_ref_init(&job->window, G_OBJECT(state->window));
GTask *task = g_task_new(NULL, NULL, identity_languages_completed, job);
g_task_set_task_data(task, job, identity_language_job_free);
g_task_run_in_thread(task, identity_languages_worker);
g_object_unref(task);
}
gtk_window_present(state->window);
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
create_person_dialog_set_initial_ocr_paned_position,
g_object_ref(ocr_paned), g_object_unref);
return TRUE;
}
void create_person_dialog_result_free(CreatePersonDialogResult *result)
{
if (result == NULL) return;
g_free(result->designation); g_free(result->declared_name);
g_free(result->pseudonym); g_free(result->status); g_free(result->notes);
g_free(result->evidence_identifier);
g_clear_pointer(&result->role_assignments, g_ptr_array_unref);
g_clear_pointer(&result->evidence_selection,
person_evidence_selection_free);
g_clear_pointer(&result->staging, evidence_staging_free);
g_clear_pointer(&result->ocr_runs, g_ptr_array_unref);
g_free(result);
}
const PersonEntityInput *create_person_dialog_result_get_input(
const CreatePersonDialogResult *result)
{
return result != NULL ? &result->input : NULL;
}
const PersonEvidenceSelection *
create_person_dialog_result_get_evidence_selection(
const CreatePersonDialogResult *result)
{
return result != NULL ? result->evidence_selection : NULL;
}
EvidenceStaging *create_person_dialog_result_steal_staging(
CreatePersonDialogResult *result)
{
EvidenceStaging *staging = result != NULL ? result->staging : NULL;
if (result != NULL) result->staging = NULL;
return staging;
}
const GPtrArray *create_person_dialog_result_get_ocr_runs(
const CreatePersonDialogResult *result)
{
return result != NULL ? result->ocr_runs : NULL;
}
gboolean create_person_dialog_test_overlay_has_region(GtkWindow *window)
{
CreatePersonDialogState *state = window != NULL
? g_object_get_data(G_OBJECT(window), "person-dialog-state") : NULL;
return state != NULL &&
ocr_provenance_overlay_has_region(state->ocr_overlay);
}
guint64 create_person_dialog_test_ocr_generation(GtkWindow *window)
{
CreatePersonDialogState *state = window != NULL
? g_object_get_data(G_OBJECT(window), "person-dialog-state") : NULL;
return state != NULL ? state->ocr_generation : 0;
}