feat(ocr): extract and create IBAN entities from evidence

This commit is contained in:
grayTerminal-sh 2026-07-22 23:43:16 +02:00
parent fb491639aa
commit e6298e4f8a
18 changed files with 495 additions and 5 deletions

View file

@ -125,6 +125,7 @@ TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service
TEST_SOCIAL_PLATFORM := tests/test_social_platform TEST_SOCIAL_PLATFORM := tests/test_social_platform
TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service
TEST_EML_ANALYZER := tests/test_eml_analyzer TEST_EML_ANALYZER := tests/test_eml_analyzer
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
all: $(TARGET) all: $(TARGET)
@ -647,6 +648,9 @@ $(TEST_PERSON_ENTITY_SERVICE): \
$(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c $(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_IBAN_ANALYZER): tests/test_iban_analyzer.c src/core/iban_analyzer.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \
tests/test_investigation_graph_load_task.c \ tests/test_investigation_graph_load_task.c \
src/core/investigation_graph_load_task.c \ src/core/investigation_graph_load_task.c \
@ -726,7 +730,8 @@ test: \
$(TEST_SOCIAL_ACCOUNT_SERVICE) \ $(TEST_SOCIAL_ACCOUNT_SERVICE) \
$(TEST_SOCIAL_PLATFORM) \ $(TEST_SOCIAL_PLATFORM) \
$(TEST_PERSON_ENTITY_SERVICE) \ $(TEST_PERSON_ENTITY_SERVICE) \
$(TEST_EML_ANALYZER) $(TEST_EML_ANALYZER) \
$(TEST_IBAN_ANALYZER)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@ -786,6 +791,7 @@ test: \
@$(TEST_SOCIAL_PLATFORM) @$(TEST_SOCIAL_PLATFORM)
@$(TEST_PERSON_ENTITY_SERVICE) @$(TEST_PERSON_ENTITY_SERVICE)
@$(TEST_EML_ANALYZER) @$(TEST_EML_ANALYZER)
@$(TEST_IBAN_ANALYZER)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c

View file

@ -0,0 +1,19 @@
/******************************************************************************
* @file iban_analyzer.h
* @brief Normalisation, validation et extraction locale d'IBAN.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_IBAN_ANALYZER_H
#define LABFY_INVESTIGATION_IBAN_ANALYZER_H
#include <glib.h>
G_BEGIN_DECLS
/** @brief Normalise un IBAN en majuscules sans séparateurs. */
char *iban_analyzer_normalize(const char *text);
/** @brief Vérifie la structure et la clé de contrôle modulo 97. */
gboolean iban_analyzer_validate(const char *iban);
/**
* @brief Extrait les IBAN valides d'un texte OCR.
* @return Tableau possédé de chaînes uniques normalisées.
*/
GPtrArray *iban_analyzer_extract(const char *ocr_text);
G_END_DECLS
#endif

13
include/core/rib_ocr.h Normal file
View file

@ -0,0 +1,13 @@
/******************************************************************************
* @file rib_ocr.h
* @brief Exécution locale de Tesseract pour les preuves de type RIB.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_RIB_OCR_H
#define LABFY_INVESTIGATION_RIB_OCR_H
#include <glib.h>
G_BEGIN_DECLS
/** @brief Exécute Tesseract localement et retourne le texte UTF-8 possédé. */
gboolean rib_ocr_extract_text(const char *image_path, char **out_text,
char **out_version, GError **error);
G_END_DECLS
#endif

View file

@ -101,6 +101,9 @@ typedef void (*MainWindowEditEvidenceCallback)(
/** @brief Callback appelé pour analyser une preuve EML. */ /** @brief Callback appelé pour analyser une preuve EML. */
typedef void (*MainWindowAnalyzeEmlCallback)(const char *evidence_identifier, typedef void (*MainWindowAnalyzeEmlCallback)(const char *evidence_identifier,
gpointer user_data); gpointer user_data);
/** @brief Callback appelé pour analyser un RIB par OCR. */
typedef void (*MainWindowAnalyzeRibCallback)(const char *evidence_identifier,
gpointer user_data);
/** /**
* @brief Callback appelé après le déplacement effectif d'un nœud. * @brief Callback appelé après le déplacement effectif d'un nœud.
@ -212,6 +215,9 @@ void main_window_set_edit_evidence_callback(
/** @brief Définit le callback d'analyse locale d'une preuve EML. */ /** @brief Définit le callback d'analyse locale d'une preuve EML. */
void main_window_set_analyze_eml_callback(MainWindow *main_window, void main_window_set_analyze_eml_callback(MainWindow *main_window,
MainWindowAnalyzeEmlCallback callback, gpointer user_data); MainWindowAnalyzeEmlCallback callback, gpointer user_data);
/** @brief Définit le callback d'analyse OCR d'un RIB. */
void main_window_set_analyze_rib_callback(MainWindow *main_window,
MainWindowAnalyzeRibCallback callback, gpointer user_data);
/** /**
* @brief Définit le callback de fin de déplacement d'un nœud. * @brief Définit le callback de fin de déplacement d'un nœud.

View file

@ -0,0 +1,15 @@
/******************************************************************************
* @file rib_ocr_review_dialog.h
* @brief Révision humaine des résultats OCR d'un RIB.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_RIB_OCR_REVIEW_DIALOG_H
#define LABFY_INVESTIGATION_RIB_OCR_REVIEW_DIALOG_H
#include <gtk/gtk.h>
G_BEGIN_DECLS
typedef void (*RibOcrReviewCallback)(const char *iban, gpointer user_data);
/** @brief Présente le texte OCR et un IBAN révisable. */
void rib_ocr_review_dialog_present(GtkWindow *parent, const char *ocr_text,
const char *suggested_iban, RibOcrReviewCallback callback,
gpointer user_data);
G_END_DECLS
#endif

View file

@ -186,7 +186,8 @@ const EntityRecord *investigation_graph_view_get_selected_entity(
/** /**
* @brief Sélectionne une entité du graphe par son UUID. * @brief Sélectionne une entité du graphe par son UUID.
* *
* La vue reste inchangée si l'identifiant est absent du graphe. * Le nœud trouvé est placé au centre de la zone visible. La vue reste
* inchangée si l'identifiant est absent du graphe.
* *
* @param graph_view Vue graphique à modifier. * @param graph_view Vue graphique à modifier.
* @param entity_identifier UUID de l'entité. * @param entity_identifier UUID de l'entité.

View file

@ -135,6 +135,9 @@ typedef void (*WorkspaceEditEvidenceCallback)(
/** @brief Callback appelé pour analyser une preuve EML. */ /** @brief Callback appelé pour analyser une preuve EML. */
typedef void (*WorkspaceAnalyzeEmlCallback)(const char *evidence_identifier, typedef void (*WorkspaceAnalyzeEmlCallback)(const char *evidence_identifier,
gpointer user_data); gpointer user_data);
/** @brief Callback appelé pour analyser un RIB par OCR. */
typedef void (*WorkspaceAnalyzeRibCallback)(const char *evidence_identifier,
gpointer user_data);
/** /**
* @brief Crée une nouvelle zone de travail. * @brief Crée une nouvelle zone de travail.
@ -268,6 +271,9 @@ void workspace_set_edit_evidence_callback(
/** @brief Définit le callback d'analyse locale d'une preuve EML. */ /** @brief Définit le callback d'analyse locale d'une preuve EML. */
void workspace_set_analyze_eml_callback(Workspace *workspace, void workspace_set_analyze_eml_callback(Workspace *workspace,
WorkspaceAnalyzeEmlCallback callback, gpointer user_data); WorkspaceAnalyzeEmlCallback callback, gpointer user_data);
/** @brief Définit le callback d'analyse OCR d'un RIB. */
void workspace_set_analyze_rib_callback(Workspace *workspace,
WorkspaceAnalyzeRibCallback callback, gpointer user_data);
/** /**
* @brief Définit le callback de vérification de la preuve affichée. * @brief Définit le callback de vérification de la preuve affichée.

View file

@ -60,6 +60,9 @@
#include "views/create_person_dialog.h" #include "views/create_person_dialog.h"
#include "views/eml_analysis_dialog.h" #include "views/eml_analysis_dialog.h"
#include "views/manage_entity_evidence_dialog.h" #include "views/manage_entity_evidence_dialog.h"
#include "views/rib_ocr_review_dialog.h"
#include "core/rib_ocr.h"
#include "core/iban_analyzer.h"
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include <errno.h> #include <errno.h>
@ -3386,6 +3389,144 @@ static void application_on_analyze_eml_requested(
evidence_dao_free(dao); g_clear_error(&error); evidence_dao_free(dao); g_clear_error(&error);
} }
/** @brief Contexte possédé pendant la révision OCR d'un RIB. */
typedef struct { Application *application; char *evidence_identifier;
char *ocr_text; char *tool_version; } ApplicationRibOcrContext;
/** @brief Libère le contexte OCR d'un RIB. */
static void application_rib_ocr_context_free(ApplicationRibOcrContext *context)
{
if (context == NULL) return;
g_free(context->evidence_identifier); g_free(context->ocr_text);
g_free(context->tool_version); g_free(context);
}
/** @brief Crée ou réutilise l'entité IBAN confirmée et la lie à la preuve. */
static void application_on_rib_ocr_confirmed(const char *iban,
gpointer user_data)
{
ApplicationRibOcrContext *context = user_data;
Application *application = context != NULL ? context->application : NULL;
Database *database = NULL; EntityDao *entity_dao = NULL;
EvidenceEntityDao *link_dao = NULL; GPtrArray *entities = NULL;
EntityRecord *record = NULL; const char *entity_identifier = NULL;
char *new_identifier = NULL; char *timestamp = NULL; char *description = NULL;
GDateTime *now = NULL; GError *error = NULL; gboolean active = FALSE;
const InvestigationProject *project = NULL;
if (iban == NULL || application == NULL || application->session == NULL)
goto cleanup;
database = investigation_session_get_database(application->session);
entity_dao = entity_dao_new(database, &error);
if (entity_dao == NULL) goto failure;
entities = entity_dao_list_all(entity_dao, &error);
if (entities == NULL) goto failure;
for (guint index = 0; index < entities->len; index++)
{
EntityRecord *candidate = g_ptr_array_index(entities, index);
if (g_strcmp0(entity_record_get_type_identifier(candidate), "iban") == 0 &&
g_strcmp0(entity_record_get_value(candidate), iban) == 0)
entity_identifier = entity_record_get_identifier(candidate);
}
if (!database_transaction_begin(database)) goto failure;
active = TRUE;
if (entity_identifier == NULL)
{
new_identifier = g_uuid_string_random(); now = g_date_time_new_now_utc();
timestamp = now != NULL ? g_date_time_format(now,
"%Y-%m-%dT%H:%M:%SZ") : NULL;
description = g_strdup_printf("IBAN extrait localement par OCR (%s).",
context->tool_version != NULL ? context->tool_version : "Tesseract");
record = entity_record_new(new_identifier, "iban", iban, "IBAN extrait du RIB",
description, 70, timestamp, timestamp, ENTITY_STATUS_ACTIVE, &error);
if (record == NULL || !entity_dao_insert(entity_dao, record, &error))
goto failure;
entity_identifier = new_identifier;
}
link_dao = evidence_entity_dao_new(database, &error);
if (link_dao == NULL) goto failure;
{
gboolean exists = FALSE;
if (!evidence_entity_dao_exists(link_dao, context->evidence_identifier,
entity_identifier, &exists, &error) ||
(!exists && !evidence_entity_dao_link(link_dao,
context->evidence_identifier, entity_identifier, &error)))
goto failure;
}
if (!database_transaction_commit(database)) goto failure;
active = FALSE; project = investigation_session_get_project(application->session);
g_free(application->pending_entity_selection_identifier);
application->pending_entity_selection_identifier =
g_strdup(entity_identifier);
main_window_set_status(application->main_window,
"Entité IBAN créée et reliée à la preuve.");
application_start_graph_loading(application,
investigation_project_get_database_path(project)); goto cleanup;
failure:
if (active) database_transaction_rollback(database);
application_present_error(application, "Intégration du RIB impossible",
error != NULL ? error->message : "La transaction a échoué.");
cleanup:
g_clear_error(&error); entity_record_free(record);
g_clear_pointer(&entities, g_ptr_array_unref); entity_dao_free(entity_dao);
evidence_entity_dao_free(link_dao); g_free(new_identifier);
g_free(timestamp); g_free(description); g_clear_pointer(&now, g_date_time_unref);
application_rib_ocr_context_free(context);
}
/** @brief Exécute localement Tesseract sur la preuve image sélectionnée. */
static void application_on_analyze_rib_requested(
const char *evidence_identifier, gpointer user_data)
{
Application *application = user_data; const InvestigationProject *project = NULL;
EvidenceDao *dao = NULL; EvidenceRecord *record = NULL;
ApplicationRibOcrContext *context = NULL; GPtrArray *ibans = NULL;
char *candidate_path = NULL; char *canonical_root = NULL; char *path = NULL;
char *output_path = NULL; GError *error = NULL;
char *work_copy_path = NULL; GFile *source_file = NULL; GFile *copy_file = NULL;
if (application == NULL || application->session == NULL) return;
dao = evidence_dao_new(investigation_session_get_database(application->session), &error);
if (dao != NULL) record = evidence_dao_find_by_identifier(dao,
evidence_identifier, &error);
project = investigation_session_get_project(application->session);
if (record == NULL || project == NULL) goto failure;
canonical_root = g_canonicalize_filename(
investigation_project_get_root_path(project), NULL);
candidate_path = g_build_filename(canonical_root,
evidence_record_get_relative_path(record), NULL);
path = g_canonicalize_filename(candidate_path, NULL);
if (path == NULL || !g_str_has_prefix(path, canonical_root) ||
(path[strlen(canonical_root)] != G_DIR_SEPARATOR &&
path[strlen(canonical_root)] != '\0')) goto failure;
work_copy_path = g_strdup_printf("%s/02_Preuves_Traitees/OCR/%s-%s",
canonical_root, evidence_identifier,
evidence_record_get_original_name(record));
source_file = g_file_new_for_path(path);
copy_file = g_file_new_for_path(work_copy_path);
if (!g_file_copy(source_file, copy_file, G_FILE_COPY_OVERWRITE,
NULL, NULL, NULL, &error)) goto failure;
context = g_new0(ApplicationRibOcrContext, 1);
context->application = application;
context->evidence_identifier = g_strdup(evidence_identifier);
if (!rib_ocr_extract_text(work_copy_path, &context->ocr_text,
&context->tool_version, &error)) goto failure;
output_path = g_strdup_printf("%s/02_Preuves_Traitees/OCR/%s-ocr.txt",
canonical_root, evidence_identifier);
if (!g_file_set_contents(output_path, context->ocr_text, -1, &error))
goto failure;
ibans = iban_analyzer_extract(context->ocr_text);
rib_ocr_review_dialog_present(main_window_get_window(application->main_window),
context->ocr_text, ibans->len > 0 ? g_ptr_array_index(ibans, 0) : NULL,
application_on_rib_ocr_confirmed, context);
context = NULL; goto cleanup;
failure:
application_rib_ocr_context_free(context); context = NULL;
application_present_error(application, "Analyse OCR impossible",
error != NULL ? error->message :
"La preuve n'a pas pu être analysée par Tesseract.");
cleanup:
g_clear_error(&error); g_clear_pointer(&ibans, g_ptr_array_unref);
g_free(output_path); g_free(path); g_free(candidate_path); g_free(canonical_root);
g_free(work_copy_path); g_clear_object(&source_file); g_clear_object(&copy_file);
evidence_record_free(record); evidence_dao_free(dao);
}
/** /**
* @brief Prépare et démarre limport asynchrone dun fichier. * @brief Prépare et démarre limport asynchrone dun fichier.
*/ */
@ -6955,6 +7096,8 @@ static void application_on_activate(
); );
main_window_set_analyze_eml_callback(application->main_window, main_window_set_analyze_eml_callback(application->main_window,
application_on_analyze_eml_requested, application); application_on_analyze_eml_requested, application);
main_window_set_analyze_rib_callback(application->main_window,
application_on_analyze_rib_requested, application);
main_window_set_graph_node_moved_callback( main_window_set_graph_node_moved_callback(
application->main_window, application->main_window,

64
src/core/iban_analyzer.c Normal file
View file

@ -0,0 +1,64 @@
/******************************************************************************
* @file iban_analyzer.c
* @brief Normalisation, validation et extraction locale d'IBAN.
******************************************************************************/
#include "core/iban_analyzer.h"
char *iban_analyzer_normalize(const char *text)
{
GString *result = NULL;
if (text == NULL) return NULL;
result = g_string_new(NULL);
for (const char *cursor = text; *cursor != '\0'; cursor++)
if (g_ascii_isalnum(*cursor))
g_string_append_c(result, g_ascii_toupper(*cursor));
if (result->len == 0) { g_string_free(result, TRUE); return NULL; }
return g_string_free(result, FALSE);
}
gboolean iban_analyzer_validate(const char *iban)
{
char *normalized = iban_analyzer_normalize(iban);
guint remainder = 0; gsize length = 0;
if (normalized == NULL) return FALSE;
length = strlen(normalized);
if (length < 15 || length > 34 || !g_ascii_isalpha(normalized[0]) ||
!g_ascii_isalpha(normalized[1]) || !g_ascii_isdigit(normalized[2]) ||
!g_ascii_isdigit(normalized[3])) { g_free(normalized); return FALSE; }
for (gsize offset = 0; offset < length; offset++)
{
char character = normalized[(offset + 4) % length];
if (g_ascii_isdigit(character))
remainder = (remainder * 10 + (guint)(character - '0')) % 97;
else if (g_ascii_isalpha(character))
{
guint value = (guint)(character - 'A') + 10;
remainder = (remainder * 10 + value / 10) % 97;
remainder = (remainder * 10 + value % 10) % 97;
}
else { g_free(normalized); return FALSE; }
}
g_free(normalized); return remainder == 1;
}
GPtrArray *iban_analyzer_extract(const char *text)
{
GPtrArray *results = g_ptr_array_new_with_free_func(g_free);
GRegex *regex = NULL; GMatchInfo *matches = NULL; GError *error = NULL;
if (text == NULL) return results;
regex = g_regex_new("(?i)\\b(?:FR[ \\t-]*[0-9]{2}(?:[ \\t-]*[A-Z0-9]){23}|[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30})\\b",
G_REGEX_OPTIMIZE, 0, &error);
if (regex == NULL) { g_clear_error(&error); return results; }
g_regex_match(regex, text, 0, &matches);
while (g_match_info_matches(matches))
{
char *raw = g_match_info_fetch(matches, 0);
char *candidate = iban_analyzer_normalize(raw);
gboolean duplicate = FALSE;
for (guint index = 0; candidate != NULL && index < results->len; index++)
if (g_strcmp0(candidate, g_ptr_array_index(results, index)) == 0)
duplicate = TRUE;
if (candidate != NULL && iban_analyzer_validate(candidate) && !duplicate)
g_ptr_array_add(results, candidate);
else g_free(candidate);
g_free(raw); g_match_info_next(matches, NULL);
}
g_match_info_free(matches); g_regex_unref(regex); return results;
}

45
src/core/rib_ocr.c Normal file
View file

@ -0,0 +1,45 @@
/******************************************************************************
* @file rib_ocr.c
* @brief Exécution locale de Tesseract pour les preuves de type RIB.
******************************************************************************/
#include "core/rib_ocr.h"
#include <gio/gio.h>
gboolean rib_ocr_extract_text(const char *image_path, char **out_text,
char **out_version, GError **error)
{
GSubprocess *process = NULL; GSubprocess *version_process = NULL;
char *stdout_text = NULL; char *stderr_text = NULL; char *version = NULL;
gboolean success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (out_text != NULL) *out_text = NULL;
if (out_version != NULL) *out_version = NULL;
if (image_path == NULL || !g_file_test(image_path, G_FILE_TEST_IS_REGULAR))
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"La preuve image à analyser est invalide."); return FALSE;
}
process = g_subprocess_new(G_SUBPROCESS_FLAGS_STDOUT_PIPE |
G_SUBPROCESS_FLAGS_STDERR_PIPE, error, "tesseract", image_path,
"stdout", "-l", "fra+eng", NULL);
if (process == NULL) goto cleanup;
if (!g_subprocess_communicate_utf8(process, NULL, NULL, &stdout_text,
&stderr_text, error) || !g_subprocess_get_successful(process))
{
if (error != NULL && *error == NULL)
g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Tesseract a échoué : %s", stderr_text != NULL
? stderr_text : "erreur inconnue");
goto cleanup;
}
version_process = g_subprocess_new(G_SUBPROCESS_FLAGS_STDOUT_PIPE |
G_SUBPROCESS_FLAGS_STDERR_PIPE, NULL, "tesseract", "--version", NULL);
if (version_process != NULL) g_subprocess_communicate_utf8(version_process,
NULL, NULL, &version, NULL, NULL);
if (out_text != NULL) *out_text = g_utf8_make_valid(stdout_text, -1);
if (out_version != NULL && version != NULL)
*out_version = g_strdup(g_strstrip(version));
success = out_text == NULL || *out_text != NULL;
cleanup:
g_free(stdout_text); g_free(stderr_text); g_free(version);
g_clear_object(&process); g_clear_object(&version_process); return success;
}

View file

@ -59,6 +59,11 @@ static const char *const tool_catalog_openssl_version_arguments[] =
"version", "version",
NULL NULL
}; };
static const char *const tool_catalog_tesseract_version_arguments[] =
{
"--version",
NULL
};
/** /**
* @brief Catalogue statique initial. * @brief Catalogue statique initial.
@ -109,6 +114,14 @@ static const ToolCatalogEntry tool_catalog_entries[] =
.version_arguments = .version_arguments =
tool_catalog_openssl_version_arguments, tool_catalog_openssl_version_arguments,
.version_argument_count = 1 .version_argument_count = 1
},
{
.identifier = "ocr.tesseract",
.display_name = "Tesseract OCR",
.executable_name = "tesseract",
.requirement = TOOL_REQUIREMENT_OPTIONAL,
.version_arguments = tool_catalog_tesseract_version_arguments,
.version_argument_count = 1
} }
}; };

View file

@ -110,6 +110,8 @@ struct MainWindow
gpointer edit_evidence_user_data; gpointer edit_evidence_user_data;
MainWindowAnalyzeEmlCallback analyze_eml_callback; MainWindowAnalyzeEmlCallback analyze_eml_callback;
gpointer analyze_eml_user_data; gpointer analyze_eml_user_data;
MainWindowAnalyzeRibCallback analyze_rib_callback;
gpointer analyze_rib_user_data;
MainWindowGraphNodeMovedCallback MainWindowGraphNodeMovedCallback
graph_node_moved_callback; graph_node_moved_callback;
@ -202,6 +204,14 @@ static void main_window_on_analyze_eml_requested(const char *identifier,
main_window->analyze_eml_callback(identifier, main_window->analyze_eml_callback(identifier,
main_window->analyze_eml_user_data); main_window->analyze_eml_user_data);
} }
/** @brief Relaie la demande d'analyse OCR d'un RIB. */
static void main_window_on_analyze_rib_requested(const char *identifier,
gpointer data)
{
MainWindow *window = data;
if (window != NULL && window->analyze_rib_callback != NULL)
window->analyze_rib_callback(identifier, window->analyze_rib_user_data);
}
/** /**
* @brief Ouvre dans le workspace l'entité choisie dans la sidebar. * @brief Ouvre dans le workspace l'entité choisie dans la sidebar.
@ -963,6 +973,8 @@ MainWindow *main_window_new(
); );
workspace_set_analyze_eml_callback(main_window->workspace, workspace_set_analyze_eml_callback(main_window->workspace,
main_window_on_analyze_eml_requested, main_window); main_window_on_analyze_eml_requested, main_window);
workspace_set_analyze_rib_callback(main_window->workspace,
main_window_on_analyze_rib_requested, main_window);
workspace_widget = workspace_get_widget( workspace_widget = workspace_get_widget(
main_window->workspace main_window->workspace
@ -1511,6 +1523,13 @@ void main_window_set_analyze_eml_callback(MainWindow *main_window,
main_window->analyze_eml_callback = callback; main_window->analyze_eml_callback = callback;
main_window->analyze_eml_user_data = user_data; main_window->analyze_eml_user_data = user_data;
} }
void main_window_set_analyze_rib_callback(MainWindow *main_window,
MainWindowAnalyzeRibCallback callback, gpointer user_data)
{
if (main_window == NULL) return;
main_window->analyze_rib_callback = callback;
main_window->analyze_rib_user_data = user_data;
}
void main_window_set_tree_selection_callback( void main_window_set_tree_selection_callback(
MainWindow *main_window, MainWindow *main_window,

View file

@ -0,0 +1,59 @@
/******************************************************************************
* @file rib_ocr_review_dialog.c
* @brief Révision humaine des résultats OCR d'un RIB.
******************************************************************************/
#include "views/rib_ocr_review_dialog.h"
#include "core/iban_analyzer.h"
typedef struct { GtkWindow *window; GtkEntry *iban; GtkLabel *error;
RibOcrReviewCallback callback; gpointer data; gboolean completed; } State;
/** @brief Libère l'état du dialogue. */
static void state_free(gpointer data) { g_free(data); }
/** @brief Valide et transmet l'IBAN corrigé. */
static void on_confirm(GtkButton *button, gpointer data)
{
State *state = data; const char *text = gtk_editable_get_text(
GTK_EDITABLE(state->iban)); char *normalized = NULL; (void) button;
normalized = iban_analyzer_normalize(text);
if (!iban_analyzer_validate(normalized))
{ gtk_label_set_text(state->error, "IBAN invalide : vérifiez les caractères OCR."); g_free(normalized); return; }
state->completed = TRUE; state->callback(normalized, state->data);
g_free(normalized); gtk_window_close(state->window);
}
/** @brief Signale l'annulation au contrôleur. */
static gboolean on_close(GtkWindow *window, gpointer data)
{
State *state = data; (void) window;
if (!state->completed) state->callback(NULL, state->data);
return FALSE;
}
void rib_ocr_review_dialog_present(GtkWindow *parent, const char *ocr_text,
const char *suggested, RibOcrReviewCallback callback, gpointer user_data)
{
State *state = g_new0(State, 1); GtkWidget *root = gtk_box_new(
GTK_ORIENTATION_VERTICAL, 10); GtkWidget *scroll = gtk_scrolled_window_new();
GtkWidget *view = gtk_text_view_new(); GtkWidget *confirm = NULL;
state->window = GTK_WINDOW(gtk_window_new()); state->callback = callback;
state->data = user_data; state->iban = GTK_ENTRY(gtk_entry_new());
state->error = GTK_LABEL(gtk_label_new(NULL));
gtk_window_set_title(state->window, "Réviser lanalyse OCR du RIB");
gtk_window_set_transient_for(state->window, parent); gtk_window_set_modal(state->window, TRUE);
gtk_window_set_default_size(state->window, 760, 620);
gtk_widget_set_margin_top(root, 16); gtk_widget_set_margin_bottom(root, 16);
gtk_widget_set_margin_start(root, 16); gtk_widget_set_margin_end(root, 16);
gtk_text_view_set_editable(GTK_TEXT_VIEW(view), FALSE);
gtk_text_buffer_set_text(gtk_text_view_get_buffer(GTK_TEXT_VIEW(view)),
ocr_text != NULL ? ocr_text : "", -1);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), view);
gtk_widget_set_vexpand(scroll, TRUE);
gtk_editable_set_text(GTK_EDITABLE(state->iban), suggested != NULL ? suggested : "");
confirm = gtk_button_new_with_label("Créer lentité IBAN");
gtk_widget_add_css_class(confirm, "suggested-action");
gtk_box_append(GTK_BOX(root), gtk_label_new("Texte OCR brut"));
gtk_box_append(GTK_BOX(root), scroll); gtk_box_append(GTK_BOX(root), gtk_label_new("IBAN détecté ou corrigé"));
gtk_box_append(GTK_BOX(root), GTK_WIDGET(state->iban)); gtk_box_append(GTK_BOX(root), GTK_WIDGET(state->error));
gtk_box_append(GTK_BOX(root), confirm); gtk_window_set_child(state->window, root);
g_object_set_data_full(G_OBJECT(state->window), "state", state, state_free);
g_signal_connect(confirm, "clicked", G_CALLBACK(on_confirm), state);
g_signal_connect(state->window, "close-request", G_CALLBACK(on_close), state);
gtk_window_present(state->window);
}

