feat(ui): ajouter l’aperçu avancé et uniformiser les dialogues

This commit is contained in:
grayTerminal-sh 2026-07-30 09:29:13 +02:00
parent dae5f63932
commit d7131aa1b6
37 changed files with 1784 additions and 378 deletions

View file

@ -2,6 +2,18 @@
### Added ### Added
- OCR didentité contrôlé et révisable dans la création dune personne,
limport normal et la fiche de preuve, avec texte brut immuable,
transcription corrigée persistante, `manual_override`, `manual_entry` et
notes documentaires factuelles.
- Consultation complète des données OCR persistées, sélection explicite dun
`OcrRun`, révision sans relance de Tesseract et création dun nouveau run
lors dune nouvelle analyse.
- Aperçu partagé avec zoom, ajustement, défilement bidirectionnel, navigation
PDF multipage, compteur de pages et provenance OCR synchronisée.
- Politique commune des dialogues GTK métiers complexes : parent transitoire,
géométrie responsive, répartition initiale 2/31/3, formulaire défilable et
barre dactions fixe.
- Préparation du pivot e-mail : pipeline EML asynchrone, extraction MIME - Préparation du pivot e-mail : pipeline EML asynchrone, extraction MIME
sécurisée, propositions bancaires IBAN/BIC et vocabulaire contrôlé. sécurisée, propositions bancaires IBAN/BIC et vocabulaire contrôlé.
@ -23,6 +35,12 @@
### Fixed ### Fixed
- Rafraîchissement immédiat de Workspace après import ou révision OCR et
persistance des données sur lUUID définitif de la preuve, y compris après
fermeture et réouverture de SQLite.
- Fermeture sûre du dialogue après enregistrement dune révision OCR, avec
protection contre le double clic, maintien ouvert en cas déchec et absence
de relance de Tesseract.
- Conservation du cadrage du graphe pendant les rechargements. - Conservation du cadrage du graphe pendant les rechargements.
- Persistance SQLite du zoom et de la position du graphe entre deux sessions. - Persistance SQLite du zoom et de la position du graphe entre deux sessions.
- Réduction de la fenêtre dintégration des extractions pour maintenir les - Réduction de la fenêtre dintégration des extractions pour maintenir les

View file

