From e6298e4f8acaa93045e97af8bfac0c63c537fab3 Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Wed, 22 Jul 2026 23:43:16 +0200 Subject: [PATCH] feat(ocr): extract and create IBAN entities from evidence --- Makefile | 8 +- include/core/iban_analyzer.h | 19 +++ include/core/rib_ocr.h | 13 ++ include/views/main_window.h | 6 + include/views/rib_ocr_review_dialog.h | 15 +++ include/widgets/investigation_graph_view.h | 3 +- include/widgets/workspace.h | 6 + src/core/application.c | 143 +++++++++++++++++++++ src/core/iban_analyzer.c | 64 +++++++++ src/core/rib_ocr.c | 45 +++++++ src/core/tool_catalog.c | 13 ++ src/views/main_window.c | 19 +++ src/views/rib_ocr_review_dialog.c | 59 +++++++++ src/widgets/investigation_graph_view.c | 21 +++ src/widgets/workspace.c | 31 +++++ tests/test_iban_analyzer.c | 23 ++++ tests/test_tool_catalog.c | 6 + tests/test_tool_initializer.c | 6 +- 18 files changed, 495 insertions(+), 5 deletions(-) create mode 100644 include/core/iban_analyzer.h create mode 100644 include/core/rib_ocr.h create mode 100644 include/views/rib_ocr_review_dialog.h create mode 100644 src/core/iban_analyzer.c create mode 100644 src/core/rib_ocr.c create mode 100644 src/views/rib_ocr_review_dialog.c create mode 100644 tests/test_iban_analyzer.c diff --git a/Makefile b/Makefile index d78971a..fd2e2fe 100644 --- a/Makefile +++ b/Makefile @@ -125,6 +125,7 @@ TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service TEST_SOCIAL_PLATFORM := tests/test_social_platform TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service TEST_EML_ANALYZER := tests/test_eml_analyzer +TEST_IBAN_ANALYZER := tests/test_iban_analyzer all: $(TARGET) @@ -647,6 +648,9 @@ $(TEST_PERSON_ENTITY_SERVICE): \ $(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c $(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): \ tests/test_investigation_graph_load_task.c \ src/core/investigation_graph_load_task.c \ @@ -726,7 +730,8 @@ test: \ $(TEST_SOCIAL_ACCOUNT_SERVICE) \ $(TEST_SOCIAL_PLATFORM) \ $(TEST_PERSON_ENTITY_SERVICE) \ - $(TEST_EML_ANALYZER) + $(TEST_EML_ANALYZER) \ + $(TEST_IBAN_ANALYZER) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -786,6 +791,7 @@ test: \ @$(TEST_SOCIAL_PLATFORM) @$(TEST_PERSON_ENTITY_SERVICE) @$(TEST_EML_ANALYZER) + @$(TEST_IBAN_ANALYZER) @echo "Tous les tests sont valides." %.o: %.c diff --git a/include/core/iban_analyzer.h b/include/core/iban_analyzer.h new file mode 100644 index 0000000..cb27ee1 --- /dev/null +++ b/include/core/iban_analyzer.h @@ -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 +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 diff --git a/include/core/rib_ocr.h b/include/core/rib_ocr.h new file mode 100644 index 0000000..679ff70 --- /dev/null +++ b/include/core/rib_ocr.h @@ -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 +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 diff --git a/include/views/main_window.h b/include/views/main_window.h index e2863c9..cd24217 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -101,6 +101,9 @@ typedef void (*MainWindowEditEvidenceCallback)( /** @brief Callback appelé pour analyser une preuve EML. */ typedef void (*MainWindowAnalyzeEmlCallback)(const char *evidence_identifier, 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. @@ -212,6 +215,9 @@ void main_window_set_edit_evidence_callback( /** @brief Définit le callback d'analyse locale d'une preuve EML. */ void main_window_set_analyze_eml_callback(MainWindow *main_window, 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. diff --git a/include/views/rib_ocr_review_dialog.h b/include/views/rib_ocr_review_dialog.h new file mode 100644 index 0000000..43c67c8 --- /dev/null +++ b/include/views/rib_ocr_review_dialog.h @@ -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 +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 diff --git a/include/widgets/investigation_graph_view.h b/include/widgets/investigation_graph_view.h index 43e41c2..7f660f5 100644 --- a/include/widgets/investigation_graph_view.h +++ b/include/widgets/investigation_graph_view.h @@ -186,7 +186,8 @@ const EntityRecord *investigation_graph_view_get_selected_entity( /** * @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 entity_identifier UUID de l'entité. diff --git a/include/widgets/workspace.h b/include/widgets/workspace.h index 7c54677..8f85dec 100644 --- a/include/widgets/workspace.h +++ b/include/widgets/workspace.h @@ -135,6 +135,9 @@ typedef void (*WorkspaceEditEvidenceCallback)( /** @brief Callback appelé pour analyser une preuve EML. */ typedef void (*WorkspaceAnalyzeEmlCallback)(const char *evidence_identifier, 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. @@ -268,6 +271,9 @@ void workspace_set_edit_evidence_callback( /** @brief Définit le callback d'analyse locale d'une preuve EML. */ void workspace_set_analyze_eml_callback(Workspace *workspace, 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. diff --git a/src/core/application.c b/src/core/application.c index 276b68f..b0cbfc5 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -60,6 +60,9 @@ #include "views/create_person_dialog.h" #include "views/eml_analysis_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 #include @@ -3386,6 +3389,144 @@ static void application_on_analyze_eml_requested( 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(©_file); + evidence_record_free(record); evidence_dao_free(dao); +} + /** * @brief Prépare et démarre l’import asynchrone d’un fichier. */ @@ -6955,6 +7096,8 @@ static void application_on_activate( ); main_window_set_analyze_eml_callback(application->main_window, 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( application->main_window, diff --git a/src/core/iban_analyzer.c b/src/core/iban_analyzer.c new file mode 100644 index 0000000..80ef5fc --- /dev/null +++ b/src/core/iban_analyzer.c @@ -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; +} diff --git a/src/core/rib_ocr.c b/src/core/rib_ocr.c new file mode 100644 index 0000000..5c88d40 --- /dev/null +++ b/src/core/rib_ocr.c @@ -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 +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; +} diff --git a/src/core/tool_catalog.c b/src/core/tool_catalog.c index 021780c..8d840b3 100644 --- a/src/core/tool_catalog.c +++ b/src/core/tool_catalog.c @@ -59,6 +59,11 @@ static const char *const tool_catalog_openssl_version_arguments[] = "version", NULL }; +static const char *const tool_catalog_tesseract_version_arguments[] = +{ + "--version", + NULL +}; /** * @brief Catalogue statique initial. @@ -109,6 +114,14 @@ static const ToolCatalogEntry tool_catalog_entries[] = .version_arguments = tool_catalog_openssl_version_arguments, .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 } }; diff --git a/src/views/main_window.c b/src/views/main_window.c index 7dcd10c..76947bf 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -110,6 +110,8 @@ struct MainWindow gpointer edit_evidence_user_data; MainWindowAnalyzeEmlCallback analyze_eml_callback; gpointer analyze_eml_user_data; + MainWindowAnalyzeRibCallback analyze_rib_callback; + gpointer analyze_rib_user_data; MainWindowGraphNodeMovedCallback 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_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. @@ -963,6 +973,8 @@ MainWindow *main_window_new( ); workspace_set_analyze_eml_callback(main_window->workspace, 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( 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_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( MainWindow *main_window, diff --git a/src/views/rib_ocr_review_dialog.c b/src/views/rib_ocr_review_dialog.c new file mode 100644 index 0000000..3925b94 --- /dev/null +++ b/src/views/rib_ocr_review_dialog.c @@ -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 l’analyse 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 l’entité 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); +} diff --git a/src/widgets/investigation_graph_view.c b/src/widgets/investigation_graph_view.c index 97af47f..b752d09 100644 --- a/src/widgets/investigation_graph_view.c +++ b/src/widgets/investigation_graph_view.c @@ -4435,6 +4435,8 @@ gboolean investigation_graph_view_select_entity( ) { InvestigationGraphNodeLayout *node_layout = NULL; + int viewport_width = 0; + int viewport_height = 0; if (graph_view == NULL || graph_view->node_layouts_by_identifier == NULL || @@ -4460,6 +4462,25 @@ gboolean investigation_graph_view_select_entity( 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; } diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index 2f28f09..2fbf35f 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -92,6 +92,7 @@ struct Workspace GtkWidget *verify_evidence_button; GtkWidget *edit_evidence_button; GtkWidget *analyze_eml_button; + GtkWidget *analyze_rib_button; GtkWidget *evidence_preview_stack; GtkWidget *evidence_preview_status; GtkWidget *evidence_preview_picture; @@ -112,6 +113,8 @@ struct Workspace gpointer edit_evidence_user_data; WorkspaceAnalyzeEmlCallback analyze_eml_callback; gpointer analyze_eml_user_data; + WorkspaceAnalyzeRibCallback analyze_rib_callback; + gpointer analyze_rib_user_data; WorkspaceGraphNodeMovedCallback 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_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) { @@ -1449,6 +1461,13 @@ Workspace *workspace_new(void) g_signal_connect(workspace->analyze_eml_button, "clicked", G_CALLBACK(workspace_on_analyze_eml_clicked), workspace); 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 = gtk_separator_new( @@ -2433,6 +2452,8 @@ void workspace_set_selected_node( gtk_widget_set_sensitive(workspace->edit_evidence_button, FALSE); if (workspace->analyze_eml_button != NULL) 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) { @@ -2598,6 +2619,9 @@ void workspace_set_selected_evidence( char *lower = name != NULL ? g_ascii_strdown(name, -1) : NULL; gtk_widget_set_sensitive(workspace->analyze_eml_button, 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); } @@ -3226,6 +3250,13 @@ void workspace_set_analyze_eml_callback(Workspace *workspace, workspace->analyze_eml_callback = callback; 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( Workspace *workspace, diff --git a/tests/test_iban_analyzer.c b/tests/test_iban_analyzer.c new file mode 100644 index 0000000..adbb4b4 --- /dev/null +++ b/tests/test_iban_analyzer.c @@ -0,0 +1,23 @@ +/****************************************************************************** + * @file test_iban_analyzer.c + * @brief Tests unitaires de l'analyseur IBAN. + ******************************************************************************/ +#include "core/iban_analyzer.h" +#include +#include +#include +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; +} diff --git a/tests/test_tool_catalog.c b/tests/test_tool_catalog.c index 67eccb7..a80c613 100644 --- a/tests/test_tool_catalog.c +++ b/tests/test_tool_catalog.c @@ -56,6 +56,12 @@ static const ExpectedToolCatalogEntry expected_catalog_entries[] = .display_name = "OpenSSL", .executable_name = "openssl", .version_argument = "version" + }, + { + .identifier = "ocr.tesseract", + .display_name = "Tesseract OCR", + .executable_name = "tesseract", + .version_argument = "--version" } }; diff --git a/tests/test_tool_initializer.c b/tests/test_tool_initializer.c index fc26583..1e0435a 100644 --- a/tests/test_tool_initializer.c +++ b/tests/test_tool_initializer.c @@ -1075,7 +1075,7 @@ static void test_tool_initializer_detect_versions(void) g_assert_cmpuint( summary.total_count, ==, - 5 + tool_catalog_get_count() ); g_assert_cmpuint( @@ -1087,7 +1087,7 @@ static void test_tool_initializer_detect_versions(void) g_assert_cmpuint( summary.missing_count, ==, - 3 + tool_catalog_get_count() - 2 ); g_assert_cmpuint( @@ -1248,7 +1248,7 @@ static void test_tool_initializer_version_failure_is_nonfatal(void) g_assert_cmpuint( summary.missing_count, ==, - 3 + tool_catalog_get_count() - 2 ); g_assert_cmpuint(