View file

@ -4435,6 +4435,8 @@ gboolean investigation_graph_view_select_entity(
) )
{ {
InvestigationGraphNodeLayout *node_layout = NULL; InvestigationGraphNodeLayout *node_layout = NULL;
int viewport_width = 0;
int viewport_height = 0;
if (graph_view == NULL || if (graph_view == NULL ||
graph_view->node_layouts_by_identifier == NULL || graph_view->node_layouts_by_identifier == NULL ||
@ -4460,6 +4462,25 @@ gboolean investigation_graph_view_select_entity(
node_layout node_layout
); );
if (graph_view->drawing_area != NULL)
{
viewport_width = gtk_widget_get_width(graph_view->drawing_area);
viewport_height = gtk_widget_get_height(graph_view->drawing_area);
if (viewport_width > 0 && viewport_height > 0)
{
graph_view->offset_x =
((double) viewport_width / 2.0) -
(investigation_graph_view_get_node_center_x(node_layout) *
graph_view->zoom);
graph_view->offset_y =
((double) viewport_height / 2.0) -
(investigation_graph_view_get_node_center_y(node_layout) *
graph_view->zoom);
gtk_widget_queue_draw(graph_view->drawing_area);
}
}
return TRUE; return TRUE;
} }

View file

@ -92,6 +92,7 @@ struct Workspace
GtkWidget *verify_evidence_button; GtkWidget *verify_evidence_button;
GtkWidget *edit_evidence_button; GtkWidget *edit_evidence_button;
GtkWidget *analyze_eml_button; GtkWidget *analyze_eml_button;
GtkWidget *analyze_rib_button;
GtkWidget *evidence_preview_stack; GtkWidget *evidence_preview_stack;
GtkWidget *evidence_preview_status; GtkWidget *evidence_preview_status;
GtkWidget *evidence_preview_picture; GtkWidget *evidence_preview_picture;
@ -112,6 +113,8 @@ struct Workspace
gpointer edit_evidence_user_data; gpointer edit_evidence_user_data;
WorkspaceAnalyzeEmlCallback analyze_eml_callback; WorkspaceAnalyzeEmlCallback analyze_eml_callback;
gpointer analyze_eml_user_data; gpointer analyze_eml_user_data;
WorkspaceAnalyzeRibCallback analyze_rib_callback;
gpointer analyze_rib_user_data;
WorkspaceGraphNodeMovedCallback WorkspaceGraphNodeMovedCallback
graph_node_moved_callback; graph_node_moved_callback;
@ -1079,6 +1082,15 @@ static void workspace_on_analyze_eml_clicked(GtkButton *button, gpointer data)
workspace->analyze_eml_callback(workspace->selected_evidence_identifier, workspace->analyze_eml_callback(workspace->selected_evidence_identifier,
workspace->analyze_eml_user_data); workspace->analyze_eml_user_data);
} }
/** @brief Transmet la demande d'analyse OCR du RIB affiché. */
static void workspace_on_analyze_rib_clicked(GtkButton *button, gpointer data)
{
Workspace *workspace = data; (void) button;
if (workspace != NULL && workspace->analyze_rib_callback != NULL &&
workspace->selected_evidence_identifier != NULL)
workspace->analyze_rib_callback(workspace->selected_evidence_identifier,
workspace->analyze_rib_user_data);
}
Workspace *workspace_new(void) Workspace *workspace_new(void)
{ {
@ -1449,6 +1461,13 @@ Workspace *workspace_new(void)
g_signal_connect(workspace->analyze_eml_button, "clicked", g_signal_connect(workspace->analyze_eml_button, "clicked",
G_CALLBACK(workspace_on_analyze_eml_clicked), workspace); G_CALLBACK(workspace_on_analyze_eml_clicked), workspace);
gtk_box_append(GTK_BOX(evidence_content), workspace->analyze_eml_button); gtk_box_append(GTK_BOX(evidence_content), workspace->analyze_eml_button);
workspace->analyze_rib_button = gtk_button_new_with_label(
"Analyser le RIB par OCR");
gtk_widget_set_halign(workspace->analyze_rib_button, GTK_ALIGN_START);
gtk_widget_set_sensitive(workspace->analyze_rib_button, FALSE);
g_signal_connect(workspace->analyze_rib_button, "clicked",
G_CALLBACK(workspace_on_analyze_rib_clicked), workspace);
gtk_box_append(GTK_BOX(evidence_content), workspace->analyze_rib_button);
evidence_separator = evidence_separator =
gtk_separator_new( gtk_separator_new(
@ -2433,6 +2452,8 @@ void workspace_set_selected_node(
gtk_widget_set_sensitive(workspace->edit_evidence_button, FALSE); gtk_widget_set_sensitive(workspace->edit_evidence_button, FALSE);
if (workspace->analyze_eml_button != NULL) if (workspace->analyze_eml_button != NULL)
gtk_widget_set_sensitive(workspace->analyze_eml_button, FALSE); gtk_widget_set_sensitive(workspace->analyze_eml_button, FALSE);
if (workspace->analyze_rib_button != NULL)
gtk_widget_set_sensitive(workspace->analyze_rib_button, FALSE);
if (node == NULL) if (node == NULL)
{ {
@ -2598,6 +2619,9 @@ void workspace_set_selected_evidence(
char *lower = name != NULL ? g_ascii_strdown(name, -1) : NULL; char *lower = name != NULL ? g_ascii_strdown(name, -1) : NULL;
gtk_widget_set_sensitive(workspace->analyze_eml_button, gtk_widget_set_sensitive(workspace->analyze_eml_button,
lower != NULL && g_str_has_suffix(lower, ".eml")); lower != NULL && g_str_has_suffix(lower, ".eml"));
gtk_widget_set_sensitive(workspace->analyze_rib_button,
lower != NULL && (g_str_has_suffix(lower, ".jpg") ||
g_str_has_suffix(lower, ".jpeg") || g_str_has_suffix(lower, ".png")));
g_free(lower); g_free(lower);
} }
@ -3226,6 +3250,13 @@ void workspace_set_analyze_eml_callback(Workspace *workspace,
workspace->analyze_eml_callback = callback; workspace->analyze_eml_callback = callback;
workspace->analyze_eml_user_data = user_data; workspace->analyze_eml_user_data = user_data;
} }
void workspace_set_analyze_rib_callback(Workspace *workspace,
WorkspaceAnalyzeRibCallback callback, gpointer user_data)
{
if (workspace == NULL) return;
workspace->analyze_rib_callback = callback;
workspace->analyze_rib_user_data = user_data;
}
void workspace_set_graph_node_moved_callback( void workspace_set_graph_node_moved_callback(
Workspace *workspace, Workspace *workspace,

View file

@ -0,0 +1,23 @@
/******************************************************************************
* @file test_iban_analyzer.c
* @brief Tests unitaires de l'analyseur IBAN.
******************************************************************************/
#include "core/iban_analyzer.h"
#include <assert.h>
#include <string.h>
#include <stdio.h>
int main(void)
{
char *normalized = iban_analyzer_normalize(
"fr76 3000 6000 0112 3456 7890 189");
GPtrArray *results = NULL;
assert(strcmp(normalized, "FR7630006000011234567890189") == 0);
assert(iban_analyzer_validate(normalized));
assert(!iban_analyzer_validate("FR7630006000011234567890188"));
results = iban_analyzer_extract(
"IBAN : FR76 3000 6000 0112 3456 7890 189\nAutre texte");
assert(results != NULL && results->len == 1);
assert(strcmp(g_ptr_array_index(results, 0), normalized) == 0);
g_ptr_array_unref(results); g_free(normalized);
puts("IbanAnalyzer : tous les tests sont valides."); return 0;
}

View file

@ -56,6 +56,12 @@ static const ExpectedToolCatalogEntry expected_catalog_entries[] =
.display_name = "OpenSSL", .display_name = "OpenSSL",
.executable_name = "openssl", .executable_name = "openssl",
.version_argument = "version" .version_argument = "version"
},
{
.identifier = "ocr.tesseract",
.display_name = "Tesseract OCR",
.executable_name = "tesseract",
.version_argument = "--version"
} }
}; };

View file

@ -1075,7 +1075,7 @@ static void test_tool_initializer_detect_versions(void)
g_assert_cmpuint( g_assert_cmpuint(
summary.total_count, summary.total_count,
==, ==,
5 tool_catalog_get_count()
); );
g_assert_cmpuint( g_assert_cmpuint(
@ -1087,7 +1087,7 @@ static void test_tool_initializer_detect_versions(void)
g_assert_cmpuint( g_assert_cmpuint(
summary.missing_count, summary.missing_count,
==, ==,
3 tool_catalog_get_count() - 2
); );
g_assert_cmpuint( g_assert_cmpuint(
@ -1248,7 +1248,7 @@ static void test_tool_initializer_version_failure_is_nonfatal(void)
g_assert_cmpuint( g_assert_cmpuint(
summary.missing_count, summary.missing_count,
==, ==,
3 tool_catalog_get_count() - 2
); );
g_assert_cmpuint( g_assert_cmpuint(