@ -164,6 +164,7 @@ TEST_OCR_PROVENANCE_OVERLAY_GTK := tests/test_ocr_provenance_overlay_gtk
TEST_EVIDENCE_METADATA_DIALOG_GTK := tests/test_evidence_metadata_dialog_gtk TEST_EVIDENCE_METADATA_DIALOG_GTK := tests/test_evidence_metadata_dialog_gtk
TEST_EVIDENCE_IDENTITY_IMPORT_GTK := tests/test_evidence_identity_import_gtk TEST_EVIDENCE_IDENTITY_IMPORT_GTK := tests/test_evidence_identity_import_gtk
TEST_WORKSPACE_IDENTITY_OCR_GTK := tests/test_workspace_identity_ocr_gtk TEST_WORKSPACE_IDENTITY_OCR_GTK := tests/test_workspace_identity_ocr_gtk
TEST_DIALOG_GEOMETRY_GTK := tests/test_dialog_geometry_gtk
FAKE_DOCUMENT_TOOL := tests/fake_document_tool FAKE_DOCUMENT_TOOL := tests/fake_document_tool
DOCUMENT_ANALYSIS_TEST_SOURCES := \ DOCUMENT_ANALYSIS_TEST_SOURCES := \
@ -178,6 +179,10 @@ $(TEST_DOCUMENT_TOOL_RUNNER): tests/test_document_tool_runner.c \
tests/test_document_tool_runner.c \ tests/test_document_tool_runner.c \
$(DOCUMENT_ANALYSIS_TEST_SOURCES) -o $@ $(TEST_LDFLAGS) $(DOCUMENT_ANALYSIS_TEST_SOURCES) -o $@ $(TEST_LDFLAGS)
$(TEST_DIALOG_GEOMETRY_GTK): tests/test_dialog_geometry_gtk.c \
src/views/dialog_geometry.c
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
all: $(TARGET) all: $(TARGET)
$(TEST_IDENTITY_OCR): tests/test_identity_ocr.c \ $(TEST_IDENTITY_OCR): tests/test_identity_ocr.c \
@ -571,6 +576,7 @@ $(TEST_EVIDENCE_TYPE_DAO): \
$(TEST_EVIDENCE_IMPORT_DIALOG): \ $(TEST_EVIDENCE_IMPORT_DIALOG): \
tests/test_evidence_import_dialog.c \ tests/test_evidence_import_dialog.c \
src/views/evidence_import_dialog.c \ src/views/evidence_import_dialog.c \
src/views/dialog_geometry.c \
src/models/evidence_type.c \ src/models/evidence_type.c \
src/widgets/evidence_preview_widget.c \ src/widgets/evidence_preview_widget.c \
src/core/evidence_preview_task.c src/core/evidence_preview.c \ src/core/evidence_preview_task.c src/core/evidence_preview.c \
@ -883,7 +889,8 @@ $(TEST_PERSON_DIALOG_LIFECYCLE): tests/test_person_dialog_lifecycle.c \
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS) $(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
$(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \ $(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
src/views/create_person_dialog.c src/core/person_dialog_lifecycle.c \ src/views/create_person_dialog.c src/views/dialog_geometry.c \
src/core/person_dialog_lifecycle.c \
src/core/person_confirmation_summary.c \ src/core/person_confirmation_summary.c \
src/core/evidence_staging.c src/core/evidence_staging_task.c \ src/core/evidence_staging.c src/core/evidence_staging_task.c \
src/core/evidence_preview_task.c src/core/evidence_preview.c \ src/core/evidence_preview_task.c src/core/evidence_preview.c \
@ -906,7 +913,8 @@ $(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
$(TEST_CREATE_PERSON_DIALOG_OCR_GTK): \ $(TEST_CREATE_PERSON_DIALOG_OCR_GTK): \
tests/test_create_person_dialog_ocr_gtk.c \ tests/test_create_person_dialog_ocr_gtk.c \
src/views/create_person_dialog.c src/core/person_dialog_lifecycle.c \ src/views/create_person_dialog.c src/views/dialog_geometry.c \
src/core/person_dialog_lifecycle.c \
src/core/person_confirmation_summary.c \ src/core/person_confirmation_summary.c \
src/core/evidence_staging.c src/core/evidence_staging_task.c \ src/core/evidence_staging.c src/core/evidence_staging_task.c \
src/core/evidence_preview_task.c src/core/evidence_preview.c \ src/core/evidence_preview_task.c src/core/evidence_preview.c \
@ -1122,7 +1130,8 @@ test: \
$(TEST_OCR_PROVENANCE_OVERLAY_GTK) \ $(TEST_OCR_PROVENANCE_OVERLAY_GTK) \
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \ $(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \ $(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
$(TEST_WORKSPACE_IDENTITY_OCR_GTK) $(TEST_WORKSPACE_IDENTITY_OCR_GTK) \
$(TEST_DIALOG_GEOMETRY_GTK)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@ -1216,6 +1225,7 @@ test: \
@$(TEST_EVIDENCE_METADATA_DIALOG_GTK) @$(TEST_EVIDENCE_METADATA_DIALOG_GTK)
@$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) @$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK)
@$(TEST_WORKSPACE_IDENTITY_OCR_GTK) @$(TEST_WORKSPACE_IDENTITY_OCR_GTK)
@$(TEST_DIALOG_GEOMETRY_GTK)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c
@ -1313,6 +1323,7 @@ clean:
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \ $(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \ $(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
$(TEST_WORKSPACE_IDENTITY_OCR_GTK) \ $(TEST_WORKSPACE_IDENTITY_OCR_GTK) \
$(TEST_DIALOG_GEOMETRY_GTK) \
$(FAKE_DOCUMENT_TOOL) $(FAKE_DOCUMENT_TOOL)

View file

@ -123,20 +123,30 @@ Les outils externes sont lancés avec `GSubprocess` et des arguments séparés.
## Fonctionnalités déjà présentes ## Fonctionnalités déjà présentes
La création dune personne propose une étape facultative dOCR contrôlé des La création dune personne et limport normal dune preuve proposent un OCR
documents didentité. Elle ne démarre que sur action explicite, travaille sur contrôlé des documents didentité. Il ne démarre que sur action explicite,
une copie vérifiée PNG, JPEG, HEIC, HEIF ou sur une page PDF choisie, et laisse travaille sur une copie vérifiée PNG, JPEG, HEIC, HEIF ou sur une page PDF
chaque champ à létat « À vérifier ». Lacceptation, la modification ou le rejet choisie, et conserve séparément le texte OCR brut immuable et une transcription
seffectuent champ par champ, sans verdict dauthenticité, reconnaissance corrigée rééditable et réinitialisable. Les propositions restent révisables :
faciale, fusion de personne ni changement automatique du statut elles peuvent être acceptées, corrigées à nouveau, restaurées depuis leur
didentification. valeur brute ou rejetées. Un champ visible mais omis par lOCR peut être saisi
avec lorigine `manual_entry` ; une correction dune valeur extraite conserve
lorigine `manual_override`. Des notes factuelles peuvent signaler un document
tronqué, flou, masqué ou incomplet, sans reconstruire une zone absente.
Ce parcours est également disponible lors de limport normal dune preuve, La fiche directe dune preuve relit depuis SQLite le texte brut, la
avec sélection explicite ou présélection de la personne. Il traite une seule transcription corrigée, les exécutions `OcrRun`, observations, champs, notes,
preuve par opération. Limport multiple sans OCR reste disponible ; plusieurs artefacts, empreintes SHA-256, personne liée et provenance graphique. Un run
preuves doivent être OCRisées successivement, chacune dans son propre est sélectionné explicitement lorsquun historique en contient plusieurs.
parcours. Les dialogues concernés affichent la liste à gauche et un grand « Réviser lanalyse OCR » modifie uniquement ce run sans relancer Tesseract ;
aperçu redimensionnable à droite. « Relancer une nouvelle analyse » crée un nouveau run sans écraser
lhistorique.
Limport multiple sans OCR reste disponible. LOCR groupé nest pas pris en
charge : les preuves importées ensemble sont ensuite analysées une par une
depuis leur fiche, sur leur UUID définitif et sans doublon. Aucun de ces
parcours ne produit de verdict dauthenticité, de reconnaissance faciale, de
fusion de personne, didentité certaine ou de rôle dauteur automatique.
Le socle actuel comprend notamment : Le socle actuel comprend notamment :
@ -212,8 +222,18 @@ Le socle actuel comprend notamment :
définitifs quaprès confirmation globale ; définitifs quaprès confirmation globale ;
- rattachement transactionnel de toutes les preuves retenues, avec rollback - rattachement transactionnel de toutes les preuves retenues, avec rollback
SQLite et suppression compensatoire des copies définitives en cas déchec. SQLite et suppression compensatoire des copies définitives en cas déchec.
- aperçu asynchrone contrôlé PNG/JPEG, HEIC/HEIF, MP4/MOV, texte, EML et - aperçu asynchrone contrôlé PNG/JPEG, HEIC/HEIF, MP4/MOV, texte, EML et PDF
première page PDF, sans OCR ni écriture SQLite ; multipage, avec zoom de 25 à 400 %, ajustement, défilements horizontal et
vertical, navigation et compteur de pages ; la provenance OCR reste alignée
avec la page, le zoom et le défilement ;
Les dialogues métiers complexes suivent une politique GTK commune : parent
réel via `transient_for`, modalité adaptée et présentation sans coordonnées
absolues sous Wayland. Ils visent 1200 × 800, avec un minimum utile de
800 × 600 lorsque la zone de travail le permet, un formulaire défilable à
gauche sur environ deux tiers, un aperçu redimensionnable à droite et une
barre dactions fixe. Les alertes simples, popups `GtkDropDown` et sélecteurs
de fichiers natifs ne sont pas concernés.
### Pivot EML ### Pivot EML
@ -390,6 +410,21 @@ Les nouveaux modules doivent être accompagnés de tests couvrant :
- les responsabilités mémoire ; - les responsabilités mémoire ;
- les régressions possibles. - les régressions possibles.
Les parcours GTK OCR et aperçu doivent aussi être exécutés séparément sur un
poste avec affichage :
```bash
G_DEBUG=fatal-criticals timeout 30s ./tests/test_create_person_dialog_ocr_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_identity_import_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_workspace_identity_ocr_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_preview_widget_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_dialog_geometry_gtk
```
Les tests emploient uniquement des documents `SPECIMEN`, des fichiers
temporaires et des bases SQLite temporaires fermées puis rouvertes. Un `SKIP`
lié à labsence daffichage ne remplace pas cette validation GTK réelle.
--- ---
## Développement ## Développement

View file

@ -1,28 +1,36 @@
# Architecture # Architecture
`EvidencePreviewWidget` est ladaptateur GTK unique de `EvidencePreview` pour `EvidencePreviewWidget` est ladaptateur GTK partagé de `EvidencePreview` pour
`CreatePersonDialog` et la fiche directe. Il encapsule les états, le rendu `CreatePersonDialog`, limport, la révision OCR et la fiche directe. Il
EML/PDF/image/texte/vidéo, lannulation, la génération, la garde de session et encapsule les états, le rendu EML/PDF/image/texte/vidéo, lannulation, la
larrêt du média. Les deux écrans lui transmettent uniquement une source génération, la garde de session et larrêt du média. Les écrans lui
interne ou de staging accompagnée du SHA-256 attendu. transmettent uniquement une source interne ou de staging accompagnée du
SHA-256 attendu.
## Aperçu contrôlé multi-format ## Aperçu contrôlé multi-format
`EvidencePreview` vérifie lempreinte du fichier interne ou de staging, `EvidencePreview` vérifie lempreinte du fichier interne ou de staging,
détecte son contenu et produit un résultat borné sans GTK. `BackgroundTask` détecte son contenu et produit un résultat borné sans GTK. `BackgroundTask`
travaille hors du thread principal ; `CreatePersonDialog` y crée ensuite travaille hors du thread principal ; le widget crée ensuite textures, buffers
textures, buffers et médias, rejette les générations périmées et arrête tout et médias sur le contexte GTK, rejette les générations périmées et arrête tout
média au changement ou à la fermeture. Laperçu ne persiste rien. média au changement ou à la fermeture. Laperçu ne persiste rien.
Pour les images et PDF, la barre compacte fournit zoom avant, zoom arrière et
retour à lajustement. Une image agrandie reste défilable horizontalement et
verticalement. Les PDF multipages disposent des actions page précédente et
suivante et dun compteur `Page X / N` ; le changement de page conserve le
zoom. `OcrProvenanceOverlay` applique le même zoom, le même défilement et la
même page afin que la provenance reste alignée.
Le mode aperçu de `EmlMimeExtractor` réutilise le parcours récursif et les Le mode aperçu de `EmlMimeExtractor` réutilise le parcours récursif et les
décodages RFC 2047/2231 sans écrire les pièces jointes. Il préfère décodages RFC 2047/2231 sans écrire les pièces jointes. Il préfère
`text/plain`, transforme un éventuel HTML en texte inerte et retourne `text/plain`, transforme un éventuel HTML en texte inerte et retourne
linventaire possédé. Le contrôleur vidéo indépendant de GTK orchestre linventaire possédé. Le contrôleur vidéo indépendant de GTK orchestre
pause, retour au début, détachement et libération via des actions injectées. pause, retour au début, détachement et libération via des actions injectées.
> **Version :** 3.1 > **Version :** 3.2
> **Dernière mise à jour :** 2026-07-28 > **Dernière mise à jour :** 2026-07-30
> **Schéma SQLite courant :** V14 > **Schéma SQLite courant :** V17
## Personnes contextuelles — SQLite V14 ## Personnes contextuelles — SQLite V14
@ -33,32 +41,64 @@ confiance et notes. Les anciens codes sont copiés littéralement depuis
les rôles et le rattachement manuel dans une transaction unique. les rôles et le rattachement manuel dans une transaction unique.
Le cœur `EvidencePreview` vérifie lintégrité avant décodage PNG/JPEG, applique Le cœur `EvidencePreview` vérifie lintégrité avant décodage PNG/JPEG, applique
des bornes et ne renvoie quun PNG mémoire réduit. Une `BackgroundTask` des bornes et ne renvoie quun rendu mémoire réduit. Une `BackgroundTask`
travaille hors du thread GTK ; la texture est créée sur le contexte principal. travaille hors du thread GTK ; la texture est créée sur le contexte principal.
Le dialogue annule lancienne tâche et rejette les générations obsolètes. Le dialogue annule lancienne tâche et rejette les générations obsolètes.
Linterface conserve une génération de session et les chemins stables du Linterface conserve une génération de session et les chemins stables du
projet et de sa base. PDF, vidéo, EML avancé et OCR restent exclus. projet et de sa base.
## OCR contrôlé didentité — SQLite V15 ## OCR contrôlé didentité — SQLite V15 à V17
La V15 sépare lexécution OCR, lobservation du document et les observations `IdentityOcrWorkflow` est lunique orchestration du moteur OCR, réutilisée par
de champs. Le moteur reçoit uniquement une copie contrôlée ou une page PDF `CreatePersonDialog`, `EvidenceIdentityOcrDialog`, limport normal et
explicitement choisie, conserve texte brut, TSV, paramètres, langues, version, Workspace. La V15 sépare lexécution OCR, lobservation du document et les
SHA-256, confiance et coordonnées, puis laisse la révision à lutilisateur. observations de champs. La V16 ajoute notamment lorigine `manual_entry` pour
Les widgets naccèdent pas à SQLite : le coordinateur persiste les décisions un champ visible mais omis par lOCR. La V17 ajoute une transcription corrigée
acceptées ou modifiées dans la transaction finale et compense les fichiers en humaine distincte du texte OCR brut.
cas déchec.
Le moteur reçoit uniquement une copie contrôlée ou une page PDF explicitement
choisie, conserve texte brut, TSV, paramètres, langues, version, SHA-256,
confiance et coordonnées, puis laisse la révision à lutilisateur. Le texte
brut nest jamais remplacé. Une correction de valeur garde
`manual_override`; une saisie sans valeur OCR garde `manual_entry`. Les notes
restent factuelles et ne reconstruisent jamais une partie absente dun
document tronqué.
Les widgets naccèdent pas à SQLite. Le coordinateur persiste sur lUUID
définitif de la preuve dans une transaction unique, avec rollback,
compensation des fichiers et garde de session juste avant commit. Limport
multiple reste sans OCR groupé : chaque preuve est analysée ensuite,
individuellement et sans réimport ni doublon.
La provenance visuelle est rendue par `OcrProvenanceOverlay`. La conversion La provenance visuelle est rendue par `OcrProvenanceOverlay`. La conversion
des coordonnées source vers la zone affichée est indépendante de GTK et tient des coordonnées source vers la zone affichée est indépendante de GTK et tient
compte du ratio, de la réduction, de lagrandissement et des marges. Le compte du ratio, de la réduction, de lagrandissement et des marges. Le
rectangle est transitoire : aucune annotation nest écrite dans la preuve. rectangle est transitoire : aucune annotation nest écrite dans la preuve.
Les langues proposées proviennent exclusivement de `tesseract --list-langs`. Les langues proposées proviennent exclusivement de `tesseract --list-langs`.
Le DAO V15 expose des lectures possédées pour les exécutions, observations de Les DAO structurés exposent des lectures possédées pour les runs, observations
documents et observations de champs, ainsi que des listes ordonnées par preuve, de documents et de champs, notes et artefacts. La fiche de preuve recharge ces
personne ou observation documentaire. données depuis SQLite, sélectionne explicitement un run dans un modèle
`GtkDropDown` stable et affiche texte brut, transcription corrigée, personne
liée, SHA-256 et provenance graphique. Réviser conserve lUUID du run et ne
relance pas Tesseract ; relancer crée un nouveau run sans écraser
lhistorique.
> **Statut :** architecture courante > **Statut :** architecture courante
## Politique des dialogues GTK métiers
`labfy_dialog_prepare()` et `labfy_dialog_present()` centralisent la séquence
des dialogues complexes : `transient_for` vers la vraie fenêtre parente,
modalité appropriée, géométrie commune, puis `gtk_window_present()`. Aucune
coordonnée absolue nest utilisée sous GTK4/Wayland.
La cible initiale est proche de 1200 × 800, avec un minimum utile de
800 × 600 lorsque la zone de travail le permet. Un `GtkPaned` place
initialement le formulaire défilable à gauche sur environ deux tiers et
laperçu à droite sur un tiers ; sa position nest appliquée quaprès la
première allocation réelle puis reste entièrement modifiable. Les actions
restent fixes en bas. Cette politique exclut les alertes simples, les popups
`GtkDropDown` et les sélecteurs de fichiers natifs.
--- ---
## 1. Objectif ## 1. Objectif
@ -355,7 +395,7 @@ validation du chemin
création de l'arborescence création de l'arborescence
initialisation transactionnelle de SQLite V14 initialisation transactionnelle de SQLite V17
création de l'identité de l'enquête création de l'identité de l'enquête
@ -460,9 +500,9 @@ sont supprimées après rollback. `PersonCreationTask` exécute cette orchestrat
hors du thread GTK. Un changement de session annule les tâches et interdit le hors du thread GTK. Un changement de session annule les tâches et interdit le
rafraîchissement dune autre enquête. rafraîchissement dune autre enquête.
Les aperçus PNG/JPEG utilisent exclusivement le fichier interne dune preuve Les aperçus utilisent exclusivement le fichier interne dune preuve existante
existante ou sa copie de staging. PDF, vidéo, EML et autres formats ne sont ou sa copie de staging. LOCR didentité nest lancé que sur demande explicite
pas analysés dans cette tranche. et sur un format compatible contrôlé.
Les tâches longues utilisent l'infrastructure de tâche d'arrière-plan et le Les tâches longues utilisent l'infrastructure de tâche d'arrière-plan et le
gestionnaire de tâches. gestionnaire de tâches.
@ -791,6 +831,14 @@ Les scénarios critiques incluent :
- conservation de la valeur brute ; - conservation de la valeur brute ;
- absence d'une dépendance optionnelle. - absence d'une dépendance optionnelle.
Les parcours OCR ferment puis rouvrent leurs bases SQLite temporaires afin de
vérifier la persistance du texte brut, de la transcription corrigée, des
champs et de lhistorique multi-run. Les tests GTK réels passent par
`MainWindow`, `Workspace` et les contrôles de production, avec des fixtures
`SPECIMEN`, `G_DEBUG=fatal-criticals` et un timeout. Ils couvrent aussi
laperçu PNG/JPEG/PDF multipage, la provenance, la géométrie, lannulation,
les résultats tardifs, les erreurs de transaction et les cycles de vie.
Validation : Validation :
```sh ```sh

View file

@ -1,17 +1,20 @@
# Guide de développement # Guide de développement
Pour la tranche OCR identité, les tests ciblés sont Pour lOCR didentité, les tests ciblés sont `tests/test_identity_ocr`,
`tests/test_identity_ocr`, `tests/test_identity_ocr_preprocessor`, `tests/test_identity_ocr_preprocessor`,
`tests/test_person_creation_coordinator`, `tests/test_person_creation_coordinator`,
`tests/test_ocr_provenance_overlay_gtk` et `tests/test_create_person_dialog_ocr_gtk`,
`tests/test_create_person_dialog_gtk`. Le test Tesseract réel est facultatif `tests/test_evidence_identity_import_gtk`,
`tests/test_evidence_metadata_dialog_gtk`,
`tests/test_workspace_identity_ocr_gtk` et
`tests/test_ocr_provenance_overlay_gtk`. Le test Tesseract réel est facultatif
et signore explicitement si loutil ou une langue compatible manque. et signore explicitement si loutil ou une langue compatible manque.
Les fixtures directes du préprocesseur sont générées en mémoire ou dans un Les fixtures directes du préprocesseur sont générées en mémoire ou dans un
répertoire temporaire : JPEG avec APP1 EXIF, HEIC/HEIF via libheif et PDF répertoire temporaire : JPEG avec APP1 EXIF, HEIC/HEIF via libheif et PDF
multipage via Cairo. Elles ne doivent jamais être remplacées par un document multipage via Cairo. Elles ne doivent jamais être remplacées par un document
réel. réel.
La validation ciblée du composant partagé comprend La validation ciblée de laperçu partagé comprend
`test_evidence_preview_widget_gtk`, `test_evidence_preview`, `test_evidence_preview_widget_gtk`, `test_evidence_preview`,
`test_evidence_video_preview_controller` et `test_evidence_video_preview_controller` et
`test_create_person_dialog_gtk`, ce dernier sous `test_create_person_dialog_gtk`, ce dernier sous
@ -27,8 +30,8 @@ conteneur synthétique. Les transformations dorientation libheif restent
appliquées par le décodage standard ; seule limage principale est rendue et appliquées par le décodage standard ; seule limage principale est rendue et
le nombre dimages de premier niveau est exposé. le nombre dimages de premier niveau est exposé.
> **Version :** 2.1 > **Version :** 2.2
> **Dernière mise à jour :** 2026-07-28 > **Dernière mise à jour :** 2026-07-30
> **Projet :** Labfy Investigation > **Projet :** Labfy Investigation
--- ---
@ -282,6 +285,30 @@ make -j8 \
Ces tests utilisent uniquement des fichiers `SPECIMEN`, des répertoires Ces tests utilisent uniquement des fichiers `SPECIMEN`, des répertoires
temporaires et des bases SQLite temporaires. temporaires et des bases SQLite temporaires.
Tests GTK réels de lOCR, de laperçu et de la géométrie :
```sh
make -j8 \
tests/test_create_person_dialog_ocr_gtk \
tests/test_evidence_identity_import_gtk \
tests/test_evidence_metadata_dialog_gtk \
tests/test_workspace_identity_ocr_gtk \
tests/test_evidence_preview_widget_gtk \
tests/test_dialog_geometry_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_create_person_dialog_ocr_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_identity_import_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_metadata_dialog_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_workspace_identity_ocr_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_preview_widget_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_dialog_geometry_gtk
```
Ils doivent cliquer sur les contrôles de production, passer par `MainWindow`
et `Workspace` lorsque le scénario le demande, puis fermer et rouvrir la base
SQLite temporaire. Un environnement sans affichage peut produire un `SKIP`,
mais celui-ci ne remplace pas lexécution séparée sur le poste GTK réel.
La fixture manuelle est La fixture manuelle est
`tests/fixtures/eml/manual_smoke_test.eml`. Elle est exclusivement `tests/fixtures/eml/manual_smoke_test.eml`. Elle est exclusivement
synthétique. Pour la validation GTK, créer une enquête neuve dans un synthétique. Pour la validation GTK, créer une enquête neuve dans un
@ -541,6 +568,15 @@ Un dialogue :
- ne réalise pas une opération longue directement ; - ne réalise pas une opération longue directement ;
- présente un résumé avant une écriture importante. - présente un résumé avant une écriture importante.
Les dialogues métiers complexes utilisent `labfy_dialog_prepare()` puis
`labfy_dialog_present()`. La vraie fenêtre parente est définie avec
`transient_for`; aucune coordonnée absolue nest utilisée. Ils visent
1200 × 800 et un minimum utile de 800 × 600, avec formulaire défilable à
gauche sur environ deux tiers, aperçu à droite et barre dactions fixe. Le
séparateur reste déplaçable et nest pas réinitialisé après sa première
allocation. Cette règle ne concerne pas les alertes, popups `GtkDropDown` ou
sélecteurs natifs.
### 11.3 Messages utilisateur ### 11.3 Messages utilisateur
Les détails techniques restent dans les journaux. Les détails techniques restent dans les journaux.
@ -663,3 +699,24 @@ Avant de considérer une modification terminée :
- [ ] `make -j8 test` passe ; - [ ] `make -j8 test` passe ;
- [ ] `git diff --check` passe ; - [ ] `git diff --check` passe ;
- [ ] la documentation est à jour. - [ ] la documentation est à jour.
## 16. Validation manuelle du ticket #109
Utiliser exclusivement une enquête, une base SQLite et des documents
`SPECIMEN` temporaires. Vérifier au minimum :
1. création dune personne avec OCR, correction de transcription, réédition
dun champ, `manual_entry`, notes et confirmation ;
2. visibilité immédiate de la preuve dans Workspace, puis fermeture et
réouverture de SQLite ;
3. import multiple sans OCR, puis analyse individuelle de chaque preuve depuis
sa fiche, sans doublon ;
4. sélection de plusieurs `OcrRun`, révision du seul run choisi sans
Tesseract, puis nouvelle analyse créant un run supplémentaire ;
5. aperçu PNG, JPEG et PDF `SPECIMEN` multipage : zoom, ajustement,
défilements, navigation, compteur et provenance ;
6. dialogues à 1200 × 800 et 760 × 560 : parent transitoire, formulaire
défilable, répartition 2/31/3 et actions toujours visibles.
La validation manuelle réussie confirme ces parcours, mais ne clôt pas le
ticket #109 et ne couvre pas les limitations encore listées dans la roadmap.

View file

@ -1,34 +1,23 @@
# Roadmap # Roadmap
La tranche 3 du ticket #109 ajoute lOCR contrôlé des documents didentité dans Le ticket #109 reste en cours. Ses tranches livrées couvrent lOCR contrôlé et
lassistant de création : lancement explicite, staging, révision champ par révisable dans la création dune personne, limport normal et la fiche de
champ et persistance transactionnelle V15. Elle ne clôt pas le ticket #109 et preuve, la transcription corrigée persistante, lhistorique multi-run, la
nengage aucune tranche suivante. provenance graphique, ainsi que laperçu avancé partagé. Limport multiple
La provenance graphique, les langues Tesseract dynamiques et la compensation reste sans OCR groupé : chaque preuve est analysée individuellement après
complète des répertoires OCR font partie de cette même tranche. import afin de préserver une association non ambiguë.
La lecture structurée V15 et les scénarios de fermeture/réouverture restent
également circonscrits à la tranche 3.
LOCR contrôlé est limité à une preuve par opération dans limport normal.
Limport multiple sans OCR reste disponible. LOCR multiple constitue un
chantier futur distinct ; les preuves sont actuellement analysées
successivement afin de conserver une association non ambiguë.
La tranche 2 partage désormais laperçu entre lassistant personne et la Laperçu couvre PNG/JPEG, HEIC/HEIF, MP4/MOV, texte, EML passif et PDF
fiche directe. La validation manuelle des codecs installés reste nécessaire ; multipage, avec zoom, ajustement, défilements, navigation et compteur. La
elle ne vaut pas achèvement du ticket #109 et ninclut aucun OCR. politique commune des dialogues métiers fournit une géométrie responsive, un
formulaire défilable, un aperçu redimensionnable et des actions fixes.
Laperçu contrôlé du ticket #109 couvre PNG/JPEG, HEIC/HEIF, MP4/MOV, texte, La validation manuelle de ces parcours est réussie. Elle ne vaut pas
EML passif et première page PDF. OCR et analyse avancée restent hors périmètre. achèvement du ticket #109.
> **Dernière mise à jour :** 2026-07-28 > **Dernière mise à jour :** 2026-07-30
> **État du projet :** développement actif > **État du projet :** développement actif
> **Schéma SQLite courant :** V14 > **Schéma SQLite courant :** V17
Les tranches 1 et 2 du ticket #109 couvrent les rôles contextuels multiples,
la sélection de plusieurs preuves existantes, le staging et limport multiple
différé, ainsi que la création transactionnelle avec compensation des
fichiers. OCR, extraction de documents didentité et analyses documentaires
avancées restent hors périmètre.
> **Usage opérationnel :** non prêt pour la production > **Usage opérationnel :** non prêt pour la production
--- ---
@ -142,7 +131,7 @@ La branche `main` contient notamment :
- création, validation et ouverture d'enquêtes ; - création, validation et ouverture d'enquêtes ;
- session d'enquête remplaçable proprement ; - session d'enquête remplaçable proprement ;
- infrastructure SQLite et migrations jusqu'à V14 ; - infrastructure SQLite et migrations jusqu'à V17 ;
- couche Database, DAO et services métier ; - couche Database, DAO et services métier ;
- import de preuves avec copie contrôlée et SHA-256 ; - import de preuves avec copie contrôlée et SHA-256 ;
- vérification d'intégrité et reclassement des preuves ; - vérification d'intégrité et reclassement des preuves ;
@ -170,6 +159,40 @@ Cette liste est une synthèse et non un contrat de stabilité.
## 5. Chantier actif ## 5. Chantier actif
### Ticket #109 — Personnes, preuves et OCR didentité
Statut technique :
```text
PARTIEL — VALIDATION MANUELLE DES TRANCHES LIVRÉES RÉUSSIE
```
Éléments livrés :
- sélection de preuves existantes, staging et import multiple différé ;
- OCR contrôlé, correction rééditable et transcription corrigée distincte ;
- persistance transactionnelle sur lUUID définitif avec historique multi-run ;
- consultation, révision sans Tesseract et nouvelle analyse depuis la fiche ;
- provenance graphique et aperçu partagé avec PDF multipage ;
- politique commune de géométrie des dialogues GTK métiers.
Limitations restantes :
1. lOCR groupé de plusieurs preuves dans une même opération nest pas pris en
charge ;
2. le statut contrôlé dauthenticité du document et la justification
obligatoire des statuts affirmatifs ne sont pas implémentés ;
3. la relation factuelle typée entre personne et preuve et certains états
didentification demandés restent incomplets ;
4. une partie du vocabulaire des rôles demeure codée en dur ;
5. la provenance uniforme de tous les dérivés et la projection des seules
valeurs OCR confirmées vers les attributs structurés de la personne restent
partielles ;
6. les tests négatifs garantissant labsence de relation automatique
attribuant lidentité ou le rôle dauteur doivent encore être renforcés.
Ces limites interdisent de présenter le ticket #109 comme terminé.
### Ticket #107 — Pivot e-mail forensique ### Ticket #107 — Pivot e-mail forensique
Statut technique : Statut technique :

View file

@ -24,6 +24,7 @@ typedef struct {
char *expected_sha256; char *expected_sha256;
char *mime_type; char *mime_type;
guint64 request_generation; guint64 request_generation;
guint pdf_page;
} EvidencePreviewRequest; } EvidencePreviewRequest;
typedef struct { typedef struct {
EvidencePreviewKind kind; EvidencePreviewKind kind;
@ -40,6 +41,7 @@ typedef struct {
char *message; char *message;
guint64 size_bytes; guint64 size_bytes;
guint item_count; guint item_count;
guint current_page;
gboolean integrity_valid; gboolean integrity_valid;
gboolean preview_available; gboolean preview_available;
gboolean truncated; gboolean truncated;
@ -47,6 +49,8 @@ typedef struct {
EvidencePreviewRequest *evidence_preview_request_new(const char *root, EvidencePreviewRequest *evidence_preview_request_new(const char *root,
const char *evidence_identifier, const char *relative_path, const char *evidence_identifier, const char *relative_path,
const char *sha256, const char *mime_type, guint64 generation); const char *sha256, const char *mime_type, guint64 generation);
void evidence_preview_request_set_pdf_page(
EvidencePreviewRequest *request, guint page);
void evidence_preview_request_free(EvidencePreviewRequest *request); void evidence_preview_request_free(EvidencePreviewRequest *request);
EvidencePreviewResult *evidence_preview_load( EvidencePreviewResult *evidence_preview_load(
const EvidencePreviewRequest *request, GCancellable *cancellable, const EvidencePreviewRequest *request, GCancellable *cancellable,

View file

@ -0,0 +1,30 @@
#ifndef LABFY_DIALOG_GEOMETRY_H
#define LABFY_DIALOG_GEOMETRY_H
#include <gtk/gtk.h>
typedef struct {
int initial_width;
int initial_height;
int minimum_width;
int minimum_height;
} LabfyDialogGeometry;
LabfyDialogGeometry labfy_dialog_geometry_calculate(
int workarea_width, int workarea_height);
void labfy_dialog_apply_standard_geometry(
GtkWindow *window, GtkWindow *parent);
void labfy_dialog_prepare(
GtkWindow *window,
GtkWindow *parent,
gboolean modal,
gboolean destroy_with_parent);
void labfy_dialog_present(GtkWindow *window);
void labfy_paned_apply_initial_ratio(
GtkPaned *paned, double ratio, int minimum_start, int minimum_end);
#endif

View file

@ -25,6 +25,19 @@ const char *evidence_preview_widget_get_state(
const EvidencePreviewWidget *widget); const EvidencePreviewWidget *widget);
char *evidence_preview_widget_dup_text( char *evidence_preview_widget_dup_text(
const EvidencePreviewWidget *widget); const EvidencePreviewWidget *widget);
void evidence_preview_widget_zoom_in(EvidencePreviewWidget *widget);
void evidence_preview_widget_zoom_out(EvidencePreviewWidget *widget);
void evidence_preview_widget_fit(EvidencePreviewWidget *widget);
double evidence_preview_widget_get_zoom(
const EvidencePreviewWidget *widget);
gboolean evidence_preview_widget_is_fit(
const EvidencePreviewWidget *widget);
gboolean evidence_preview_widget_set_pdf_page(
EvidencePreviewWidget *widget, guint page);
guint evidence_preview_widget_get_pdf_page(
const EvidencePreviewWidget *widget);
guint evidence_preview_widget_get_pdf_page_count(
const EvidencePreviewWidget *widget);
G_END_DECLS G_END_DECLS
#endif #endif

View file

@ -15,6 +15,13 @@ void ocr_provenance_overlay_set_field(OcrProvenanceOverlay *overlay,
const IdentityFieldObservation *field, guint64 generation); const IdentityFieldObservation *field, guint64 generation);
void ocr_provenance_overlay_clear(OcrProvenanceOverlay *overlay, void ocr_provenance_overlay_clear(OcrProvenanceOverlay *overlay,
guint64 generation); guint64 generation);
void ocr_provenance_overlay_set_page(
OcrProvenanceOverlay *overlay, guint page, guint64 generation);
void ocr_provenance_overlay_zoom_in(OcrProvenanceOverlay *overlay);
void ocr_provenance_overlay_zoom_out(OcrProvenanceOverlay *overlay);
void ocr_provenance_overlay_fit(OcrProvenanceOverlay *overlay);
double ocr_provenance_overlay_get_zoom(
const OcrProvenanceOverlay *overlay);
gboolean ocr_provenance_overlay_has_region( gboolean ocr_provenance_overlay_has_region(
const OcrProvenanceOverlay *overlay); const OcrProvenanceOverlay *overlay);

View file

@ -299,8 +299,8 @@ failed:
if (handle != NULL) heif_image_handle_release(handle); if (handle != NULL) heif_image_handle_release(handle);
heif_context_free(context); return FALSE; heif_context_free(context); return FALSE;
} }
static gboolean load_pdf(const char *path, EvidencePreviewResult *result, static gboolean load_pdf(const char *path, guint requested_page,
GError **error) EvidencePreviewResult *result, GError **error)
{ {
char *uri = g_filename_to_uri(path, NULL, error); char *uri = g_filename_to_uri(path, NULL, error);
if (uri == NULL) return FALSE; if (uri == NULL) return FALSE;
@ -309,7 +309,21 @@ static gboolean load_pdf(const char *path, EvidencePreviewResult *result,
gint pages = poppler_document_get_n_pages(document); gint pages = poppler_document_get_n_pages(document);
if (pages < 1) { g_object_unref(document); g_set_error_literal(error, if (pages < 1) { g_object_unref(document); g_set_error_literal(error,
preview_error(), 7, "Le PDF ne contient aucune page."); return FALSE; } preview_error(), 7, "Le PDF ne contient aucune page."); return FALSE; }
PopplerPage *page = poppler_document_get_page(document, 0); if (requested_page >= (guint) pages) {
g_object_unref(document);
g_set_error(error, preview_error(), 8,
"La page PDF %u est hors limites (document de %d pages).",
requested_page + 1U, pages);
return FALSE;
}
PopplerPage *page = poppler_document_get_page(
document, (gint) requested_page);
if (page == NULL) {
g_object_unref(document);
g_set_error_literal(error, preview_error(), 7,
"La page PDF demandée est indisponible.");
return FALSE;
}
double page_width = 0, page_height = 0; double page_width = 0, page_height = 0;
poppler_page_get_size(page, &page_width, &page_height); poppler_page_get_size(page, &page_width, &page_height);
if (!valid_dimensions((gint) page_width, (gint) page_height, error)) { if (!valid_dimensions((gint) page_width, (gint) page_height, error)) {
@ -324,6 +338,7 @@ static gboolean load_pdf(const char *path, EvidencePreviewResult *result,
cairo_t *cr = cairo_create(surface); cairo_set_source_rgb(cr, 1, 1, 1); cairo_t *cr = cairo_create(surface); cairo_set_source_rgb(cr, 1, 1, 1);
cairo_paint(cr); cairo_scale(cr, scale, scale); poppler_page_render(page, cr); cairo_paint(cr); cairo_scale(cr, scale, scale); poppler_page_render(page, cr);
cairo_destroy(cr); result->item_count = (guint) pages; cairo_destroy(cr); result->item_count = (guint) pages;
result->current_page = requested_page + 1U;
gboolean ok = surface_to_result(surface, width, height, result, error); gboolean ok = surface_to_result(surface, width, height, result, error);
cairo_surface_destroy(surface); g_object_unref(page); cairo_surface_destroy(surface); g_object_unref(page);
g_object_unref(document); return ok; g_object_unref(document); return ok;
@ -396,6 +411,12 @@ EvidencePreviewRequest *evidence_preview_request_new(const char *root,
request->mime_type = g_strdup(mime_type); request->mime_type = g_strdup(mime_type);
request->request_generation = generation; return request; request->request_generation = generation; return request;
} }
void evidence_preview_request_set_pdf_page(
EvidencePreviewRequest *request, guint page)
{
if (request != NULL) request->pdf_page = page;
}
void evidence_preview_request_free(EvidencePreviewRequest *request) void evidence_preview_request_free(EvidencePreviewRequest *request)
{ {
if (request == NULL) return; if (request == NULL) return;
@ -501,7 +522,7 @@ EvidencePreviewResult *evidence_preview_load(
result->kind = EVIDENCE_PREVIEW_KIND_PDF; result->kind = EVIDENCE_PREVIEW_KIND_PDF;
result->effective_mime_type = g_strdup("application/pdf"); result->effective_mime_type = g_strdup("application/pdf");
result->effective_format = g_strdup("PDF"); result->effective_format = g_strdup("PDF");
ok = load_pdf(path, result, error); break; ok = load_pdf(path, request->pdf_page, result, error); break;
default: default:
result->message = g_strdup("Format non pris en charge ou contenu invalide."); result->message = g_strdup("Format non pris en charge ou contenu invalide.");
result->effective_format = g_strdup("Inconnu"); break; result->effective_format = g_strdup("Inconnu"); break;

View file

@ -2,9 +2,12 @@
static EvidencePreviewRequest *copy_request(const EvidencePreviewRequest *r) static EvidencePreviewRequest *copy_request(const EvidencePreviewRequest *r)
{ {
return evidence_preview_request_new(r->investigation_root_path, EvidencePreviewRequest *copy = evidence_preview_request_new(
r->investigation_root_path,
r->evidence_identifier, r->relative_path, r->expected_sha256, r->evidence_identifier, r->relative_path, r->expected_sha256,
r->mime_type, r->request_generation); r->mime_type, r->request_generation);
evidence_preview_request_set_pdf_page(copy, r->pdf_page);
return copy;
} }
static gboolean worker(BackgroundTask *task, GCancellable *cancellable, static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
gpointer data, gpointer *result, GError **error) gpointer data, gpointer *result, GError **error)

View file

@ -4,22 +4,81 @@
#include "core/identity_field_extractor.h" #include "core/identity_field_extractor.h"
#include "core/ocr_analysis.h" #include "core/ocr_analysis.h"
#include <string.h>
static gboolean identity_ocr_workflow_languages_are_valid(
const char *languages)
{
char **codes;
gboolean valid = TRUE;
if (languages == NULL || languages[0] == '\0')
return FALSE;
codes = g_strsplit(languages, "+", -1);
for (guint index = 0; codes[index] != NULL && valid; index++) {
const char *code = codes[index];
valid = strlen(code) == 3;
for (guint character = 0; valid && code[character] != '\0';
character++)
valid = g_ascii_islower(code[character]);
}
g_strfreev(codes);
return valid;
}
static gboolean identity_ocr_workflow_validate_request(
const IdentityOcrWorkflowRequest *request,
GError **error)
{
#define REQUIRE_REQUEST_FIELD(condition, message) \
G_STMT_START { \
if (!(condition)) { \
g_set_error_literal(error, G_IO_ERROR, \
G_IO_ERROR_INVALID_ARGUMENT, (message)); \
return FALSE; \
} \
} G_STMT_END
REQUIRE_REQUEST_FIELD(request != NULL,
"La demande OCR didentité est absente.");
REQUIRE_REQUEST_FIELD(request->root_path != NULL,
"Le chemin racine de lenquête est absent de la demande OCR.");
REQUIRE_REQUEST_FIELD(request->evidence_identifier != NULL,
"LUUID de la preuve est absent de la demande OCR.");
REQUIRE_REQUEST_FIELD(request->relative_path != NULL,
"Le chemin relatif de la preuve est absent de la demande OCR.");
REQUIRE_REQUEST_FIELD(request->expected_sha256 != NULL,
"Le SHA-256 attendu est absent de la demande OCR.");
REQUIRE_REQUEST_FIELD(request->mime_type != NULL,
"Le type MIME de la preuve est absent de la demande OCR.");
REQUIRE_REQUEST_FIELD(
identity_ocr_document_type_is_valid(request->document_type),
"Le type de document didentité est invalide dans la demande OCR.");
REQUIRE_REQUEST_FIELD(
identity_ocr_document_side_is_valid(request->document_side),
"La face du document didentité est invalide dans la demande OCR.");
REQUIRE_REQUEST_FIELD(
identity_ocr_workflow_languages_are_valid(request->languages),
"Le code de langue Tesseract est invalide dans la demande OCR "
"(un code ISO 639-3 comme « fra » ou « eng » est requis).");
REQUIRE_REQUEST_FIELD(request->preprocessing_profile != NULL,
"Le profil de prétraitement est absent de la demande OCR.");
REQUIRE_REQUEST_FIELD(request->tesseract_executable != NULL,
"Le chemin de lexécutable Tesseract est absent de la demande OCR.");
REQUIRE_REQUEST_FIELD(request->page_number > 0,
"Le numéro de page est invalide dans la demande OCR.");
REQUIRE_REQUEST_FIELD(request->profile <= IDENTITY_OCR_PREPROCESS_UPSCALE,
"Le profil de prétraitement est invalide dans la demande OCR.");
#undef REQUIRE_REQUEST_FIELD
return TRUE;
}
gboolean identity_ocr_workflow_request_is_valid( gboolean identity_ocr_workflow_request_is_valid(
const IdentityOcrWorkflowRequest *request) const IdentityOcrWorkflowRequest *request)
{ {
return request != NULL && return identity_ocr_workflow_validate_request(request, NULL);
request->root_path != NULL &&
request->evidence_identifier != NULL &&
request->relative_path != NULL &&
request->expected_sha256 != NULL &&
request->mime_type != NULL &&
identity_ocr_document_type_is_valid(request->document_type) &&
identity_ocr_document_side_is_valid(request->document_side) &&
request->languages != NULL &&
request->preprocessing_profile != NULL &&
request->tesseract_executable != NULL &&
request->page_number > 0 &&
request->profile <= IDENTITY_OCR_PREPROCESS_UPSCALE;
} }
IdentityOcrRun *identity_ocr_workflow_execute( IdentityOcrRun *identity_ocr_workflow_execute(
@ -37,11 +96,8 @@ IdentityOcrRun *identity_ocr_workflow_execute(
gsize preview_length = 0; gsize preview_length = 0;
g_return_val_if_fail(error == NULL || *error == NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (!identity_ocr_workflow_request_is_valid(request)) { if (!identity_ocr_workflow_validate_request(request, error))
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"La demande du parcours OCR didentité est invalide.");
return NULL; return NULL;
}
preview_request = evidence_preview_request_new(request->root_path, preview_request = evidence_preview_request_new(request->root_path,
request->evidence_identifier, request->relative_path, request->evidence_identifier, request->relative_path,
request->expected_sha256, request->mime_type, request->generation); request->expected_sha256, request->mime_type, request->generation);

View file

@ -3,6 +3,7 @@
* @brief Formulaire GTK de création d'une personne observée. * @brief Formulaire GTK de création d'une personne observée.
******************************************************************************/ ******************************************************************************/
#include "views/create_person_dialog.h" #include "views/create_person_dialog.h"
#include "views/dialog_geometry.h"
#include "core/evidence_staging.h" #include "core/evidence_staging.h"
#include "core/evidence_staging_task.h" #include "core/evidence_staging_task.h"
#include "core/person_confirmation_summary.h" #include "core/person_confirmation_summary.h"
@ -49,6 +50,7 @@ typedef struct
EvidencePreviewWidget *preview; EvidencePreviewWidget *preview;
GtkLabel *progress; GtkLabel *progress;
GtkLabel *summary; GtkLabel *summary;
GtkScrolledWindow *summary_scroll;
GtkStack *stack; GtkStack *stack;
GtkButton *previous; GtkButton *previous;
GtkButton *next; GtkButton *next;
@ -132,25 +134,6 @@ static void create_person_dialog_on_retained_changed(
static void create_person_dialog_render_ocr_fields( static void create_person_dialog_render_ocr_fields(
CreatePersonDialogState *state, IdentityOcrRun *run); 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) static void identity_language_job_free(gpointer data)
{ {
IdentityLanguageJob *job = data; IdentityLanguageJob *job = data;
@ -187,6 +170,53 @@ static const char *identity_language_label(const char *code)
return code; return code;
} }
static guint identity_default_language_index(const GPtrArray *codes)
{
guint english_index = GTK_INVALID_LIST_POSITION;
for (guint index = 0; codes != NULL && index < codes->len; index++) {
const char *code = g_ptr_array_index((GPtrArray *) codes, index);
if (g_strcmp0(code, "fra") == 0)
return index;
if (g_strcmp0(code, "eng") == 0)
english_index = index;
}
return english_index != GTK_INVALID_LIST_POSITION
? english_index
: (codes != NULL && codes->len > 0
? 0
: GTK_INVALID_LIST_POSITION);
}
static const char *create_person_dialog_effective_ocr_mime(
const PersonEvidenceSelectionItem *item)
{
const char *mime =
person_evidence_selection_item_get_mime_type(item);
const char *name;
const char *suffix;
if (mime != NULL && mime[0] != '\0')
return mime;
name = person_evidence_selection_item_get_original_name(item);
suffix = name != NULL ? strrchr(name, '.') : NULL;
if (suffix == NULL)
return NULL;
if (g_ascii_strcasecmp(suffix, ".png") == 0)
return "image/png";
if (g_ascii_strcasecmp(suffix, ".jpg") == 0 ||
g_ascii_strcasecmp(suffix, ".jpeg") == 0)
return "image/jpeg";
if (g_ascii_strcasecmp(suffix, ".heic") == 0)
return "image/heic";
if (g_ascii_strcasecmp(suffix, ".heif") == 0)
return "image/heif";
if (g_ascii_strcasecmp(suffix, ".pdf") == 0)
return "application/pdf";
return NULL;
}
static void identity_languages_completed(GObject *source, static void identity_languages_completed(GObject *source,
GAsyncResult *result, gpointer data) GAsyncResult *result, gpointer data)
{ {
@ -209,9 +239,7 @@ static void identity_languages_completed(GObject *source,
} }
gtk_drop_down_set_model(state->ocr_languages, G_LIST_MODEL(labels)); gtk_drop_down_set_model(state->ocr_languages, G_LIST_MODEL(labels));
gtk_drop_down_set_selected(state->ocr_languages, gtk_drop_down_set_selected(state->ocr_languages,
state->ocr_language_codes != NULL && identity_default_language_index(state->ocr_language_codes));
state->ocr_language_codes->len > 0 ? 0 :
GTK_INVALID_LIST_POSITION);
gtk_widget_set_sensitive(GTK_WIDGET(state->ocr_start), gtk_widget_set_sensitive(GTK_WIDGET(state->ocr_start),
state->ocr_language_codes != NULL && state->ocr_language_codes != NULL &&
state->ocr_language_codes->len > 0); state->ocr_language_codes->len > 0);
@ -598,6 +626,8 @@ static void create_person_dialog_ocr_completed(GObject *source,
create_person_dialog_render_ocr_fields(state, run); create_person_dialog_render_ocr_fields(state, run);
ocr_provenance_overlay_set_image(state->ocr_overlay, ocr_provenance_overlay_set_image(state->ocr_overlay,
identity_ocr_run_get_preview(run), state->ocr_generation); identity_ocr_run_get_preview(run), state->ocr_generation);
ocr_provenance_overlay_set_page(state->ocr_overlay,
identity_ocr_run_get_page(run), state->ocr_generation);
const GPtrArray *fields = identity_ocr_run_get_fields(run); const GPtrArray *fields = identity_ocr_run_get_fields(run);
if (fields != NULL && fields->len > 0) if (fields != NULL && fields->len > 0)
ocr_provenance_overlay_set_field(state->ocr_overlay, ocr_provenance_overlay_set_field(state->ocr_overlay,
@ -653,7 +683,7 @@ static void create_person_dialog_on_ocr_start(GtkButton *button,
job->identifier = g_strdup( job->identifier = g_strdup(
person_evidence_selection_item_get_identifier(item)); person_evidence_selection_item_get_identifier(item));
job->sha256 = g_strdup(person_evidence_selection_item_get_sha256(item)); job->sha256 = g_strdup(person_evidence_selection_item_get_sha256(item));
job->mime = g_strdup(person_evidence_selection_item_get_mime_type(item)); job->mime = g_strdup(create_person_dialog_effective_ocr_mime(item));
if (person_evidence_selection_item_get_origin(item) == if (person_evidence_selection_item_get_origin(item) ==
PERSON_EVIDENCE_ORIGIN_EXISTING) { PERSON_EVIDENCE_ORIGIN_EXISTING) {
record = person_evidence_selection_item_get_record(item); record = person_evidence_selection_item_get_record(item);
@ -1430,6 +1460,10 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat
} }
} }
gtk_label_set_text(state->summary, confirmation->str); gtk_label_set_text(state->summary, confirmation->str);
GtkAdjustment *adjustment = gtk_scrolled_window_get_vadjustment(
state->summary_scroll);
gtk_adjustment_set_value(adjustment,
gtk_adjustment_get_lower(adjustment));
g_string_free(confirmation, TRUE); g_string_free(confirmation, TRUE);
g_free(text); g_free(text);
g_free(notes); g_free(notes);
@ -1524,9 +1558,7 @@ gboolean create_person_dialog_present(GtkWindow *parent,
gtk_window_set_application(state->window, gtk_window_set_application(state->window,
gtk_window_get_application(parent)); gtk_window_get_application(parent));
gtk_window_set_title(state->window, "Ajouter une personne"); gtk_window_set_title(state->window, "Ajouter une personne");
gtk_window_set_transient_for(state->window, parent); labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
gtk_window_set_modal(state->window, TRUE);
gtk_window_set_default_size(state->window, 960, 700);
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); 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_start(box, 16); gtk_widget_set_margin_end(box, 16);
gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16); gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16);
@ -1554,6 +1586,8 @@ gboolean create_person_dialog_present(GtkWindow *parent,
} }
state->evidence = GTK_DROP_DOWN(gtk_drop_down_new( state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(
G_LIST_MODEL(g_object_ref(state->evidence_labels)), NULL)); G_LIST_MODEL(g_object_ref(state->evidence_labels)), NULL));
gtk_widget_set_name(GTK_WIDGET(state->evidence),
"create-person-evidence-dropdown");
state->search = GTK_ENTRY(gtk_entry_new()); state->search = GTK_ENTRY(gtk_entry_new());
gtk_entry_set_placeholder_text(state->search, gtk_entry_set_placeholder_text(state->search,
"Rechercher par nom, description ou type"); "Rechercher par nom, description ou type");
@ -1565,6 +1599,8 @@ gboolean create_person_dialog_present(GtkWindow *parent,
state->type_filter = GTK_DROP_DOWN( state->type_filter = GTK_DROP_DOWN(
gtk_drop_down_new(G_LIST_MODEL( gtk_drop_down_new(G_LIST_MODEL(
g_object_ref(state->type_filter_labels)), NULL)); g_object_ref(state->type_filter_labels)), NULL));
gtk_widget_set_name(GTK_WIDGET(state->type_filter),
"create-person-evidence-type-filter");
/* /*
* gtk_drop_down_new() prend la propriété complète du modèle. Une * 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 * libération ici laisserait les vues internes du GtkDropDown avec un
@ -1655,10 +1691,20 @@ gboolean create_person_dialog_present(GtkWindow *parent,
gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
gtk_widget_set_name(evidence_paned, "create-person-evidence-paned"); gtk_widget_set_name(evidence_paned, "create-person-evidence-paned");
gtk_widget_set_size_request(evidence_box, 320, -1); gtk_widget_set_size_request(evidence_box, 320, -1);
gtk_paned_set_start_child(GTK_PANED(evidence_paned), evidence_box); GtkWidget *evidence_scroll = gtk_scrolled_window_new();
gtk_widget_set_name(evidence_scroll,
"create-person-evidence-scroll");
gtk_scrolled_window_set_policy(
GTK_SCROLLED_WINDOW(evidence_scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_child(
GTK_SCROLLED_WINDOW(evidence_scroll), evidence_box);
gtk_paned_set_start_child(
GTK_PANED(evidence_paned), evidence_scroll);
gtk_paned_set_end_child(GTK_PANED(evidence_paned), gtk_paned_set_end_child(GTK_PANED(evidence_paned),
evidence_preview_widget_get_widget(state->preview)); evidence_preview_widget_get_widget(state->preview));
gtk_paned_set_position(GTK_PANED(evidence_paned), 420); labfy_paned_apply_initial_ratio(
GTK_PANED(evidence_paned), 2.0 / 3.0, 480, 240);
gtk_paned_set_resize_start_child( gtk_paned_set_resize_start_child(
GTK_PANED(evidence_paned), FALSE); GTK_PANED(evidence_paned), FALSE);
gtk_paned_set_resize_end_child(GTK_PANED(evidence_paned), TRUE); gtk_paned_set_resize_end_child(GTK_PANED(evidence_paned), TRUE);
@ -1689,6 +1735,8 @@ gboolean create_person_dialog_present(GtkWindow *parent,
"Détection des langues…", NULL}; "Détection des langues…", NULL};
state->ocr_languages = GTK_DROP_DOWN( state->ocr_languages = GTK_DROP_DOWN(
gtk_drop_down_new_from_strings(pending_languages)); gtk_drop_down_new_from_strings(pending_languages));
gtk_widget_set_name(GTK_WIDGET(state->ocr_languages),
"create-person-ocr-languages");
} }
state->ocr_profile = GTK_DROP_DOWN( state->ocr_profile = GTK_DROP_DOWN(
gtk_drop_down_new_from_strings(profile_labels)); gtk_drop_down_new_from_strings(profile_labels));
@ -1835,9 +1883,23 @@ gboolean create_person_dialog_present(GtkWindow *parent,
state->summary = GTK_LABEL(gtk_label_new("")); state->summary = GTK_LABEL(gtk_label_new(""));
summary = GTK_WIDGET(state->summary); summary = GTK_WIDGET(state->summary);
gtk_label_set_wrap(state->summary, TRUE); gtk_label_set_wrap(state->summary, TRUE);
gtk_label_set_wrap_mode(state->summary, PANGO_WRAP_WORD_CHAR);
gtk_label_set_max_width_chars(state->summary, 100);
gtk_label_set_xalign(state->summary, 0.0f); gtk_label_set_xalign(state->summary, 0.0f);
gtk_label_set_selectable(state->summary, TRUE); gtk_label_set_selectable(state->summary, TRUE);
gtk_stack_add_titled(state->stack, summary, "summary", "5 — Confirmation"); gtk_widget_set_hexpand(summary, TRUE);
gtk_widget_set_valign(summary, GTK_ALIGN_START);
state->summary_scroll = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new());
gtk_widget_set_name(GTK_WIDGET(state->summary_scroll),
"create-person-confirmation-scroll");
gtk_scrolled_window_set_policy(state->summary_scroll,
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_propagate_natural_height(
state->summary_scroll, FALSE);
gtk_widget_set_vexpand(GTK_WIDGET(state->summary_scroll), TRUE);
gtk_scrolled_window_set_child(state->summary_scroll, summary);
gtk_stack_add_titled(state->stack,
GTK_WIDGET(state->summary_scroll), "summary", "5 — Confirmation");
state->error = GTK_LABEL(gtk_label_new(NULL)); gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE); 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); actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_name(actions, "create-person-actions"); gtk_widget_set_name(actions, "create-person-actions");
@ -1900,10 +1962,9 @@ gboolean create_person_dialog_present(GtkWindow *parent,
g_task_run_in_thread(task, identity_languages_worker); g_task_run_in_thread(task, identity_languages_worker);
g_object_unref(task); g_object_unref(task);
} }
gtk_window_present(state->window); labfy_paned_apply_initial_ratio(
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, GTK_PANED(ocr_paned), 2.0 / 3.0, 520, 240);
create_person_dialog_set_initial_ocr_paned_position, labfy_dialog_present(state->window);
g_object_ref(ocr_paned), g_object_unref);
return TRUE; return TRUE;
} }
void create_person_dialog_result_free(CreatePersonDialogResult *result) void create_person_dialog_result_free(CreatePersonDialogResult *result)

View file

@ -4,6 +4,7 @@
******************************************************************************/ ******************************************************************************/
#include "views/create_relation_dialog.h" #include "views/create_relation_dialog.h"
#include "views/dialog_geometry.h"
#include "models/entity_record.h" #include "models/entity_record.h"
#include "models/evidence_record.h" #include "models/evidence_record.h"
@ -1231,26 +1232,7 @@ static gboolean create_relation_dialog_present_internal(
: "Ajouter une relation" : "Ajouter une relation"
); );
gtk_window_set_transient_for( labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
state->window,
parent
);
gtk_window_set_modal(
state->window,
TRUE
);
gtk_window_set_destroy_with_parent(
state->window,
TRUE
);
gtk_window_set_default_size(
state->window,
920,
780
);
root_box = root_box =
gtk_box_new( gtk_box_new(
@ -1912,9 +1894,7 @@ static gboolean create_relation_dialog_present_internal(
) )
); );
gtk_window_present( labfy_dialog_present(state->window);
state->window
);
return TRUE; return TRUE;
} }

View file

@ -4,6 +4,7 @@
******************************************************************************/ ******************************************************************************/
#include "views/create_social_account_dialog.h" #include "views/create_social_account_dialog.h"
#include "views/dialog_geometry.h"
struct CreateSocialAccountDialogResult struct CreateSocialAccountDialogResult
{ {
@ -200,9 +201,7 @@ gboolean create_social_account_dialog_present(
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free); state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
state->window = GTK_WINDOW(gtk_window_new()); state->window = GTK_WINDOW(gtk_window_new());
gtk_window_set_title(state->window, "Ajouter un compte social"); gtk_window_set_title(state->window, "Ajouter un compte social");
gtk_window_set_transient_for(state->window, parent); labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
gtk_window_set_modal(state->window, TRUE);
gtk_window_set_default_size(state->window, 620, 560);
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); 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_start(box, 16); gtk_widget_set_margin_end(box, 16);
gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16); gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16);
@ -266,7 +265,7 @@ gboolean create_social_account_dialog_present(
g_signal_connect(create, "clicked", G_CALLBACK(create_social_account_dialog_on_create), state); g_signal_connect(create, "clicked", G_CALLBACK(create_social_account_dialog_on_create), state);
g_object_set_data_full(G_OBJECT(state->window), "social-account-state", state, g_object_set_data_full(G_OBJECT(state->window), "social-account-state", state,
create_social_account_dialog_state_free); create_social_account_dialog_state_free);
gtk_window_present(state->window); labfy_dialog_present(state->window);
g_free(now_text); g_date_time_unref(now); g_free(now_text); g_date_time_unref(now);
return TRUE; return TRUE;
} }

145
src/views/dialog_geometry.c Normal file
View file

@ -0,0 +1,145 @@
#include "views/dialog_geometry.h"
#define LABFY_DIALOG_INITIAL_WIDTH 1200
#define LABFY_DIALOG_INITIAL_HEIGHT 800
#define LABFY_DIALOG_MINIMUM_WIDTH 800
#define LABFY_DIALOG_MINIMUM_HEIGHT 600
#define LABFY_DIALOG_WORKAREA_FACTOR 0.90
typedef struct {
double ratio;
int minimum_start;
int minimum_end;
} LabfyPanedGeometry;
LabfyDialogGeometry labfy_dialog_geometry_calculate(
int workarea_width, int workarea_height)
{
LabfyDialogGeometry geometry = {0};
int usable_width = workarea_width > 0
? (workarea_width >= LABFY_DIALOG_MINIMUM_WIDTH
? workarea_width
: MAX(1, (int) (
workarea_width * LABFY_DIALOG_WORKAREA_FACTOR)))
: LABFY_DIALOG_INITIAL_WIDTH;
int usable_height = workarea_height > 0
? (workarea_height >= LABFY_DIALOG_MINIMUM_HEIGHT
? workarea_height
: MAX(1, (int) (
workarea_height * LABFY_DIALOG_WORKAREA_FACTOR)))
: LABFY_DIALOG_INITIAL_HEIGHT;
geometry.initial_width = MIN(LABFY_DIALOG_INITIAL_WIDTH, usable_width);
geometry.initial_height = MIN(LABFY_DIALOG_INITIAL_HEIGHT, usable_height);
geometry.minimum_width = workarea_width >= LABFY_DIALOG_MINIMUM_WIDTH
? LABFY_DIALOG_MINIMUM_WIDTH : usable_width;
geometry.minimum_height = workarea_height >= LABFY_DIALOG_MINIMUM_HEIGHT
? LABFY_DIALOG_MINIMUM_HEIGHT : usable_height;
geometry.minimum_width = MIN(
geometry.minimum_width, geometry.initial_width);
geometry.minimum_height = MIN(
geometry.minimum_height, geometry.initial_height);
return geometry;
}
static GdkMonitor *dialog_geometry_find_monitor(
GtkWindow *window, GtkWindow *parent)
{
GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(window));
GtkNative *native = parent != NULL
? gtk_widget_get_native(GTK_WIDGET(parent)) : NULL;
GdkSurface *surface = native != NULL
? gtk_native_get_surface(native) : NULL;
if (surface != NULL) {
GdkMonitor *monitor =
gdk_display_get_monitor_at_surface(display, surface);
if (monitor != NULL) return g_object_ref(monitor);
}
GListModel *monitors = gdk_display_get_monitors(display);
return monitors != NULL && g_list_model_get_n_items(monitors) > 0
? g_list_model_get_item(monitors, 0) : NULL;
}
void labfy_dialog_apply_standard_geometry(
GtkWindow *window, GtkWindow *parent)
{
GdkRectangle workarea = {0};
GdkMonitor *monitor;
LabfyDialogGeometry geometry;
if (!GTK_IS_WINDOW(window)) return;
monitor = dialog_geometry_find_monitor(window, parent);
if (monitor != NULL) gdk_monitor_get_geometry(monitor, &workarea);
geometry = labfy_dialog_geometry_calculate(
workarea.width, workarea.height);
gtk_window_set_resizable(window, TRUE);
gtk_window_set_default_size(window,
geometry.initial_width, geometry.initial_height);
gtk_widget_set_size_request(GTK_WIDGET(window),
geometry.minimum_width, geometry.minimum_height);
g_clear_object(&monitor);
}
void labfy_dialog_prepare(
GtkWindow *window,
GtkWindow *parent,
gboolean modal,
gboolean destroy_with_parent)
{
if (!GTK_IS_WINDOW(window)) return;
gtk_window_set_transient_for(window,
GTK_IS_WINDOW(parent) ? parent : NULL);
gtk_window_set_modal(window, modal);
gtk_window_set_destroy_with_parent(window,
destroy_with_parent && GTK_IS_WINDOW(parent));
}
void labfy_dialog_present(GtkWindow *window)
{
GtkWindow *parent;
if (!GTK_IS_WINDOW(window)) return;
parent = gtk_window_get_transient_for(window);
labfy_dialog_apply_standard_geometry(window, parent);
gtk_window_present(window);
}
static void paned_geometry_apply(
GtkPaned *paned, LabfyPanedGeometry *geometry)
{
int width = gtk_widget_get_width(GTK_WIDGET(paned));
int position;
if (width <= 0 || geometry == NULL) return;
position = (int) (width * geometry->ratio);
position = MAX(geometry->minimum_start, position);
position = MIN(position, MAX(0, width - geometry->minimum_end));
gtk_paned_set_position(paned, position);
}
static gboolean paned_geometry_idle(gpointer data)
{
GtkPaned *paned = GTK_PANED(data);
LabfyPanedGeometry *geometry = g_object_get_data(
G_OBJECT(paned), "labfy-paned-initial-geometry");
if (geometry == NULL) return G_SOURCE_REMOVE;
if (gtk_widget_get_width(GTK_WIDGET(paned)) <= 0)
return G_SOURCE_CONTINUE;
paned_geometry_apply(paned, geometry);
return G_SOURCE_REMOVE;
}
void labfy_paned_apply_initial_ratio(
GtkPaned *paned, double ratio, int minimum_start, int minimum_end)
{
LabfyPanedGeometry *geometry;
if (!GTK_IS_PANED(paned)) return;
if (g_object_get_data(
G_OBJECT(paned), "labfy-paned-initial-geometry") != NULL)
return;
geometry = g_new0(LabfyPanedGeometry, 1);
geometry->ratio = CLAMP(ratio, 0.1, 0.9);
geometry->minimum_start = MAX(0, minimum_start);
geometry->minimum_end = MAX(0, minimum_end);
g_object_set_data_full(G_OBJECT(paned),
"labfy-paned-initial-geometry", geometry, g_free);
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, paned_geometry_idle,
g_object_ref(paned), g_object_unref);
}

View file

@ -3,6 +3,7 @@
* @brief Présentation en lecture seule d'une analyse EML. * @brief Présentation en lecture seule d'une analyse EML.
******************************************************************************/ ******************************************************************************/
#include "views/eml_analysis_dialog.h" #include "views/eml_analysis_dialog.h"
#include "views/dialog_geometry.h"
#include "widgets/controlled_vocab_dropdown.h" #include "widgets/controlled_vocab_dropdown.h"
typedef struct { GtkWindow *window; GtkWidget *proposals_box; typedef struct { GtkWindow *window; GtkWidget *proposals_box;
GtkWidget *summary_label; GtkWidget *summary_label;
@ -234,8 +235,7 @@ void eml_analysis_dialog_present(GtkWindow *parent,
state->callback = callback; state->user_data = user_data; state->callback = callback; state->user_data = user_data;
window = GTK_WINDOW(gtk_window_new()); window = GTK_WINDOW(gtk_window_new());
gtk_window_set_title(window, "Analyse locale du courriel EML"); gtk_window_set_title(window, "Analyse locale du courriel EML");
gtk_window_set_transient_for(window, parent); gtk_window_set_modal(window, TRUE); labfy_dialog_prepare(window, parent, TRUE, TRUE);
gtk_window_set_default_size(window, 720, 560);
state->window = window; state->window = window;
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start(box, 14); gtk_widget_set_margin_end(box, 14); gtk_widget_set_margin_start(box, 14); gtk_widget_set_margin_end(box, 14);
@ -314,7 +314,7 @@ void eml_analysis_dialog_present(GtkWindow *parent,
G_CALLBACK(eml_analysis_dialog_on_window_close), state); G_CALLBACK(eml_analysis_dialog_on_window_close), state);
g_object_set_data_full(G_OBJECT(window), "eml-dialog-state", state, g_object_set_data_full(G_OBJECT(window), "eml-dialog-state", state,
eml_analysis_dialog_state_free); eml_analysis_dialog_state_free);
gtk_window_present(window); labfy_dialog_present(window);
g_free(received); g_free(emails); g_free(domains); g_free(received); g_free(emails); g_free(domains);
g_free(sender_ips); g_free(destination_ips); g_free(sender_ips); g_free(destination_ips);
} }
@ -418,9 +418,7 @@ void eml_analysis_dialog_present_pipeline(
state->callback = callback; state->user_data = user_data; state->callback = callback; state->user_data = user_data;
state->window = GTK_WINDOW(gtk_window_new()); state->window = GTK_WINDOW(gtk_window_new());
gtk_window_set_title(state->window, "Révision de lanalyse EML"); gtk_window_set_title(state->window, "Révision de lanalyse EML");
gtk_window_set_transient_for(state->window, parent); labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
gtk_window_set_modal(state->window, TRUE);
gtk_window_set_default_size(state->window, 900, 700);
GtkWidget *outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); GtkWidget *outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start(outer, 14); gtk_widget_set_margin_start(outer, 14);
gtk_widget_set_margin_end(outer, 14); gtk_widget_set_margin_end(outer, 14);
@ -538,7 +536,7 @@ void eml_analysis_dialog_present_pipeline(
G_CALLBACK(eml_analysis_dialog_on_window_close), state); G_CALLBACK(eml_analysis_dialog_on_window_close), state);
g_object_set_data_full(G_OBJECT(state->window), "eml-dialog-state", g_object_set_data_full(G_OBJECT(state->window), "eml-dialog-state",
state, eml_analysis_dialog_state_free); state, eml_analysis_dialog_state_free);
gtk_window_present(state->window); labfy_dialog_present(state->window);
g_free(received); g_free(received);
g_string_free(attachments, TRUE); g_string_free(attachments, TRUE);
g_string_free(texts, TRUE); g_string_free(texts, TRUE);

View file

@ -1,4 +1,5 @@
#include "views/evidence_identity_ocr_dialog.h" #include "views/evidence_identity_ocr_dialog.h"
#include "views/dialog_geometry.h"
#include "core/file_hash.h" #include "core/file_hash.h"
#include "core/identity_ocr_workflow.h" #include "core/identity_ocr_workflow.h"
@ -129,24 +130,6 @@ static void on_reset_transcription(GtkButton *button, gpointer data)
gtk_widget_set_sensitive(GTK_WIDGET(state->save_transcription), TRUE); gtk_widget_set_sensitive(GTK_WIDGET(state->save_transcription), TRUE);
} }
static gboolean set_initial_paned_position(gpointer data)
{
GtkWidget *paned = data;
int width;
int position;
if (g_object_get_data(
G_OBJECT(paned), "identity-initial-position-set") != NULL)
return G_SOURCE_REMOVE;
width = gtk_widget_get_width(paned);
if (width <= 0) return G_SOURCE_CONTINUE;
position = MAX(520, (width * 2) / 3);
position = MIN(position, MAX(0, width - 180));
gtk_paned_set_position(GTK_PANED(paned), position);
g_object_set_data(G_OBJECT(paned),
"identity-initial-position-set", GINT_TO_POINTER(1));
return G_SOURCE_REMOVE;
}
static const char *mime_from_path(const char *path) static const char *mime_from_path(const char *path)
{ {
const char *dot = path != NULL ? strrchr(path, '.') : NULL; const char *dot = path != NULL ? strrchr(path, '.') : NULL;
@ -545,6 +528,8 @@ static void workflow_completed(GObject *source, GAsyncResult *result,
GTK_WIDGET(state->save_transcription), TRUE); GTK_WIDGET(state->save_transcription), TRUE);
ocr_provenance_overlay_set_image(state->overlay, ocr_provenance_overlay_set_image(state->overlay,
identity_ocr_run_get_preview(state->run), state->generation); identity_ocr_run_get_preview(state->run), state->generation);
ocr_provenance_overlay_set_page(state->overlay,
identity_ocr_run_get_page(state->run), state->generation);
gtk_label_set_text(state->status, gtk_label_set_text(state->status,
"OCR terminé. Vérifiez chaque proposition."); "OCR terminé. Vérifiez chaque proposition.");
gtk_widget_set_sensitive( gtk_widget_set_sensitive(
@ -668,11 +653,9 @@ gboolean evidence_identity_ocr_dialog_present(
state->window = GTK_WINDOW(gtk_window_new()); state->window = GTK_WINDOW(gtk_window_new());
gtk_window_set_application(state->window, gtk_window_set_application(state->window,
gtk_window_get_application(parent)); gtk_window_get_application(parent));
gtk_window_set_transient_for(state->window, parent); labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
gtk_window_set_modal(state->window, TRUE);
gtk_window_set_title(state->window, gtk_window_set_title(state->window,
"Import — OCR didentité facultatif"); "Import — OCR didentité facultatif");
gtk_window_set_default_size(state->window, 1200, 800);
GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_name(root, "identity-form-content"); gtk_widget_set_name(root, "identity-form-content");
gtk_widget_set_margin_start(root, 14); gtk_widget_set_margin_start(root, 14);
@ -881,9 +864,9 @@ gboolean evidence_identity_ocr_dialog_present(
state->tesseract_path != NULL); state->tesseract_path != NULL);
if (state->tesseract_path != NULL && if (state->tesseract_path != NULL &&
!populate_languages(state, error)) goto failure_window; !populate_languages(state, error)) goto failure_window;
gtk_window_present(state->window); labfy_paned_apply_initial_ratio(
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, GTK_PANED(paned), 2.0 / 3.0, 520, 240);
set_initial_paned_position, g_object_ref(paned), g_object_unref); labfy_dialog_present(state->window);
return TRUE; return TRUE;
failure_window: failure_window:

View file

@ -4,6 +4,7 @@
******************************************************************************/ ******************************************************************************/
#include "views/evidence_import_dialog.h" #include "views/evidence_import_dialog.h"
#include "views/dialog_geometry.h"
#include "models/evidence_type.h" #include "models/evidence_type.h"
#include "core/file_hash.h" #include "core/file_hash.h"
#include "core/task_manager.h" #include "core/task_manager.h"
@ -974,10 +975,7 @@ gboolean evidence_import_dialog_present(
"Importer une preuve" "Importer une preuve"
); );
gtk_window_set_transient_for( labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
state->window,
parent
);
gtk_window_set_application( gtk_window_set_application(
state->window, state->window,
@ -986,22 +984,6 @@ gboolean evidence_import_dialog_present(
) )
); );
gtk_window_set_modal(
state->window,
TRUE
);
gtk_window_set_destroy_with_parent(
state->window,
TRUE
);
gtk_window_set_default_size(
state->window,
1200,
800
);
root_box = root_box =
gtk_box_new( gtk_box_new(
GTK_ORIENTATION_VERTICAL, GTK_ORIENTATION_VERTICAL,
@ -1411,10 +1393,26 @@ gboolean evidence_import_dialog_present(
button_box button_box
); );
GtkWidget *left_panel = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
GtkWidget *form_scroll = gtk_scrolled_window_new();
gtk_widget_set_name(form_scroll, "evidence-import-form-scroll");
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(form_scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(form_scroll, TRUE);
g_object_ref(button_box);
gtk_box_remove(GTK_BOX(root_box), button_box);
gtk_scrolled_window_set_child(
GTK_SCROLLED_WINDOW(form_scroll), root_box);
gtk_box_append(GTK_BOX(left_panel), form_scroll);
gtk_widget_set_name(button_box, "evidence-import-actions");
gtk_widget_set_margin_end(button_box, 18);
gtk_widget_set_margin_bottom(button_box, 12);
gtk_box_append(GTK_BOX(left_panel), button_box);
g_object_unref(button_box);
paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
gtk_widget_set_name(paned, "evidence-import-paned"); gtk_widget_set_name(paned, "evidence-import-paned");
gtk_paned_set_start_child(GTK_PANED(paned), root_box); gtk_paned_set_start_child(GTK_PANED(paned), left_panel);
gtk_widget_set_size_request(root_box, 320, -1); gtk_widget_set_size_request(left_panel, 320, -1);
state->preview_task_manager = task_manager_new(); state->preview_task_manager = task_manager_new();
state->preview = evidence_preview_widget_new( state->preview = evidence_preview_widget_new(
state->preview_task_manager, NULL, NULL); state->preview_task_manager, NULL, NULL);
@ -1425,7 +1423,8 @@ gboolean evidence_import_dialog_present(
} }
gtk_paned_set_end_child(GTK_PANED(paned), gtk_paned_set_end_child(GTK_PANED(paned),
evidence_preview_widget_get_widget(state->preview)); evidence_preview_widget_get_widget(state->preview));
gtk_paned_set_position(GTK_PANED(paned), 420); labfy_paned_apply_initial_ratio(
GTK_PANED(paned), 2.0 / 3.0, 480, 240);
gtk_paned_set_resize_start_child(GTK_PANED(paned), FALSE); gtk_paned_set_resize_start_child(GTK_PANED(paned), FALSE);
gtk_paned_set_resize_end_child(GTK_PANED(paned), TRUE); gtk_paned_set_resize_end_child(GTK_PANED(paned), TRUE);
gtk_window_set_child(state->window, paned); gtk_window_set_child(state->window, paned);
@ -1500,9 +1499,7 @@ gboolean evidence_import_dialog_present(
import_button import_button
); );
gtk_window_present( labfy_dialog_present(state->window);
state->window
);
return TRUE; return TRUE;
} }

View file

@ -4,6 +4,7 @@
******************************************************************************/ ******************************************************************************/
#include "views/evidence_metadata_dialog.h" #include "views/evidence_metadata_dialog.h"
#include "views/dialog_geometry.h"
#include "models/evidence_type.h" #include "models/evidence_type.h"
#include "dao/identity_ocr_dao.h" #include "dao/identity_ocr_dao.h"
@ -258,6 +259,8 @@ static void evidence_metadata_dialog_render_ocr(
IdentityOcrRunRecord *run = g_ptr_array_index( IdentityOcrRunRecord *run = g_ptr_array_index(
context->ocr_runs, selected); context->ocr_runs, selected);
ocr_provenance_overlay_set_page(context->ocr_overlay,
(guint) run->page_number, context->ocr_generation);
const char *selected_identifier = g_ptr_array_index( const char *selected_identifier = g_ptr_array_index(
context->ocr_run_identifiers, selected); context->ocr_run_identifiers, selected);
if (g_strcmp0(run->id, selected_identifier) != 0) return; if (g_strcmp0(run->id, selected_identifier) != 0) return;
@ -492,13 +495,10 @@ gboolean evidence_metadata_dialog_present_with_ocr(
evidence_record_get_type_identifier(record)) == 0) selected = index; evidence_record_get_type_identifier(record)) == 0) selected = index;
} }
gtk_window_set_title(context->window, "Modifier les métadonnées"); gtk_window_set_title(context->window, "Modifier les métadonnées");
gtk_window_set_default_size(context->window, 560, 420); labfy_dialog_prepare(context->window, parent, TRUE, TRUE);
gtk_window_set_modal(context->window, TRUE);
gtk_window_set_destroy_with_parent(context->window, TRUE);
if (parent != NULL) { if (parent != NULL) {
gtk_window_set_application(context->window, gtk_window_set_application(context->window,
gtk_window_get_application(parent)); gtk_window_get_application(parent));
gtk_window_set_transient_for(context->window, parent);
} }
g_object_set_data_full(G_OBJECT(context->window), "evidence-metadata-context", g_object_set_data_full(G_OBJECT(context->window), "evidence-metadata-context",
context, evidence_metadata_dialog_context_free); context, evidence_metadata_dialog_context_free);
@ -577,7 +577,7 @@ gboolean evidence_metadata_dialog_present_with_ocr(
gtk_box_append(GTK_BOX(buttons), cancel); gtk_box_append(GTK_BOX(buttons), save); gtk_box_append(GTK_BOX(buttons), cancel); gtk_box_append(GTK_BOX(buttons), save);
gtk_box_append(GTK_BOX(main_box), buttons); gtk_box_append(GTK_BOX(main_box), buttons);
gtk_window_set_child(context->window, main_box); gtk_window_set_child(context->window, main_box);
gtk_window_present(context->window); labfy_dialog_present(context->window);
return TRUE; return TRUE;
} }

