From f142de866317a739afbdaaf0b9aa846cf6044b0a Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Wed, 22 Jul 2026 18:47:38 +0200 Subject: [PATCH] feat(evidence): preview image and video evidence --- include/views/main_window.h | 4 + include/widgets/workspace.h | 9 ++ src/core/application.c | 31 ++++++ src/views/main_window.c | 8 ++ src/widgets/workspace.c | 188 ++++++++++++++++++++++++++++++++++++ 5 files changed, 240 insertions(+) diff --git a/include/views/main_window.h b/include/views/main_window.h index e121a18..20d737a 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -411,6 +411,10 @@ void main_window_set_selected_evidence( const EvidenceRecord *evidence_record ); +/** @brief Transmet au workspace un chemin absolu validé pour l'aperçu. */ +void main_window_set_evidence_preview(MainWindow *main_window, + const char *file_path, const char *display_name); + /** * @brief Affiche l'état de chargement du graphe. diff --git a/include/widgets/workspace.h b/include/widgets/workspace.h index 5ac2962..16e4d8e 100644 --- a/include/widgets/workspace.h +++ b/include/widgets/workspace.h @@ -165,6 +165,15 @@ void workspace_set_selected_evidence( const EvidenceRecord *evidence_record ); +/** + * @brief Affiche l'aperçu local d'un fichier image ou vidéo. + * + * file_path doit être un chemin absolu déjà validé par l'application. + * Passer NULL libère l'aperçu courant. + */ +void workspace_set_evidence_preview(Workspace *workspace, + const char *file_path, const char *display_name); + /** * @brief Sélectionne une entité dans le graphe affiché. * diff --git a/src/core/application.c b/src/core/application.c index 5de727e..fc64a1c 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -4404,6 +4404,13 @@ static void application_on_evidence_selected( const char *original_name = NULL; + const InvestigationProject *project = NULL; + const char *investigation_root = NULL; + const char *relative_path = NULL; + char *canonical_root = NULL; + char *candidate_path = NULL; + char *canonical_path = NULL; + char *status_message = NULL; @@ -4579,6 +4586,27 @@ static void application_on_evidence_selected( evidence_record ); + project = investigation_session_get_project(application->session); + investigation_root = project != NULL + ? investigation_project_get_root_path(project) : NULL; + relative_path = evidence_record_get_relative_path(evidence_record); + if (investigation_root != NULL && relative_path != NULL) + { + canonical_root = g_canonicalize_filename(investigation_root, NULL); + candidate_path = g_build_filename(investigation_root, + relative_path, NULL); + canonical_path = g_canonicalize_filename(candidate_path, NULL); + if (canonical_root != NULL && canonical_path != NULL && + g_str_has_prefix(canonical_path, canonical_root) && + canonical_path[strlen(canonical_root)] == G_DIR_SEPARATOR && + g_file_test(canonical_path, G_FILE_TEST_IS_REGULAR)) + { + main_window_set_evidence_preview(application->main_window, + canonical_path, + evidence_record_get_original_name(evidence_record)); + } + } + original_name = evidence_record_get_original_name( evidence_record @@ -4601,6 +4629,9 @@ static void application_on_evidence_selected( g_free( status_message ); + g_free(canonical_path); + g_free(candidate_path); + g_free(canonical_root); evidence_record_free( evidence_record diff --git a/src/views/main_window.c b/src/views/main_window.c index c6e1eab..26c2ab2 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -1727,6 +1727,14 @@ void main_window_set_selected_evidence( ); } +void main_window_set_evidence_preview(MainWindow *main_window, + const char *file_path, const char *display_name) +{ + if (main_window == NULL) return; + workspace_set_evidence_preview(main_window->workspace, file_path, + display_name); +} + void main_window_free( MainWindow *main_window ) diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index 178fbb8..3b8ac4b 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -90,6 +90,13 @@ struct Workspace GtkWidget *verify_evidence_button; GtkWidget *edit_evidence_button; GtkWidget *analyze_eml_button; + GtkWidget *evidence_preview_stack; + GtkWidget *evidence_preview_status; + GtkWidget *evidence_preview_picture; + GtkWidget *evidence_preview_video; + GtkWidget *evidence_preview_expand_button; + char *evidence_preview_path; + gboolean evidence_preview_is_video; char *selected_evidence_identifier; @@ -581,6 +588,79 @@ static const char *workspace_get_integrity_status_text( } } +/** @brief Affiche une erreur asynchrone produite par le moteur vidéo. */ +static void workspace_on_video_error_changed(GObject *object, + GParamSpec *parameter, gpointer user_data) +{ + Workspace *workspace = user_data; + const GError *error = NULL; + (void) parameter; + if (workspace == NULL || !GTK_IS_MEDIA_STREAM(object)) return; + error = gtk_media_stream_get_error(GTK_MEDIA_STREAM(object)); + if (error == NULL) return; + gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status), + error->message != NULL ? error->message : + "Le moteur vidéo ne peut pas lire ce fichier."); + gtk_stack_set_visible_child_name(GTK_STACK( + workspace->evidence_preview_stack), "status"); + gtk_widget_set_sensitive(workspace->evidence_preview_expand_button, + FALSE); +} + +/** @brief Arrête et détache le média vidéo actuellement affiché. */ +static void workspace_clear_evidence_preview(Workspace *workspace) +{ + GtkMediaStream *stream = NULL; + if (workspace == NULL) return; + if (workspace->evidence_preview_video != NULL) + { + stream = gtk_video_get_media_stream( + GTK_VIDEO(workspace->evidence_preview_video)); + if (stream != NULL) + { + g_signal_handlers_disconnect_by_data(stream, workspace); + gtk_media_stream_pause(stream); + } + gtk_video_set_media_stream(GTK_VIDEO( + workspace->evidence_preview_video), NULL); + } + if (workspace->evidence_preview_picture != NULL) + gtk_picture_set_paintable(GTK_PICTURE( + workspace->evidence_preview_picture), NULL); + g_clear_pointer(&workspace->evidence_preview_path, g_free); + workspace->evidence_preview_is_video = FALSE; + if (workspace->evidence_preview_expand_button != NULL) + gtk_widget_set_sensitive(workspace->evidence_preview_expand_button, + FALSE); +} + +/** @brief Ouvre le média courant dans une fenêtre d'aperçu agrandie. */ +static void workspace_on_expand_evidence_preview(GtkButton *button, + gpointer user_data) +{ + Workspace *workspace = user_data; + GtkWindow *window = NULL; + GtkWidget *content = NULL; + (void) button; + if (workspace == NULL || workspace->evidence_preview_path == NULL) return; + window = GTK_WINDOW(gtk_window_new()); + gtk_window_set_title(window, "Aperçu de la preuve"); + gtk_window_set_default_size(window, 960, 720); + if (workspace->evidence_preview_is_video) + { + content = gtk_video_new_for_filename(workspace->evidence_preview_path); + gtk_video_set_autoplay(GTK_VIDEO(content), FALSE); + } + else + { + content = gtk_picture_new_for_filename(workspace->evidence_preview_path); + gtk_picture_set_can_shrink(GTK_PICTURE(content), TRUE); + gtk_picture_set_content_fit(GTK_PICTURE(content), GTK_CONTENT_FIT_CONTAIN); + } + gtk_window_set_child(window, content); + gtk_window_present(window); +} + /** @brief Convertit le statut d'une relation en texte utilisateur. */ static const char *workspace_get_relation_status_text(RelationStatus status) { @@ -1209,6 +1289,38 @@ Workspace *workspace_new(void) workspace->evidence_name_label ); + workspace->evidence_preview_stack = gtk_stack_new(); + workspace->evidence_preview_status = gtk_label_new( + "Aucun aperçu disponible pour cette preuve."); + workspace->evidence_preview_picture = gtk_picture_new(); + workspace->evidence_preview_video = gtk_video_new(); + workspace->evidence_preview_expand_button = + gtk_button_new_with_label("Agrandir l’aperçu"); + gtk_widget_set_size_request(workspace->evidence_preview_stack, -1, 360); + gtk_widget_set_hexpand(workspace->evidence_preview_stack, TRUE); + gtk_picture_set_can_shrink(GTK_PICTURE( + workspace->evidence_preview_picture), TRUE); + gtk_picture_set_content_fit(GTK_PICTURE( + workspace->evidence_preview_picture), GTK_CONTENT_FIT_CONTAIN); + gtk_video_set_autoplay(GTK_VIDEO(workspace->evidence_preview_video), FALSE); + gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack), + workspace->evidence_preview_status, "status"); + gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack), + workspace->evidence_preview_picture, "image"); + gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack), + workspace->evidence_preview_video, "video"); + gtk_stack_set_visible_child_name(GTK_STACK( + workspace->evidence_preview_stack), "status"); + gtk_widget_set_halign(workspace->evidence_preview_expand_button, + GTK_ALIGN_START); + gtk_widget_set_sensitive(workspace->evidence_preview_expand_button, FALSE); + g_signal_connect(workspace->evidence_preview_expand_button, "clicked", + G_CALLBACK(workspace_on_expand_evidence_preview), workspace); + gtk_box_append(GTK_BOX(evidence_content), + workspace->evidence_preview_stack); + gtk_box_append(GTK_BOX(evidence_content), + workspace->evidence_preview_expand_button); + workspace->verify_evidence_button = gtk_button_new_with_label( "Vérifier l'intégrité" @@ -2339,6 +2451,11 @@ void workspace_set_selected_evidence( return; } + workspace_clear_evidence_preview(workspace); + if (workspace->evidence_preview_stack != NULL) + gtk_stack_set_visible_child_name(GTK_STACK( + workspace->evidence_preview_stack), "status"); + g_clear_pointer( &workspace->selected_evidence_identifier, g_free @@ -2520,6 +2637,75 @@ void workspace_set_selected_evidence( ); } +void workspace_set_evidence_preview(Workspace *workspace, + const char *file_path, const char *display_name) +{ + GFile *file = NULL; + GFileInfo *info = NULL; + const char *content_type = NULL; + char *mime_type = NULL; + GtkMediaStream *stream = NULL; + GError *error = NULL; + if (workspace == NULL) return; + workspace_clear_evidence_preview(workspace); + if (file_path == NULL || file_path[0] == '\0') + { + gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status), + "Aucun aperçu disponible pour cette preuve."); + gtk_stack_set_visible_child_name(GTK_STACK( + workspace->evidence_preview_stack), "status"); + return; + } + file = g_file_new_for_path(file_path); + info = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, NULL, &error); + if (info != NULL) content_type = g_file_info_get_content_type(info); + if (content_type != NULL) mime_type = g_content_type_get_mime_type( + content_type); + if (mime_type != NULL && g_str_has_prefix(mime_type, "image/")) + { + gtk_picture_set_filename(GTK_PICTURE( + workspace->evidence_preview_picture), file_path); + gtk_stack_set_visible_child_name(GTK_STACK( + workspace->evidence_preview_stack), "image"); + workspace->evidence_preview_is_video = FALSE; + } + else if (mime_type != NULL && g_str_has_prefix(mime_type, "video/")) + { + stream = gtk_media_file_new_for_filename(file_path); + g_signal_connect(stream, "notify::error", + G_CALLBACK(workspace_on_video_error_changed), workspace); + gtk_video_set_media_stream(GTK_VIDEO( + workspace->evidence_preview_video), stream); + g_object_unref(stream); + gtk_stack_set_visible_child_name(GTK_STACK( + workspace->evidence_preview_stack), "video"); + workspace->evidence_preview_is_video = TRUE; + } + else + { + char *message = g_strdup_printf( + "Aperçu indisponible pour %s%s%s.", + display_name != NULL ? display_name : "ce fichier", + error != NULL ? " : " : "", + error != NULL ? error->message : "format non pris en charge"); + gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status), + message); + gtk_stack_set_visible_child_name(GTK_STACK( + workspace->evidence_preview_stack), "status"); + g_free(message); + goto cleanup; + } + workspace->evidence_preview_path = g_strdup(file_path); + gtk_widget_set_sensitive(workspace->evidence_preview_expand_button, + workspace->evidence_preview_path != NULL); +cleanup: + g_clear_error(&error); + g_free(mime_type); + g_clear_object(&info); + g_clear_object(&file); +} + gboolean workspace_select_graph_entity( Workspace *workspace, const char *entity_identifier @@ -3049,6 +3235,8 @@ void workspace_free(Workspace *workspace) return; } + workspace_clear_evidence_preview(workspace); + g_clear_pointer( &workspace->osint_selection_context, osint_selection_context_free