View file

@ -3,6 +3,7 @@
* @brief Dialogue de sélection des preuves associées à une entité. * @brief Dialogue de sélection des preuves associées à une entité.
******************************************************************************/ ******************************************************************************/
#include "views/manage_entity_evidence_dialog.h" #include "views/manage_entity_evidence_dialog.h"
#include "views/dialog_geometry.h"
#include "widgets/evidence_preview_widget.h" #include "widgets/evidence_preview_widget.h"
#include <gio/gio.h> #include <gio/gio.h>
typedef struct typedef struct
@ -116,9 +117,7 @@ gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
state->import_callback = import_callback; state->import_callback = import_callback;
state->window = GTK_WINDOW(gtk_window_new()); state->window = GTK_WINDOW(gtk_window_new());
gtk_window_set_title(state->window, "Pièces jointes de la personne"); gtk_window_set_title(state->window, "Pièces jointes de la personne");
gtk_window_set_transient_for(state->window, parent); labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
gtk_window_set_modal(state->window, TRUE);
gtk_window_set_default_size(state->window, 1200, 800);
root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
gtk_widget_set_margin_top(root, 16); gtk_widget_set_margin_bottom(root, 16); 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_widget_set_margin_start(root, 16); gtk_widget_set_margin_end(root, 16);
@ -178,7 +177,8 @@ gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
gtk_paned_set_start_child(GTK_PANED(content), scroll); gtk_paned_set_start_child(GTK_PANED(content), scroll);
gtk_paned_set_end_child(GTK_PANED(content), gtk_paned_set_end_child(GTK_PANED(content),
evidence_preview_widget_get_widget(state->preview)); evidence_preview_widget_get_widget(state->preview));
gtk_paned_set_position(GTK_PANED(content), 380); labfy_paned_apply_initial_ratio(
GTK_PANED(content), 2.0 / 3.0, 480, 240);
gtk_paned_set_resize_start_child(GTK_PANED(content), FALSE); gtk_paned_set_resize_start_child(GTK_PANED(content), FALSE);
gtk_paned_set_resize_end_child(GTK_PANED(content), TRUE); gtk_paned_set_resize_end_child(GTK_PANED(content), TRUE);
save = gtk_button_new_with_label("Enregistrer les pièces jointes"); save = gtk_button_new_with_label("Enregistrer les pièces jointes");
@ -194,5 +194,5 @@ gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
g_signal_connect(save, "clicked", G_CALLBACK(manage_evidence_on_save), state); g_signal_connect(save, "clicked", G_CALLBACK(manage_evidence_on_save), state);
g_signal_connect(import, "clicked", G_CALLBACK(manage_evidence_on_import), state); g_signal_connect(import, "clicked", G_CALLBACK(manage_evidence_on_import), state);
g_signal_connect(state->window, "close-request", G_CALLBACK(manage_evidence_on_close), state); g_signal_connect(state->window, "close-request", G_CALLBACK(manage_evidence_on_close), state);
gtk_window_present(state->window); return TRUE; labfy_dialog_present(state->window); return TRUE;
} }

View file

@ -4,6 +4,7 @@
******************************************************************************/ ******************************************************************************/
#include "views/metadata_analysis_dialog.h" #include "views/metadata_analysis_dialog.h"
#include "views/dialog_geometry.h"
static gboolean metadata_analysis_dialog_on_escape( static gboolean metadata_analysis_dialog_on_escape(
GtkWidget *widget, GVariant *arguments, gpointer user_data) GtkWidget *widget, GVariant *arguments, gpointer user_data)
@ -30,9 +31,7 @@ void metadata_analysis_dialog_present(GtkWindow *parent, const char *summary,
window = gtk_window_new(); window = gtk_window_new();
gtk_window_set_title(GTK_WINDOW(window), "Métadonnées de la preuve"); gtk_window_set_title(GTK_WINDOW(window), "Métadonnées de la preuve");
gtk_window_set_default_size(GTK_WINDOW(window), 760, 560); labfy_dialog_prepare(GTK_WINDOW(window), parent, TRUE, TRUE);
gtk_window_set_modal(GTK_WINDOW(window), TRUE);
if (parent != NULL) gtk_window_set_transient_for(GTK_WINDOW(window), parent);
content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
gtk_widget_set_margin_top(content, 16); gtk_widget_set_margin_top(content, 16);
gtk_widget_set_margin_bottom(content, 16); gtk_widget_set_margin_bottom(content, 16);
@ -81,7 +80,7 @@ void metadata_analysis_dialog_present(GtkWindow *parent, const char *summary,
} }
gtk_box_append(GTK_BOX(content), close_button); gtk_box_append(GTK_BOX(content), close_button);
gtk_window_set_child(GTK_WINDOW(window), content); gtk_window_set_child(GTK_WINDOW(window), content);
gtk_window_present(GTK_WINDOW(window)); labfy_dialog_present(GTK_WINDOW(window));
g_free(heading_text); g_free(heading_text);
g_free(path_text); g_free(path_text);
} }

View file

@ -4,6 +4,7 @@
******************************************************************************/ ******************************************************************************/
#include "views/osint_dns_review_dialog.h" #include "views/osint_dns_review_dialog.h"
#include "views/dialog_geometry.h"
#include "models/osint_dns_proposal.h" #include "models/osint_dns_proposal.h"
@ -211,11 +212,7 @@ void osint_dns_review_dialog_present(
context->user_data = user_data; context->user_data = user_data;
gtk_window_set_title(context->window, "Intégrer les propositions DNS"); gtk_window_set_title(context->window, "Intégrer les propositions DNS");
gtk_window_set_default_size(context->window, 720, 560); labfy_dialog_prepare(context->window, parent_window, TRUE, TRUE);
gtk_window_set_modal(context->window, TRUE);
gtk_window_set_destroy_with_parent(context->window, TRUE);
if (parent_window != NULL)
gtk_window_set_transient_for(context->window, parent_window);
g_object_set_data_full( g_object_set_data_full(
G_OBJECT(context->window), "osint-dns-review-context", context, G_OBJECT(context->window), "osint-dns-review-context", context,
osint_dns_review_dialog_context_free osint_dns_review_dialog_context_free
@ -279,5 +276,5 @@ void osint_dns_review_dialog_present(
gtk_box_append(GTK_BOX(main_box), scrolled_window); gtk_box_append(GTK_BOX(main_box), scrolled_window);
gtk_box_append(GTK_BOX(main_box), button_box); gtk_box_append(GTK_BOX(main_box), button_box);
gtk_window_set_child(context->window, main_box); gtk_window_set_child(context->window, main_box);
gtk_window_present(context->window); labfy_dialog_present(context->window);
} }

View file

@ -4,6 +4,7 @@
******************************************************************************/ ******************************************************************************/
#include "views/osint_execution_history_dialog.h" #include "views/osint_execution_history_dialog.h"
#include "views/dialog_geometry.h"
#include "core/osint_execution_integrity.h" #include "core/osint_execution_integrity.h"
#include "models/osint_execution_record.h" #include "models/osint_execution_record.h"
@ -153,10 +154,7 @@ void osint_execution_history_dialog_present(
context->records = g_ptr_array_ref(records); context->records = g_ptr_array_ref(records);
context->linked_objects = g_hash_table_ref(linked_objects); context->linked_objects = g_hash_table_ref(linked_objects);
gtk_window_set_title(context->window, "Historique OSINT"); gtk_window_set_title(context->window, "Historique OSINT");
gtk_window_set_default_size(context->window, 980, 650); labfy_dialog_prepare(context->window, parent_window, TRUE, TRUE);
gtk_window_set_modal(context->window, TRUE);
gtk_window_set_destroy_with_parent(context->window, TRUE);
if (parent_window != NULL) gtk_window_set_transient_for(context->window, parent_window);
g_object_set_data_full(G_OBJECT(context->window), "osint-history-context", g_object_set_data_full(G_OBJECT(context->window), "osint-history-context",
context, osint_execution_history_dialog_context_free); context, osint_execution_history_dialog_context_free);
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
@ -214,5 +212,5 @@ void osint_execution_history_dialog_present(
gtk_window_set_child(context->window, main_box); gtk_window_set_child(context->window, main_box);
osint_execution_history_dialog_on_record_clicked( osint_execution_history_dialog_on_record_clicked(
GTK_BUTTON(gtk_widget_get_first_child(history_box)), context); GTK_BUTTON(gtk_widget_get_first_child(history_box)), context);
gtk_window_present(context->window); labfy_dialog_present(context->window);
} }

View file

@ -3,6 +3,7 @@
* @brief Révision humaine des résultats OCR d'un RIB. * @brief Révision humaine des résultats OCR d'un RIB.
******************************************************************************/ ******************************************************************************/
#include "views/rib_ocr_review_dialog.h" #include "views/rib_ocr_review_dialog.h"
#include "views/dialog_geometry.h"
#include "core/iban_analyzer.h" #include "core/iban_analyzer.h"
typedef struct { GtkWindow *window; GtkEntry *iban; GtkLabel *error; typedef struct { GtkWindow *window; GtkEntry *iban; GtkLabel *error;
RibOcrReviewCallback callback; gpointer data; gboolean completed; } State; RibOcrReviewCallback callback; gpointer data; gboolean completed; } State;
@ -36,8 +37,7 @@ void rib_ocr_review_dialog_present(GtkWindow *parent, const char *ocr_text,
state->data = user_data; state->iban = GTK_ENTRY(gtk_entry_new()); state->data = user_data; state->iban = GTK_ENTRY(gtk_entry_new());
state->error = GTK_LABEL(gtk_label_new(NULL)); state->error = GTK_LABEL(gtk_label_new(NULL));
gtk_window_set_title(state->window, "Réviser lanalyse OCR du RIB"); 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); labfy_dialog_prepare(state->window, parent, TRUE, 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_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_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_view_set_editable(GTK_TEXT_VIEW(view), FALSE);
@ -55,5 +55,5 @@ void rib_ocr_review_dialog_present(GtkWindow *parent, const char *ocr_text,
g_object_set_data_full(G_OBJECT(state->window), "state", state, state_free); 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(confirm, "clicked", G_CALLBACK(on_confirm), state);
g_signal_connect(state->window, "close-request", G_CALLBACK(on_close), state); g_signal_connect(state->window, "close-request", G_CALLBACK(on_close), state);
gtk_window_present(state->window); labfy_dialog_present(state->window);
} }

View file

@ -6,9 +6,19 @@ struct EvidencePreviewWidget {
GtkWidget *root; GtkWidget *root;
GtkLabel *metadata; GtkLabel *metadata;
GtkLabel *status; GtkLabel *status;
GtkWidget *toolbar;
GtkStack *stack; GtkStack *stack;
GtkPicture *image; GtkPicture *image;
GtkPicture *pdf; GtkPicture *pdf;
GtkScrolledWindow *image_scroll;
GtkScrolledWindow *pdf_scroll;
GtkButton *zoom_out;
GtkButton *fit;
GtkButton *zoom_in;
GtkLabel *zoom_label;
GtkButton *previous_page;
GtkButton *next_page;
GtkLabel *page_label;
GtkVideo *video; GtkVideo *video;
GtkTextView *text; GtkTextView *text;
GtkTextView *email; GtkTextView *email;
@ -19,6 +29,16 @@ struct EvidencePreviewWidget {
gpointer session_data; gpointer session_data;
guint64 generation; guint64 generation;
char *identifier; char *identifier;
EvidencePreviewRequest *request;
char *metadata_text;
gint raster_width;
gint raster_height;
guint zoom_index;
gboolean fit_mode;
gboolean raster_available;
gboolean pdf_available;
guint pdf_page;
guint pdf_page_count;
}; };
typedef struct { typedef struct {
@ -77,6 +97,148 @@ static void preview_widget_reset_content(EvidencePreviewWidget *widget)
gtk_text_buffer_set_text(gtk_text_view_get_buffer(widget->email), "", -1); gtk_text_buffer_set_text(gtk_text_view_get_buffer(widget->email), "", -1);
} }
static const double zoom_factors[] = {
0.25, 0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 3.00, 4.00
};
static GtkPicture *preview_widget_active_picture(
EvidencePreviewWidget *widget)
{
return widget->pdf_available ? widget->pdf : widget->image;
}
static GtkScrolledWindow *preview_widget_active_scroll(
EvidencePreviewWidget *widget)
{
return widget->pdf_available ? widget->pdf_scroll : widget->image_scroll;
}
static void preview_widget_update_page_controls(
EvidencePreviewWidget *widget)
{
char *text = g_strdup_printf("Page %u / %u",
widget->pdf_page_count > 0 ? widget->pdf_page + 1U : 0U,
widget->pdf_page_count);
gtk_label_set_text(widget->page_label, text);
g_free(text);
gtk_widget_set_visible(GTK_WIDGET(widget->previous_page),
widget->pdf_available);
gtk_widget_set_visible(GTK_WIDGET(widget->next_page),
widget->pdf_available);
gtk_widget_set_visible(GTK_WIDGET(widget->page_label),
widget->pdf_available);
gtk_widget_set_sensitive(GTK_WIDGET(widget->previous_page),
widget->pdf_available && widget->pdf_page > 0);
gtk_widget_set_sensitive(GTK_WIDGET(widget->next_page),
widget->pdf_available &&
widget->pdf_page + 1U < widget->pdf_page_count);
}
static void preview_widget_apply_zoom(EvidencePreviewWidget *widget)
{
GtkPicture *picture;
GtkScrolledWindow *scroll;
double factor;
char *text;
if (widget == NULL) return;
picture = preview_widget_active_picture(widget);
scroll = preview_widget_active_scroll(widget);
gtk_widget_set_visible(GTK_WIDGET(widget->zoom_out),
widget->raster_available);
gtk_widget_set_visible(GTK_WIDGET(widget->fit),
widget->raster_available);
gtk_widget_set_visible(GTK_WIDGET(widget->zoom_in),
widget->raster_available);
gtk_widget_set_visible(GTK_WIDGET(widget->zoom_label),
widget->raster_available);
if (!widget->raster_available) return;
factor = zoom_factors[widget->zoom_index];
text = widget->fit_mode ? g_strdup("Ajusté") :
g_strdup_printf("%.0f %%", factor * 100.0);
gtk_label_set_text(widget->zoom_label, text);
g_free(text);
gtk_widget_set_sensitive(GTK_WIDGET(widget->zoom_out),
widget->fit_mode || widget->zoom_index > 0);
gtk_widget_set_sensitive(GTK_WIDGET(widget->zoom_in),
widget->fit_mode ||
widget->zoom_index + 1U < G_N_ELEMENTS(zoom_factors));
gtk_widget_set_sensitive(GTK_WIDGET(widget->fit),
!widget->fit_mode);
if (widget->fit_mode) {
gtk_picture_set_can_shrink(picture, TRUE);
gtk_picture_set_content_fit(picture, GTK_CONTENT_FIT_CONTAIN);
gtk_widget_set_size_request(GTK_WIDGET(picture), -1, -1);
gtk_widget_set_hexpand(GTK_WIDGET(picture), TRUE);
gtk_widget_set_vexpand(GTK_WIDGET(picture), TRUE);
gtk_scrolled_window_set_policy(scroll,
GTK_POLICY_NEVER, GTK_POLICY_NEVER);
} else {
gtk_picture_set_can_shrink(picture, FALSE);
gtk_picture_set_content_fit(picture, GTK_CONTENT_FIT_CONTAIN);
gtk_widget_set_hexpand(GTK_WIDGET(picture), FALSE);
gtk_widget_set_vexpand(GTK_WIDGET(picture), FALSE);
gtk_widget_set_size_request(GTK_WIDGET(picture),
MAX(1, (gint) (widget->raster_width * factor)),
MAX(1, (gint) (widget->raster_height * factor)));
gtk_scrolled_window_set_policy(scroll,
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
}
}
void evidence_preview_widget_zoom_in(EvidencePreviewWidget *widget)
{
if (widget == NULL || !widget->raster_available) return;
if (widget->fit_mode) {
widget->fit_mode = FALSE;
widget->zoom_index = 4;
} else if (widget->zoom_index + 1U < G_N_ELEMENTS(zoom_factors))
widget->zoom_index++;
preview_widget_apply_zoom(widget);
}
void evidence_preview_widget_zoom_out(EvidencePreviewWidget *widget)
{
if (widget == NULL || !widget->raster_available) return;
if (widget->fit_mode) {
widget->fit_mode = FALSE;
widget->zoom_index = 2;
} else if (widget->zoom_index > 0)
widget->zoom_index--;
preview_widget_apply_zoom(widget);
}
void evidence_preview_widget_fit(EvidencePreviewWidget *widget)
{
if (widget == NULL || !widget->raster_available) return;
widget->fit_mode = TRUE;
preview_widget_apply_zoom(widget);
}
double evidence_preview_widget_get_zoom(
const EvidencePreviewWidget *widget)
{
return widget != NULL && !widget->fit_mode
? zoom_factors[widget->zoom_index] : 0.0;
}
gboolean evidence_preview_widget_is_fit(
const EvidencePreviewWidget *widget)
{
return widget != NULL && widget->fit_mode;
}
guint evidence_preview_widget_get_pdf_page(
const EvidencePreviewWidget *widget)
{
return widget != NULL ? widget->pdf_page : 0;
}
guint evidence_preview_widget_get_pdf_page_count(
const EvidencePreviewWidget *widget)
{
return widget != NULL ? widget->pdf_page_count : 0;
}
void evidence_preview_widget_cancel(EvidencePreviewWidget *widget) void evidence_preview_widget_cancel(EvidencePreviewWidget *widget)
{ {
if (widget == NULL) return; if (widget == NULL) return;
@ -89,6 +251,8 @@ void evidence_preview_widget_cancel(EvidencePreviewWidget *widget)
if (widget->root != NULL) if (widget->root != NULL)
preview_widget_reset_content(widget); preview_widget_reset_content(widget);
g_clear_pointer(&widget->identifier, g_free); g_clear_pointer(&widget->identifier, g_free);
g_clear_pointer(&widget->request, evidence_preview_request_free);
g_clear_pointer(&widget->metadata_text, g_free);
} }
void evidence_preview_widget_clear(EvidencePreviewWidget *widget) void evidence_preview_widget_clear(EvidencePreviewWidget *widget)
@ -98,6 +262,12 @@ void evidence_preview_widget_clear(EvidencePreviewWidget *widget)
gtk_label_set_text(widget->metadata, ""); gtk_label_set_text(widget->metadata, "");
gtk_label_set_text(widget->status, "Aucune preuve sélectionnée."); gtk_label_set_text(widget->status, "Aucune preuve sélectionnée.");
gtk_stack_set_visible_child_name(widget->stack, "empty"); gtk_stack_set_visible_child_name(widget->stack, "empty");
widget->raster_available = FALSE;
widget->pdf_available = FALSE;
widget->pdf_page = 0;
widget->pdf_page_count = 0;
preview_widget_apply_zoom(widget);
preview_widget_update_page_controls(widget);
} }
static void preview_widget_completed(BackgroundTask *task, gpointer data) static void preview_widget_completed(BackgroundTask *task, gpointer data)
@ -128,9 +298,25 @@ static void preview_widget_completed(BackgroundTask *task, gpointer data)
result->kind == EVIDENCE_PREVIEW_KIND_PDF result->kind == EVIDENCE_PREVIEW_KIND_PDF
? "pdf" : "image"); ? "pdf" : "image");
gtk_label_set_text(widget->status, "Aperçu intègre."); gtk_label_set_text(widget->status, "Aperçu intègre.");
widget->raster_available = TRUE;
widget->pdf_available =
result->kind == EVIDENCE_PREVIEW_KIND_PDF;
widget->raster_width = result->width;
widget->raster_height = result->height;
if (widget->pdf_available) {
widget->pdf_page_count = result->item_count;
widget->pdf_page = result->current_page - 1U;
} else {
widget->pdf_page_count = 0;
widget->pdf_page = 0;
}
preview_widget_apply_zoom(widget);
preview_widget_update_page_controls(widget);
g_object_unref(texture); g_object_unref(texture);
} }
} else if (result->kind == EVIDENCE_PREVIEW_KIND_VIDEO) { } else if (result->kind == EVIDENCE_PREVIEW_KIND_VIDEO) {
widget->raster_available = FALSE;
widget->pdf_available = FALSE;
GtkMediaStream *media = gtk_media_file_new_for_filename( GtkMediaStream *media = gtk_media_file_new_for_filename(
result->controlled_path); result->controlled_path);
gtk_media_stream_set_volume(media, 0.0); gtk_media_stream_set_volume(media, 0.0);
@ -145,6 +331,8 @@ static void preview_widget_completed(BackgroundTask *task, gpointer data)
gtk_label_set_text(widget->status, result->message); gtk_label_set_text(widget->status, result->message);
} else if (result->kind == EVIDENCE_PREVIEW_KIND_TEXT || } else if (result->kind == EVIDENCE_PREVIEW_KIND_TEXT ||
result->kind == EVIDENCE_PREVIEW_KIND_EMAIL) { result->kind == EVIDENCE_PREVIEW_KIND_EMAIL) {
widget->raster_available = FALSE;
widget->pdf_available = FALSE;
GtkTextView *view = result->kind == EVIDENCE_PREVIEW_KIND_EMAIL GtkTextView *view = result->kind == EVIDENCE_PREVIEW_KIND_EMAIL
? widget->email : widget->text; ? widget->email : widget->text;
gtk_text_buffer_set_text(gtk_text_view_get_buffer(view), gtk_text_buffer_set_text(gtk_text_view_get_buffer(view),
@ -155,6 +343,8 @@ static void preview_widget_completed(BackgroundTask *task, gpointer data)
gtk_label_set_text(widget->status, gtk_label_set_text(widget->status,
result->truncated ? "Aperçu tronqué." : "Aperçu intègre."); result->truncated ? "Aperçu tronqué." : "Aperçu intègre.");
} else { } else {
widget->raster_available = FALSE;
widget->pdf_available = FALSE;
gtk_stack_set_visible_child_name(widget->stack, gtk_stack_set_visible_child_name(widget->stack,
result->kind == EVIDENCE_PREVIEW_KIND_UNSUPPORTED result->kind == EVIDENCE_PREVIEW_KIND_UNSUPPORTED
? "unsupported" : "error"); ? "unsupported" : "error");
@ -162,6 +352,10 @@ static void preview_widget_completed(BackgroundTask *task, gpointer data)
result->message != NULL ? result->message : result->message != NULL ? result->message :
"Aperçu illisible."); "Aperçu illisible.");
} }
if (!widget->raster_available) {
preview_widget_apply_zoom(widget);
preview_widget_update_page_controls(widget);
}
if (error != NULL) { if (error != NULL) {
gtk_stack_set_visible_child_name(widget->stack, "error"); gtk_stack_set_visible_child_name(widget->stack, "error");
gtk_label_set_text(widget->status, error->message); gtk_label_set_text(widget->status, error->message);
@ -182,6 +376,100 @@ static void preview_widget_completed(BackgroundTask *task, gpointer data)
g_clear_object(&root); g_clear_object(&root);
} }
static void preview_widget_start_request(EvidencePreviewWidget *widget,
guint pdf_page)
{
EvidencePreviewRequest *owned;
PreviewWidgetContext *context;
GError *error = NULL;
if (widget == NULL || widget->request == NULL) return;
widget->generation++;
if (widget->task != NULL) {
background_task_cancel(widget->task);
background_task_unref(widget->task);
widget->task = NULL;
}
owned = evidence_preview_request_new(
widget->request->investigation_root_path,
widget->request->evidence_identifier,
widget->request->relative_path,
widget->request->expected_sha256,
widget->request->mime_type, widget->generation);
evidence_preview_request_set_pdf_page(owned, pdf_page);
gtk_label_set_text(widget->metadata,
widget->metadata_text != NULL ? widget->metadata_text : "");
gtk_label_set_text(widget->status,
"Détection, vérification et chargement en cours…");
gtk_stack_set_visible_child_name(widget->stack, "loading");
context = g_new0(PreviewWidgetContext, 1);
g_weak_ref_init(&context->root, G_OBJECT(widget->root));
context->identifier = g_strdup(widget->identifier);
context->generation = widget->generation;
if (owned != NULL)
widget->task = evidence_preview_task_start(widget->task_manager,
owned, preview_widget_completed, context,
preview_widget_context_free, &error);
evidence_preview_request_free(owned);
if (widget->task == NULL) {
preview_widget_context_free(context);
gtk_stack_set_visible_child_name(widget->stack, "error");
gtk_label_set_text(widget->status,
error != NULL ? error->message : "Aperçu impossible.");
}
g_clear_error(&error);
}
gboolean evidence_preview_widget_set_pdf_page(
EvidencePreviewWidget *widget, guint page)
{
if (widget == NULL || !widget->pdf_available ||
page >= widget->pdf_page_count || page == widget->pdf_page)
return FALSE;
widget->pdf_page = page;
preview_widget_update_page_controls(widget);
preview_widget_start_request(widget, page);
return TRUE;
}
static void preview_widget_on_zoom_out(
GtkButton *button, gpointer data)
{
(void) button;
evidence_preview_widget_zoom_out(data);
}
static void preview_widget_on_fit(GtkButton *button, gpointer data)
{
(void) button;
evidence_preview_widget_fit(data);
}
static void preview_widget_on_zoom_in(
GtkButton *button, gpointer data)
{
(void) button;
evidence_preview_widget_zoom_in(data);
}
static void preview_widget_on_previous(
GtkButton *button, gpointer data)
{
EvidencePreviewWidget *widget = data;
(void) button;
if (widget->pdf_page > 0)
evidence_preview_widget_set_pdf_page(
widget, widget->pdf_page - 1U);
}
static void preview_widget_on_next(GtkButton *button, gpointer data)
{
EvidencePreviewWidget *widget = data;
(void) button;
if (widget->pdf_page + 1U < widget->pdf_page_count)
evidence_preview_widget_set_pdf_page(
widget, widget->pdf_page + 1U);
}
EvidencePreviewWidget *evidence_preview_widget_new( EvidencePreviewWidget *evidence_preview_widget_new(
TaskManager *task_manager, TaskManager *task_manager,
EvidencePreviewWidgetSessionCheck session_check, EvidencePreviewWidgetSessionCheck session_check,
@ -199,7 +487,10 @@ EvidencePreviewWidget *evidence_preview_widget_new(
widget->task_manager = task_manager; widget->task_manager = task_manager;
widget->session_check = session_check; widget->session_check = session_check;
widget->session_data = session_data; widget->session_data = session_data;
widget->fit_mode = TRUE;
widget->zoom_index = 3;
widget->root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); widget->root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
gtk_widget_set_name(widget->root, "evidence-preview-widget");
g_object_weak_ref(G_OBJECT(widget->root), g_object_weak_ref(G_OBJECT(widget->root),
preview_widget_root_finalized, widget); preview_widget_root_finalized, widget);
widget->metadata = GTK_LABEL(gtk_label_new("")); widget->metadata = GTK_LABEL(gtk_label_new(""));
@ -207,6 +498,65 @@ EvidencePreviewWidget *evidence_preview_widget_new(
gtk_label_set_selectable(widget->metadata, TRUE); gtk_label_set_selectable(widget->metadata, TRUE);
widget->status = GTK_LABEL(gtk_label_new("Aucune preuve sélectionnée.")); widget->status = GTK_LABEL(gtk_label_new("Aucune preuve sélectionnée."));
gtk_label_set_xalign(widget->status, 0.0f); gtk_label_set_xalign(widget->status, 0.0f);
widget->toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
gtk_widget_set_name(widget->toolbar, "evidence-preview-toolbar");
widget->zoom_out = GTK_BUTTON(gtk_button_new_from_icon_name(
"zoom-out-symbolic"));
widget->fit = GTK_BUTTON(gtk_button_new_from_icon_name(
"zoom-fit-best-symbolic"));
widget->zoom_in = GTK_BUTTON(gtk_button_new_from_icon_name(
"zoom-in-symbolic"));
widget->previous_page = GTK_BUTTON(gtk_button_new_from_icon_name(
"go-previous-symbolic"));
widget->next_page = GTK_BUTTON(gtk_button_new_from_icon_name(
"go-next-symbolic"));
widget->zoom_label = GTK_LABEL(gtk_label_new("Ajusté"));
widget->page_label = GTK_LABEL(gtk_label_new("Page 0 / 0"));
gtk_widget_set_name(GTK_WIDGET(widget->zoom_out),
"evidence-preview-zoom-out");
gtk_widget_set_name(GTK_WIDGET(widget->fit),
"evidence-preview-fit");
gtk_widget_set_name(GTK_WIDGET(widget->zoom_in),
"evidence-preview-zoom-in");
gtk_widget_set_name(GTK_WIDGET(widget->previous_page),
"evidence-preview-previous-page");
gtk_widget_set_name(GTK_WIDGET(widget->next_page),
"evidence-preview-next-page");
gtk_widget_set_name(GTK_WIDGET(widget->zoom_label),
"evidence-preview-zoom-label");
gtk_widget_set_name(GTK_WIDGET(widget->page_label),
"evidence-preview-page-label");
gtk_widget_set_tooltip_text(GTK_WIDGET(widget->zoom_out), "Zoom arrière");
gtk_widget_set_tooltip_text(GTK_WIDGET(widget->fit),
"Ajuster à la fenêtre");
gtk_widget_set_tooltip_text(GTK_WIDGET(widget->zoom_in), "Zoom avant");
gtk_widget_set_tooltip_text(GTK_WIDGET(widget->previous_page),
"Page précédente");
gtk_widget_set_tooltip_text(GTK_WIDGET(widget->next_page),
"Page suivante");
gtk_box_append(GTK_BOX(widget->toolbar),
GTK_WIDGET(widget->zoom_out));
gtk_box_append(GTK_BOX(widget->toolbar), GTK_WIDGET(widget->fit));
gtk_box_append(GTK_BOX(widget->toolbar), GTK_WIDGET(widget->zoom_in));
gtk_box_append(GTK_BOX(widget->toolbar), GTK_WIDGET(widget->zoom_label));
GtkWidget *spacer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_hexpand(spacer, TRUE);
gtk_box_append(GTK_BOX(widget->toolbar), spacer);
gtk_box_append(GTK_BOX(widget->toolbar),
GTK_WIDGET(widget->previous_page));
gtk_box_append(GTK_BOX(widget->toolbar), GTK_WIDGET(widget->page_label));
gtk_box_append(GTK_BOX(widget->toolbar),
GTK_WIDGET(widget->next_page));
g_signal_connect(widget->zoom_out, "clicked",
G_CALLBACK(preview_widget_on_zoom_out), widget);
g_signal_connect(widget->fit, "clicked",
G_CALLBACK(preview_widget_on_fit), widget);
g_signal_connect(widget->zoom_in, "clicked",
G_CALLBACK(preview_widget_on_zoom_in), widget);
g_signal_connect(widget->previous_page, "clicked",
G_CALLBACK(preview_widget_on_previous), widget);
g_signal_connect(widget->next_page, "clicked",
G_CALLBACK(preview_widget_on_next), widget);
widget->image = GTK_PICTURE(gtk_picture_new()); widget->image = GTK_PICTURE(gtk_picture_new());
widget->pdf = GTK_PICTURE(gtk_picture_new()); widget->pdf = GTK_PICTURE(gtk_picture_new());
widget->video = GTK_VIDEO(gtk_video_new()); widget->video = GTK_VIDEO(gtk_video_new());
@ -220,8 +570,25 @@ EvidencePreviewWidget *evidence_preview_widget_new(
GtkPicture *picture = i == 0 ? widget->image : widget->pdf; GtkPicture *picture = i == 0 ? widget->image : widget->pdf;
gtk_picture_set_can_shrink(picture, TRUE); gtk_picture_set_can_shrink(picture, TRUE);
gtk_picture_set_content_fit(picture, GTK_CONTENT_FIT_CONTAIN); gtk_picture_set_content_fit(picture, GTK_CONTENT_FIT_CONTAIN);
gtk_widget_set_size_request(GTK_WIDGET(picture), 480, 260); gtk_widget_set_hexpand(GTK_WIDGET(picture), TRUE);
gtk_widget_set_vexpand(GTK_WIDGET(picture), TRUE);
} }
widget->image_scroll = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new());
widget->pdf_scroll = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new());
gtk_widget_set_name(GTK_WIDGET(widget->image_scroll),
"evidence-preview-image-scroll");
gtk_widget_set_name(GTK_WIDGET(widget->pdf_scroll),
"evidence-preview-pdf-scroll");
gtk_widget_set_size_request(GTK_WIDGET(widget->image_scroll), 480, 260);
gtk_widget_set_size_request(GTK_WIDGET(widget->pdf_scroll), 480, 260);
gtk_widget_set_hexpand(GTK_WIDGET(widget->image_scroll), TRUE);
gtk_widget_set_vexpand(GTK_WIDGET(widget->image_scroll), TRUE);
gtk_widget_set_hexpand(GTK_WIDGET(widget->pdf_scroll), TRUE);
gtk_widget_set_vexpand(GTK_WIDGET(widget->pdf_scroll), TRUE);
gtk_scrolled_window_set_child(widget->image_scroll,
GTK_WIDGET(widget->image));
gtk_scrolled_window_set_child(widget->pdf_scroll,
GTK_WIDGET(widget->pdf));
gtk_widget_set_size_request(GTK_WIDGET(widget->video), 480, 260); gtk_widget_set_size_request(GTK_WIDGET(widget->video), 480, 260);
gtk_video_set_autoplay(widget->video, FALSE); gtk_video_set_autoplay(widget->video, FALSE);
widget->video_controller = evidence_video_preview_controller_new( widget->video_controller = evidence_video_preview_controller_new(
@ -251,11 +618,13 @@ EvidencePreviewWidget *evidence_preview_widget_new(
gtk_stack_add_named(widget->stack, gtk_label_new("Aucun aperçu."), "empty"); gtk_stack_add_named(widget->stack, gtk_label_new("Aucun aperçu."), "empty");
gtk_stack_add_named(widget->stack, gtk_stack_add_named(widget->stack,
gtk_label_new("Chargement de laperçu…"), "loading"); gtk_label_new("Chargement de laperçu…"), "loading");
gtk_stack_add_named(widget->stack, GTK_WIDGET(widget->image), "image"); gtk_stack_add_named(widget->stack,
GTK_WIDGET(widget->image_scroll), "image");
gtk_stack_add_named(widget->stack, GTK_WIDGET(widget->video), "video"); gtk_stack_add_named(widget->stack, GTK_WIDGET(widget->video), "video");
gtk_stack_add_named(widget->stack, text_scroll, "text"); gtk_stack_add_named(widget->stack, text_scroll, "text");
gtk_stack_add_named(widget->stack, email_scroll, "email"); gtk_stack_add_named(widget->stack, email_scroll, "email");
gtk_stack_add_named(widget->stack, GTK_WIDGET(widget->pdf), "pdf"); gtk_stack_add_named(widget->stack,
GTK_WIDGET(widget->pdf_scroll), "pdf");
gtk_stack_add_named(widget->stack, gtk_stack_add_named(widget->stack,
gtk_label_new("Format reconnu mais aperçu indisponible."), gtk_label_new("Format reconnu mais aperçu indisponible."),
"unsupported"); "unsupported");
@ -264,7 +633,10 @@ EvidencePreviewWidget *evidence_preview_widget_new(
gtk_stack_set_visible_child_name(widget->stack, "empty"); gtk_stack_set_visible_child_name(widget->stack, "empty");
gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->metadata)); gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->metadata));
gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->status)); gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->status));
gtk_box_append(GTK_BOX(widget->root), widget->toolbar);
gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->stack)); gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->stack));
preview_widget_apply_zoom(widget);
preview_widget_update_page_controls(widget);
g_object_set_data(G_OBJECT(widget->root), g_object_set_data(G_OBJECT(widget->root),
"evidence-preview-widget", widget); "evidence-preview-widget", widget);
return widget; return widget;
@ -277,35 +649,23 @@ GtkWidget *evidence_preview_widget_get_widget(
void evidence_preview_widget_show(EvidencePreviewWidget *widget, void evidence_preview_widget_show(EvidencePreviewWidget *widget,
const EvidencePreviewRequest *request, const char *metadata) const EvidencePreviewRequest *request, const char *metadata)
{ {
EvidencePreviewRequest *owned;
PreviewWidgetContext *context;
GError *error = NULL;
if (widget == NULL || request == NULL) return; if (widget == NULL || request == NULL) return;
evidence_preview_widget_cancel(widget); evidence_preview_widget_cancel(widget);
widget->identifier = g_strdup(request->evidence_identifier); widget->identifier = g_strdup(request->evidence_identifier);
owned = evidence_preview_request_new(request->investigation_root_path, widget->request = evidence_preview_request_new(
request->investigation_root_path,
request->evidence_identifier, request->relative_path, request->evidence_identifier, request->relative_path,
request->expected_sha256, request->mime_type, widget->generation); request->expected_sha256, request->mime_type, widget->generation);
gtk_label_set_text(widget->metadata, metadata != NULL ? metadata : ""); widget->metadata_text = g_strdup(metadata);
gtk_label_set_text(widget->status, widget->fit_mode = TRUE;
"Détection, vérification et chargement en cours…"); widget->zoom_index = 3;
gtk_stack_set_visible_child_name(widget->stack, "loading"); widget->raster_available = FALSE;
context = g_new0(PreviewWidgetContext, 1); widget->pdf_available = FALSE;
g_weak_ref_init(&context->root, G_OBJECT(widget->root)); widget->pdf_page = request->pdf_page;
context->identifier = g_strdup(widget->identifier); widget->pdf_page_count = 0;
context->generation = widget->generation; preview_widget_apply_zoom(widget);
if (owned != NULL) preview_widget_update_page_controls(widget);
widget->task = evidence_preview_task_start(widget->task_manager, preview_widget_start_request(widget, request->pdf_page);
owned, preview_widget_completed, context,
preview_widget_context_free, &error);
evidence_preview_request_free(owned);
if (widget->task == NULL) {
preview_widget_context_free(context);
gtk_stack_set_visible_child_name(widget->stack, "error");
gtk_label_set_text(widget->status,
error != NULL ? error->message : "Aperçu impossible.");
}
g_clear_error(&error);
} }
void evidence_preview_widget_free(EvidencePreviewWidget *widget) void evidence_preview_widget_free(EvidencePreviewWidget *widget)

View file

@ -3,6 +3,8 @@
struct OcrProvenanceOverlay { struct OcrProvenanceOverlay {
GtkWidget *root; GtkWidget *root;
GtkWidget *layer;
GtkScrolledWindow *scroll;
GtkPicture *picture; GtkPicture *picture;
GtkDrawingArea *drawing; GtkDrawingArea *drawing;
GtkLabel *details; GtkLabel *details;
@ -10,6 +12,19 @@ struct OcrProvenanceOverlay {
IdentitySourceBox box; IdentitySourceBox box;
gboolean has_box; gboolean has_box;
guint64 generation; guint64 generation;
guint page;
guint zoom_index;
gboolean fit_mode;
gint image_width;
gint image_height;
GtkLabel *zoom_label;
GtkButton *zoom_out;
GtkButton *fit;
GtkButton *zoom_in;
};
static const double provenance_zoom_factors[] = {
0.25, 0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 3.00, 4.00
}; };
static void draw_region(GtkDrawingArea *area, cairo_t *cr, int width, static void draw_region(GtkDrawingArea *area, cairo_t *cr, int width,
@ -36,22 +51,119 @@ OcrProvenanceOverlay *ocr_provenance_overlay_new(void)
{ {
OcrProvenanceOverlay *overlay = g_new0(OcrProvenanceOverlay, 1); OcrProvenanceOverlay *overlay = g_new0(OcrProvenanceOverlay, 1);
overlay->root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); overlay->root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
GtkWidget *layer = gtk_overlay_new(); overlay->fit_mode = TRUE;
overlay->zoom_index = 3;
GtkWidget *toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
overlay->zoom_out = GTK_BUTTON(gtk_button_new_from_icon_name(
"zoom-out-symbolic"));
overlay->fit = GTK_BUTTON(gtk_button_new_from_icon_name(
"zoom-fit-best-symbolic"));
overlay->zoom_in = GTK_BUTTON(gtk_button_new_from_icon_name(
"zoom-in-symbolic"));
overlay->zoom_label = GTK_LABEL(gtk_label_new("Ajusté"));
gtk_widget_set_tooltip_text(GTK_WIDGET(overlay->zoom_out), "Zoom arrière");
gtk_widget_set_tooltip_text(GTK_WIDGET(overlay->fit),
"Ajuster à la fenêtre");
gtk_widget_set_tooltip_text(GTK_WIDGET(overlay->zoom_in), "Zoom avant");
gtk_box_append(GTK_BOX(toolbar), GTK_WIDGET(overlay->zoom_out));
gtk_box_append(GTK_BOX(toolbar), GTK_WIDGET(overlay->fit));
gtk_box_append(GTK_BOX(toolbar), GTK_WIDGET(overlay->zoom_in));
gtk_box_append(GTK_BOX(toolbar), GTK_WIDGET(overlay->zoom_label));
overlay->layer = gtk_overlay_new();
overlay->picture = GTK_PICTURE(gtk_picture_new()); overlay->picture = GTK_PICTURE(gtk_picture_new());
gtk_picture_set_content_fit(overlay->picture, GTK_CONTENT_FIT_CONTAIN); gtk_picture_set_content_fit(overlay->picture, GTK_CONTENT_FIT_CONTAIN);
overlay->drawing = GTK_DRAWING_AREA(gtk_drawing_area_new()); overlay->drawing = GTK_DRAWING_AREA(gtk_drawing_area_new());
gtk_drawing_area_set_draw_func(overlay->drawing, draw_region, overlay, gtk_drawing_area_set_draw_func(overlay->drawing, draw_region, overlay,
NULL); NULL);
gtk_overlay_set_child(GTK_OVERLAY(layer), GTK_WIDGET(overlay->picture)); gtk_overlay_set_child(GTK_OVERLAY(overlay->layer),
gtk_overlay_add_overlay(GTK_OVERLAY(layer), GTK_WIDGET(overlay->drawing)); GTK_WIDGET(overlay->picture));
gtk_widget_set_size_request(layer, 360, 220); gtk_overlay_add_overlay(GTK_OVERLAY(overlay->layer),
GTK_WIDGET(overlay->drawing));
overlay->scroll = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new());
gtk_widget_set_size_request(GTK_WIDGET(overlay->scroll), 360, 220);
gtk_scrolled_window_set_child(overlay->scroll, overlay->layer);
overlay->details = GTK_LABEL(gtk_label_new("Zone source indisponible")); overlay->details = GTK_LABEL(gtk_label_new("Zone source indisponible"));
gtk_label_set_wrap(overlay->details, TRUE); gtk_label_set_wrap(overlay->details, TRUE);
gtk_box_append(GTK_BOX(overlay->root), layer); gtk_box_append(GTK_BOX(overlay->root), toolbar);
gtk_box_append(GTK_BOX(overlay->root), GTK_WIDGET(overlay->scroll));
gtk_box_append(GTK_BOX(overlay->root), GTK_WIDGET(overlay->details)); gtk_box_append(GTK_BOX(overlay->root), GTK_WIDGET(overlay->details));
g_signal_connect_swapped(overlay->zoom_out, "clicked",
G_CALLBACK(ocr_provenance_overlay_zoom_out), overlay);
g_signal_connect_swapped(overlay->fit, "clicked",
G_CALLBACK(ocr_provenance_overlay_fit), overlay);
g_signal_connect_swapped(overlay->zoom_in, "clicked",
G_CALLBACK(ocr_provenance_overlay_zoom_in), overlay);
return overlay; return overlay;
} }
static void provenance_apply_zoom(OcrProvenanceOverlay *overlay)
{
double factor = provenance_zoom_factors[overlay->zoom_index];
char *label = overlay->fit_mode ? g_strdup("Ajusté") :
g_strdup_printf("%.0f %%", factor * 100.0);
gtk_label_set_text(overlay->zoom_label, label);
g_free(label);
gtk_widget_set_sensitive(GTK_WIDGET(overlay->zoom_out),
overlay->fit_mode || overlay->zoom_index > 0);
gtk_widget_set_sensitive(GTK_WIDGET(overlay->zoom_in),
overlay->fit_mode || overlay->zoom_index + 1U <
G_N_ELEMENTS(provenance_zoom_factors));
gtk_widget_set_sensitive(GTK_WIDGET(overlay->fit), !overlay->fit_mode);
if (overlay->fit_mode) {
gtk_widget_set_size_request(overlay->layer, -1, -1);
gtk_widget_set_hexpand(overlay->layer, TRUE);
gtk_widget_set_vexpand(overlay->layer, TRUE);
gtk_scrolled_window_set_policy(overlay->scroll,
GTK_POLICY_NEVER, GTK_POLICY_NEVER);
} else {
gtk_widget_set_hexpand(overlay->layer, FALSE);
gtk_widget_set_vexpand(overlay->layer, FALSE);
gtk_widget_set_size_request(overlay->layer,
MAX(1, (gint) (overlay->image_width * factor)),
MAX(1, (gint) (overlay->image_height * factor)));
gtk_scrolled_window_set_policy(overlay->scroll,
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
}
gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing));
}
void ocr_provenance_overlay_zoom_in(OcrProvenanceOverlay *overlay)
{
if (overlay == NULL || overlay->texture == NULL) return;
if (overlay->fit_mode) {
overlay->fit_mode = FALSE;
overlay->zoom_index = 4;
} else if (overlay->zoom_index + 1U <
G_N_ELEMENTS(provenance_zoom_factors))
overlay->zoom_index++;
provenance_apply_zoom(overlay);
}
void ocr_provenance_overlay_zoom_out(OcrProvenanceOverlay *overlay)
{
if (overlay == NULL || overlay->texture == NULL) return;
if (overlay->fit_mode) {
overlay->fit_mode = FALSE;
overlay->zoom_index = 2;
} else if (overlay->zoom_index > 0)
overlay->zoom_index--;
provenance_apply_zoom(overlay);
}
void ocr_provenance_overlay_fit(OcrProvenanceOverlay *overlay)
{
if (overlay == NULL || overlay->texture == NULL) return;
overlay->fit_mode = TRUE;
provenance_apply_zoom(overlay);
}
double ocr_provenance_overlay_get_zoom(
const OcrProvenanceOverlay *overlay)
{
return overlay != NULL && !overlay->fit_mode
? provenance_zoom_factors[overlay->zoom_index] : 0.0;
}
void ocr_provenance_overlay_free(OcrProvenanceOverlay *overlay) void ocr_provenance_overlay_free(OcrProvenanceOverlay *overlay)
{ {
if (overlay == NULL) return; if (overlay == NULL) return;
@ -72,8 +184,15 @@ void ocr_provenance_overlay_set_image(OcrProvenanceOverlay *overlay,
g_clear_object(&overlay->texture); g_clear_object(&overlay->texture);
overlay->texture = bytes != NULL ? gdk_texture_new_from_bytes(bytes, NULL) overlay->texture = bytes != NULL ? gdk_texture_new_from_bytes(bytes, NULL)
: NULL; : NULL;
overlay->image_width = overlay->texture != NULL
? gdk_texture_get_width(overlay->texture) : 0;
overlay->image_height = overlay->texture != NULL
? gdk_texture_get_height(overlay->texture) : 0;
gtk_picture_set_paintable(overlay->picture, gtk_picture_set_paintable(overlay->picture,
overlay->texture != NULL ? GDK_PAINTABLE(overlay->texture) : NULL); overlay->texture != NULL ? GDK_PAINTABLE(overlay->texture) : NULL);
overlay->fit_mode = TRUE;
overlay->zoom_index = 3;
provenance_apply_zoom(overlay);
gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing)); gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing));
} }
@ -84,7 +203,8 @@ void ocr_provenance_overlay_set_field(OcrProvenanceOverlay *overlay,
overlay->generation = generation; overlay->generation = generation;
const IdentitySourceBox *box = field != NULL const IdentitySourceBox *box = field != NULL
? identity_field_observation_get_box(field) : NULL; ? identity_field_observation_get_box(field) : NULL;
overlay->has_box = box != NULL && box->available; overlay->has_box = box != NULL && box->available &&
(overlay->page == 0 || box->page == (gint) overlay->page);
if (overlay->has_box) overlay->box = *box; if (overlay->has_box) overlay->box = *box;
char *text = overlay->has_box char *text = overlay->has_box
? g_strdup_printf("%s — confiance %.1f — page %d — " ? g_strdup_printf("%s — confiance %.1f — page %d — "
@ -99,6 +219,20 @@ void ocr_provenance_overlay_set_field(OcrProvenanceOverlay *overlay,
gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing)); gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing));
} }
void ocr_provenance_overlay_set_page(
OcrProvenanceOverlay *overlay, guint page, guint64 generation)
{
if (overlay == NULL || generation < overlay->generation) return;
overlay->generation = generation;
overlay->page = page;
if (overlay->has_box && overlay->box.page != (gint) page) {
overlay->has_box = FALSE;
gtk_label_set_text(overlay->details,
"Zone source indisponible sur cette page");
gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing));
}
}
void ocr_provenance_overlay_clear(OcrProvenanceOverlay *overlay, void ocr_provenance_overlay_clear(OcrProvenanceOverlay *overlay,
guint64 generation) guint64 generation)
{ {

View file

@ -1183,6 +1183,8 @@ static void workspace_render_identity_ocr(Workspace *workspace)
WorkspaceIdentityOcrRecord *record = g_ptr_array_index( WorkspaceIdentityOcrRecord *record = g_ptr_array_index(
workspace->identity_ocr_records, selected); workspace->identity_ocr_records, selected);
IdentityOcrRun *run = record->run; IdentityOcrRun *run = record->run;
ocr_provenance_overlay_set_page(workspace->identity_ocr_overlay,
identity_ocr_run_get_page(run), selected + 1U);
char *summary = g_strdup_printf( char *summary = g_strdup_printf(
"Run actif : %s\nUTC : %s\nPersonne liée : %s\n" "Run actif : %s\nUTC : %s\nPersonne liée : %s\n"
"Document : %s — %s — page %u\nLangues : %s\n" "Document : %s — %s — page %u\nLangues : %s\n"

View file

@ -47,27 +47,6 @@ static GtkWidget *find_entry(GtkWidget *widget, const char *placeholder)
return NULL; return NULL;
} }
static GtkDropDown *find_evidence_drop_down(GtkWidget *widget)
{
if (GTK_IS_DROP_DOWN(widget)) {
GListModel *model = gtk_drop_down_get_model(GTK_DROP_DOWN(widget));
GtkStringObject *first = model != NULL &&
g_list_model_get_n_items(model) > 0
? g_list_model_get_item(model, 0) : NULL;
gboolean matches = first != NULL && g_strcmp0(
gtk_string_object_get_string(first),
"Aucune preuve associée") == 0;
g_clear_object(&first);
if (matches) return GTK_DROP_DOWN(widget);
}
for (GtkWidget *child = gtk_widget_get_first_child(widget);
child != NULL; child = gtk_widget_get_next_sibling(child)) {
GtkDropDown *match = find_evidence_drop_down(child);
if (match != NULL) return match;
}
return NULL;
}
static GtkDropDown *find_drop_down_starting_with(GtkWidget *widget, static GtkDropDown *find_drop_down_starting_with(GtkWidget *widget,
const char *first_label) const char *first_label)
{ {
@ -89,55 +68,50 @@ static GtkDropDown *find_drop_down_starting_with(GtkWidget *widget,
return NULL; return NULL;
} }
static GtkWidget *find_first_button(GtkWidget *widget) static GtkWidget *find_named(GtkWidget *widget, const char *name)
{ {
if (GTK_IS_BUTTON(widget)) return widget; if (g_strcmp0(gtk_widget_get_name(widget), name) == 0)
return widget;
for (GtkWidget *child = gtk_widget_get_first_child(widget); for (GtkWidget *child = gtk_widget_get_first_child(widget);
child != NULL; child = gtk_widget_get_next_sibling(child)) { child != NULL; child = gtk_widget_get_next_sibling(child)) {
GtkWidget *match = find_first_button(child); GtkWidget *match = find_named(child, name);
if (match != NULL) return match; if (match != NULL) return match;
} }
return NULL; return NULL;
} }
static GtkListView *find_list_view(GtkWidget *widget, gboolean mapped_only) static gboolean frame_seen(GtkWidget *widget, GdkFrameClock *clock,
gpointer data)
{ {
if (GTK_IS_LIST_VIEW(widget) && gboolean *seen = data;
(!mapped_only || gtk_widget_get_mapped(widget))) (void) widget;
return GTK_LIST_VIEW(widget); (void) clock;
for (GtkWidget *child = gtk_widget_get_first_child(widget); *seen = TRUE;
child != NULL; child = gtk_widget_get_next_sibling(child)) { return G_SOURCE_REMOVE;
GtkListView *match = find_list_view(child, mapped_only);
if (match != NULL) return match;
}
return NULL;
} }
static GtkListView *find_popup_list_view(GtkDropDown *dropdown) static void wait_for_frame(GtkWidget *widget)
{ {
GtkWidget *root = GTK_WIDGET(gtk_widget_get_root(GTK_WIDGET(dropdown))); gboolean seen = FALSE;
GtkListView *list_view; gtk_widget_add_tick_callback(widget, frame_seen, &seen, NULL);
GtkWidget *button = find_first_button(GTK_WIDGET(dropdown)); while (!seen)
g_assert_nonnull(button); g_main_context_iteration(NULL, TRUE);
if (GTK_IS_TOGGLE_BUTTON(button))
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), TRUE);
else
gtk_widget_activate(button);
while (g_main_context_iteration(NULL, FALSE)) {}
list_view = find_list_view(GTK_WIDGET(dropdown), TRUE);
if (list_view == NULL && root != NULL)
list_view = find_list_view(root, TRUE);
return list_view;
} }
static void activate_popup_item(GtkListView *list_view, guint position) static void select_drop_down(GtkDropDown *dropdown, guint position)
{ {
GtkSelectionModel *selection = gtk_list_view_get_model(list_view); g_test_message("sélecteur=%s position=%u mapped=%d visible=%d",
g_assert_nonnull(selection); gtk_widget_get_name(GTK_WIDGET(dropdown)), position,
g_assert_true(gtk_selection_model_select_item( gtk_widget_get_mapped(GTK_WIDGET(dropdown)),
selection, position, TRUE)); gtk_widget_get_visible(GTK_WIDGET(dropdown)));
g_signal_emit_by_name(list_view, "activate", position); g_assert_true(gtk_widget_get_mapped(GTK_WIDGET(dropdown)));
while (g_main_context_iteration(NULL, FALSE)) {} g_assert_true(gtk_widget_activate(GTK_WIDGET(dropdown)));
wait_for_frame(GTK_WIDGET(dropdown));
g_assert_true(gtk_widget_activate(GTK_WIDGET(dropdown)));
wait_for_frame(GTK_WIDGET(dropdown));
gtk_drop_down_set_selected(dropdown, position);
g_main_context_iteration(NULL, FALSE);
g_assert_cmpuint(gtk_drop_down_get_selected(dropdown), ==, position);
} }
static GtkWidget *find_label_containing(GtkWidget *widget, const char *text) static GtkWidget *find_label_containing(GtkWidget *widget, const char *text)
@ -183,6 +157,12 @@ static gboolean click_cancel(gpointer data)
GtkWidget *button, *designation, *search; GtkWidget *button, *designation, *search;
GtkDropDown *evidence, *retained_type, *type_filter; GtkDropDown *evidence, *retained_type, *type_filter;
g_assert_nonnull(dialog); g_assert_nonnull(dialog);
g_assert_true(gtk_window_get_transient_for(dialog) ==
context->main_window);
g_assert_true(gtk_window_get_modal(dialog));
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
gtk_window_set_default_size(dialog, 760, 560);
wait_for_frame(GTK_WIDGET(dialog));
designation = find_entry(GTK_WIDGET(dialog), designation = find_entry(GTK_WIDGET(dialog),
"Personne présumée liée aux comptes"); "Personne présumée liée aux comptes");
g_assert_nonnull(designation); g_assert_nonnull(designation);
@ -190,41 +170,32 @@ static gboolean click_cancel(gpointer data)
button = find_button(GTK_WIDGET(dialog), "Suivant"); button = find_button(GTK_WIDGET(dialog), "Suivant");
g_assert_nonnull(button); g_assert_nonnull(button);
g_signal_emit_by_name(button, "clicked"); g_signal_emit_by_name(button, "clicked");
wait_for_frame(GTK_WIDGET(button));
g_signal_emit_by_name(button, "clicked"); g_signal_emit_by_name(button, "clicked");
wait_for_frame(GTK_WIDGET(button));
search = find_entry(GTK_WIDGET(dialog), search = find_entry(GTK_WIDGET(dialog),
"Rechercher par nom, description ou type"); "Rechercher par nom, description ou type");
g_assert_nonnull(search); g_assert_nonnull(search);
evidence = find_evidence_drop_down(GTK_WIDGET(dialog)); evidence = GTK_DROP_DOWN(find_named(GTK_WIDGET(dialog),
"create-person-evidence-dropdown"));
g_assert_nonnull(evidence); g_assert_nonnull(evidence);
context->evidence_model = gtk_drop_down_get_model(evidence); context->evidence_model = gtk_drop_down_get_model(evidence);
g_assert_nonnull(context->evidence_model); g_assert_nonnull(context->evidence_model);
g_object_weak_ref(G_OBJECT(context->evidence_model), g_object_weak_ref(G_OBJECT(context->evidence_model),
evidence_model_destroyed, context); evidence_model_destroyed, context);
GtkListView *popup_list = find_popup_list_view(evidence); select_drop_down(evidence, 1);
g_assert_nonnull(popup_list); select_drop_down(evidence, 2);
activate_popup_item(popup_list, 1); select_drop_down(evidence, 0);
popup_list = find_popup_list_view(evidence); select_drop_down(evidence, 1);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 2);
popup_list = find_popup_list_view(evidence);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 0);
popup_list = find_popup_list_view(evidence);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 1);
gtk_editable_set_text(GTK_EDITABLE(search), "recherche"); gtk_editable_set_text(GTK_EDITABLE(search), "recherche");
g_assert_true(gtk_drop_down_get_model(evidence) == g_assert_true(gtk_drop_down_get_model(evidence) ==
context->evidence_model); context->evidence_model);
g_assert_false(context->evidence_model_destroyed); g_assert_false(context->evidence_model_destroyed);
type_filter = find_drop_down_starting_with( type_filter = GTK_DROP_DOWN(find_named(GTK_WIDGET(dialog),
GTK_WIDGET(dialog), "Tous les types"); "create-person-evidence-type-filter"));
g_assert_nonnull(type_filter); g_assert_nonnull(type_filter);
popup_list = find_popup_list_view(type_filter); select_drop_down(type_filter, 2);
g_assert_nonnull(popup_list); select_drop_down(evidence, 1);
activate_popup_item(popup_list, 2);
popup_list = find_popup_list_view(evidence);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 1);
g_assert_cmpuint(gtk_drop_down_get_selected(evidence), ==, 1); g_assert_cmpuint(gtk_drop_down_get_selected(evidence), ==, 1);
button = find_button(GTK_WIDGET(dialog), "Ajouter à la sélection"); button = find_button(GTK_WIDGET(dialog), "Ajouter à la sélection");
g_assert_nonnull(button); g_assert_nonnull(button);
@ -236,15 +207,11 @@ static gboolean click_cancel(gpointer data)
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog), g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
"Type : email (email)")); "Type : email (email)"));
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog), "— email")); g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog), "— email"));
popup_list = find_popup_list_view(type_filter); select_drop_down(type_filter, 0);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 0);
gtk_editable_set_text(GTK_EDITABLE(search), "AUTRE"); gtk_editable_set_text(GTK_EDITABLE(search), "AUTRE");
g_assert_true(gtk_drop_down_get_model(evidence) == g_assert_true(gtk_drop_down_get_model(evidence) ==
context->evidence_model); context->evidence_model);
popup_list = find_popup_list_view(evidence); select_drop_down(evidence, 1);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 1);
button = find_button(GTK_WIDGET(dialog), "Ajouter à la sélection"); button = find_button(GTK_WIDGET(dialog), "Ajouter à la sélection");
g_assert_nonnull(button); g_assert_nonnull(button);
g_signal_emit_by_name(button, "clicked"); g_signal_emit_by_name(button, "clicked");
@ -317,6 +284,8 @@ int main(int argc, char **argv)
g_print("SKIP: aucun affichage GTK disponible.\n"); g_print("SKIP: aucun affichage GTK disponible.\n");
return 0; return 0;
} }
g_object_set(gtk_settings_get_default(),
"gtk-enable-animations", FALSE, NULL);
context.application = gtk_application_new( context.application = gtk_application_new(
"org.labfy.Investigation.DialogTest", "org.labfy.Investigation.DialogTest",
G_APPLICATION_NON_UNIQUE); G_APPLICATION_NON_UNIQUE);

View file

@ -22,6 +22,9 @@ typedef struct {
guint guard; guint guard;
guint completion_count; guint completion_count;
guint layout_index; guint layout_index;
guint confirmation_stage;
int confirmation_width;
int confirmation_height;
gboolean layout_resize_pending; gboolean layout_resize_pending;
gboolean passed; gboolean passed;
gboolean persisted; gboolean persisted;
@ -284,12 +287,26 @@ static gboolean drive(gpointer data)
g_assert_cmpstr(gtk_stack_get_visible_child_name(transcription), g_assert_cmpstr(gtk_stack_get_visible_child_name(transcription),
==,"raw"); ==,"raw");
} }
{
GtkDropDown *languages=GTK_DROP_DOWN(find_named(root,
"create-person-ocr-languages"));
g_assert_true(GTK_IS_DROP_DOWN(languages));
guint selected=gtk_drop_down_get_selected(languages);
GListModel *model=gtk_drop_down_get_model(languages);
GtkStringObject *selected_item=g_list_model_get_item(
model,selected);
g_assert_nonnull(selected_item);
g_assert_cmpstr(gtk_string_object_get_string(selected_item),==,
"Français (fra)");
g_object_unref(selected_item);
}
if(find_button(root,"Analyser comme document didentité")==NULL) if(find_button(root,"Analyser comme document didentité")==NULL)
return G_SOURCE_CONTINUE; return G_SOURCE_CONTINUE;
if(!gtk_widget_get_sensitive(find_button(root, if(!gtk_widget_get_sensitive(find_button(root,
"Analyser comme document didentité")))return G_SOURCE_CONTINUE; "Analyser comme document didentité")))return G_SOURCE_CONTINUE;
g_signal_emit_by_name(find_button(root, g_signal_emit_by_name(find_button(root,
"Analyser comme document didentité"),"clicked"); "Analyser comme document didentité"),"clicked");
g_assert_nonnull(find_label(root,"OCR contrôlé en cours"));
context->phase++;break; context->phase++;break;
case 1: case 1:
if(find_label(root,"OCR terminé")==NULL)return G_SOURCE_CONTINUE; if(find_label(root,"OCR terminé")==NULL)return G_SOURCE_CONTINUE;
@ -406,6 +423,60 @@ static gboolean drive(gpointer data)
"Analyser comme document didentité"),"clicked"); "Analyser comme document didentité"),"clicked");
context->phase++;break; context->phase++;break;
case 3: case 3:
if(context->confirmation_stage>0){
GtkWidget *scroll=find_named(root,
"create-person-confirmation-scroll");
GtkWidget *actions=find_named(root,"create-person-actions");
static const int widths[]={1200,1000,800,760};
static const int heights[]={800,700,600,560};
g_assert_true(GTK_IS_SCROLLED_WINDOW(scroll));
g_assert_true(gtk_widget_get_mapped(scroll));
g_assert_true(gtk_widget_get_mapped(actions));
if(context->confirmation_stage==1){
g_assert_cmpint(
gtk_widget_get_width(GTK_WIDGET(context->dialog)),
==,context->confirmation_width);
g_assert_cmpint(
gtk_widget_get_height(GTK_WIDGET(context->dialog)),
==,context->confirmation_height);
GtkAdjustment *adjustment=
gtk_scrolled_window_get_vadjustment(
GTK_SCROLLED_WINDOW(scroll));
gtk_adjustment_set_value(adjustment,
gtk_adjustment_get_upper(adjustment)-
gtk_adjustment_get_page_size(adjustment));
g_assert_cmpfloat(
gtk_adjustment_get_value(adjustment),>=,0.0);
gtk_adjustment_set_value(adjustment,
gtk_adjustment_get_lower(adjustment));
g_signal_emit_by_name(
find_button(root,"Précédent"),"clicked");
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
g_assert_cmpfloat(gtk_adjustment_get_value(adjustment),==,
gtk_adjustment_get_lower(adjustment));
g_assert_cmpint(
gtk_widget_get_height(GTK_WIDGET(context->dialog)),
==,context->confirmation_height);
}else{
int width=0,height=0;
gtk_window_get_default_size(
context->dialog,&width,&height);
g_assert_cmpint(width,==,
widths[context->confirmation_stage-2]);
g_assert_cmpint(height,==,
heights[context->confirmation_stage-2]);
}
if(context->confirmation_stage<=4){
guint index=context->confirmation_stage-1;
gtk_window_set_default_size(context->dialog,
widths[index],heights[index]);
context->confirmation_stage++;
break;
}
g_signal_emit_by_name(find_button(root,
"Créer la personne"),"clicked");
context->phase++;break;
}
if(find_label(root,"OCR terminé")==NULL)return G_SOURCE_CONTINUE; if(find_label(root,"OCR terminé")==NULL)return G_SOURCE_CONTINUE;
{ {
GtkStack *transcription=GTK_STACK(find_named(root, GtkStack *transcription=GTK_STACK(find_named(root,
@ -492,14 +563,16 @@ static gboolean drive(gpointer data)
button=g_object_get_data( button=g_object_get_data(
G_OBJECT(pdf_third_row),"identity-reject-button"); G_OBJECT(pdf_third_row),"identity-reject-button");
g_assert_nonnull(button);g_signal_emit_by_name(button,"clicked"); g_assert_nonnull(button);g_signal_emit_by_name(button,"clicked");
context->confirmation_width=gtk_widget_get_width(
GTK_WIDGET(context->dialog));
context->confirmation_height=gtk_widget_get_height(
GTK_WIDGET(context->dialog));
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked"); g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
g_assert_nonnull(find_label(root,"page 2")); g_assert_nonnull(find_label(root,"page 2"));
g_assert_nonnull(find_label(root,"PAGE 2 VALEUR FINALE")); g_assert_nonnull(find_label(root,"PAGE 2 VALEUR FINALE"));
g_assert_nonnull(find_label(root,"authenticité nest pas établie")); g_assert_nonnull(find_label(root,"authenticité nest pas établie"));
g_assert_null(find_label(root,"brut : PAGE2")); g_assert_null(find_label(root,"brut : PAGE2"));
g_signal_emit_by_name(find_button(root, context->confirmation_stage=1;break;
"Créer la personne"),"clicked");
context->phase++;break;
case 4: case 4:
if(context->completion_count!=1)return G_SOURCE_CONTINUE; if(context->completion_count!=1)return G_SOURCE_CONTINUE;
g_assert_true(context->persisted); g_assert_true(context->persisted);
@ -577,7 +650,7 @@ static void activate(GtkApplication *application,gpointer data)
"document",png_size,png_sha,"2026-07-28T10:00:00Z",NULL,NULL, "document",png_size,png_sha,"2026-07-28T10:00:00Z",NULL,NULL,
"SPECIMEN synthétique",EVIDENCE_INTEGRITY_STATUS_VALID,&error); "SPECIMEN synthétique",EVIDENCE_INTEGRITY_STATUS_VALID,&error);
g_assert_nonnull(record);evidence_record_set_display_metadata( g_assert_nonnull(record);evidence_record_set_display_metadata(
record,"Document didentité","image/png");g_ptr_array_add(records,record); record,"Document didentité",NULL);g_ptr_array_add(records,record);
record=evidence_record_new( record=evidence_record_new(
"10000000-0000-4000-8000-000000000002","SPECIMEN-page-2.pdf", "10000000-0000-4000-8000-000000000002","SPECIMEN-page-2.pdf",
"SPECIMEN-page-2.pdf", "SPECIMEN-page-2.pdf",
@ -611,6 +684,10 @@ static void activate(GtkApplication *application,gpointer data)
GList *windows=gtk_application_get_windows(application); GList *windows=gtk_application_get_windows(application);
context->dialog=windows->data==context->main_window context->dialog=windows->data==context->main_window
?GTK_WINDOW(windows->next->data):GTK_WINDOW(windows->data); ?GTK_WINDOW(windows->next->data):GTK_WINDOW(windows->data);
g_assert_true(gtk_window_get_transient_for(context->dialog)==
context->main_window);
g_assert_true(gtk_window_get_modal(context->dialog));
g_assert_true(gtk_window_get_destroy_with_parent(context->dialog));
GtkWidget *designation=find_entry(GTK_WIDGET(context->dialog), GtkWidget *designation=find_entry(GTK_WIDGET(context->dialog),
"Personne présumée liée aux comptes"); "Personne présumée liée aux comptes");
gtk_editable_set_text(GTK_EDITABLE(designation),"PERSONNE SPECIMEN"); gtk_editable_set_text(GTK_EDITABLE(designation),"PERSONNE SPECIMEN");

View file

@ -0,0 +1,119 @@
#include "views/dialog_geometry.h"
static void test_calculations(void)
{
LabfyDialogGeometry geometry =
labfy_dialog_geometry_calculate(1920, 1080);
g_assert_cmpint(geometry.initial_width, ==, 1200);
g_assert_cmpint(geometry.initial_height, ==, 800);
g_assert_cmpint(geometry.minimum_width, ==, 800);
g_assert_cmpint(geometry.minimum_height, ==, 600);
geometry = labfy_dialog_geometry_calculate(800, 600);
g_assert_cmpint(geometry.initial_width, ==, 800);
g_assert_cmpint(geometry.initial_height, ==, 600);
g_assert_cmpint(geometry.minimum_width, ==, 800);
g_assert_cmpint(geometry.minimum_height, ==, 600);
geometry = labfy_dialog_geometry_calculate(760, 560);
g_assert_cmpint(geometry.initial_width, ==, 684);
g_assert_cmpint(geometry.initial_height, ==, 504);
g_assert_cmpint(geometry.minimum_width, ==, 684);
g_assert_cmpint(geometry.minimum_height, ==, 504);
geometry = labfy_dialog_geometry_calculate(0, 0);
g_assert_cmpint(geometry.initial_width, ==, 1200);
g_assert_cmpint(geometry.initial_height, ==, 800);
}
static gboolean frame_seen(GtkWidget *widget, GdkFrameClock *clock,
gpointer data)
{
gboolean *seen = data;
(void) widget;
(void) clock;
*seen = TRUE;
return G_SOURCE_REMOVE;
}
static void wait_for_frame(GtkWidget *widget)
{
gboolean seen = FALSE;
gtk_widget_add_tick_callback(widget, frame_seen, &seen, NULL);
while (!seen)
g_main_context_iteration(NULL, TRUE);
}
static void test_paned_applied_once(void)
{
GtkWindow *window = GTK_WINDOW(gtk_window_new());
GtkPaned *paned = GTK_PANED(
gtk_paned_new(GTK_ORIENTATION_HORIZONTAL));
gtk_paned_set_start_child(paned, gtk_label_new("Formulaire"));
gtk_paned_set_end_child(paned, gtk_label_new("Aperçu"));
gtk_window_set_default_size(window, 900, 600);
gtk_window_set_child(window, GTK_WIDGET(paned));
labfy_paned_apply_initial_ratio(paned, 2.0 / 3.0, 320, 180);
gtk_window_present(window);
for (guint frame = 0; frame < 6; frame++)
wait_for_frame(GTK_WIDGET(window));
int expected = (gtk_widget_get_width(GTK_WIDGET(paned)) * 2) / 3;
g_assert_cmpint(ABS(gtk_paned_get_position(paned) - expected), <=, 2);
gtk_paned_set_position(paned, 450);
gtk_window_set_default_size(window, 1000, 700);
wait_for_frame(GTK_WIDGET(window));
g_assert_cmpint(gtk_paned_get_position(paned), ==, 450);
labfy_paned_apply_initial_ratio(paned, 0.5, 100, 100);
g_assert_cmpint(gtk_paned_get_position(paned), ==, 450);
gtk_window_destroy(window);
}
static void test_parent_and_presentation(void)
{
for (guint cycle = 0; cycle < 20; cycle++) {
GtkWindow *parent = GTK_WINDOW(gtk_window_new());
GtkWindow *dialog = GTK_WINDOW(gtk_window_new());
GtkWindow *nested = GTK_WINDOW(gtk_window_new());
GtkWindow *dialog_observer = dialog;
GtkWindow *nested_observer = nested;
int width = 0;
int height = 0;
gtk_window_present(parent);
labfy_dialog_prepare(dialog, parent, TRUE, TRUE);
g_assert_true(gtk_window_get_transient_for(dialog) == parent);
g_assert_true(gtk_window_get_modal(dialog));
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
labfy_dialog_present(dialog);
gtk_window_get_default_size(dialog, &width, &height);
g_assert_cmpint(width, >, 0);
g_assert_cmpint(height, >, 0);
g_assert_true(gtk_widget_get_visible(GTK_WIDGET(dialog)));
labfy_dialog_prepare(nested, dialog, TRUE, TRUE);
g_assert_true(gtk_window_get_transient_for(nested) == dialog);
labfy_dialog_present(nested);
g_assert_true(gtk_widget_get_visible(GTK_WIDGET(nested)));
g_object_add_weak_pointer(
G_OBJECT(dialog), (gpointer *) &dialog_observer);
g_object_add_weak_pointer(
G_OBJECT(nested), (gpointer *) &nested_observer);
gtk_window_destroy(parent);
while (g_main_context_iteration(NULL, FALSE));
g_assert_null(dialog_observer);
g_assert_null(nested_observer);
}
}
int main(int argc, char **argv)
{
if (!gtk_init_check()) {
g_print("SKIP: aucun affichage GTK disponible.\n");
return 0;
}
g_test_init(&argc, &argv, NULL);
g_test_add_func("/dialog-geometry/calculations", test_calculations);
g_test_add_func("/dialog-geometry/paned-applied-once",
test_paned_applied_once);
g_test_add_func("/dialog-geometry/parent-and-presentation",
test_parent_and_presentation);
return g_test_run();
}

View file

@ -452,6 +452,10 @@ static gboolean drive(gpointer data)
if (context->phase == 0) { if (context->phase == 0) {
GtkWindow *dialog = find_window(context, "Importer une preuve"); GtkWindow *dialog = find_window(context, "Importer une preuve");
if (dialog == NULL) return G_SOURCE_CONTINUE; if (dialog == NULL) return G_SOURCE_CONTINUE;
g_assert_true(gtk_window_get_transient_for(dialog) ==
context->main_window);
g_assert_true(gtk_window_get_modal(dialog));
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
GtkWidget *paned = GtkWidget *paned =
find_named(GTK_WIDGET(dialog), "evidence-import-paned"); find_named(GTK_WIDGET(dialog), "evidence-import-paned");
g_assert_true(GTK_IS_PANED(paned)); g_assert_true(GTK_IS_PANED(paned));
@ -465,11 +469,12 @@ static gboolean drive(gpointer data)
GtkWindow *dialog = find_window( GtkWindow *dialog = find_window(
context, "Import — OCR didentité facultatif"); context, "Import — OCR didentité facultatif");
if (dialog == NULL) return G_SOURCE_CONTINUE; if (dialog == NULL) return G_SOURCE_CONTINUE;
g_assert_nonnull(gtk_window_get_transient_for(dialog));
g_assert_true(gtk_window_get_modal(dialog));
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
GtkWidget *root = GTK_WIDGET(dialog); GtkWidget *root = GTK_WIDGET(dialog);
int default_width = 0; int default_width = 0;
int default_height = 0; int default_height = 0;
if (!context->layout_resize_pending)
gtk_window_set_default_size(dialog, 1200, 800);
gtk_window_get_default_size( gtk_window_get_default_size(
dialog, &default_width, &default_height); dialog, &default_width, &default_height);
g_assert_cmpint(default_width, ==, g_assert_cmpint(default_width, ==,
@ -490,14 +495,16 @@ static gboolean drive(gpointer data)
g_assert_true(gtk_paned_get_start_child(GTK_PANED(paned)) == left); g_assert_true(gtk_paned_get_start_child(GTK_PANED(paned)) == left);
g_assert_nonnull(gtk_paned_get_end_child(GTK_PANED(paned))); g_assert_nonnull(gtk_paned_get_end_child(GTK_PANED(paned)));
g_assert_false(is_descendant_of(actions, form_scroll)); g_assert_false(is_descendant_of(actions, form_scroll));
if (g_object_get_data( if (gtk_widget_get_width(paned) <= 0 ||
G_OBJECT(paned), "identity-initial-position-set") == NULL) gtk_paned_get_position(GTK_PANED(paned)) <= 0)
return G_SOURCE_CONTINUE; return G_SOURCE_CONTINUE;
if (!context->layout_resize_pending) { if (!context->layout_resize_pending) {
int initial_position = int initial_position =
gtk_paned_get_position(GTK_PANED(paned)); gtk_paned_get_position(GTK_PANED(paned));
g_assert_cmpint(initial_position, >=, 780); int expected_position =
g_assert_cmpint(initial_position, <=, 820); (gtk_widget_get_width(paned) * 2) / 3;
g_assert_cmpint(
ABS(initial_position - expected_position), <=, 3);
gtk_paned_set_position(GTK_PANED(paned), 650); gtk_paned_set_position(GTK_PANED(paned), 650);
g_assert_cmpint( g_assert_cmpint(
gtk_paned_get_position(GTK_PANED(paned)), ==, 650); gtk_paned_get_position(GTK_PANED(paned)), ==, 650);

View file

@ -322,6 +322,12 @@ static gboolean drive(gpointer data)
g_test_message("drive phase=%u fenêtre=%s cycles=%u", g_test_message("drive phase=%u fenêtre=%s cycles=%u",
context->phase, dialog != NULL ? "oui" : "non", context->phase, dialog != NULL ? "oui" : "non",
context->close_cycles); context->close_cycles);
if (dialog != NULL) {
g_assert_true(gtk_window_get_transient_for(dialog) ==
context->main_window);
g_assert_true(gtk_window_get_modal(dialog));
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
}
if (dialog == NULL) { if (dialog == NULL) {
if (context->phase == 1) { if (context->phase == 1) {
insert_run(context, FALSE); insert_run(context, FALSE);

View file

@ -222,6 +222,64 @@ static void test_preview_pdf_first_page(void)
g_assert_cmpint(g_rmdir(root), ==, 0); g_free(path); g_free(root); g_assert_cmpint(g_rmdir(root), ==, 0); g_free(path); g_free(root);
} }
static void test_preview_pdf_multiple_pages_and_limits(void)
{
GError *error = NULL;
char *root = g_dir_make_tmp("labfy-preview-pdf-pages-XXXXXX", &error);
char *path = g_build_filename(root, "SPECIMEN-3-pages.pdf", NULL);
cairo_surface_t *surface = cairo_pdf_surface_create(path, 320, 200);
cairo_t *cr = cairo_create(surface);
for (guint page = 1; page <= 3; page++) {
char *label = g_strdup_printf("PAGE %u SPECIMEN", page);
cairo_set_source_rgb(cr, page / 3.0, 0.2, 0.4);
cairo_paint(cr);
cairo_set_source_rgb(cr, 1, 1, 1);
cairo_move_to(cr, 20, 40);
cairo_show_text(cr, label);
cairo_show_page(cr);
g_free(label);
}
cairo_destroy(cr);
cairo_surface_destroy(surface);
char *sha256 = NULL;
guint64 size = 0;
g_assert_true(file_hash_compute_sha256(
path, NULL, &sha256, &size, &error));
g_assert_no_error(error);
EvidencePreviewRequest *request = evidence_preview_request_new(
root, "10000000-0000-4000-8000-000000000120",
"SPECIMEN-3-pages.pdf", sha256, "application/pdf", 20);
EvidencePreviewResult *first =
evidence_preview_load(request, NULL, &error);
g_assert_no_error(error);
g_assert_cmpuint(first->item_count, ==, 3);
g_assert_cmpuint(first->current_page, ==, 1);
evidence_preview_result_free(first);
evidence_preview_request_set_pdf_page(request, 1);
EvidencePreviewResult *second =
evidence_preview_load(request, NULL, &error);
g_assert_no_error(error);
g_assert_cmpuint(second->current_page, ==, 2);
evidence_preview_result_free(second);
evidence_preview_request_set_pdf_page(request, 2);
EvidencePreviewResult *last =
evidence_preview_load(request, NULL, &error);
g_assert_no_error(error);
g_assert_cmpuint(last->current_page, ==, 3);
evidence_preview_result_free(last);
evidence_preview_request_set_pdf_page(request, 3);
g_assert_null(evidence_preview_load(request, NULL, &error));
g_assert_error(error,
g_quark_from_static_string("evidence-preview-error"), 8);
g_clear_error(&error);
evidence_preview_request_free(request);
g_free(sha256);
g_assert_cmpint(g_remove(path), ==, 0);
g_assert_cmpint(g_rmdir(root), ==, 0);
g_free(path);
g_free(root);
}
static gboolean write_heic_specimen(const char *path, gint width, gint height) static gboolean write_heic_specimen(const char *path, gint width, gint height)
{ {
struct heif_context *context = heif_context_alloc(); struct heif_context *context = heif_context_alloc();
@ -379,6 +437,8 @@ int main(int argc, char **argv)
test_preview_dispatch_text_video_email); test_preview_dispatch_text_video_email);
g_test_add_func("/evidence-preview/pdf-first-page", g_test_add_func("/evidence-preview/pdf-first-page",
test_preview_pdf_first_page); test_preview_pdf_first_page);
g_test_add_func("/evidence-preview/pdf-multiple-pages-limits",
test_preview_pdf_multiple_pages_and_limits);
g_test_add_func("/evidence-preview/real-heic", g_test_add_func("/evidence-preview/real-heic",
test_preview_real_heic); test_preview_real_heic);
g_test_add_func("/evidence-preview/heic-cancel-invalid", g_test_add_func("/evidence-preview/heic-cancel-invalid",

View file

@ -11,6 +11,58 @@ static void drain_until(EvidencePreviewWidget *widget, const char *state)
g_main_context_iteration(NULL, TRUE); g_main_context_iteration(NULL, TRUE);
} }
static gboolean mark_frame(GtkWidget *widget, GdkFrameClock *clock,
gpointer data)
{
gboolean *frame_seen = data;
(void) widget;
(void) clock;
*frame_seen = TRUE;
return G_SOURCE_REMOVE;
}
static void wait_for_frame(GtkWidget *widget)
{
gboolean frame_seen = FALSE;
gtk_widget_add_tick_callback(widget, mark_frame, &frame_seen, NULL);
while (!frame_seen)
g_main_context_iteration(NULL, TRUE);
}
static gboolean task_manager_has_running_task(TaskManager *manager)
{
gsize count = task_manager_get_count(manager);
for (gsize index = 0; index < count; index++) {
BackgroundTask *task = task_manager_get_task(manager, index);
BackgroundTaskState state = background_task_get_state(task);
background_task_unref(task);
if (state == BACKGROUND_TASK_STATE_PENDING ||
state == BACKGROUND_TASK_STATE_RUNNING)
return TRUE;
}
return FALSE;
}
static GtkWidget *find_named(GtkWidget *widget, const char *name)
{
if (g_strcmp0(gtk_widget_get_name(widget), name) == 0) return widget;
for (GtkWidget *child = gtk_widget_get_first_child(widget);
child != NULL; child = gtk_widget_get_next_sibling(child)) {
GtkWidget *found = find_named(child, name);
if (found != NULL) return found;
}
return NULL;
}
static void click_named(EvidencePreviewWidget *widget, const char *name)
{
GtkWidget *button = find_named(
evidence_preview_widget_get_widget(widget), name);
g_assert_nonnull(button);
g_assert_true(gtk_widget_get_sensitive(button));
g_signal_emit_by_name(button, "clicked");
}
static void test_widget_lifecycle_and_eml(void) static void test_widget_lifecycle_and_eml(void)
{ {
static const char eml[] = static const char eml[] =
@ -89,12 +141,17 @@ static void write_pdf(const char *path)
{ {
cairo_surface_t *surface = cairo_pdf_surface_create(path, 640, 360); cairo_surface_t *surface = cairo_pdf_surface_create(path, 640, 360);
cairo_t *cr = cairo_create(surface); cairo_t *cr = cairo_create(surface);
cairo_set_source_rgb(cr, 1, 1, 1); for (guint page = 1; page <= 3; page++) {
cairo_paint(cr); char *label = g_strdup_printf("PAGE PDF %u SPECIMEN", page);
cairo_set_source_rgb(cr, 0, 0, 0); cairo_set_source_rgb(cr, page == 1 ? 0.8 : 0.2,
cairo_move_to(cr, 30, 80); page == 2 ? 0.8 : 0.2, page == 3 ? 0.8 : 0.2);
cairo_show_text(cr, "PAGE PDF SPECIMEN"); cairo_paint(cr);
cairo_show_page(cr); cairo_set_source_rgb(cr, 0, 0, 0);
cairo_move_to(cr, 30, 80);
cairo_show_text(cr, label);
cairo_show_page(cr);
g_free(label);
}
cairo_destroy(cr); cairo_destroy(cr);
cairo_surface_destroy(surface); cairo_surface_destroy(surface);
} }
@ -161,6 +218,34 @@ static void test_paned_png_jpeg_pdf(void)
evidence_preview_widget_show(widget, png_request, "PNG SPECIMEN"); evidence_preview_widget_show(widget, png_request, "PNG SPECIMEN");
drain_until(widget, "image"); drain_until(widget, "image");
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "image"); g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "image");
g_assert_true(evidence_preview_widget_is_fit(widget));
click_named(widget, "evidence-preview-zoom-in");
g_assert_cmpfloat(evidence_preview_widget_get_zoom(widget), ==, 1.25);
for (guint index = 0; index < 4; index++)
click_named(widget, "evidence-preview-zoom-in");
g_assert_cmpfloat(evidence_preview_widget_get_zoom(widget), ==, 4.0);
wait_for_frame(GTK_WIDGET(window));
GtkScrolledWindow *image_scroll = GTK_SCROLLED_WINDOW(find_named(
evidence_preview_widget_get_widget(widget),
"evidence-preview-image-scroll"));
GtkAdjustment *horizontal =
gtk_scrolled_window_get_hadjustment(image_scroll);
GtkAdjustment *vertical =
gtk_scrolled_window_get_vadjustment(image_scroll);
g_assert_cmpfloat(gtk_adjustment_get_upper(horizontal), >,
gtk_adjustment_get_page_size(horizontal));
g_assert_cmpfloat(gtk_adjustment_get_upper(vertical), >,
gtk_adjustment_get_page_size(vertical));
gtk_adjustment_set_value(horizontal,
gtk_adjustment_get_upper(horizontal) / 3.0);
gtk_adjustment_set_value(vertical,
gtk_adjustment_get_upper(vertical) / 3.0);
g_assert_cmpfloat(gtk_adjustment_get_value(horizontal), >, 0.0);
g_assert_cmpfloat(gtk_adjustment_get_value(vertical), >, 0.0);
click_named(widget, "evidence-preview-zoom-out");
g_assert_cmpfloat(evidence_preview_widget_get_zoom(widget), ==, 3.0);
click_named(widget, "evidence-preview-fit");
g_assert_true(evidence_preview_widget_is_fit(widget));
EvidencePreviewRequest *jpeg_request = request_for( EvidencePreviewRequest *jpeg_request = request_for(
directory, "SPECIMEN.jpg", directory, "SPECIMEN.jpg",
"10000000-0000-4000-8000-000000000002", "image/jpeg", 2); "10000000-0000-4000-8000-000000000002", "image/jpeg", 2);
@ -173,10 +258,48 @@ static void test_paned_png_jpeg_pdf(void)
evidence_preview_widget_show(widget, pdf_request, "PDF SPECIMEN"); evidence_preview_widget_show(widget, pdf_request, "PDF SPECIMEN");
drain_until(widget, "pdf"); drain_until(widget, "pdf");
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "pdf"); g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "pdf");
g_assert_cmpuint(evidence_preview_widget_get_pdf_page_count(widget), ==, 3);
g_assert_cmpuint(evidence_preview_widget_get_pdf_page(widget), ==, 0);
GtkWidget *previous = find_named(
evidence_preview_widget_get_widget(widget),
"evidence-preview-previous-page");
GtkWidget *next = find_named(
evidence_preview_widget_get_widget(widget),
"evidence-preview-next-page");
GtkLabel *page_label = GTK_LABEL(find_named(
evidence_preview_widget_get_widget(widget),
"evidence-preview-page-label"));
g_assert_false(gtk_widget_get_sensitive(previous));
g_assert_true(gtk_widget_get_sensitive(next));
g_assert_cmpstr(gtk_label_get_text(page_label), ==, "Page 1 / 3");
click_named(widget, "evidence-preview-next-page");
drain_until(widget, "pdf");
g_assert_cmpuint(evidence_preview_widget_get_pdf_page(widget), ==, 1);
g_assert_cmpstr(gtk_label_get_text(page_label), ==, "Page 2 / 3");
click_named(widget, "evidence-preview-zoom-in");
g_assert_cmpfloat(evidence_preview_widget_get_zoom(widget), ==, 1.25);
click_named(widget, "evidence-preview-next-page");
drain_until(widget, "pdf");
g_assert_cmpuint(evidence_preview_widget_get_pdf_page(widget), ==, 2);
g_assert_false(gtk_widget_get_sensitive(next));
g_assert_cmpfloat(evidence_preview_widget_get_zoom(widget), ==, 1.25);
click_named(widget, "evidence-preview-previous-page");
click_named(widget, "evidence-preview-previous-page");
drain_until(widget, "pdf");
g_assert_cmpuint(evidence_preview_widget_get_pdf_page(widget), ==, 0);
g_assert_false(gtk_widget_get_sensitive(previous));
click_named(widget, "evidence-preview-next-page");
click_named(widget, "evidence-preview-next-page");
click_named(widget, "evidence-preview-previous-page");
drain_until(widget, "pdf");
g_assert_cmpuint(evidence_preview_widget_get_pdf_page(widget), ==, 1);
gtk_window_set_default_size(window, 760, 560); gtk_window_set_default_size(window, 760, 560);
gtk_paned_set_position(GTK_PANED(paned), 300); gtk_paned_set_position(GTK_PANED(paned), 300);
while (g_main_context_iteration(NULL, FALSE)); while (g_main_context_iteration(NULL, FALSE));
g_assert_cmpint(gtk_paned_get_position(GTK_PANED(paned)), ==, 300); g_assert_cmpint(gtk_paned_get_position(GTK_PANED(paned)), ==, 300);
g_assert_true(gtk_widget_get_mapped(find_named(
evidence_preview_widget_get_widget(widget),
"evidence-preview-toolbar")));
evidence_preview_widget_show(widget, png_request, "résultat ancien"); evidence_preview_widget_show(widget, png_request, "résultat ancien");
evidence_preview_widget_show(widget, jpeg_request, "résultat courant"); evidence_preview_widget_show(widget, jpeg_request, "résultat courant");
drain_until(widget, "image"); drain_until(widget, "image");
@ -199,6 +322,46 @@ static void test_paned_png_jpeg_pdf(void)
g_free(directory); g_free(directory);
} }
static void test_pdf_twenty_open_close_cycles(void)
{
GError *error = NULL;
char *directory =
g_dir_make_tmp("labfy-preview-cycles-XXXXXX", &error);
g_assert_no_error(error);
char *pdf = g_build_filename(directory, "SPECIMEN-multipage.pdf", NULL);
write_pdf(pdf);
TaskManager *manager = task_manager_new();
EvidencePreviewRequest *request = request_for(
directory, "SPECIMEN-multipage.pdf",
"10000000-0000-4000-8000-000000000020", "application/pdf", 20);
for (guint cycle = 0; cycle < 20; cycle++) {
EvidencePreviewWidget *widget =
evidence_preview_widget_new(manager, NULL, NULL);
GtkWindow *window = GTK_WINDOW(gtk_window_new());
gtk_window_set_default_size(window, 760, 560);
gtk_window_set_child(window,
evidence_preview_widget_get_widget(widget));
gtk_window_present(window);
evidence_preview_widget_show(widget, request, "PDF SPECIMEN");
if ((cycle % 2U) == 0) {
drain_until(widget, "pdf");
click_named(widget, "evidence-preview-next-page");
}
gtk_window_destroy(window);
evidence_preview_widget_cancel(widget);
evidence_preview_widget_free(widget);
}
while (task_manager_has_running_task(manager))
g_main_context_iteration(NULL, TRUE);
while (g_main_context_iteration(NULL, FALSE));
task_manager_free(manager);
evidence_preview_request_free(request);
g_unlink(pdf);
g_rmdir(directory);
g_free(pdf);
g_free(directory);
}
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
if (!gtk_init_check()) { if (!gtk_init_check()) {
@ -210,5 +373,7 @@ int main(int argc, char **argv)
test_widget_lifecycle_and_eml); test_widget_lifecycle_and_eml);
g_test_add_func("/evidence-preview-widget/paned-png-jpeg-pdf", g_test_add_func("/evidence-preview-widget/paned-png-jpeg-pdf",
test_paned_png_jpeg_pdf); test_paned_png_jpeg_pdf);
g_test_add_func("/evidence-preview-widget/pdf-twenty-open-close-cycles",
test_pdf_twenty_open_close_cycles);
return g_test_run(); return g_test_run();
} }

View file

@ -1,4 +1,5 @@
#include "widgets/ocr_provenance_overlay.h" #include "widgets/ocr_provenance_overlay.h"
#include <glib/gstdio.h>
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
@ -10,22 +11,45 @@ int main(int argc, char **argv)
OcrProvenanceOverlay *overlay = ocr_provenance_overlay_new(); OcrProvenanceOverlay *overlay = ocr_provenance_overlay_new();
GtkWidget *widget = ocr_provenance_overlay_get_widget(overlay); GtkWidget *widget = ocr_provenance_overlay_get_widget(overlay);
g_object_ref_sink(widget); g_object_ref_sink(widget);
cairo_surface_t *surface = cairo_image_surface_create(
CAIRO_FORMAT_ARGB32, 800, 500);
char *path = g_strdup_printf("%s/labfy-provenance-%u.png",
g_get_tmp_dir(), g_random_int());
g_assert_cmpint(cairo_surface_write_to_png(surface, path),
==, CAIRO_STATUS_SUCCESS);
cairo_surface_destroy(surface);
char *contents = NULL;
gsize length = 0;
g_assert_true(g_file_get_contents(path, &contents, &length, NULL));
GBytes *bytes = g_bytes_new_take(contents, length);
ocr_provenance_overlay_set_image(overlay, bytes, 1);
ocr_provenance_overlay_set_page(overlay, 2, 2);
IdentitySourceBox box = {.page=2,.x=10,.y=20,.width=80,.height=25, IdentitySourceBox box = {.page=2,.x=10,.y=20,.width=80,.height=25,
.image_width=800,.image_height=500,.available=TRUE}; .image_width=800,.image_height=500,.available=TRUE};
IdentityFieldObservation *field = identity_field_observation_new( IdentityFieldObservation *field = identity_field_observation_new(
"surname", "SPECIMEN", 88, &box, 0); "surname", "SPECIMEN", 88, &box, 0);
ocr_provenance_overlay_set_field(overlay, field, 2); ocr_provenance_overlay_set_field(overlay, field, 2);
g_assert_true(ocr_provenance_overlay_has_region(overlay)); g_assert_true(ocr_provenance_overlay_has_region(overlay));
ocr_provenance_overlay_zoom_in(overlay);
g_assert_cmpfloat(ocr_provenance_overlay_get_zoom(overlay), ==, 1.25);
ocr_provenance_overlay_zoom_out(overlay);
ocr_provenance_overlay_fit(overlay);
g_assert_cmpfloat(ocr_provenance_overlay_get_zoom(overlay), ==, 0.0);
ocr_provenance_overlay_set_page(overlay, 3, 3);
g_assert_false(ocr_provenance_overlay_has_region(overlay));
IdentityFieldObservation *without = identity_field_observation_new( IdentityFieldObservation *without = identity_field_observation_new(
"given_names", "ALICE", 70, NULL, 1); "given_names", "ALICE", 70, NULL, 1);
ocr_provenance_overlay_set_field(overlay, without, 3); ocr_provenance_overlay_set_field(overlay, without, 4);
g_assert_false(ocr_provenance_overlay_has_region(overlay)); g_assert_false(ocr_provenance_overlay_has_region(overlay));
ocr_provenance_overlay_set_field(overlay, field, 2); ocr_provenance_overlay_set_field(overlay, field, 2);
g_assert_false(ocr_provenance_overlay_has_region(overlay)); g_assert_false(ocr_provenance_overlay_has_region(overlay));
ocr_provenance_overlay_clear(overlay, 4); ocr_provenance_overlay_clear(overlay, 5);
identity_field_observation_free(field); identity_field_observation_free(field);
identity_field_observation_free(without); identity_field_observation_free(without);
ocr_provenance_overlay_free(overlay); ocr_provenance_overlay_free(overlay);
g_object_unref(widget); g_object_unref(widget);
g_bytes_unref(bytes);
g_remove(path);
g_free(path);
return 0; return 0;
} }