feat(ocr): intégrer la révision contrôlée aux preuves d’identité
This commit is contained in:
parent
b0ae90090a
commit
dae5f63932
72 changed files with 9808 additions and 406 deletions
116
Makefile
116
Makefile
|
|
@ -135,6 +135,7 @@ TEST_EVIDENCE_SELECTION_MODEL := tests/test_evidence_selection_model
|
||||||
TEST_PERSON_CREATION_GUARD := tests/test_person_creation_guard
|
TEST_PERSON_CREATION_GUARD := tests/test_person_creation_guard
|
||||||
TEST_PERSON_DIALOG_LIFECYCLE := tests/test_person_dialog_lifecycle
|
TEST_PERSON_DIALOG_LIFECYCLE := tests/test_person_dialog_lifecycle
|
||||||
TEST_CREATE_PERSON_DIALOG_GTK := tests/test_create_person_dialog_gtk
|
TEST_CREATE_PERSON_DIALOG_GTK := tests/test_create_person_dialog_gtk
|
||||||
|
TEST_CREATE_PERSON_DIALOG_OCR_GTK := tests/test_create_person_dialog_ocr_gtk
|
||||||
TEST_EVIDENCE_PREVIEW_WIDGET_GTK := tests/test_evidence_preview_widget_gtk
|
TEST_EVIDENCE_PREVIEW_WIDGET_GTK := tests/test_evidence_preview_widget_gtk
|
||||||
TEST_PERSON_CONFIRMATION_SUMMARY := tests/test_person_confirmation_summary
|
TEST_PERSON_CONFIRMATION_SUMMARY := tests/test_person_confirmation_summary
|
||||||
TEST_PERSON_ROLE_ASSIGNMENT_DAO := tests/test_person_role_assignment_dao
|
TEST_PERSON_ROLE_ASSIGNMENT_DAO := tests/test_person_role_assignment_dao
|
||||||
|
|
@ -157,6 +158,12 @@ TEST_EXIFTOOL_ANALYSIS := tests/test_exiftool_analysis
|
||||||
TEST_OCR_ANALYSIS := tests/test_ocr_analysis
|
TEST_OCR_ANALYSIS := tests/test_ocr_analysis
|
||||||
TEST_PDF_ANALYSIS := tests/test_pdf_analysis
|
TEST_PDF_ANALYSIS := tests/test_pdf_analysis
|
||||||
TEST_DOCUMENT_TOOL_RUNNER := tests/test_document_tool_runner
|
TEST_DOCUMENT_TOOL_RUNNER := tests/test_document_tool_runner
|
||||||
|
TEST_IDENTITY_OCR := tests/test_identity_ocr
|
||||||
|
TEST_IDENTITY_OCR_PREPROCESSOR := tests/test_identity_ocr_preprocessor
|
||||||
|
TEST_OCR_PROVENANCE_OVERLAY_GTK := tests/test_ocr_provenance_overlay_gtk
|
||||||
|
TEST_EVIDENCE_METADATA_DIALOG_GTK := tests/test_evidence_metadata_dialog_gtk
|
||||||
|
TEST_EVIDENCE_IDENTITY_IMPORT_GTK := tests/test_evidence_identity_import_gtk
|
||||||
|
TEST_WORKSPACE_IDENTITY_OCR_GTK := tests/test_workspace_identity_ocr_gtk
|
||||||
FAKE_DOCUMENT_TOOL := tests/fake_document_tool
|
FAKE_DOCUMENT_TOOL := tests/fake_document_tool
|
||||||
|
|
||||||
DOCUMENT_ANALYSIS_TEST_SOURCES := \
|
DOCUMENT_ANALYSIS_TEST_SOURCES := \
|
||||||
|
|
@ -173,6 +180,42 @@ $(TEST_DOCUMENT_TOOL_RUNNER): tests/test_document_tool_runner.c \
|
||||||
|
|
||||||
all: $(TARGET)
|
all: $(TARGET)
|
||||||
|
|
||||||
|
$(TEST_IDENTITY_OCR): tests/test_identity_ocr.c \
|
||||||
|
src/models/identity_ocr.c src/core/identity_field_extractor.c \
|
||||||
|
src/core/mrz_parser.c src/core/ocr_region_geometry.c \
|
||||||
|
src/core/ocr_analysis.c $(DOCUMENT_ANALYSIS_TEST_SOURCES)
|
||||||
|
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
|
||||||
|
|
||||||
|
$(TEST_IDENTITY_OCR_PREPROCESSOR): tests/test_identity_ocr_preprocessor.c \
|
||||||
|
src/core/identity_ocr_preprocessor.c src/core/evidence_preview.c \
|
||||||
|
src/core/eml_analyzer.c src/core/eml_mime_extractor.c \
|
||||||
|
src/core/evidence_integrity_verifier.c src/core/file_hash.c \
|
||||||
|
src/core/ocr_analysis.c src/core/document_analysis.c \
|
||||||
|
src/core/document_tool_runner.c src/core/tool_process.c \
|
||||||
|
src/core/tool_registry.c
|
||||||
|
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
|
$(TEST_OCR_PROVENANCE_OVERLAY_GTK): \
|
||||||
|
tests/test_ocr_provenance_overlay_gtk.c \
|
||||||
|
src/widgets/ocr_provenance_overlay.c src/core/ocr_region_geometry.c \
|
||||||
|
src/models/identity_ocr.c
|
||||||
|
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
|
$(TEST_EVIDENCE_METADATA_DIALOG_GTK): \
|
||||||
|
tests/test_evidence_metadata_dialog_gtk.c \
|
||||||
|
$(filter-out src/main.c,$(SRC))
|
||||||
|
$(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
|
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK): \
|
||||||
|
tests/test_evidence_identity_import_gtk.c \
|
||||||
|
$(filter-out src/main.c,$(SRC)) $(FAKE_DOCUMENT_TOOL)
|
||||||
|
$(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
|
$(TEST_WORKSPACE_IDENTITY_OCR_GTK): \
|
||||||
|
tests/test_workspace_identity_ocr_gtk.c \
|
||||||
|
$(filter-out src/main.c,$(SRC))
|
||||||
|
$(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
$(TEST_BANK_PROPOSAL): \
|
$(TEST_BANK_PROPOSAL): \
|
||||||
tests/test_bank_proposal.c \
|
tests/test_bank_proposal.c \
|
||||||
src/core/bank_proposal.c \
|
src/core/bank_proposal.c \
|
||||||
|
|
@ -528,10 +571,16 @@ $(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/models/evidence_type.c
|
src/models/evidence_type.c \
|
||||||
$(CC) $(EVIDENCE_IMPORT_DIALOG_TEST_CFLAGS) $^ \
|
src/widgets/evidence_preview_widget.c \
|
||||||
|
src/core/evidence_preview_task.c src/core/evidence_preview.c \
|
||||||
|
src/core/evidence_video_preview_controller.c \
|
||||||
|
src/core/evidence_integrity_verifier.c src/core/file_hash.c \
|
||||||
|
src/core/background_task.c src/core/task_manager.c \
|
||||||
|
src/core/eml_analyzer.c src/core/eml_mime_extractor.c
|
||||||
|
$(CC) $(CFLAGS) $^ \
|
||||||
-o $@ \
|
-o $@ \
|
||||||
$(EVIDENCE_IMPORT_DIALOG_TEST_LDFLAGS)
|
$(LDFLAGS)
|
||||||
|
|
||||||
$(TEST_EVIDENCE_CATEGORY_ITEM): \
|
$(TEST_EVIDENCE_CATEGORY_ITEM): \
|
||||||
tests/test_evidence_category_item.c \
|
tests/test_evidence_category_item.c \
|
||||||
|
|
@ -840,6 +889,13 @@ $(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
|
||||||
src/core/evidence_preview_task.c src/core/evidence_preview.c \
|
src/core/evidence_preview_task.c src/core/evidence_preview.c \
|
||||||
src/core/eml_analyzer.c src/core/eml_mime_extractor.c \
|
src/core/eml_analyzer.c src/core/eml_mime_extractor.c \
|
||||||
src/core/evidence_video_preview_controller.c \
|
src/core/evidence_video_preview_controller.c \
|
||||||
|
src/core/identity_ocr_preprocessor.c \
|
||||||
|
src/core/identity_ocr_workflow.c \
|
||||||
|
src/core/identity_field_extractor.c src/core/ocr_analysis.c \
|
||||||
|
src/core/document_analysis.c src/core/document_tool_runner.c \
|
||||||
|
src/core/tool_registry.c \
|
||||||
|
src/models/identity_ocr.c \
|
||||||
|
src/widgets/ocr_provenance_overlay.c src/core/ocr_region_geometry.c \
|
||||||
src/widgets/evidence_preview_widget.c \
|
src/widgets/evidence_preview_widget.c \
|
||||||
src/core/evidence_integrity_verifier.c src/core/file_hash.c \
|
src/core/evidence_integrity_verifier.c src/core/file_hash.c \
|
||||||
src/core/background_task.c src/core/task_manager.c \
|
src/core/background_task.c src/core/task_manager.c \
|
||||||
|
|
@ -848,6 +904,36 @@ $(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
|
||||||
src/models/person_evidence_selection.c
|
src/models/person_evidence_selection.c
|
||||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
|
$(TEST_CREATE_PERSON_DIALOG_OCR_GTK): \
|
||||||
|
tests/test_create_person_dialog_ocr_gtk.c \
|
||||||
|
src/views/create_person_dialog.c src/core/person_dialog_lifecycle.c \
|
||||||
|
src/core/person_confirmation_summary.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/eml_analyzer.c src/core/eml_mime_extractor.c \
|
||||||
|
src/core/evidence_video_preview_controller.c \
|
||||||
|
src/core/identity_ocr_preprocessor.c \
|
||||||
|
src/core/identity_ocr_workflow.c \
|
||||||
|
src/core/identity_field_extractor.c src/core/ocr_analysis.c \
|
||||||
|
src/core/person_creation_coordinator.c \
|
||||||
|
src/core/document_analysis.c src/core/document_tool_runner.c \
|
||||||
|
src/core/tool_registry.c src/models/identity_ocr.c \
|
||||||
|
src/dao/identity_ocr_dao.c src/dao/entity_dao.c \
|
||||||
|
src/dao/evidence_dao.c src/dao/evidence_entity_dao.c \
|
||||||
|
src/dao/person_role_assignment_dao.c \
|
||||||
|
src/models/entity_record.c src/models/evidence_observation.c \
|
||||||
|
src/widgets/ocr_provenance_overlay.c src/core/ocr_region_geometry.c \
|
||||||
|
src/widgets/evidence_preview_widget.c \
|
||||||
|
src/core/evidence_integrity_verifier.c src/core/file_hash.c \
|
||||||
|
src/core/background_task.c src/core/task_manager.c \
|
||||||
|
src/models/evidence_selection_model.c src/models/evidence_record.c \
|
||||||
|
src/models/person_role_assignment.c \
|
||||||
|
src/models/person_evidence_selection.c \
|
||||||
|
src/database/database.c src/database/schema.c src/database/statement.c \
|
||||||
|
src/database/transaction.c src/database/error.c \
|
||||||
|
src/core/relation_type_normalizer.c $(FAKE_DOCUMENT_TOOL)
|
||||||
|
$(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK): \
|
$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK): \
|
||||||
tests/test_evidence_preview_widget_gtk.c \
|
tests/test_evidence_preview_widget_gtk.c \
|
||||||
src/widgets/evidence_preview_widget.c \
|
src/widgets/evidence_preview_widget.c \
|
||||||
|
|
@ -886,6 +972,7 @@ $(TEST_PERSON_CREATION_COORDINATOR): \
|
||||||
tests/test_person_creation_coordinator.c \
|
tests/test_person_creation_coordinator.c \
|
||||||
src/core/person_creation_coordinator.c src/core/evidence_staging.c \
|
src/core/person_creation_coordinator.c src/core/evidence_staging.c \
|
||||||
src/core/file_hash.c \
|
src/core/file_hash.c \
|
||||||
|
src/models/identity_ocr.c src/dao/identity_ocr_dao.c \
|
||||||
src/models/person_evidence_selection.c src/models/evidence_record.c \
|
src/models/person_evidence_selection.c src/models/evidence_record.c \
|
||||||
src/models/person_role_assignment.c src/models/entity_record.c \
|
src/models/person_role_assignment.c src/models/entity_record.c \
|
||||||
src/models/evidence_observation.c \
|
src/models/evidence_observation.c \
|
||||||
|
|
@ -1007,6 +1094,7 @@ test: \
|
||||||
$(TEST_PERSON_CREATION_GUARD) \
|
$(TEST_PERSON_CREATION_GUARD) \
|
||||||
$(TEST_PERSON_DIALOG_LIFECYCLE) \
|
$(TEST_PERSON_DIALOG_LIFECYCLE) \
|
||||||
$(TEST_CREATE_PERSON_DIALOG_GTK) \
|
$(TEST_CREATE_PERSON_DIALOG_GTK) \
|
||||||
|
$(TEST_CREATE_PERSON_DIALOG_OCR_GTK) \
|
||||||
$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK) \
|
$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK) \
|
||||||
$(TEST_PERSON_CONFIRMATION_SUMMARY) \
|
$(TEST_PERSON_CONFIRMATION_SUMMARY) \
|
||||||
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO) \
|
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO) \
|
||||||
|
|
@ -1028,7 +1116,13 @@ test: \
|
||||||
$(TEST_EXIFTOOL_ANALYSIS) \
|
$(TEST_EXIFTOOL_ANALYSIS) \
|
||||||
$(TEST_OCR_ANALYSIS) \
|
$(TEST_OCR_ANALYSIS) \
|
||||||
$(TEST_PDF_ANALYSIS) \
|
$(TEST_PDF_ANALYSIS) \
|
||||||
$(TEST_DOCUMENT_TOOL_RUNNER)
|
$(TEST_DOCUMENT_TOOL_RUNNER) \
|
||||||
|
$(TEST_IDENTITY_OCR) \
|
||||||
|
$(TEST_IDENTITY_OCR_PREPROCESSOR) \
|
||||||
|
$(TEST_OCR_PROVENANCE_OVERLAY_GTK) \
|
||||||
|
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
|
||||||
|
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
|
||||||
|
$(TEST_WORKSPACE_IDENTITY_OCR_GTK)
|
||||||
@echo "Exécution des tests..."
|
@echo "Exécution des tests..."
|
||||||
@./$(TEST_NODE)
|
@./$(TEST_NODE)
|
||||||
@./$(TEST_TREE_MODEL)
|
@./$(TEST_TREE_MODEL)
|
||||||
|
|
@ -1093,6 +1187,7 @@ test: \
|
||||||
@$(TEST_PERSON_CREATION_GUARD)
|
@$(TEST_PERSON_CREATION_GUARD)
|
||||||
@$(TEST_PERSON_DIALOG_LIFECYCLE)
|
@$(TEST_PERSON_DIALOG_LIFECYCLE)
|
||||||
@$(TEST_CREATE_PERSON_DIALOG_GTK)
|
@$(TEST_CREATE_PERSON_DIALOG_GTK)
|
||||||
|
@$(TEST_CREATE_PERSON_DIALOG_OCR_GTK)
|
||||||
@$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK)
|
@$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK)
|
||||||
@$(TEST_PERSON_CONFIRMATION_SUMMARY)
|
@$(TEST_PERSON_CONFIRMATION_SUMMARY)
|
||||||
@$(TEST_PERSON_ROLE_ASSIGNMENT_DAO)
|
@$(TEST_PERSON_ROLE_ASSIGNMENT_DAO)
|
||||||
|
|
@ -1115,6 +1210,12 @@ test: \
|
||||||
@$(TEST_OCR_ANALYSIS)
|
@$(TEST_OCR_ANALYSIS)
|
||||||
@$(TEST_PDF_ANALYSIS)
|
@$(TEST_PDF_ANALYSIS)
|
||||||
@$(TEST_DOCUMENT_TOOL_RUNNER)
|
@$(TEST_DOCUMENT_TOOL_RUNNER)
|
||||||
|
@$(TEST_IDENTITY_OCR)
|
||||||
|
@$(TEST_IDENTITY_OCR_PREPROCESSOR)
|
||||||
|
@$(TEST_OCR_PROVENANCE_OVERLAY_GTK)
|
||||||
|
@$(TEST_EVIDENCE_METADATA_DIALOG_GTK)
|
||||||
|
@$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK)
|
||||||
|
@$(TEST_WORKSPACE_IDENTITY_OCR_GTK)
|
||||||
@echo "Tous les tests sont valides."
|
@echo "Tous les tests sont valides."
|
||||||
|
|
||||||
%.o: %.c
|
%.o: %.c
|
||||||
|
|
@ -1186,6 +1287,7 @@ clean:
|
||||||
$(TEST_PERSON_CREATION_GUARD) \
|
$(TEST_PERSON_CREATION_GUARD) \
|
||||||
$(TEST_PERSON_DIALOG_LIFECYCLE) \
|
$(TEST_PERSON_DIALOG_LIFECYCLE) \
|
||||||
$(TEST_CREATE_PERSON_DIALOG_GTK) \
|
$(TEST_CREATE_PERSON_DIALOG_GTK) \
|
||||||
|
$(TEST_CREATE_PERSON_DIALOG_OCR_GTK) \
|
||||||
$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK) \
|
$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK) \
|
||||||
$(TEST_PERSON_CONFIRMATION_SUMMARY) \
|
$(TEST_PERSON_CONFIRMATION_SUMMARY) \
|
||||||
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO) \
|
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO) \
|
||||||
|
|
@ -1205,6 +1307,12 @@ clean:
|
||||||
$(TEST_OCR_ANALYSIS) \
|
$(TEST_OCR_ANALYSIS) \
|
||||||
$(TEST_PDF_ANALYSIS) \
|
$(TEST_PDF_ANALYSIS) \
|
||||||
$(TEST_DOCUMENT_TOOL_RUNNER) \
|
$(TEST_DOCUMENT_TOOL_RUNNER) \
|
||||||
|
$(TEST_IDENTITY_OCR) \
|
||||||
|
$(TEST_IDENTITY_OCR_PREPROCESSOR) \
|
||||||
|
$(TEST_OCR_PROVENANCE_OVERLAY_GTK) \
|
||||||
|
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
|
||||||
|
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
|
||||||
|
$(TEST_WORKSPACE_IDENTITY_OCR_GTK) \
|
||||||
$(FAKE_DOCUMENT_TOOL)
|
$(FAKE_DOCUMENT_TOOL)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -123,6 +123,21 @@ 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 d’une personne propose une étape facultative d’OCR contrôlé des
|
||||||
|
documents d’identité. Elle ne démarre que sur action explicite, travaille sur
|
||||||
|
une copie vérifiée PNG, JPEG, HEIC, HEIF ou sur une page PDF choisie, et laisse
|
||||||
|
chaque champ à l’état « À vérifier ». L’acceptation, la modification ou le rejet
|
||||||
|
s’effectuent champ par champ, sans verdict d’authenticité, reconnaissance
|
||||||
|
faciale, fusion de personne ni changement automatique du statut
|
||||||
|
d’identification.
|
||||||
|
|
||||||
|
Ce parcours est également disponible lors de l’import normal d’une preuve,
|
||||||
|
avec sélection explicite ou présélection de la personne. Il traite une seule
|
||||||
|
preuve par opération. L’import multiple sans OCR reste disponible ; plusieurs
|
||||||
|
preuves doivent être OCRisées successivement, chacune dans son propre
|
||||||
|
parcours. Les dialogues concernés affichent la liste à gauche et un grand
|
||||||
|
aperçu redimensionnable à droite.
|
||||||
|
|
||||||
Le socle actuel comprend notamment :
|
Le socle actuel comprend notamment :
|
||||||
|
|
||||||
- création et ouverture d’enquêtes ;
|
- création et ouverture d’enquêtes ;
|
||||||
|
|
|
||||||
|
|
@ -265,3 +265,56 @@ CREATE INDEX IF NOT EXISTS idx_person_role_assignments_entity
|
||||||
ON person_role_assignments(entity_id);
|
ON person_role_assignments(entity_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_person_role_assignments_evidence
|
CREATE INDEX IF NOT EXISTS idx_person_role_assignments_evidence
|
||||||
ON person_role_assignments(evidence_id);
|
ON person_role_assignments(evidence_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS identity_ocr_runs (
|
||||||
|
id TEXT PRIMARY KEY, evidence_id TEXT NOT NULL,
|
||||||
|
expected_sha256 TEXT NOT NULL CHECK(length(expected_sha256)=64),
|
||||||
|
page_number INTEGER NOT NULL CHECK(page_number>0),
|
||||||
|
document_type TEXT NOT NULL, document_side TEXT NOT NULL,
|
||||||
|
engine TEXT NOT NULL, engine_version TEXT, requested_languages TEXT NOT NULL,
|
||||||
|
available_languages TEXT NOT NULL, parameters TEXT NOT NULL,
|
||||||
|
preprocessing_profile TEXT NOT NULL, executed_at TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL, error_message TEXT,
|
||||||
|
text_relative_path TEXT NOT NULL, text_sha256 TEXT NOT NULL,
|
||||||
|
tsv_relative_path TEXT NOT NULL, tsv_sha256 TEXT NOT NULL,
|
||||||
|
work_image_relative_path TEXT, work_image_sha256 TEXT,
|
||||||
|
corrected_transcription TEXT,
|
||||||
|
transcription_is_human INTEGER NOT NULL DEFAULT 0
|
||||||
|
CHECK(transcription_is_human IN (0,1)),
|
||||||
|
transcription_corrected_at TEXT,
|
||||||
|
transcription_origin TEXT
|
||||||
|
CHECK(transcription_origin IS NULL OR transcription_origin='human'),
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE CASCADE);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_identity_ocr_runs_evidence
|
||||||
|
ON identity_ocr_runs(evidence_id);
|
||||||
|
CREATE TABLE IF NOT EXISTS identity_document_observations (
|
||||||
|
id TEXT PRIMARY KEY, person_id TEXT NOT NULL, evidence_id TEXT NOT NULL,
|
||||||
|
ocr_run_id TEXT NOT NULL UNIQUE, document_type TEXT NOT NULL,
|
||||||
|
issuing_country_declared TEXT, document_side TEXT NOT NULL,
|
||||||
|
page_number INTEGER NOT NULL, review_state TEXT NOT NULL,
|
||||||
|
observed_at TEXT NOT NULL, factual_notes TEXT,
|
||||||
|
FOREIGN KEY(person_id) REFERENCES entites(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE CASCADE);
|
||||||
|
CREATE TABLE IF NOT EXISTS identity_field_observations (
|
||||||
|
id TEXT PRIMARY KEY, observation_id TEXT NOT NULL, field_code TEXT NOT NULL,
|
||||||
|
raw_value TEXT, corrected_value TEXT, normalized_value TEXT,
|
||||||
|
confidence REAL, review_status TEXT NOT NULL, origin TEXT NOT NULL
|
||||||
|
CHECK(origin IN ('ocr','mrz','manual_override','manual_entry')),
|
||||||
|
evidence_id TEXT NOT NULL, ocr_run_id TEXT NOT NULL,
|
||||||
|
page_number INTEGER NOT NULL, source_x INTEGER, source_y INTEGER,
|
||||||
|
source_width INTEGER, source_height INTEGER, source_image_width INTEGER,
|
||||||
|
source_image_height INTEGER, display_order INTEGER NOT NULL,
|
||||||
|
reviewed_at TEXT NOT NULL, review_note TEXT,
|
||||||
|
CHECK (
|
||||||
|
(origin = 'manual_entry' AND raw_value IS NULL
|
||||||
|
AND corrected_value IS NOT NULL AND confidence IS NULL)
|
||||||
|
OR
|
||||||
|
(origin <> 'manual_entry' AND raw_value IS NOT NULL)
|
||||||
|
),
|
||||||
|
FOREIGN KEY(observation_id) REFERENCES identity_document_observations(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE CASCADE);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_identity_fields_observation
|
||||||
|
ON identity_field_observations(observation_id,display_order);
|
||||||
|
|
|
||||||
78
database/schema_v15.sql
Normal file
78
database/schema_v15.sql
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
/* Migration V15 — OCR contrôlé des documents d'identité. */
|
||||||
|
CREATE TABLE IF NOT EXISTS identity_ocr_runs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
evidence_id TEXT NOT NULL,
|
||||||
|
expected_sha256 TEXT NOT NULL CHECK(length(expected_sha256)=64),
|
||||||
|
page_number INTEGER NOT NULL CHECK(page_number > 0),
|
||||||
|
document_type TEXT NOT NULL CHECK(document_type IN
|
||||||
|
('identity_card','passport','driving_licence','residence_permit','other')),
|
||||||
|
document_side TEXT NOT NULL CHECK(document_side IN
|
||||||
|
('front','back','identity_page','other_page')),
|
||||||
|
engine TEXT NOT NULL,
|
||||||
|
engine_version TEXT,
|
||||||
|
requested_languages TEXT NOT NULL,
|
||||||
|
available_languages TEXT NOT NULL,
|
||||||
|
parameters TEXT NOT NULL,
|
||||||
|
preprocessing_profile TEXT NOT NULL CHECK(preprocessing_profile IN
|
||||||
|
('none','orientation','grayscale','upscale')),
|
||||||
|
executed_at TEXT NOT NULL CHECK(length(executed_at)=20),
|
||||||
|
status TEXT NOT NULL CHECK(status IN ('success','partial','error','cancelled')),
|
||||||
|
error_message TEXT,
|
||||||
|
text_relative_path TEXT NOT NULL,
|
||||||
|
text_sha256 TEXT NOT NULL CHECK(length(text_sha256)=64),
|
||||||
|
tsv_relative_path TEXT NOT NULL,
|
||||||
|
tsv_sha256 TEXT NOT NULL CHECK(length(tsv_sha256)=64),
|
||||||
|
work_image_relative_path TEXT,
|
||||||
|
work_image_sha256 TEXT,
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_identity_ocr_runs_evidence ON identity_ocr_runs(evidence_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS identity_document_observations (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
person_id TEXT NOT NULL,
|
||||||
|
evidence_id TEXT NOT NULL,
|
||||||
|
ocr_run_id TEXT NOT NULL UNIQUE,
|
||||||
|
document_type TEXT NOT NULL,
|
||||||
|
issuing_country_declared TEXT,
|
||||||
|
document_side TEXT NOT NULL,
|
||||||
|
page_number INTEGER NOT NULL CHECK(page_number > 0),
|
||||||
|
review_state TEXT NOT NULL CHECK(review_state IN
|
||||||
|
('proposed','accepted','modified','rejected','conflict')),
|
||||||
|
observed_at TEXT NOT NULL CHECK(length(observed_at)=20),
|
||||||
|
factual_notes TEXT,
|
||||||
|
FOREIGN KEY(person_id) REFERENCES entites(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS identity_field_observations (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
observation_id TEXT NOT NULL,
|
||||||
|
field_code TEXT NOT NULL,
|
||||||
|
raw_value TEXT NOT NULL,
|
||||||
|
corrected_value TEXT,
|
||||||
|
normalized_value TEXT,
|
||||||
|
confidence REAL CHECK(confidence IS NULL OR confidence BETWEEN 0 AND 100),
|
||||||
|
review_status TEXT NOT NULL CHECK(review_status IN
|
||||||
|
('accepted','modified')),
|
||||||
|
origin TEXT NOT NULL CHECK(origin IN ('ocr','mrz','manual_override')),
|
||||||
|
evidence_id TEXT NOT NULL,
|
||||||
|
ocr_run_id TEXT NOT NULL,
|
||||||
|
page_number INTEGER NOT NULL CHECK(page_number > 0),
|
||||||
|
source_x INTEGER,
|
||||||
|
source_y INTEGER,
|
||||||
|
source_width INTEGER,
|
||||||
|
source_height INTEGER,
|
||||||
|
source_image_width INTEGER,
|
||||||
|
source_image_height INTEGER,
|
||||||
|
display_order INTEGER NOT NULL CHECK(display_order >= 0),
|
||||||
|
reviewed_at TEXT NOT NULL CHECK(length(reviewed_at)=20),
|
||||||
|
review_note TEXT,
|
||||||
|
FOREIGN KEY(observation_id) REFERENCES identity_document_observations(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_identity_fields_observation
|
||||||
|
ON identity_field_observations(observation_id,display_order);
|
||||||
39
database/schema_v16.sql
Normal file
39
database/schema_v16.sql
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* Migration V16 — saisie manuelle contrôlée des champs d'identité omis. */
|
||||||
|
CREATE TABLE identity_field_observations_v16 (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
observation_id TEXT NOT NULL,
|
||||||
|
field_code TEXT NOT NULL,
|
||||||
|
raw_value TEXT,
|
||||||
|
corrected_value TEXT,
|
||||||
|
normalized_value TEXT,
|
||||||
|
confidence REAL CHECK(confidence IS NULL OR confidence BETWEEN 0 AND 100),
|
||||||
|
review_status TEXT NOT NULL CHECK(review_status IN ('accepted','modified')),
|
||||||
|
origin TEXT NOT NULL CHECK(origin IN
|
||||||
|
('ocr','mrz','manual_override','manual_entry')),
|
||||||
|
evidence_id TEXT NOT NULL,
|
||||||
|
ocr_run_id TEXT NOT NULL,
|
||||||
|
page_number INTEGER NOT NULL CHECK(page_number > 0),
|
||||||
|
source_x INTEGER, source_y INTEGER, source_width INTEGER,
|
||||||
|
source_height INTEGER, source_image_width INTEGER,
|
||||||
|
source_image_height INTEGER,
|
||||||
|
display_order INTEGER NOT NULL CHECK(display_order >= 0),
|
||||||
|
reviewed_at TEXT NOT NULL CHECK(length(reviewed_at)=20),
|
||||||
|
review_note TEXT,
|
||||||
|
CHECK (
|
||||||
|
(origin = 'manual_entry' AND raw_value IS NULL
|
||||||
|
AND corrected_value IS NOT NULL AND confidence IS NULL)
|
||||||
|
OR
|
||||||
|
(origin <> 'manual_entry' AND raw_value IS NOT NULL)
|
||||||
|
),
|
||||||
|
FOREIGN KEY(observation_id) REFERENCES identity_document_observations(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO identity_field_observations_v16
|
||||||
|
SELECT * FROM identity_field_observations;
|
||||||
|
DROP TABLE identity_field_observations;
|
||||||
|
ALTER TABLE identity_field_observations_v16
|
||||||
|
RENAME TO identity_field_observations;
|
||||||
|
CREATE INDEX idx_identity_fields_observation
|
||||||
|
ON identity_field_observations(observation_id,display_order);
|
||||||
11
database/schema_v17.sql
Normal file
11
database/schema_v17.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
/* Migration V17 — transcription humaine distincte du texte OCR brut. */
|
||||||
|
ALTER TABLE identity_ocr_runs
|
||||||
|
ADD COLUMN corrected_transcription TEXT;
|
||||||
|
ALTER TABLE identity_ocr_runs
|
||||||
|
ADD COLUMN transcription_is_human INTEGER NOT NULL DEFAULT 0
|
||||||
|
CHECK(transcription_is_human IN (0,1));
|
||||||
|
ALTER TABLE identity_ocr_runs
|
||||||
|
ADD COLUMN transcription_corrected_at TEXT;
|
||||||
|
ALTER TABLE identity_ocr_runs
|
||||||
|
ADD COLUMN transcription_origin TEXT
|
||||||
|
CHECK(transcription_origin IS NULL OR transcription_origin='human');
|
||||||
|
|
@ -38,6 +38,25 @@ travaille hors du thread GTK ; la texture est créée sur le contexte principal.
|
||||||
Le dialogue annule l’ancienne tâche et rejette les générations obsolètes.
|
Le dialogue annule l’ancienne tâche et rejette les générations obsolètes.
|
||||||
L’interface conserve une génération de session et les chemins stables du
|
L’interface 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. PDF, vidéo, EML avancé et OCR restent exclus.
|
||||||
|
|
||||||
|
## OCR contrôlé d’identité — SQLite V15
|
||||||
|
|
||||||
|
La V15 sépare l’exécution OCR, l’observation du document et les observations
|
||||||
|
de champs. 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 à l’utilisateur.
|
||||||
|
Les widgets n’accèdent pas à SQLite : le coordinateur persiste les décisions
|
||||||
|
acceptées ou modifiées dans la transaction finale et compense les fichiers en
|
||||||
|
cas d’échec.
|
||||||
|
|
||||||
|
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
|
||||||
|
compte du ratio, de la réduction, de l’agrandissement et des marges. Le
|
||||||
|
rectangle est transitoire : aucune annotation n’est écrite dans la preuve.
|
||||||
|
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
|
||||||
|
documents et observations de champs, ainsi que des listes ordonnées par preuve,
|
||||||
|
personne ou observation documentaire.
|
||||||
> **Statut :** architecture courante
|
> **Statut :** architecture courante
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -437,6 +437,20 @@ interprétation ultérieure.
|
||||||
|
|
||||||
Un résultat OCR ou OSINT est une proposition à vérifier, pas un fait confirmé.
|
Un résultat OCR ou OSINT est une proposition à vérifier, pas un fait confirmé.
|
||||||
|
|
||||||
|
Pour un document d’identité, l’OCR est exclusivement explicite et chaque champ
|
||||||
|
est révisé séparément. Il n’existe pas d’action « Tout accepter ». Une
|
||||||
|
extraction décrit seulement ce qui est présenté sur le document : elle ne
|
||||||
|
confirme pas son authenticité, ne fusionne aucune personne, ne change aucun
|
||||||
|
statut d’identification et n’effectue aucune reconnaissance faciale.
|
||||||
|
|
||||||
|
Une zone OCR affichée est une surimpression éphémère calculée depuis les
|
||||||
|
coordonnées originales. Une zone absente est indiquée comme indisponible et
|
||||||
|
n’est jamais reconstruite arbitrairement.
|
||||||
|
|
||||||
|
Les enregistrements lus par `IdentityOcrDao` possèdent toutes leurs chaînes.
|
||||||
|
Ils doivent être libérés avec la fonction `*_record_free()` correspondante ;
|
||||||
|
les listes retournées possèdent leurs éléments.
|
||||||
|
|
||||||
### 7.4 Suppression
|
### 7.4 Suppression
|
||||||
|
|
||||||
La suppression logique est privilégiée pour les objets de traçabilité, mais la
|
La suppression logique est privilégiée pour les objets de traçabilité, mais la
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,13 @@ sudo pacman -S python python-pip
|
||||||
|
|
||||||
## Vérification dans l’application
|
## Vérification dans l’application
|
||||||
|
|
||||||
|
Tesseract est optionnel. L’étape OCR d’identité reste visible mais son lancement
|
||||||
|
est désactivé lorsque le registre d’outils ne fournit pas un exécutable vérifié.
|
||||||
|
Les langues réellement installées sont interrogées avec `--list-langs`; aucun
|
||||||
|
téléchargement n’est effectué par l’application.
|
||||||
|
Les choix sont triés et dédupliqués. `fra+eng` n’est proposé que lorsque les
|
||||||
|
deux packs sont réellement installés.
|
||||||
|
|
||||||
Au démarrage, le catalogue des outils indique le nombre d’exécutables
|
Au démarrage, le catalogue des outils indique le nombre d’exécutables
|
||||||
disponibles et leurs versions. Une commande non détectée doit d’abord être
|
disponibles et leurs versions. Une commande non détectée doit d’abord être
|
||||||
vérifiée avec `command -v`, puis avec son option de version (`--version` ou
|
vérifiée avec `command -v`, puis avec son option de version (`--version` ou
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
# Guide de développement
|
# Guide de développement
|
||||||
|
|
||||||
|
Pour la tranche OCR identité, les tests ciblés sont
|
||||||
|
`tests/test_identity_ocr`, `tests/test_identity_ocr_preprocessor`,
|
||||||
|
`tests/test_person_creation_coordinator`,
|
||||||
|
`tests/test_ocr_provenance_overlay_gtk` et
|
||||||
|
`tests/test_create_person_dialog_gtk`. Le test Tesseract réel est facultatif
|
||||||
|
et s’ignore explicitement si l’outil ou une langue compatible manque.
|
||||||
|
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
|
||||||
|
multipage via Cairo. Elles ne doivent jamais être remplacées par un document
|
||||||
|
réel.
|
||||||
|
|
||||||
La validation ciblée du composant partagé comprend
|
La validation ciblée du composant 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
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,18 @@
|
||||||
# Roadmap
|
# Roadmap
|
||||||
|
|
||||||
|
La tranche 3 du ticket #109 ajoute l’OCR contrôlé des documents d’identité dans
|
||||||
|
l’assistant de création : lancement explicite, staging, révision champ par
|
||||||
|
champ et persistance transactionnelle V15. Elle ne clôt pas le ticket #109 et
|
||||||
|
n’engage aucune tranche suivante.
|
||||||
|
La provenance graphique, les langues Tesseract dynamiques et la compensation
|
||||||
|
complète des répertoires OCR font partie de cette même tranche.
|
||||||
|
La lecture structurée V15 et les scénarios de fermeture/réouverture restent
|
||||||
|
également circonscrits à la tranche 3.
|
||||||
|
L’OCR contrôlé est limité à une preuve par opération dans l’import normal.
|
||||||
|
L’import multiple sans OCR reste disponible. L’OCR 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 l’aperçu entre l’assistant personne et la
|
La tranche 2 partage désormais l’aperçu entre l’assistant personne et la
|
||||||
fiche directe. La validation manuelle des codecs installés reste nécessaire ;
|
fiche directe. La validation manuelle des codecs installés reste nécessaire ;
|
||||||
elle ne vaut pas achèvement du ticket #109 et n’inclut aucun OCR.
|
elle ne vaut pas achèvement du ticket #109 et n’inclut aucun OCR.
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
# Test manuel — ticket #109, tranche 1
|
|
||||||
|
|
||||||
> Le parcours « Preuve » est désormais nommé « Preuves » et accepte une
|
|
||||||
> sélection multiple ainsi qu’un import différé. Pour le scénario historique
|
|
||||||
> de tranche 1, sélectionner une seule preuve existante avec « Ajouter à la
|
|
||||||
> sélection ». Le protocole étendu se trouve dans
|
|
||||||
> `docs/TEST_MANUEL_TICKET_109_TRANCHE_2.md`.
|
|
||||||
|
|
||||||
Utiliser uniquement une enquête de démonstration et des fichiers synthétiques
|
|
||||||
marqués `SPECIMEN`.
|
|
||||||
|
|
||||||
1. Cliquer sur Annuler à chacune des quatre étapes doit fermer uniquement
|
|
||||||
l’assistant et laisser la fenêtre principale utilisable. Refaire le contrôle
|
|
||||||
avec la croix, puis pendant le chargement d’un aperçu.
|
|
||||||
2. Ouvrir une enquête synthétique, puis demander l’ajout d’une personne.
|
|
||||||
3. Vérifier les quatre étapes `Personne`, `Rôles`, `Preuve`, `Confirmation`.
|
|
||||||
4. Revenir en arrière : les champs, rôles multiples et la preuve choisie
|
|
||||||
doivent être conservés. Aucun rôle sensible ne doit être précoché.
|
|
||||||
5. Rechercher par nom, description et type, combiner avec le filtre métier,
|
|
||||||
puis revenir à `Tous les types`.
|
|
||||||
6. Choisir des PNG et JPEG synthétiques : vérifier chargement, aperçu et
|
|
||||||
métadonnées complètes, puis les états invalide et absent.
|
|
||||||
7. Sélectionner rapidement plusieurs preuves : seul le dernier aperçu doit
|
|
||||||
apparaître, puis fermer la fenêtre pendant un chargement.
|
|
||||||
8. Choisir zéro puis une preuve existante. Aucun import, OCR, PDF, vidéo ou
|
|
||||||
analyse EML avancée ne doit être proposé.
|
|
||||||
9. Confirmer et vérifier que personne, rôles, rattachement et source `manual`
|
|
||||||
apparaissent ensemble après actualisation.
|
|
||||||
10. Changer d’enquête pendant un aperçu puis avant confirmation : le résultat
|
|
||||||
tardif et la création doivent être refusés sans écriture ni rafraîchissement.
|
|
||||||
|
|
||||||
L’aperçu de tranche 1 accepte uniquement PNG/JPEG intègres. Il refuse les
|
|
||||||
fichiers absents, modifiés, corrompus ou supérieurs aux limites de 25 Mio,
|
|
||||||
12 000 pixels par côté et 40 millions de pixels. Le rendu mémoire est borné à
|
|
||||||
1 024 × 1 024 et aucun dérivé n’est persisté.
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
# Test manuel — ticket #109, tranche 2
|
|
||||||
|
|
||||||
Comparer le même EML synthétique dans la fiche directe et dans l’assistant :
|
|
||||||
From, To, Cc, Date, Subject, Message-ID, corps et inventaire détaillé des
|
|
||||||
pièces jointes doivent être identiques. Tester ensuite PNG/JPEG/HEIC/HEIF,
|
|
||||||
PDF, MP4/MOV et texte, les changements rapides, la fermeture pendant
|
|
||||||
chargement ou vidéo, la réouverture et l’ouverture d’une fiche personne.
|
|
||||||
Vérifier l’absence de critique GTK, d’écriture SQLite et d’OCR.
|
|
||||||
|
|
||||||
Utiliser exclusivement une enquête temporaire et des fichiers synthétiques
|
|
||||||
dont le nom ou le contenu porte `SPECIMEN`.
|
|
||||||
|
|
||||||
1. Créer une enquête temporaire et y importer préalablement deux preuves
|
|
||||||
synthétiques PNG/JPEG.
|
|
||||||
2. Ouvrir « Ajouter une personne », renseigner une désignation fictive et
|
|
||||||
sélectionner plusieurs rôles.
|
|
||||||
3. Dans « Preuves », rechercher puis ajouter les deux preuves existantes.
|
|
||||||
Vérifier qu’un second ajout est refusé sans modifier la preuve.
|
|
||||||
4. Choisir « Importer des fichiers » et sélectionner simultanément un PNG, un
|
|
||||||
JPEG et un PDF synthétiques. Vérifier que rien n’apparaît encore dans
|
|
||||||
`01_Preuves_Originales` ni dans SQLite.
|
|
||||||
5. Activer chaque élément retenu. Vérifier les aperçus PNG/JPEG et le message
|
|
||||||
explicite d’indisponibilité du PDF.
|
|
||||||
6. Modifier individuellement les types métier, naviguer en arrière puis en
|
|
||||||
avant et vérifier la conservation de l’état.
|
|
||||||
7. Retirer une preuve existante et un fichier nouveau. Vérifier que la preuve
|
|
||||||
existante reste dans l’enquête et que la copie de staging retirée disparaît.
|
|
||||||
8. Sélectionner deux fois le même fichier, puis deux fichiers de même contenu.
|
|
||||||
Vérifier le message de doublon et l’unicité de la sélection.
|
|
||||||
9. Ajouter un fichier identique à une preuve existante. Vérifier la
|
|
||||||
réutilisation explicite de cette preuve.
|
|
||||||
10. Atteindre Confirmation et contrôler la liste complète, les origines,
|
|
||||||
types, MIME, tailles, empreintes abrégées et compteurs.
|
|
||||||
11. Annuler puis vérifier l’absence de nouvelle personne, preuve, rôle,
|
|
||||||
rattachement et copie définitive, ainsi que le nettoyage du staging.
|
|
||||||
12. Recommencer, puis fermer la fenêtre pendant le staging. Vérifier la
|
|
||||||
fermeture propre, le nettoyage et le maintien de l’application.
|
|
||||||
13. Recommencer et changer d’enquête pendant une préparation ou une création.
|
|
||||||
Vérifier l’annulation, l’absence d’écriture dans la nouvelle enquête et
|
|
||||||
l’absence de rafraîchissement tardif.
|
|
||||||
14. Confirmer normalement. Vérifier que toutes les preuves sont rattachées à
|
|
||||||
la personne avec une source `manual`, puis rouvrir l’enquête.
|
|
||||||
15. Comparer les empreintes des fichiers sources avant et après le parcours.
|
|
||||||
Elles doivent être identiques.
|
|
||||||
16. Vérifier qu’aucun OCR ni outil documentaire avancé n’a été lancé.
|
|
||||||
17. Ouvrir la liste des preuves disponibles, choisir successivement deux
|
|
||||||
preuves, revenir à « Aucune preuve associée », activer une recherche et un
|
|
||||||
filtre, puis sélectionner de nouveau une preuve. Vérifier l’absence de
|
|
||||||
crash et de critique GTK.
|
|
||||||
18. Ajouter une image JPEG historique synthétique dont le MIME enregistré est
|
|
||||||
NULL et le type métier `document`. Vérifier que le contenu JPEG intègre est
|
|
||||||
détecté, que l’aperçu apparaît et que « MIME détecté : image/jpeg » est
|
|
||||||
affiché sans modification SQLite.
|
|
||||||
19. Renommer un fichier texte synthétique avec l’extension `.jpg`, puis
|
|
||||||
vérifier que l’aperçu est refusé malgré l’extension. Refaire avec un MIME
|
|
||||||
historique `image/jpeg` incohérent et vérifier le même refus.
|
|
||||||
20. Modifier le type individuel de l’image de `document` vers `email`.
|
|
||||||
Vérifier immédiatement la même valeur dans la ligne retenue, le sélecteur,
|
|
||||||
les métadonnées et le résumé après un aller-retour entre les étapes.
|
|
||||||
|
|
||||||
21. Tester des fixtures PNG, JPEG, HEIC, HEIF, MP4, MOV, EML, TXT, CSV, LOG,
|
|
||||||
Markdown et PDF. Vérifier l’image, le lecteur sans lecture automatique,
|
|
||||||
le texte passif, les en-têtes EML et la première page PDF.
|
|
||||||
22. Tester format invalide, extension trompeuse et fichier trop volumineux.
|
|
||||||
23. Changer rapidement de preuve et vérifier le rejet tardif et l’arrêt vidéo.
|
|
||||||
24. Fermer puis changer d’enquête pendant un aperçu ; vérifier l’annulation.
|
|
||||||
25. Vérifier les empreintes inchangées et l’absence d’OCR, d’accès distant et
|
|
||||||
d’écriture SQLite pendant l’aperçu.
|
|
||||||
26. Avec un EML multipart synthétique, vérifier que le corps `text/plain` est
|
|
||||||
préféré au HTML et que nom, MIME, taille, disposition et Content-ID des
|
|
||||||
pièces jointes sont visibles sans fichier extrait persistant.
|
|
||||||
|
|
||||||
Limites : aucun OCR ni analyse avancée. La vidéo dépend des codecs GStreamer.
|
|
||||||
45
include/core/evidence_identity_import_task.h
Normal file
45
include/core/evidence_identity_import_task.h
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
#ifndef LABFY_EVIDENCE_IDENTITY_IMPORT_TASK_H
|
||||||
|
#define LABFY_EVIDENCE_IDENTITY_IMPORT_TASK_H
|
||||||
|
|
||||||
|
#include "core/background_task.h"
|
||||||
|
#include "core/task_manager.h"
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
|
#include "core/person_creation_coordinator.h"
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
typedef struct EvidenceIdentityImportTaskRequest
|
||||||
|
EvidenceIdentityImportTaskRequest;
|
||||||
|
|
||||||
|
EvidenceIdentityImportTaskRequest *
|
||||||
|
evidence_identity_import_task_request_new(
|
||||||
|
const char *database_path, const char *investigation_root_path,
|
||||||
|
const char *source_path, const char *person_identifier,
|
||||||
|
const char *type_identifier, const char *collected_at,
|
||||||
|
const char *source, const char *description,
|
||||||
|
const IdentityOcrRun *run);
|
||||||
|
EvidenceIdentityImportTaskRequest *
|
||||||
|
evidence_identity_import_task_request_new_existing(
|
||||||
|
const char *database_path, const char *investigation_root_path,
|
||||||
|
const char *evidence_identifier, const char *person_identifier,
|
||||||
|
const IdentityOcrRun *run);
|
||||||
|
EvidenceIdentityImportTaskRequest *
|
||||||
|
evidence_identity_import_task_request_new_review(
|
||||||
|
const char *database_path, const char *investigation_root_path,
|
||||||
|
const char *evidence_identifier, const IdentityOcrRun *run);
|
||||||
|
void evidence_identity_import_task_request_free(
|
||||||
|
EvidenceIdentityImportTaskRequest *request);
|
||||||
|
void evidence_identity_import_task_request_set_session_check(
|
||||||
|
EvidenceIdentityImportTaskRequest *request,
|
||||||
|
PersonCreationSessionCheck session_check,
|
||||||
|
gpointer session_check_data);
|
||||||
|
BackgroundTask *evidence_identity_import_task_start(
|
||||||
|
TaskManager *manager,
|
||||||
|
const EvidenceIdentityImportTaskRequest *request,
|
||||||
|
BackgroundTaskCompletionCallback callback,
|
||||||
|
gpointer callback_data, GDestroyNotify callback_data_destroy,
|
||||||
|
GError **error);
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
|
||||||
|
#endif
|
||||||
8
include/core/identity_field_extractor.h
Normal file
8
include/core/identity_field_extractor.h
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#ifndef LABFY_IDENTITY_FIELD_EXTRACTOR_H
|
||||||
|
#define LABFY_IDENTITY_FIELD_EXTRACTOR_H
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
GPtrArray *identity_field_extractor_extract(const char *text,
|
||||||
|
const char *tsv, gint image_width, gint image_height, GError **error);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
18
include/core/identity_ocr_preprocessor.h
Normal file
18
include/core/identity_ocr_preprocessor.h
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#ifndef LABFY_IDENTITY_OCR_PREPROCESSOR_H
|
||||||
|
#define LABFY_IDENTITY_OCR_PREPROCESSOR_H
|
||||||
|
#include "core/evidence_preview.h"
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
typedef enum { IDENTITY_OCR_PREPROCESS_NONE, IDENTITY_OCR_PREPROCESS_ORIENTATION,
|
||||||
|
IDENTITY_OCR_PREPROCESS_GRAYSCALE, IDENTITY_OCR_PREPROCESS_UPSCALE
|
||||||
|
} IdentityOcrPreprocessProfile;
|
||||||
|
typedef struct { char *path; char *sha256; gint width; gint height;
|
||||||
|
guint page; } IdentityOcrWorkImage;
|
||||||
|
IdentityOcrWorkImage *identity_ocr_preprocessor_prepare(
|
||||||
|
const EvidencePreviewRequest *request, guint pdf_page,
|
||||||
|
IdentityOcrPreprocessProfile profile, GCancellable *cancellable,
|
||||||
|
GError **error);
|
||||||
|
void identity_ocr_work_image_free(IdentityOcrWorkImage *image);
|
||||||
|
gboolean identity_ocr_preprocessor_dimensions_are_safe(gint width,
|
||||||
|
gint height, IdentityOcrPreprocessProfile profile);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
35
include/core/identity_ocr_workflow.h
Normal file
35
include/core/identity_ocr_workflow.h
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
#ifndef LABFY_IDENTITY_OCR_WORKFLOW_H
|
||||||
|
#define LABFY_IDENTITY_OCR_WORKFLOW_H
|
||||||
|
|
||||||
|
#include "core/identity_ocr_preprocessor.h"
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *root_path;
|
||||||
|
const char *evidence_identifier;
|
||||||
|
const char *relative_path;
|
||||||
|
const char *expected_sha256;
|
||||||
|
const char *mime_type;
|
||||||
|
const char *document_type;
|
||||||
|
const char *document_side;
|
||||||
|
const char *languages;
|
||||||
|
const char *preprocessing_profile;
|
||||||
|
const char *tesseract_executable;
|
||||||
|
const char *tesseract_version;
|
||||||
|
guint page_number;
|
||||||
|
IdentityOcrPreprocessProfile profile;
|
||||||
|
guint64 generation;
|
||||||
|
} IdentityOcrWorkflowRequest;
|
||||||
|
|
||||||
|
gboolean identity_ocr_workflow_request_is_valid(
|
||||||
|
const IdentityOcrWorkflowRequest *request);
|
||||||
|
IdentityOcrRun *identity_ocr_workflow_execute(
|
||||||
|
const IdentityOcrWorkflowRequest *request,
|
||||||
|
GCancellable *cancellable,
|
||||||
|
GError **error);
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
|
||||||
|
#endif
|
||||||
12
include/core/mrz_parser.h
Normal file
12
include/core/mrz_parser.h
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#ifndef LABFY_MRZ_PARSER_H
|
||||||
|
#define LABFY_MRZ_PARSER_H
|
||||||
|
#include <glib.h>
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
typedef struct { gboolean structure_valid; gboolean check_digits_valid;
|
||||||
|
char *document_number; char *nationality; char *surname; char *given_names;
|
||||||
|
GPtrArray *warnings; } MrzParseResult;
|
||||||
|
MrzParseResult *mrz_parser_parse(const char *text);
|
||||||
|
void mrz_parse_result_free(MrzParseResult *result);
|
||||||
|
gint mrz_parser_check_digit(const char *value);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
|
|
@ -14,7 +14,9 @@ typedef struct
|
||||||
{
|
{
|
||||||
DocumentToolExecution *execution;
|
DocumentToolExecution *execution;
|
||||||
char *requested_languages;
|
char *requested_languages;
|
||||||
|
char *available_languages;
|
||||||
char *text;
|
char *text;
|
||||||
|
char *tsv;
|
||||||
} OcrAnalysisResult;
|
} OcrAnalysisResult;
|
||||||
|
|
||||||
OcrAnalysisResult *ocr_analysis_run(
|
OcrAnalysisResult *ocr_analysis_run(
|
||||||
|
|
@ -34,6 +36,10 @@ OcrAnalysisResult *ocr_analysis_run_with_limits(
|
||||||
);
|
);
|
||||||
void ocr_analysis_result_free(OcrAnalysisResult *result);
|
void ocr_analysis_result_free(OcrAnalysisResult *result);
|
||||||
gboolean ocr_analysis_mime_is_compatible(const char *mime_type);
|
gboolean ocr_analysis_mime_is_compatible(const char *mime_type);
|
||||||
|
char *ocr_analysis_list_languages(const char *executable,
|
||||||
|
GCancellable *cancellable, GError **error);
|
||||||
|
GPtrArray *ocr_analysis_parse_languages(const char *output);
|
||||||
|
GPtrArray *ocr_analysis_build_language_choices(const GPtrArray *languages);
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
23
include/core/ocr_region_geometry.h
Normal file
23
include/core/ocr_region_geometry.h
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#ifndef LABFY_OCR_REGION_GEOMETRY_H
|
||||||
|
#define LABFY_OCR_REGION_GEOMETRY_H
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
OCR_REGION_FIT_CONTAIN,
|
||||||
|
OCR_REGION_FIT_FILL
|
||||||
|
} OcrRegionFit;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
double x;
|
||||||
|
double y;
|
||||||
|
double width;
|
||||||
|
double height;
|
||||||
|
} OcrDisplayRegion;
|
||||||
|
|
||||||
|
gboolean ocr_region_geometry_transform(gint source_width, gint source_height,
|
||||||
|
gint region_x, gint region_y, gint region_width, gint region_height,
|
||||||
|
double display_width, double display_height, OcrRegionFit fit,
|
||||||
|
OcrDisplayRegion *out_region);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include "core/person_entity_service.h"
|
#include "core/person_entity_service.h"
|
||||||
#include "models/person_evidence_selection.h"
|
#include "models/person_evidence_selection.h"
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
#include <gio/gio.h>
|
#include <gio/gio.h>
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
|
|
@ -12,10 +13,66 @@ typedef struct {
|
||||||
GPtrArray *evidence_identifiers;
|
GPtrArray *evidence_identifiers;
|
||||||
} PersonCreationCoordinatorResult;
|
} PersonCreationCoordinatorResult;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
PERSON_CREATION_FAILURE_NONE,
|
||||||
|
PERSON_CREATION_FAILURE_VALIDATE,
|
||||||
|
PERSON_CREATION_FAILURE_SESSION_BEFORE_START,
|
||||||
|
PERSON_CREATION_FAILURE_SOURCE_HASH,
|
||||||
|
PERSON_CREATION_FAILURE_CREATE_OCR_DIRECTORY,
|
||||||
|
PERSON_CREATION_FAILURE_COPY_OCR_TEXT,
|
||||||
|
PERSON_CREATION_FAILURE_HASH_OCR_TEXT,
|
||||||
|
PERSON_CREATION_FAILURE_COPY_OCR_TSV,
|
||||||
|
PERSON_CREATION_FAILURE_HASH_OCR_TSV,
|
||||||
|
PERSON_CREATION_FAILURE_CREATE_PERSON,
|
||||||
|
PERSON_CREATION_FAILURE_CREATE_ROLE,
|
||||||
|
PERSON_CREATION_FAILURE_IMPORT_EVIDENCE,
|
||||||
|
PERSON_CREATION_FAILURE_LINK_EVIDENCE,
|
||||||
|
PERSON_CREATION_FAILURE_INSERT_OCR_RUN,
|
||||||
|
PERSON_CREATION_FAILURE_INSERT_DOCUMENT_OBSERVATION,
|
||||||
|
PERSON_CREATION_FAILURE_INSERT_FIELD,
|
||||||
|
PERSON_CREATION_FAILURE_CREATE_SOURCE,
|
||||||
|
PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,
|
||||||
|
PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,
|
||||||
|
PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,
|
||||||
|
PERSON_CREATION_FAILURE_COMMIT,
|
||||||
|
PERSON_CREATION_FAILURE_COMPENSATION
|
||||||
|
} PersonCreationFailurePoint;
|
||||||
|
typedef gboolean (*PersonCreationSessionCheck)(gpointer user_data);
|
||||||
|
typedef struct {
|
||||||
|
PersonCreationFailurePoint failure_point;
|
||||||
|
guint failure_occurrence;
|
||||||
|
gboolean inject_compensation_failure;
|
||||||
|
PersonCreationSessionCheck session_check;
|
||||||
|
gpointer session_check_data;
|
||||||
|
} PersonCreationCoordinatorOptions;
|
||||||
|
typedef struct {
|
||||||
|
const char *collected_at;
|
||||||
|
const char *source;
|
||||||
|
const char *description;
|
||||||
|
const char *type_identifier;
|
||||||
|
} PersonCreationCoordinatorEvidenceMetadata;
|
||||||
|
|
||||||
PersonCreationCoordinatorResult *person_creation_coordinator_execute(
|
PersonCreationCoordinatorResult *person_creation_coordinator_execute(
|
||||||
Database *database, const char *investigation_root_path,
|
Database *database, const char *investigation_root_path,
|
||||||
const PersonEntityInput *person,
|
const PersonEntityInput *person,
|
||||||
const PersonEvidenceSelection *selection,
|
const PersonEvidenceSelection *selection,
|
||||||
|
const GPtrArray *ocr_runs,
|
||||||
|
GCancellable *cancellable, GError **error);
|
||||||
|
PersonCreationCoordinatorResult *person_creation_coordinator_execute_with_options(
|
||||||
|
Database *database, const char *investigation_root_path,
|
||||||
|
const PersonEntityInput *person,
|
||||||
|
const PersonEvidenceSelection *selection,
|
||||||
|
const GPtrArray *ocr_runs,
|
||||||
|
const PersonCreationCoordinatorOptions *options,
|
||||||
|
GCancellable *cancellable, GError **error);
|
||||||
|
PersonCreationCoordinatorResult *
|
||||||
|
person_creation_coordinator_attach_to_existing_person(
|
||||||
|
Database *database, const char *investigation_root_path,
|
||||||
|
const char *person_identifier,
|
||||||
|
const PersonEvidenceSelection *selection,
|
||||||
|
const GPtrArray *ocr_runs,
|
||||||
|
const PersonCreationCoordinatorEvidenceMetadata *metadata,
|
||||||
|
const PersonCreationCoordinatorOptions *options,
|
||||||
GCancellable *cancellable, GError **error);
|
GCancellable *cancellable, GError **error);
|
||||||
void person_creation_coordinator_result_free(
|
void person_creation_coordinator_result_free(
|
||||||
PersonCreationCoordinatorResult *result);
|
PersonCreationCoordinatorResult *result);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ typedef struct PersonCreationTaskRequest PersonCreationTaskRequest;
|
||||||
PersonCreationTaskRequest *person_creation_task_request_new(
|
PersonCreationTaskRequest *person_creation_task_request_new(
|
||||||
const char *database_path, const char *investigation_root_path,
|
const char *database_path, const char *investigation_root_path,
|
||||||
const PersonEntityInput *person,
|
const PersonEntityInput *person,
|
||||||
const PersonEvidenceSelection *selection);
|
const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs);
|
||||||
void person_creation_task_request_free(PersonCreationTaskRequest *request);
|
void person_creation_task_request_free(PersonCreationTaskRequest *request);
|
||||||
BackgroundTask *person_creation_task_start(TaskManager *manager,
|
BackgroundTask *person_creation_task_start(TaskManager *manager,
|
||||||
const PersonCreationTaskRequest *request,
|
const PersonCreationTaskRequest *request,
|
||||||
|
|
|
||||||
62
include/dao/identity_ocr_dao.h
Normal file
62
include/dao/identity_ocr_dao.h
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
#ifndef LABFY_IDENTITY_OCR_DAO_H
|
||||||
|
#define LABFY_IDENTITY_OCR_DAO_H
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
typedef struct IdentityOcrDao IdentityOcrDao;
|
||||||
|
typedef struct {
|
||||||
|
char *id,*evidence_id,*expected_sha256,*document_type,*document_side;
|
||||||
|
char *engine,*engine_version,*requested_languages,*available_languages;
|
||||||
|
char *parameters,*preprocessing_profile,*executed_at,*status,*error_message;
|
||||||
|
char *text_relative_path,*text_sha256,*tsv_relative_path,*tsv_sha256;
|
||||||
|
char *work_image_relative_path,*work_image_sha256; gint64 page_number;
|
||||||
|
char *corrected_transcription,*transcription_corrected_at;
|
||||||
|
char *transcription_origin; gboolean transcription_is_human;
|
||||||
|
} IdentityOcrRunRecord;
|
||||||
|
typedef struct {
|
||||||
|
char *id,*person_id,*evidence_id,*ocr_run_id,*document_type;
|
||||||
|
char *issuing_country_declared,*document_side,*review_state,*observed_at;
|
||||||
|
char *factual_notes; gint64 page_number;
|
||||||
|
} IdentityDocumentObservationRecord;
|
||||||
|
typedef struct {
|
||||||
|
char *id,*observation_id,*field_code,*raw_value,*corrected_value;
|
||||||
|
char *normalized_value,*review_status,*origin,*evidence_id,*ocr_run_id;
|
||||||
|
char *reviewed_at,*review_note; double confidence; gboolean has_confidence;
|
||||||
|
gint64 page_number,source_x,source_y,source_width,source_height;
|
||||||
|
gint64 source_image_width,source_image_height,display_order;
|
||||||
|
gboolean has_source_box;
|
||||||
|
} IdentityFieldObservationRecord;
|
||||||
|
IdentityOcrDao *identity_ocr_dao_new(Database *database);
|
||||||
|
void identity_ocr_dao_free(IdentityOcrDao *dao);
|
||||||
|
gboolean identity_ocr_dao_insert(IdentityOcrDao *dao,
|
||||||
|
const char *person_id,const char *evidence_id,const IdentityOcrRun *run,
|
||||||
|
const char *text_relative,const char *text_sha,
|
||||||
|
const char *tsv_relative,const char *tsv_sha,const char *timestamp,
|
||||||
|
GError **error);
|
||||||
|
IdentityOcrRunRecord *identity_ocr_dao_find_run(IdentityOcrDao *dao,
|
||||||
|
const char *identifier,GError **error);
|
||||||
|
GPtrArray *identity_ocr_dao_list_runs_by_evidence(IdentityOcrDao *dao,
|
||||||
|
const char *evidence_identifier,GError **error);
|
||||||
|
IdentityDocumentObservationRecord *identity_ocr_dao_find_document(
|
||||||
|
IdentityOcrDao *dao,const char *identifier,GError **error);
|
||||||
|
GPtrArray *identity_ocr_dao_list_documents_by_person(IdentityOcrDao *dao,
|
||||||
|
const char *person_identifier,GError **error);
|
||||||
|
GPtrArray *identity_ocr_dao_list_documents_by_evidence(IdentityOcrDao *dao,
|
||||||
|
const char *evidence_identifier,GError **error);
|
||||||
|
IdentityFieldObservationRecord *identity_ocr_dao_find_field(
|
||||||
|
IdentityOcrDao *dao,const char *identifier,GError **error);
|
||||||
|
GPtrArray *identity_ocr_dao_list_fields_by_document(IdentityOcrDao *dao,
|
||||||
|
const char *document_identifier,GError **error);
|
||||||
|
IdentityOcrRun *identity_ocr_dao_load_run(
|
||||||
|
IdentityOcrDao *dao,const char *investigation_root,
|
||||||
|
const char *run_identifier,char **person_identifier,GError **error);
|
||||||
|
gboolean identity_ocr_dao_update_review(
|
||||||
|
IdentityOcrDao *dao,const IdentityOcrRun *run,
|
||||||
|
const char *reviewed_at,GError **error);
|
||||||
|
void identity_ocr_run_record_free(IdentityOcrRunRecord *record);
|
||||||
|
void identity_document_observation_record_free(
|
||||||
|
IdentityDocumentObservationRecord *record);
|
||||||
|
void identity_field_observation_record_free(
|
||||||
|
IdentityFieldObservationRecord *record);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
|
|
@ -95,6 +95,9 @@ bool schema_install_v11(Database *database);
|
||||||
bool schema_install_v12(Database *database);
|
bool schema_install_v12(Database *database);
|
||||||
bool schema_install_v13(Database *database);
|
bool schema_install_v13(Database *database);
|
||||||
bool schema_install_v14(Database *database);
|
bool schema_install_v14(Database *database);
|
||||||
|
bool schema_install_v15(Database *database);
|
||||||
|
bool schema_install_v16(Database *database);
|
||||||
|
bool schema_install_v17(Database *database);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Garantit la présence des extensions du schéma courant V2.
|
* @brief Garantit la présence des extensions du schéma courant V2.
|
||||||
|
|
|
||||||
96
include/models/identity_ocr.h
Normal file
96
include/models/identity_ocr.h
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
#ifndef LABFY_IDENTITY_OCR_H
|
||||||
|
#define LABFY_IDENTITY_OCR_H
|
||||||
|
#include <glib.h>
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
typedef enum {
|
||||||
|
IDENTITY_REVIEW_PROPOSED, IDENTITY_REVIEW_ACCEPTED,
|
||||||
|
IDENTITY_REVIEW_MODIFIED, IDENTITY_REVIEW_REJECTED,
|
||||||
|
IDENTITY_REVIEW_CONFLICT
|
||||||
|
} IdentityReviewStatus;
|
||||||
|
typedef struct {
|
||||||
|
gint page, x, y, width, height, image_width, image_height;
|
||||||
|
gboolean available;
|
||||||
|
} IdentitySourceBox;
|
||||||
|
typedef struct IdentityFieldObservation IdentityFieldObservation;
|
||||||
|
typedef struct IdentityOcrRun IdentityOcrRun;
|
||||||
|
|
||||||
|
IdentityFieldObservation *identity_field_observation_new(
|
||||||
|
const char *code, const char *raw_value, double confidence,
|
||||||
|
const IdentitySourceBox *box, guint order);
|
||||||
|
IdentityFieldObservation *identity_field_observation_new_manual(
|
||||||
|
const char *code, const char *value, guint order);
|
||||||
|
IdentityFieldObservation *identity_field_observation_copy(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
void identity_field_observation_free(IdentityFieldObservation *field);
|
||||||
|
gboolean identity_field_observation_accept(IdentityFieldObservation *field);
|
||||||
|
gboolean identity_field_observation_modify(IdentityFieldObservation *field,
|
||||||
|
const char *corrected_value, const char *note);
|
||||||
|
gboolean identity_field_observation_restore_raw(
|
||||||
|
IdentityFieldObservation *field);
|
||||||
|
void identity_field_observation_reject(IdentityFieldObservation *field);
|
||||||
|
gboolean identity_field_observation_set_origin(
|
||||||
|
IdentityFieldObservation *field, const char *origin);
|
||||||
|
void identity_field_observation_mark_conflict(IdentityFieldObservation *field);
|
||||||
|
const char *identity_field_observation_get_code(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
const char *identity_field_observation_get_raw_value(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
const char *identity_field_observation_get_corrected_value(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
IdentityReviewStatus identity_field_observation_get_status(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
double identity_field_observation_get_confidence(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
const IdentitySourceBox *identity_field_observation_get_box(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
|
||||||
|
IdentityOcrRun *identity_ocr_run_new(const char *evidence_id,
|
||||||
|
const char *expected_sha256, const char *document_type,
|
||||||
|
const char *document_side, guint page, const char *languages,
|
||||||
|
const char *profile);
|
||||||
|
IdentityOcrRun *identity_ocr_run_copy(const IdentityOcrRun *run);
|
||||||
|
void identity_ocr_run_free(IdentityOcrRun *run);
|
||||||
|
void identity_ocr_run_set_outputs(IdentityOcrRun *run, const char *version,
|
||||||
|
const char *available_languages, const char *parameters,
|
||||||
|
const char *raw_text, const char *tsv);
|
||||||
|
void identity_ocr_run_set_preview(IdentityOcrRun *run, GBytes *png_bytes);
|
||||||
|
GBytes *identity_ocr_run_get_preview(const IdentityOcrRun *run);
|
||||||
|
void identity_ocr_run_add_field(IdentityOcrRun *run,
|
||||||
|
IdentityFieldObservation *field);
|
||||||
|
gboolean identity_ocr_run_replace_evidence_id(
|
||||||
|
IdentityOcrRun *run, const char *evidence_identifier);
|
||||||
|
gboolean identity_ocr_run_replace_identifier(
|
||||||
|
IdentityOcrRun *run, const char *identifier);
|
||||||
|
void identity_ocr_run_set_factual_notes(
|
||||||
|
IdentityOcrRun *run, const char *notes);
|
||||||
|
gboolean identity_ocr_run_set_corrected_transcription(
|
||||||
|
IdentityOcrRun *run, const char *transcription, const char *corrected_at);
|
||||||
|
void identity_ocr_run_reset_corrected_transcription(IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_corrected_transcription(
|
||||||
|
const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_transcription_corrected_at(
|
||||||
|
const IdentityOcrRun *run);
|
||||||
|
gboolean identity_ocr_run_has_human_transcription(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_factual_notes(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_identifier(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_evidence_id(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_raw_text(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_tsv(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_document_type(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_document_side(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_languages(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_expected_sha256(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_profile(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_version(const IdentityOcrRun *run);
|
||||||
|
const char *identity_ocr_run_get_available_languages(const IdentityOcrRun *run);
|
||||||
|
const char *identity_field_observation_get_origin(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
guint identity_field_observation_get_order(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
guint identity_ocr_run_get_page(const IdentityOcrRun *run);
|
||||||
|
const GPtrArray *identity_ocr_run_get_fields(const IdentityOcrRun *run);
|
||||||
|
gboolean identity_ocr_document_type_is_valid(const char *value);
|
||||||
|
gboolean identity_ocr_document_side_is_valid(const char *value);
|
||||||
|
gboolean identity_ocr_field_code_is_valid(const char *value);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include "models/evidence_record.h"
|
#include "models/evidence_record.h"
|
||||||
#include "models/person_evidence_selection.h"
|
#include "models/person_evidence_selection.h"
|
||||||
#include "core/task_manager.h"
|
#include "core/task_manager.h"
|
||||||
|
#include "core/tool_registry.h"
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
/** @brief Résultat opaque possédé par le callback. */
|
/** @brief Résultat opaque possédé par le callback. */
|
||||||
|
|
@ -27,7 +28,8 @@ typedef gboolean (*CreatePersonDialogSessionCheck)(gpointer user_data);
|
||||||
*/
|
*/
|
||||||
gboolean create_person_dialog_present(GtkWindow *parent,
|
gboolean create_person_dialog_present(GtkWindow *parent,
|
||||||
const GPtrArray *evidence_records, const char *investigation_root_path,
|
const GPtrArray *evidence_records, const char *investigation_root_path,
|
||||||
TaskManager *task_manager, CreatePersonDialogSessionCheck session_check,
|
TaskManager *task_manager, const ToolInfo *tesseract_tool,
|
||||||
|
CreatePersonDialogSessionCheck session_check,
|
||||||
CreatePersonDialogCallback callback, gpointer user_data,
|
CreatePersonDialogCallback callback, gpointer user_data,
|
||||||
GDestroyNotify user_data_destroy);
|
GDestroyNotify user_data_destroy);
|
||||||
/** @brief Libère un résultat. */
|
/** @brief Libère un résultat. */
|
||||||
|
|
@ -39,5 +41,9 @@ const PersonEvidenceSelection *create_person_dialog_result_get_evidence_selectio
|
||||||
const CreatePersonDialogResult *result);
|
const CreatePersonDialogResult *result);
|
||||||
EvidenceStaging *create_person_dialog_result_steal_staging(
|
EvidenceStaging *create_person_dialog_result_steal_staging(
|
||||||
CreatePersonDialogResult *result);
|
CreatePersonDialogResult *result);
|
||||||
|
const GPtrArray *create_person_dialog_result_get_ocr_runs(
|
||||||
|
const CreatePersonDialogResult *result);
|
||||||
|
gboolean create_person_dialog_test_overlay_has_region(GtkWindow *window);
|
||||||
|
guint64 create_person_dialog_test_ocr_generation(GtkWindow *window);
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
60
include/views/evidence_identity_ocr_dialog.h
Normal file
60
include/views/evidence_identity_ocr_dialog.h
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#ifndef LABFY_EVIDENCE_IDENTITY_OCR_DIALOG_H
|
||||||
|
#define LABFY_EVIDENCE_IDENTITY_OCR_DIALOG_H
|
||||||
|
|
||||||
|
#include "core/tool_registry.h"
|
||||||
|
#include "core/background_task.h"
|
||||||
|
#include "models/entity_record.h"
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
typedef struct EvidenceIdentityOcrDialogResult
|
||||||
|
EvidenceIdentityOcrDialogResult;
|
||||||
|
typedef gboolean (*EvidenceIdentityOcrDialogSessionCheck)(gpointer user_data);
|
||||||
|
typedef void (*EvidenceIdentityOcrDialogCallback)(
|
||||||
|
EvidenceIdentityOcrDialogResult *result, gpointer user_data);
|
||||||
|
|
||||||
|
gboolean evidence_identity_ocr_dialog_file_is_compatible(
|
||||||
|
const char *file_path);
|
||||||
|
gboolean evidence_identity_ocr_dialog_present(
|
||||||
|
GtkWindow *parent, const char *file_path, const GPtrArray *persons,
|
||||||
|
const char *preselected_person_identifier,
|
||||||
|
const ToolInfo *tesseract_tool,
|
||||||
|
EvidenceIdentityOcrDialogSessionCheck session_check,
|
||||||
|
EvidenceIdentityOcrDialogCallback callback,
|
||||||
|
gpointer user_data, GDestroyNotify user_data_destroy,
|
||||||
|
GError **error);
|
||||||
|
gboolean evidence_identity_ocr_dialog_present_review(
|
||||||
|
GtkWindow *parent,const char *file_path,const GPtrArray *persons,
|
||||||
|
const char *preselected_person_identifier,
|
||||||
|
const IdentityOcrRun *persisted_run,
|
||||||
|
EvidenceIdentityOcrDialogSessionCheck session_check,
|
||||||
|
EvidenceIdentityOcrDialogCallback callback,
|
||||||
|
gpointer user_data,GDestroyNotify user_data_destroy,GError **error);
|
||||||
|
void evidence_identity_ocr_dialog_result_free(
|
||||||
|
EvidenceIdentityOcrDialogResult *result);
|
||||||
|
gboolean evidence_identity_ocr_dialog_result_has_ocr(
|
||||||
|
const EvidenceIdentityOcrDialogResult *result);
|
||||||
|
const char *evidence_identity_ocr_dialog_result_get_person_identifier(
|
||||||
|
const EvidenceIdentityOcrDialogResult *result);
|
||||||
|
const char *evidence_identity_ocr_dialog_result_get_temporary_identifier(
|
||||||
|
const EvidenceIdentityOcrDialogResult *result);
|
||||||
|
const char *evidence_identity_ocr_dialog_result_get_sha256(
|
||||||
|
const EvidenceIdentityOcrDialogResult *result);
|
||||||
|
guint64 evidence_identity_ocr_dialog_result_get_size(
|
||||||
|
const EvidenceIdentityOcrDialogResult *result);
|
||||||
|
const char *evidence_identity_ocr_dialog_result_get_mime_type(
|
||||||
|
const EvidenceIdentityOcrDialogResult *result);
|
||||||
|
IdentityOcrRun *evidence_identity_ocr_dialog_result_steal_run(
|
||||||
|
EvidenceIdentityOcrDialogResult *result);
|
||||||
|
GtkWindow *evidence_identity_ocr_dialog_result_get_dialog(
|
||||||
|
const EvidenceIdentityOcrDialogResult *result);
|
||||||
|
void evidence_identity_ocr_dialog_finish_import(
|
||||||
|
GtkWindow *dialog, const GError *error);
|
||||||
|
void evidence_identity_ocr_dialog_set_submission_task(
|
||||||
|
GtkWindow *dialog, BackgroundTask *task);
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#define LABFY_INVESTIGATION_EVIDENCE_METADATA_DIALOG_H
|
#define LABFY_INVESTIGATION_EVIDENCE_METADATA_DIALOG_H
|
||||||
|
|
||||||
#include "models/evidence_record.h"
|
#include "models/evidence_record.h"
|
||||||
|
#include "database/database.h"
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
|
@ -19,6 +20,10 @@ typedef struct EvidenceMetadataDialogResult EvidenceMetadataDialogResult;
|
||||||
typedef void (*EvidenceMetadataDialogCallback)(
|
typedef void (*EvidenceMetadataDialogCallback)(
|
||||||
EvidenceMetadataDialogResult *result, gpointer user_data
|
EvidenceMetadataDialogResult *result, gpointer user_data
|
||||||
);
|
);
|
||||||
|
typedef void (*EvidenceMetadataDialogAnalyzeCallback)(
|
||||||
|
GtkWindow *dialog, const char *evidence_identifier,
|
||||||
|
gboolean revise_existing, const char *ocr_run_identifier,
|
||||||
|
gpointer user_data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Présente le dialogue prérempli de modification des métadonnées.
|
* @brief Présente le dialogue prérempli de modification des métadonnées.
|
||||||
|
|
@ -36,6 +41,12 @@ gboolean evidence_metadata_dialog_present(
|
||||||
EvidenceMetadataDialogCallback callback,
|
EvidenceMetadataDialogCallback callback,
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
gboolean evidence_metadata_dialog_present_with_ocr(
|
||||||
|
GtkWindow *parent, const EvidenceRecord *record,
|
||||||
|
const GPtrArray *evidence_types, Database *database,
|
||||||
|
const char *investigation_root,
|
||||||
|
EvidenceMetadataDialogAnalyzeCallback analyze_callback,
|
||||||
|
EvidenceMetadataDialogCallback callback, gpointer user_data);
|
||||||
|
|
||||||
/** @brief Libère un résultat, ou accepte NULL. */
|
/** @brief Libère un résultat, ou accepte NULL. */
|
||||||
void evidence_metadata_dialog_result_free(EvidenceMetadataDialogResult *result);
|
void evidence_metadata_dialog_result_free(EvidenceMetadataDialogResult *result);
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,9 @@ typedef void (*MainWindowAnalyzeEmlCallback)(const char *evidence_identifier,
|
||||||
/** @brief Callback appelé pour analyser un RIB par OCR. */
|
/** @brief Callback appelé pour analyser un RIB par OCR. */
|
||||||
typedef void (*MainWindowAnalyzeRibCallback)(const char *evidence_identifier,
|
typedef void (*MainWindowAnalyzeRibCallback)(const char *evidence_identifier,
|
||||||
gpointer user_data);
|
gpointer user_data);
|
||||||
|
typedef void (*MainWindowIdentityOcrCallback)(
|
||||||
|
const char *evidence_identifier, gboolean revise_existing,
|
||||||
|
const char *ocr_run_identifier, gpointer user_data);
|
||||||
/** @brief Callback appelé pour extraire les métadonnées d'une preuve. */
|
/** @brief Callback appelé pour extraire les métadonnées d'une preuve. */
|
||||||
typedef void (*MainWindowExtractMetadataCallback)(const char *evidence_identifier,
|
typedef void (*MainWindowExtractMetadataCallback)(const char *evidence_identifier,
|
||||||
gpointer user_data);
|
gpointer user_data);
|
||||||
|
|
@ -237,6 +240,9 @@ void main_window_set_extract_metadata_callback(MainWindow *main_window,
|
||||||
/** @brief Définit le callback de récupération d'un mot de passe PDF. */
|
/** @brief Définit le callback de récupération d'un mot de passe PDF. */
|
||||||
void main_window_set_recover_pdf_password_callback(MainWindow *main_window,
|
void main_window_set_recover_pdf_password_callback(MainWindow *main_window,
|
||||||
MainWindowRecoverPdfPasswordCallback callback, gpointer user_data);
|
MainWindowRecoverPdfPasswordCallback callback, gpointer user_data);
|
||||||
|
void main_window_set_identity_ocr_callback(
|
||||||
|
MainWindow *main_window, MainWindowIdentityOcrCallback callback,
|
||||||
|
gpointer user_data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Définit le callback de fin de déplacement d'un nœud.
|
* @brief Définit le callback de fin de déplacement d'un nœud.
|
||||||
|
|
@ -497,6 +503,8 @@ void main_window_set_selected_evidence(
|
||||||
MainWindow *main_window,
|
MainWindow *main_window,
|
||||||
const EvidenceRecord *evidence_record
|
const EvidenceRecord *evidence_record
|
||||||
);
|
);
|
||||||
|
void main_window_set_identity_ocr_runs(
|
||||||
|
MainWindow *main_window, GPtrArray *owned_records);
|
||||||
void main_window_set_evidence_observations(MainWindow *main_window,
|
void main_window_set_evidence_observations(MainWindow *main_window,
|
||||||
const GPtrArray *observations);
|
const GPtrArray *observations);
|
||||||
void main_window_set_observation_remove_callback(MainWindow *main_window,
|
void main_window_set_observation_remove_callback(MainWindow *main_window,
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@
|
||||||
#ifndef LABFY_INVESTIGATION_MANAGE_ENTITY_EVIDENCE_DIALOG_H
|
#ifndef LABFY_INVESTIGATION_MANAGE_ENTITY_EVIDENCE_DIALOG_H
|
||||||
#define LABFY_INVESTIGATION_MANAGE_ENTITY_EVIDENCE_DIALOG_H
|
#define LABFY_INVESTIGATION_MANAGE_ENTITY_EVIDENCE_DIALOG_H
|
||||||
#include "models/evidence_record.h"
|
#include "models/evidence_record.h"
|
||||||
|
#include "core/task_manager.h"
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
typedef void (*ManageEntityEvidenceDialogCallback)(
|
typedef void (*ManageEntityEvidenceDialogCallback)(
|
||||||
GPtrArray *selected_identifiers, gpointer user_data);
|
GPtrArray *selected_identifiers, gpointer user_data);
|
||||||
|
typedef void (*ManageEntityEvidenceImportCallback)(gpointer user_data);
|
||||||
/**
|
/**
|
||||||
* @brief Présente la sélection multiple de preuves avec aperçu.
|
* @brief Présente la sélection multiple de preuves avec aperçu.
|
||||||
* @param parent Fenêtre parente.
|
* @param parent Fenêtre parente.
|
||||||
|
|
@ -23,7 +25,9 @@ typedef void (*ManageEntityEvidenceDialogCallback)(
|
||||||
gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
|
gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
|
||||||
const GPtrArray *evidence_records, const GPtrArray *selected_identifiers,
|
const GPtrArray *evidence_records, const GPtrArray *selected_identifiers,
|
||||||
const char *investigation_root_path,
|
const char *investigation_root_path,
|
||||||
|
TaskManager *task_manager,
|
||||||
ManageEntityEvidenceDialogCallback callback, gpointer user_data,
|
ManageEntityEvidenceDialogCallback callback, gpointer user_data,
|
||||||
|
ManageEntityEvidenceImportCallback import_callback,
|
||||||
GError **error);
|
GError **error);
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
21
include/widgets/ocr_provenance_overlay.h
Normal file
21
include/widgets/ocr_provenance_overlay.h
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#ifndef LABFY_OCR_PROVENANCE_OVERLAY_H
|
||||||
|
#define LABFY_OCR_PROVENANCE_OVERLAY_H
|
||||||
|
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
typedef struct OcrProvenanceOverlay OcrProvenanceOverlay;
|
||||||
|
|
||||||
|
OcrProvenanceOverlay *ocr_provenance_overlay_new(void);
|
||||||
|
void ocr_provenance_overlay_free(OcrProvenanceOverlay *overlay);
|
||||||
|
GtkWidget *ocr_provenance_overlay_get_widget(OcrProvenanceOverlay *overlay);
|
||||||
|
void ocr_provenance_overlay_set_image(OcrProvenanceOverlay *overlay,
|
||||||
|
GBytes *png_bytes, guint64 generation);
|
||||||
|
void ocr_provenance_overlay_set_field(OcrProvenanceOverlay *overlay,
|
||||||
|
const IdentityFieldObservation *field, guint64 generation);
|
||||||
|
void ocr_provenance_overlay_clear(OcrProvenanceOverlay *overlay,
|
||||||
|
guint64 generation);
|
||||||
|
gboolean ocr_provenance_overlay_has_region(
|
||||||
|
const OcrProvenanceOverlay *overlay);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include "models/evidence_observation.h"
|
#include "models/evidence_observation.h"
|
||||||
#include "models/entity_record.h"
|
#include "models/entity_record.h"
|
||||||
#include "models/osint_action_catalog.h"
|
#include "models/osint_action_catalog.h"
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
#include "core/task_manager.h"
|
#include "core/task_manager.h"
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
|
@ -157,6 +158,26 @@ typedef void (*WorkspaceRecoverPdfPasswordCallback)(
|
||||||
const char *evidence_identifier, gpointer user_data);
|
const char *evidence_identifier, gpointer user_data);
|
||||||
typedef void (*WorkspaceObservationRemoveCallback)(
|
typedef void (*WorkspaceObservationRemoveCallback)(
|
||||||
const char *observation_identifier, gpointer user_data);
|
const char *observation_identifier, gpointer user_data);
|
||||||
|
typedef void (*WorkspaceIdentityOcrCallback)(
|
||||||
|
const char *evidence_identifier, gboolean revise_existing,
|
||||||
|
const char *ocr_run_identifier, gpointer user_data);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
IdentityOcrRun *run;
|
||||||
|
char *person_identifier;
|
||||||
|
char *executed_at;
|
||||||
|
char *text_relative_path;
|
||||||
|
char *text_sha256;
|
||||||
|
char *tsv_relative_path;
|
||||||
|
char *tsv_sha256;
|
||||||
|
} WorkspaceIdentityOcrRecord;
|
||||||
|
|
||||||
|
WorkspaceIdentityOcrRecord *workspace_identity_ocr_record_new(
|
||||||
|
IdentityOcrRun *owned_run, const char *person_identifier,
|
||||||
|
const char *executed_at, const char *text_relative_path,
|
||||||
|
const char *text_sha256, const char *tsv_relative_path,
|
||||||
|
const char *tsv_sha256);
|
||||||
|
void workspace_identity_ocr_record_free(WorkspaceIdentityOcrRecord *record);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Crée une nouvelle zone de travail.
|
* @brief Crée une nouvelle zone de travail.
|
||||||
|
|
@ -207,6 +228,11 @@ void workspace_set_selected_evidence(
|
||||||
Workspace *workspace,
|
Workspace *workspace,
|
||||||
const EvidenceRecord *evidence_record
|
const EvidenceRecord *evidence_record
|
||||||
);
|
);
|
||||||
|
void workspace_set_identity_ocr_runs(
|
||||||
|
Workspace *workspace, GPtrArray *owned_records);
|
||||||
|
void workspace_set_identity_ocr_callback(
|
||||||
|
Workspace *workspace, WorkspaceIdentityOcrCallback callback,
|
||||||
|
gpointer user_data);
|
||||||
void workspace_set_evidence_observations(Workspace *workspace,
|
void workspace_set_evidence_observations(Workspace *workspace,
|
||||||
const GPtrArray *observations);
|
const GPtrArray *observations);
|
||||||
void workspace_set_observation_remove_callback(Workspace *workspace,
|
void workspace_set_observation_remove_callback(Workspace *workspace,
|
||||||
|
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
"enabled_providers": [
|
|
||||||
"llama.cpp"
|
|
||||||
],
|
|
||||||
"provider": {
|
|
||||||
"llama.cpp": {
|
|
||||||
"npm": "@ai-sdk/openai-compatible",
|
|
||||||
"name": "llama-server local",
|
|
||||||
"options": {
|
|
||||||
"baseURL": "http://127.0.0.1:8080/v1",
|
|
||||||
"timeout": 600000,
|
|
||||||
"chunkTimeout": 180000
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"qwen3-8b": {
|
|
||||||
"name": "Qwen3 8B — Architecte et reviewer",
|
|
||||||
"limit": {
|
|
||||||
"context": 32768,
|
|
||||||
"output": 2048
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"qwen2.5-coder-7b": {
|
|
||||||
"name": "Qwen2.5-Coder 7B — Développeur C17",
|
|
||||||
"limit": {
|
|
||||||
"context": 32768,
|
|
||||||
"output": 2048
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model": "llama.cpp/qwen3-8b",
|
|
||||||
"small_model": "llama.cpp/qwen3-8b",
|
|
||||||
"default_agent": "orchestrateur",
|
|
||||||
"subagent_depth": 1,
|
|
||||||
"instructions": [
|
|
||||||
".opencode/labfy.md"
|
|
||||||
],
|
|
||||||
"compaction": {
|
|
||||||
"auto": true,
|
|
||||||
"prune": true,
|
|
||||||
"reserved": 8000
|
|
||||||
},
|
|
||||||
"permission": {
|
|
||||||
"external_directory": "deny",
|
|
||||||
"edit": "ask",
|
|
||||||
"bash": "ask"
|
|
||||||
},
|
|
||||||
"agent": {
|
|
||||||
"orchestrateur": {
|
|
||||||
"description": "Agent principal. Inspecte le dépôt, prépare le plan, consulte l’architecte, délègue la production de code C17, applique les changements autorisés, compile, teste et présente le résultat.",
|
|
||||||
"mode": "primary",
|
|
||||||
"model": "llama.cpp/qwen3-8b",
|
|
||||||
"temperature": 0.1,
|
|
||||||
"steps": 40,
|
|
||||||
"permission": {
|
|
||||||
"read": "allow",
|
|
||||||
"glob": "allow",
|
|
||||||
"grep": "allow",
|
|
||||||
"list": "allow",
|
|
||||||
"edit": "ask",
|
|
||||||
"bash": {
|
|
||||||
"*": "ask",
|
|
||||||
"pwd": "allow",
|
|
||||||
"ls *": "allow",
|
|
||||||
"grep *": "allow",
|
|
||||||
"rg *": "allow",
|
|
||||||
"head *": "allow",
|
|
||||||
"tail *": "allow",
|
|
||||||
"file *": "allow",
|
|
||||||
"git status*": "allow",
|
|
||||||
"git diff*": "allow",
|
|
||||||
"git log*": "allow",
|
|
||||||
"make -j8*": "allow",
|
|
||||||
"make test*": "allow",
|
|
||||||
"git commit*": "deny",
|
|
||||||
"git push*": "deny",
|
|
||||||
"git reset*": "deny",
|
|
||||||
"git clean*": "deny",
|
|
||||||
"git checkout*": "deny",
|
|
||||||
"git restore*": "deny",
|
|
||||||
"rm *": "deny",
|
|
||||||
"sudo *": "deny",
|
|
||||||
"doas *": "deny"
|
|
||||||
},
|
|
||||||
"task": {
|
|
||||||
"*": "deny",
|
|
||||||
"architecte-reviewer": "allow",
|
|
||||||
"coder-c17": "allow"
|
|
||||||
},
|
|
||||||
"external_directory": "deny",
|
|
||||||
"todowrite": "allow",
|
|
||||||
"lsp": "allow",
|
|
||||||
"question": "allow",
|
|
||||||
"skill": "deny",
|
|
||||||
"websearch": "allow",
|
|
||||||
"webfetch": "allow"
|
|
||||||
},
|
|
||||||
"prompt": "Tu pilotes le travail dans Labfy Investigation. Pour une nouvelle API, une migration, une modification transversale ou un changement sensible, consulte architecte-reviewer avant l’implémentation. Transmets ensuite à coder-c17 tout le contexte nécessaire : objectif, fichiers pertinents, interfaces, extraits de code, contraintes et diagnostics. Le Coder ne dispose d’aucun outil : il renvoie uniquement du code ou un patch textuel. Tu restes seul responsable de l’application des changements, de l’exécution de make -j8, des tests, de git diff --check et de la validation finale. Ne crée jamais de commit ni de push sans autorisation explicite."
|
|
||||||
},
|
|
||||||
"coder-c17": {
|
|
||||||
"description": "Spécialiste ISO C17 sans outils. Reçoit le contexte transmis par l’orchestrateur et produit du code ou un patch textuel précis.",
|
|
||||||
"mode": "all",
|
|
||||||
"model": "llama.cpp/qwen2.5-coder-7b",
|
|
||||||
"temperature": 0.1,
|
|
||||||
"permission": {
|
|
||||||
"read": "deny",
|
|
||||||
"glob": "deny",
|
|
||||||
"grep": "deny",
|
|
||||||
"list": "deny",
|
|
||||||
"edit": "deny",
|
|
||||||
"bash": "deny",
|
|
||||||
"task": "deny",
|
|
||||||
"external_directory": "deny",
|
|
||||||
"todowrite": "deny",
|
|
||||||
"lsp": "deny",
|
|
||||||
"skill": "deny",
|
|
||||||
"question": "deny",
|
|
||||||
"webfetch": "deny",
|
|
||||||
"websearch": "deny",
|
|
||||||
"doom_loop": "deny"
|
|
||||||
},
|
|
||||||
"prompt": "Tu es le spécialiste C17 de Labfy Investigation. Tu ne disposes d’aucun outil et tu ne peux ni lire le dépôt, ni modifier un fichier, ni compiler, ni exécuter un test. Travaille uniquement à partir du contexte transmis. Produis du code ISO C17 compatible avec -std=c17 -Wall -Wextra -Wpedantic -Werror. Respecte les interfaces existantes, les noms snake_case, les préfixes de modules, l’ownership GLib, l’architecture GTK4/DAO/services et SQLite. Retourne de préférence un diff unifié applicable, ou des blocs de code complets avec les chemins concernés. Signale clairement les hypothèses. Ne prétends jamais avoir lu, modifié, compilé ou testé quoi que ce soit."
|
|
||||||
},
|
|
||||||
"architecte-reviewer": {
|
|
||||||
"description": "Architecte et reviewer en lecture seule. Contrôle l’architecture, la cohérence des API, l’ownership, GTK4/GLib, SQLite, les risques de régression et les tests manquants.",
|
|
||||||
"mode": "subagent",
|
|
||||||
"model": "llama.cpp/qwen3-8b",
|
|
||||||
"temperature": 0.1,
|
|
||||||
"steps": 30,
|
|
||||||
"permission": {
|
|
||||||
"read": "allow",
|
|
||||||
"glob": "allow",
|
|
||||||
"grep": "allow",
|
|
||||||
"list": "allow",
|
|
||||||
"edit": "deny",
|
|
||||||
"bash": "deny",
|
|
||||||
"task": "deny",
|
|
||||||
"external_directory": "deny",
|
|
||||||
"todowrite": "deny",
|
|
||||||
"lsp": "allow",
|
|
||||||
"skill": "deny",
|
|
||||||
"question": "deny",
|
|
||||||
"webfetch": "deny",
|
|
||||||
"websearch": "deny"
|
|
||||||
},
|
|
||||||
"prompt": "Tu es l’architecte et reviewer de Labfy Investigation. Tu travailles strictement en lecture seule. Examine le plan, les fichiers pertinents et le diff. Vérifie la séparation widgets/modèles/DAO/services, la cohérence entre .h et .c, l’ownership et les durées de vie GLib/GTK, les transactions SQLite, la gestion d’erreurs, la sécurité, les régressions et les tests manquants. Classe les remarques par gravité : bloquant, important, amélioration. Propose des corrections précises sans modifier le dépôt et sans créer de commit."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
#include "views/create_investigation_dialog.h"
|
#include "views/create_investigation_dialog.h"
|
||||||
#include "views/folder_dialog.h"
|
#include "views/folder_dialog.h"
|
||||||
#include "views/main_window.h"
|
#include "views/main_window.h"
|
||||||
|
#include "widgets/workspace.h"
|
||||||
#include "views/application_message_dialog.h"
|
#include "views/application_message_dialog.h"
|
||||||
#include "core/task_manager.h"
|
#include "core/task_manager.h"
|
||||||
#include "core/background_task.h"
|
#include "core/background_task.h"
|
||||||
|
|
@ -30,6 +31,7 @@
|
||||||
#include "core/evidence_reclassification.h"
|
#include "core/evidence_reclassification.h"
|
||||||
#include "views/file_dialog.h"
|
#include "views/file_dialog.h"
|
||||||
#include "core/evidence_import_task.h"
|
#include "core/evidence_import_task.h"
|
||||||
|
#include "core/evidence_identity_import_task.h"
|
||||||
#include "models/evidence_record.h"
|
#include "models/evidence_record.h"
|
||||||
#include "dao/evidence_type_dao.h"
|
#include "dao/evidence_type_dao.h"
|
||||||
#include "dao/graph_node_position_dao.h"
|
#include "dao/graph_node_position_dao.h"
|
||||||
|
|
@ -37,11 +39,13 @@
|
||||||
#include "models/relation_type.h"
|
#include "models/relation_type.h"
|
||||||
#include "core/relation_type_service.h"
|
#include "core/relation_type_service.h"
|
||||||
#include "views/evidence_import_dialog.h"
|
#include "views/evidence_import_dialog.h"
|
||||||
|
#include "views/evidence_identity_ocr_dialog.h"
|
||||||
#include "views/evidence_metadata_dialog.h"
|
#include "views/evidence_metadata_dialog.h"
|
||||||
#include "views/create_relation_dialog.h"
|
#include "views/create_relation_dialog.h"
|
||||||
#include "views/osint_dns_review_dialog.h"
|
#include "views/osint_dns_review_dialog.h"
|
||||||
#include "views/osint_execution_history_dialog.h"
|
#include "views/osint_execution_history_dialog.h"
|
||||||
#include "dao/evidence_dao.h"
|
#include "dao/evidence_dao.h"
|
||||||
|
#include "dao/identity_ocr_dao.h"
|
||||||
#include "dao/evidence_entity_dao.h"
|
#include "dao/evidence_entity_dao.h"
|
||||||
#include "dao/entity_dao.h"
|
#include "dao/entity_dao.h"
|
||||||
#include "dao/relation_dao.h"
|
#include "dao/relation_dao.h"
|
||||||
|
|
@ -146,12 +150,14 @@ struct Application
|
||||||
InvestigationGraphLayout *graph_layout;
|
InvestigationGraphLayout *graph_layout;
|
||||||
guint64 graph_load_generation;
|
guint64 graph_load_generation;
|
||||||
guint64 session_generation;
|
guint64 session_generation;
|
||||||
|
gint identity_import_session_generation;
|
||||||
gboolean graph_viewport_restored;
|
gboolean graph_viewport_restored;
|
||||||
guint graph_viewport_save_source_id;
|
guint graph_viewport_save_source_id;
|
||||||
|
|
||||||
char *selected_evidence_identifier;
|
char *selected_evidence_identifier;
|
||||||
char *pending_relation_selection_identifier;
|
char *pending_relation_selection_identifier;
|
||||||
char *pending_entity_selection_identifier;
|
char *pending_entity_selection_identifier;
|
||||||
|
char *pending_evidence_import_person_identifier;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
|
|
@ -201,6 +207,13 @@ static void application_observation_removal_context_free(
|
||||||
g_free(context);
|
g_free(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Garde immuable d'une tâche d'import OCR. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
Application *application;
|
||||||
|
gint generation;
|
||||||
|
} ApplicationEvidenceIdentitySessionGuard;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Contexte ApplicationOpenErrorconservé jusqu’à la fin d’un import.
|
* @brief Contexte ApplicationOpenErrorconservé jusqu’à la fin d’un import.
|
||||||
*/
|
*/
|
||||||
|
|
@ -209,8 +222,9 @@ typedef struct
|
||||||
Application *application;
|
Application *application;
|
||||||
char *investigation_root_path;
|
char *investigation_root_path;
|
||||||
gpointer batch_context;
|
gpointer batch_context;
|
||||||
|
GtkWindow *identity_dialog;
|
||||||
|
ApplicationEvidenceIdentitySessionGuard identity_session_guard;
|
||||||
} ApplicationEvidenceImportContext;
|
} ApplicationEvidenceImportContext;
|
||||||
|
|
||||||
/** @brief État d'un import groupé traité séquentiellement. */
|
/** @brief État d'un import groupé traité séquentiellement. */
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
|
|
@ -242,8 +256,30 @@ typedef struct
|
||||||
{
|
{
|
||||||
Application *application;
|
Application *application;
|
||||||
char *file_path;
|
char *file_path;
|
||||||
|
char *type_identifier;
|
||||||
|
char *collected_at;
|
||||||
|
char *source;
|
||||||
|
char *description;
|
||||||
|
char *root_path;
|
||||||
|
char *preselected_person_identifier;
|
||||||
} ApplicationEvidenceDialogContext;
|
} ApplicationEvidenceDialogContext;
|
||||||
|
|
||||||
|
static gboolean application_session_matches_root(
|
||||||
|
const Application *application, const char *expected_root_path);
|
||||||
|
static gboolean application_refresh_investigation_tree(
|
||||||
|
Application *application, const char *investigation_root_path);
|
||||||
|
static gboolean application_refresh_evidence_models(
|
||||||
|
Application *application, GError **error);
|
||||||
|
static void application_start_evidence_import(
|
||||||
|
Application *application, const char *file_path,
|
||||||
|
const char *type_identifier, const char *collected_at,
|
||||||
|
const char *source, const char *description,
|
||||||
|
ApplicationEvidenceBatchContext *batch_context);
|
||||||
|
static void application_evidence_import_context_free(gpointer user_data);
|
||||||
|
static gboolean application_evidence_identity_session_check(gpointer data);
|
||||||
|
static void application_on_edit_evidence_requested(
|
||||||
|
const char *evidence_identifier, gpointer user_data);
|
||||||
|
|
||||||
/** @brief Contexte possédé pendant l'édition d'une preuve. */
|
/** @brief Contexte possédé pendant l'édition d'une preuve. */
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
|
|
@ -251,6 +287,17 @@ typedef struct
|
||||||
char *evidence_identifier;
|
char *evidence_identifier;
|
||||||
} ApplicationEvidenceMetadataContext;
|
} ApplicationEvidenceMetadataContext;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
Application *application;
|
||||||
|
char *evidence_identifier;
|
||||||
|
char *root_path;
|
||||||
|
char *database_path;
|
||||||
|
GtkWindow *dialog;
|
||||||
|
char *ocr_run_identifier;
|
||||||
|
gboolean revision;
|
||||||
|
} ApplicationExistingEvidenceOcrContext;
|
||||||
|
|
||||||
/** @brief Données d'actualisation différée après reclassement. */
|
/** @brief Données d'actualisation différée après reclassement. */
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
|
|
@ -855,12 +902,175 @@ static void application_evidence_dialog_context_free(
|
||||||
g_free(
|
g_free(
|
||||||
context->file_path
|
context->file_path
|
||||||
);
|
);
|
||||||
|
g_free(context->type_identifier);
|
||||||
|
g_free(context->collected_at);
|
||||||
|
g_free(context->source);
|
||||||
|
g_free(context->description);
|
||||||
|
g_free(context->root_path);
|
||||||
|
g_free(context->preselected_person_identifier);
|
||||||
|
|
||||||
g_free(
|
g_free(
|
||||||
context
|
context
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Vérifie que le dialogue OCR appartient toujours à la session active. */
|
||||||
|
static gboolean application_evidence_ocr_session_is_current(gpointer user_data)
|
||||||
|
{
|
||||||
|
ApplicationEvidenceDialogContext *context = user_data;
|
||||||
|
return context != NULL &&
|
||||||
|
application_session_matches_root(context->application,
|
||||||
|
context->root_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Actualise l'interface après l'import atomique d'une preuve OCR. */
|
||||||
|
static void application_on_evidence_identity_import_completed(
|
||||||
|
BackgroundTask *task, gpointer user_data)
|
||||||
|
{
|
||||||
|
ApplicationEvidenceImportContext *context = user_data;
|
||||||
|
Application *application =
|
||||||
|
context != NULL ? context->application : NULL;
|
||||||
|
GError *refresh_error = NULL;
|
||||||
|
if (task == NULL || application == NULL ||
|
||||||
|
application->main_window == NULL) return;
|
||||||
|
if (background_task_get_state(task) == BACKGROUND_TASK_STATE_COMPLETED)
|
||||||
|
{
|
||||||
|
if (application_session_matches_root(
|
||||||
|
application, context->investigation_root_path))
|
||||||
|
{
|
||||||
|
application_refresh_investigation_tree(
|
||||||
|
application, context->investigation_root_path);
|
||||||
|
application_refresh_evidence_models(application, &refresh_error);
|
||||||
|
PersonCreationCoordinatorResult *result =
|
||||||
|
background_task_get_result(task);
|
||||||
|
if (result != NULL &&
|
||||||
|
result->evidence_identifiers != NULL &&
|
||||||
|
result->evidence_identifiers->len > 0)
|
||||||
|
application_on_evidence_selected(
|
||||||
|
g_ptr_array_index(result->evidence_identifiers, 0),
|
||||||
|
application);
|
||||||
|
main_window_set_status(application->main_window,
|
||||||
|
"Preuve et observations OCR importées.");
|
||||||
|
evidence_identity_ocr_dialog_finish_import(
|
||||||
|
context->identity_dialog, NULL);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GError *session_error = g_error_new_literal(
|
||||||
|
G_IO_ERROR, G_IO_ERROR_CANCELLED,
|
||||||
|
"La session d’enquête a changé avant la fin de l’import.");
|
||||||
|
evidence_identity_ocr_dialog_finish_import(
|
||||||
|
context->identity_dialog, session_error);
|
||||||
|
g_error_free(session_error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GError *task_error = background_task_dup_error(task);
|
||||||
|
application_present_error(application, "Import OCR impossible",
|
||||||
|
task_error != NULL ? task_error->message :
|
||||||
|
"L’import atomique de la preuve a échoué.");
|
||||||
|
GError *display_error = task_error != NULL
|
||||||
|
? g_error_copy(task_error)
|
||||||
|
: g_error_new_literal(G_IO_ERROR, G_IO_ERROR_FAILED,
|
||||||
|
"L’import atomique de la preuve a échoué.");
|
||||||
|
evidence_identity_ocr_dialog_finish_import(
|
||||||
|
context->identity_dialog, display_error);
|
||||||
|
g_error_free(display_error);
|
||||||
|
g_clear_error(&task_error);
|
||||||
|
}
|
||||||
|
g_clear_error(&refresh_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Traite le choix facultatif effectué dans l'étape OCR. */
|
||||||
|
static void application_on_evidence_identity_ocr_completed(
|
||||||
|
EvidenceIdentityOcrDialogResult *result, gpointer user_data)
|
||||||
|
{
|
||||||
|
ApplicationEvidenceDialogContext *context = user_data;
|
||||||
|
Application *application =
|
||||||
|
context != NULL ? context->application : NULL;
|
||||||
|
if (result == NULL || application == NULL) return;
|
||||||
|
if (!evidence_identity_ocr_dialog_result_has_ocr(result))
|
||||||
|
{
|
||||||
|
application_start_evidence_import(application, context->file_path,
|
||||||
|
context->type_identifier, context->collected_at,
|
||||||
|
context->source, context->description, NULL);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const InvestigationProject *project =
|
||||||
|
investigation_session_get_project(application->session);
|
||||||
|
GtkWindow *identity_dialog =
|
||||||
|
evidence_identity_ocr_dialog_result_get_dialog(result);
|
||||||
|
IdentityOcrRun *run =
|
||||||
|
evidence_identity_ocr_dialog_result_steal_run(result);
|
||||||
|
EvidenceIdentityImportTaskRequest *request = NULL;
|
||||||
|
ApplicationEvidenceImportContext *task_context = NULL;
|
||||||
|
BackgroundTask *task = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
if (project == NULL) {
|
||||||
|
GError *session_error = g_error_new_literal(
|
||||||
|
G_IO_ERROR, G_IO_ERROR_CANCELLED,
|
||||||
|
"La session d’enquête n’est plus active.");
|
||||||
|
evidence_identity_ocr_dialog_finish_import(
|
||||||
|
identity_dialog, session_error);
|
||||||
|
g_error_free(session_error);
|
||||||
|
identity_ocr_run_free(run);
|
||||||
|
evidence_identity_ocr_dialog_result_free(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request = evidence_identity_import_task_request_new(
|
||||||
|
investigation_project_get_database_path(project),
|
||||||
|
investigation_project_get_root_path(project), context->file_path,
|
||||||
|
evidence_identity_ocr_dialog_result_get_person_identifier(result),
|
||||||
|
context->type_identifier, context->collected_at,
|
||||||
|
context->source, context->description, run);
|
||||||
|
task_context = g_new0(ApplicationEvidenceImportContext, 1);
|
||||||
|
task_context->application = application;
|
||||||
|
task_context->investigation_root_path =
|
||||||
|
g_strdup(context->root_path);
|
||||||
|
task_context->identity_dialog = identity_dialog != NULL
|
||||||
|
? g_object_ref(identity_dialog) : NULL;
|
||||||
|
task_context->identity_session_guard.application = application;
|
||||||
|
task_context->identity_session_guard.generation = g_atomic_int_get(
|
||||||
|
&application->identity_import_session_generation);
|
||||||
|
if (request != NULL) {
|
||||||
|
evidence_identity_import_task_request_set_session_check(
|
||||||
|
request, application_evidence_identity_session_check,
|
||||||
|
&task_context->identity_session_guard);
|
||||||
|
task = evidence_identity_import_task_start(
|
||||||
|
application->task_manager, request,
|
||||||
|
application_on_evidence_identity_import_completed,
|
||||||
|
task_context, application_evidence_import_context_free,
|
||||||
|
&error);
|
||||||
|
}
|
||||||
|
if (task == NULL)
|
||||||
|
{
|
||||||
|
GError *display_error = error != NULL
|
||||||
|
? g_error_copy(error)
|
||||||
|
: g_error_new_literal(G_IO_ERROR, G_IO_ERROR_FAILED,
|
||||||
|
"La tâche d’import OCR n’a pas pu démarrer.");
|
||||||
|
evidence_identity_ocr_dialog_finish_import(
|
||||||
|
identity_dialog, display_error);
|
||||||
|
g_error_free(display_error);
|
||||||
|
application_evidence_import_context_free(task_context);
|
||||||
|
application_present_error(application, "Import OCR impossible",
|
||||||
|
error != NULL ? error->message :
|
||||||
|
"La tâche d’import OCR n’a pas pu démarrer.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
main_window_set_status(application->main_window,
|
||||||
|
"Import atomique de la preuve et des observations OCR…");
|
||||||
|
background_task_unref(task);
|
||||||
|
}
|
||||||
|
evidence_identity_import_task_request_free(request);
|
||||||
|
identity_ocr_run_free(run);
|
||||||
|
g_clear_error(&error);
|
||||||
|
}
|
||||||
|
evidence_identity_ocr_dialog_result_free(result);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Libère le contexte d’un import asynchrone.
|
* @brief Libère le contexte d’un import asynchrone.
|
||||||
*/
|
*/
|
||||||
|
|
@ -879,12 +1089,22 @@ static void application_evidence_import_context_free(
|
||||||
g_free(
|
g_free(
|
||||||
context->investigation_root_path
|
context->investigation_root_path
|
||||||
);
|
);
|
||||||
|
g_clear_object(&context->identity_dialog);
|
||||||
|
|
||||||
g_free(
|
g_free(
|
||||||
context
|
context
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static gboolean application_evidence_identity_session_check(gpointer data)
|
||||||
|
{
|
||||||
|
ApplicationEvidenceIdentitySessionGuard *guard = data;
|
||||||
|
return guard != NULL && guard->application != NULL &&
|
||||||
|
g_atomic_int_get(
|
||||||
|
&guard->application->identity_import_session_generation) ==
|
||||||
|
guard->generation;
|
||||||
|
}
|
||||||
|
|
||||||
/** @brief Libère l'état possédé d'un import groupé. */
|
/** @brief Libère l'état possédé d'un import groupé. */
|
||||||
static void application_evidence_batch_context_free(
|
static void application_evidence_batch_context_free(
|
||||||
ApplicationEvidenceBatchContext *context
|
ApplicationEvidenceBatchContext *context
|
||||||
|
|
@ -2763,6 +2983,7 @@ static gboolean application_install_session(
|
||||||
application->tree_model = new_tree_model;
|
application->tree_model = new_tree_model;
|
||||||
application->session = new_session;
|
application->session = new_session;
|
||||||
application->session_generation++;
|
application->session_generation++;
|
||||||
|
g_atomic_int_inc(&application->identity_import_session_generation);
|
||||||
application->graph_viewport_restored = FALSE;
|
application->graph_viewport_restored = FALSE;
|
||||||
|
|
||||||
main_window_set_tree_model(
|
main_window_set_tree_model(
|
||||||
|
|
@ -3683,6 +3904,309 @@ static void application_evidence_metadata_context_free(
|
||||||
g_free(context->evidence_identifier); g_free(context);
|
g_free(context->evidence_identifier); g_free(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void application_existing_evidence_ocr_context_free(gpointer data)
|
||||||
|
{
|
||||||
|
ApplicationExistingEvidenceOcrContext *context = data;
|
||||||
|
if (context == NULL) return;
|
||||||
|
g_free(context->evidence_identifier);
|
||||||
|
g_free(context->root_path);
|
||||||
|
g_free(context->database_path);
|
||||||
|
g_clear_object(&context->dialog);
|
||||||
|
g_free(context->ocr_run_identifier);
|
||||||
|
g_free(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean application_existing_evidence_ocr_session_is_current(
|
||||||
|
gpointer data)
|
||||||
|
{
|
||||||
|
ApplicationExistingEvidenceOcrContext *context = data;
|
||||||
|
return context != NULL &&
|
||||||
|
application_session_matches_root(
|
||||||
|
context->application, context->root_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void application_on_existing_evidence_ocr_persisted(
|
||||||
|
BackgroundTask *task, gpointer data)
|
||||||
|
{
|
||||||
|
ApplicationExistingEvidenceOcrContext *context = data;
|
||||||
|
Application *application =
|
||||||
|
context != NULL ? context->application : NULL;
|
||||||
|
if (application == NULL || application->main_window == NULL) return;
|
||||||
|
if (background_task_get_state(task) == BACKGROUND_TASK_STATE_COMPLETED &&
|
||||||
|
application_session_matches_root(application, context->root_path)) {
|
||||||
|
main_window_set_status(application->main_window,
|
||||||
|
"Analyse OCR enregistrée sur la preuve existante.");
|
||||||
|
application_on_evidence_selected(
|
||||||
|
context->evidence_identifier, application);
|
||||||
|
} else {
|
||||||
|
GError *error = background_task_dup_error(task);
|
||||||
|
application_present_error(application,
|
||||||
|
"Analyse OCR impossible",
|
||||||
|
error != NULL ? error->message :
|
||||||
|
"L’analyse n’a pas pu être liée à la preuve existante.");
|
||||||
|
g_clear_error(&error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void application_on_existing_evidence_ocr_reviewed(
|
||||||
|
BackgroundTask *task, gpointer data)
|
||||||
|
{
|
||||||
|
ApplicationExistingEvidenceOcrContext *context = data;
|
||||||
|
Application *application =
|
||||||
|
context != NULL ? context->application : NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
gboolean success = FALSE;
|
||||||
|
if (application == NULL || application->main_window == NULL ||
|
||||||
|
context->dialog == NULL) return;
|
||||||
|
if (background_task_get_state(task) == BACKGROUND_TASK_STATE_COMPLETED &&
|
||||||
|
application_session_matches_root(application, context->root_path)) {
|
||||||
|
IdentityOcrRun *verified = background_task_get_result(task);
|
||||||
|
success = verified != NULL &&
|
||||||
|
g_strcmp0(identity_ocr_run_get_identifier(verified),
|
||||||
|
context->ocr_run_identifier) == 0;
|
||||||
|
if (success) {
|
||||||
|
application_on_evidence_selected(
|
||||||
|
context->evidence_identifier, application);
|
||||||
|
main_window_set_status(application->main_window,
|
||||||
|
"Révision OCR enregistrée sans relancer Tesseract.");
|
||||||
|
evidence_identity_ocr_dialog_finish_import(
|
||||||
|
context->dialog, NULL);
|
||||||
|
gtk_window_present(
|
||||||
|
main_window_get_window(application->main_window));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
error = background_task_dup_error(task);
|
||||||
|
if (error == NULL)
|
||||||
|
error = g_error_new_literal(G_IO_ERROR, G_IO_ERROR_FAILED,
|
||||||
|
"La révision OCR n’a pas pu être validée.");
|
||||||
|
evidence_identity_ocr_dialog_finish_import(
|
||||||
|
context->dialog, error);
|
||||||
|
application_present_error(application,
|
||||||
|
"Révision OCR impossible", error->message);
|
||||||
|
}
|
||||||
|
g_clear_error(&error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void application_on_existing_evidence_ocr_completed(
|
||||||
|
EvidenceIdentityOcrDialogResult *result, gpointer data)
|
||||||
|
{
|
||||||
|
ApplicationExistingEvidenceOcrContext *context = data;
|
||||||
|
Application *application =
|
||||||
|
context != NULL ? context->application : NULL;
|
||||||
|
EvidenceIdentityImportTaskRequest *request = NULL;
|
||||||
|
ApplicationExistingEvidenceOcrContext *task_context = NULL;
|
||||||
|
BackgroundTask *task = NULL;
|
||||||
|
IdentityOcrRun *run = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
if (result == NULL || application == NULL ||
|
||||||
|
!evidence_identity_ocr_dialog_result_has_ocr(result))
|
||||||
|
goto cleanup;
|
||||||
|
run = evidence_identity_ocr_dialog_result_steal_run(result);
|
||||||
|
if (context->revision) {
|
||||||
|
GtkWindow *dialog =
|
||||||
|
evidence_identity_ocr_dialog_result_get_dialog(result);
|
||||||
|
task_context = g_new0(ApplicationExistingEvidenceOcrContext, 1);
|
||||||
|
task_context->application = application;
|
||||||
|
task_context->evidence_identifier =
|
||||||
|
g_strdup(context->evidence_identifier);
|
||||||
|
task_context->root_path = g_strdup(context->root_path);
|
||||||
|
task_context->database_path = g_strdup(context->database_path);
|
||||||
|
task_context->dialog = dialog != NULL
|
||||||
|
? g_object_ref(dialog) : NULL;
|
||||||
|
task_context->ocr_run_identifier = g_strdup(
|
||||||
|
identity_ocr_run_get_identifier(run));
|
||||||
|
task_context->revision = TRUE;
|
||||||
|
request = evidence_identity_import_task_request_new_review(
|
||||||
|
context->database_path, context->root_path,
|
||||||
|
context->evidence_identifier, run);
|
||||||
|
if (request != NULL)
|
||||||
|
evidence_identity_import_task_request_set_session_check(
|
||||||
|
request,
|
||||||
|
application_existing_evidence_ocr_session_is_current,
|
||||||
|
task_context);
|
||||||
|
if (request != NULL)
|
||||||
|
task = evidence_identity_import_task_start(
|
||||||
|
application->task_manager, request,
|
||||||
|
application_on_existing_evidence_ocr_reviewed,
|
||||||
|
task_context, application_existing_evidence_ocr_context_free,
|
||||||
|
&error);
|
||||||
|
if (task == NULL) {
|
||||||
|
GError *display_error = error != NULL
|
||||||
|
? g_error_copy(error)
|
||||||
|
: g_error_new_literal(G_IO_ERROR, G_IO_ERROR_FAILED,
|
||||||
|
"La tâche de révision OCR n’a pas pu démarrer.");
|
||||||
|
evidence_identity_ocr_dialog_finish_import(
|
||||||
|
dialog, display_error);
|
||||||
|
application_present_error(application,
|
||||||
|
"Révision OCR impossible", display_error->message);
|
||||||
|
g_error_free(display_error);
|
||||||
|
} else {
|
||||||
|
evidence_identity_ocr_dialog_set_submission_task(dialog, task);
|
||||||
|
task_context = NULL;
|
||||||
|
background_task_unref(task);
|
||||||
|
main_window_set_status(application->main_window,
|
||||||
|
"Enregistrement de la révision OCR…");
|
||||||
|
}
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
task_context = g_new0(ApplicationExistingEvidenceOcrContext, 1);
|
||||||
|
task_context->application = application;
|
||||||
|
task_context->evidence_identifier =
|
||||||
|
g_strdup(context->evidence_identifier);
|
||||||
|
task_context->root_path = g_strdup(context->root_path);
|
||||||
|
task_context->database_path = g_strdup(context->database_path);
|
||||||
|
request = evidence_identity_import_task_request_new_existing(
|
||||||
|
context->database_path, context->root_path,
|
||||||
|
context->evidence_identifier,
|
||||||
|
evidence_identity_ocr_dialog_result_get_person_identifier(result),
|
||||||
|
run);
|
||||||
|
if (request != NULL)
|
||||||
|
evidence_identity_import_task_request_set_session_check(
|
||||||
|
request, application_existing_evidence_ocr_session_is_current,
|
||||||
|
task_context);
|
||||||
|
if (request != NULL)
|
||||||
|
task = evidence_identity_import_task_start(
|
||||||
|
application->task_manager, request,
|
||||||
|
application_on_existing_evidence_ocr_persisted,
|
||||||
|
task_context, application_existing_evidence_ocr_context_free,
|
||||||
|
&error);
|
||||||
|
if (task == NULL) {
|
||||||
|
application_present_error(application, "Analyse OCR impossible",
|
||||||
|
error != NULL ? error->message :
|
||||||
|
"La conservation de l’analyse n’a pas pu démarrer.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
task_context = NULL;
|
||||||
|
main_window_set_status(application->main_window,
|
||||||
|
"Conservation de l’analyse OCR sur la preuve existante…");
|
||||||
|
cleanup:
|
||||||
|
evidence_identity_import_task_request_free(request);
|
||||||
|
identity_ocr_run_free(run);
|
||||||
|
evidence_identity_ocr_dialog_result_free(result);
|
||||||
|
application_existing_evidence_ocr_context_free(task_context);
|
||||||
|
g_clear_error(&error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void application_on_analyze_existing_evidence(
|
||||||
|
GtkWindow *parent, const char *evidence_identifier,
|
||||||
|
gboolean revise_existing, const char *ocr_run_identifier,
|
||||||
|
gpointer data)
|
||||||
|
{
|
||||||
|
ApplicationEvidenceMetadataContext *metadata_context = data;
|
||||||
|
Application *application = metadata_context != NULL
|
||||||
|
? metadata_context->application : NULL;
|
||||||
|
Database *database = NULL;
|
||||||
|
EvidenceDao *evidence_dao = NULL;
|
||||||
|
EntityDao *entity_dao = NULL;
|
||||||
|
EvidenceRecord *record = NULL;
|
||||||
|
GPtrArray *persons = NULL;
|
||||||
|
ApplicationExistingEvidenceOcrContext *context = NULL;
|
||||||
|
const InvestigationProject *project = NULL;
|
||||||
|
const ToolInfo *tesseract = NULL;
|
||||||
|
ToolRegistry *registry = NULL;
|
||||||
|
char *path = NULL;
|
||||||
|
IdentityOcrRun *persisted_run = NULL;
|
||||||
|
char *persisted_person = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
if (application == NULL || application->session == NULL ||
|
||||||
|
evidence_identifier == NULL) return;
|
||||||
|
database = investigation_session_get_database(application->session);
|
||||||
|
project = investigation_session_get_project(application->session);
|
||||||
|
evidence_dao = evidence_dao_new(database, &error);
|
||||||
|
entity_dao = entity_dao_new(database, &error);
|
||||||
|
if (evidence_dao != NULL)
|
||||||
|
record = evidence_dao_find_by_identifier(
|
||||||
|
evidence_dao, evidence_identifier, &error);
|
||||||
|
if (entity_dao != NULL)
|
||||||
|
persons = entity_dao_list_all(entity_dao, &error);
|
||||||
|
if (record != NULL && project != NULL)
|
||||||
|
path = g_build_filename(
|
||||||
|
investigation_project_get_root_path(project),
|
||||||
|
evidence_record_get_relative_path(record), NULL);
|
||||||
|
registry = tool_initializer_get_registry(application->tool_initializer);
|
||||||
|
if (registry != NULL)
|
||||||
|
tesseract = tool_registry_find(registry, "ocr.tesseract");
|
||||||
|
context = g_new0(ApplicationExistingEvidenceOcrContext, 1);
|
||||||
|
context->application = application;
|
||||||
|
context->evidence_identifier = g_strdup(evidence_identifier);
|
||||||
|
context->root_path = project != NULL
|
||||||
|
? g_strdup(investigation_project_get_root_path(project)) : NULL;
|
||||||
|
context->database_path = project != NULL
|
||||||
|
? g_strdup(investigation_project_get_database_path(project)) : NULL;
|
||||||
|
context->revision = revise_existing;
|
||||||
|
if (revise_existing && record != NULL && project != NULL) {
|
||||||
|
IdentityOcrDao *ocr_dao = identity_ocr_dao_new(database);
|
||||||
|
GPtrArray *runs = identity_ocr_dao_list_runs_by_evidence(
|
||||||
|
ocr_dao, evidence_identifier, &error);
|
||||||
|
if (runs != NULL && ocr_run_identifier != NULL) {
|
||||||
|
IdentityOcrRunRecord *selected = NULL;
|
||||||
|
for (guint index = 0; index < runs->len; index++) {
|
||||||
|
IdentityOcrRunRecord *candidate =
|
||||||
|
g_ptr_array_index(runs, index);
|
||||||
|
if (g_strcmp0(candidate->id,
|
||||||
|
ocr_run_identifier) == 0) {
|
||||||
|
selected = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selected != NULL)
|
||||||
|
persisted_run = identity_ocr_dao_load_run(
|
||||||
|
ocr_dao, investigation_project_get_root_path(project),
|
||||||
|
selected->id, &persisted_person, &error);
|
||||||
|
}
|
||||||
|
g_clear_pointer(&runs, g_ptr_array_unref);
|
||||||
|
identity_ocr_dao_free(ocr_dao);
|
||||||
|
}
|
||||||
|
if (record == NULL || persons == NULL || path == NULL ||
|
||||||
|
(revise_existing ? persisted_run == NULL ||
|
||||||
|
g_strcmp0(identity_ocr_run_get_evidence_id(persisted_run),
|
||||||
|
evidence_identifier) != 0 ||
|
||||||
|
g_strcmp0(identity_ocr_run_get_expected_sha256(persisted_run),
|
||||||
|
evidence_record_get_sha256(record)) != 0 ||
|
||||||
|
!evidence_identity_ocr_dialog_present_review(
|
||||||
|
parent, path, persons, persisted_person, persisted_run,
|
||||||
|
application_existing_evidence_ocr_session_is_current,
|
||||||
|
application_on_existing_evidence_ocr_completed,
|
||||||
|
context, application_existing_evidence_ocr_context_free,
|
||||||
|
&error)
|
||||||
|
: !evidence_identity_ocr_dialog_present(
|
||||||
|
parent, path, persons, NULL, tesseract,
|
||||||
|
application_existing_evidence_ocr_session_is_current,
|
||||||
|
application_on_existing_evidence_ocr_completed,
|
||||||
|
context, application_existing_evidence_ocr_context_free,
|
||||||
|
&error))) {
|
||||||
|
application_present_error(application, "Étape OCR indisponible",
|
||||||
|
error != NULL ? error->message :
|
||||||
|
"La preuve ou la liste des personnes est indisponible.");
|
||||||
|
application_existing_evidence_ocr_context_free(context);
|
||||||
|
}
|
||||||
|
g_free(path);
|
||||||
|
g_free(persisted_person);
|
||||||
|
identity_ocr_run_free(persisted_run);
|
||||||
|
g_clear_pointer(&persons, g_ptr_array_unref);
|
||||||
|
evidence_record_free(record);
|
||||||
|
entity_dao_free(entity_dao);
|
||||||
|
evidence_dao_free(evidence_dao);
|
||||||
|
g_clear_error(&error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void application_on_workspace_identity_ocr(
|
||||||
|
const char *evidence_identifier, gboolean revise_existing,
|
||||||
|
const char *ocr_run_identifier, gpointer data)
|
||||||
|
{
|
||||||
|
Application *application = data;
|
||||||
|
if (application == NULL || application->main_window == NULL) return;
|
||||||
|
ApplicationEvidenceMetadataContext context = {
|
||||||
|
.application = application,
|
||||||
|
.evidence_identifier = (char *) evidence_identifier
|
||||||
|
};
|
||||||
|
application_on_analyze_existing_evidence(
|
||||||
|
main_window_get_window(application->main_window),
|
||||||
|
evidence_identifier, revise_existing, ocr_run_identifier,
|
||||||
|
&context);
|
||||||
|
}
|
||||||
|
|
||||||
/** @brief Actualise les vues après la destruction du dialogue d'édition. */
|
/** @brief Actualise les vues après la destruction du dialogue d'édition. */
|
||||||
static gboolean application_refresh_after_evidence_reclassification(
|
static gboolean application_refresh_after_evidence_reclassification(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
|
|
@ -3784,8 +4308,12 @@ static void application_on_edit_evidence_requested(
|
||||||
context = g_new0(ApplicationEvidenceMetadataContext, 1);
|
context = g_new0(ApplicationEvidenceMetadataContext, 1);
|
||||||
context->application = application;
|
context->application = application;
|
||||||
context->evidence_identifier = g_strdup(evidence_identifier);
|
context->evidence_identifier = g_strdup(evidence_identifier);
|
||||||
if (!evidence_metadata_dialog_present(
|
const InvestigationProject *project =
|
||||||
|
investigation_session_get_project(application->session);
|
||||||
|
if (!evidence_metadata_dialog_present_with_ocr(
|
||||||
main_window_get_window(application->main_window), record, types,
|
main_window_get_window(application->main_window), record, types,
|
||||||
|
database, investigation_project_get_root_path(project),
|
||||||
|
application_on_analyze_existing_evidence,
|
||||||
application_on_evidence_metadata_edited, context))
|
application_on_evidence_metadata_edited, context))
|
||||||
{
|
{
|
||||||
application_evidence_metadata_context_free(context);
|
application_evidence_metadata_context_free(context);
|
||||||
|
|
@ -4874,6 +5402,12 @@ static void application_on_evidence_metadata_completed(
|
||||||
user_data;
|
user_data;
|
||||||
|
|
||||||
Application *application = NULL;
|
Application *application = NULL;
|
||||||
|
const InvestigationProject *project = NULL;
|
||||||
|
EntityDao *entity_dao = NULL;
|
||||||
|
GPtrArray *persons = NULL;
|
||||||
|
ToolRegistry *registry = NULL;
|
||||||
|
const ToolInfo *tesseract = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
|
||||||
if (context == NULL)
|
if (context == NULL)
|
||||||
{
|
{
|
||||||
|
|
@ -4918,31 +5452,53 @@ static void application_on_evidence_metadata_completed(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
application_start_evidence_import(
|
context->type_identifier = g_strdup(
|
||||||
application,
|
evidence_import_dialog_result_get_type_identifier(result));
|
||||||
context->file_path,
|
context->collected_at = g_strdup(
|
||||||
evidence_import_dialog_result_get_type_identifier(
|
evidence_import_dialog_result_get_collected_at(result));
|
||||||
result
|
context->source = g_strdup(
|
||||||
),
|
evidence_import_dialog_result_get_source(result));
|
||||||
evidence_import_dialog_result_get_collected_at(
|
context->description = g_strdup(
|
||||||
result
|
evidence_import_dialog_result_get_description(result));
|
||||||
),
|
project = investigation_session_get_project(application->session);
|
||||||
evidence_import_dialog_result_get_source(
|
context->root_path = project != NULL ? g_strdup(
|
||||||
result
|
investigation_project_get_root_path(project)) : NULL;
|
||||||
),
|
|
||||||
evidence_import_dialog_result_get_description(
|
|
||||||
result
|
|
||||||
),
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
evidence_import_dialog_result_free(
|
evidence_import_dialog_result_free(
|
||||||
result
|
result
|
||||||
);
|
);
|
||||||
|
|
||||||
application_evidence_dialog_context_free(
|
if (!evidence_identity_ocr_dialog_file_is_compatible(context->file_path))
|
||||||
context
|
{
|
||||||
);
|
application_start_evidence_import(application, context->file_path,
|
||||||
|
context->type_identifier, context->collected_at,
|
||||||
|
context->source, context->description, NULL);
|
||||||
|
application_evidence_dialog_context_free(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity_dao = entity_dao_new(
|
||||||
|
investigation_session_get_database(application->session), &error);
|
||||||
|
if (entity_dao != NULL) persons = entity_dao_list_all(entity_dao, &error);
|
||||||
|
registry = tool_initializer_get_registry(application->tool_initializer);
|
||||||
|
if (registry != NULL) tesseract =
|
||||||
|
tool_registry_find(registry, "ocr.tesseract");
|
||||||
|
if (persons == NULL ||
|
||||||
|
!evidence_identity_ocr_dialog_present(
|
||||||
|
main_window_get_window(application->main_window),
|
||||||
|
context->file_path, persons,
|
||||||
|
context->preselected_person_identifier, tesseract,
|
||||||
|
application_evidence_ocr_session_is_current,
|
||||||
|
application_on_evidence_identity_ocr_completed,
|
||||||
|
context, application_evidence_dialog_context_free, &error))
|
||||||
|
{
|
||||||
|
application_present_error(application, "Étape OCR indisponible",
|
||||||
|
error != NULL ? error->message :
|
||||||
|
"La liste des personnes ou le dialogue OCR est indisponible.");
|
||||||
|
application_evidence_dialog_context_free(context);
|
||||||
|
}
|
||||||
|
g_clear_pointer(&persons, g_ptr_array_unref);
|
||||||
|
entity_dao_free(entity_dao);
|
||||||
|
g_clear_error(&error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -5127,6 +5683,10 @@ static void application_on_evidence_file_selected(
|
||||||
g_strdup(
|
g_strdup(
|
||||||
file_path
|
file_path
|
||||||
);
|
);
|
||||||
|
dialog_context->preselected_person_identifier = g_strdup(
|
||||||
|
application->pending_evidence_import_person_identifier);
|
||||||
|
g_clear_pointer(
|
||||||
|
&application->pending_evidence_import_person_identifier, g_free);
|
||||||
|
|
||||||
if (dialog_context->file_path == NULL)
|
if (dialog_context->file_path == NULL)
|
||||||
{
|
{
|
||||||
|
|
@ -5290,7 +5850,10 @@ static void application_on_evidence_files_selected(
|
||||||
{
|
{
|
||||||
char *message = g_strdup_printf(
|
char *message = g_strdup_printf(
|
||||||
"%u fichiers ont été sélectionnés. Chaque fichier sera révisé "
|
"%u fichiers ont été sélectionnés. Chaque fichier sera révisé "
|
||||||
"puis importé séparément avec sa propre empreinte SHA-256.",
|
"puis importé séparément avec sa propre empreinte SHA-256.\n\n"
|
||||||
|
"L’analyse OCR contrôlée traite actuellement une preuve à la "
|
||||||
|
"fois. Importez les fichiers puis analysez-les séparément.\n\n"
|
||||||
|
"L’import groupé reste disponible sans OCR.",
|
||||||
paths->len);
|
paths->len);
|
||||||
application_message_dialog_present_confirmation(
|
application_message_dialog_present_confirmation(
|
||||||
main_window_get_window(application->main_window),
|
main_window_get_window(application->main_window),
|
||||||
|
|
@ -5319,6 +5882,8 @@ static void application_on_import_evidence_requested(
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
g_clear_pointer(
|
||||||
|
&application->pending_evidence_import_person_identifier, g_free);
|
||||||
|
|
||||||
if (application->session == NULL)
|
if (application->session == NULL)
|
||||||
{
|
{
|
||||||
|
|
@ -5437,7 +6002,20 @@ static void application_on_person_creation_task_completed(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
main_window_set_status(application->main_window,
|
main_window_set_status(application->main_window,
|
||||||
"Personne et preuves ajoutées. Actualisation du graphe…");
|
"Personne et preuves ajoutées. Actualisation des vues…");
|
||||||
|
{
|
||||||
|
GError *refresh_error = NULL;
|
||||||
|
application_refresh_investigation_tree(
|
||||||
|
application, context->root_path);
|
||||||
|
if (!application_refresh_evidence_models(
|
||||||
|
application, &refresh_error)) {
|
||||||
|
application_present_error(application,
|
||||||
|
"Actualisation incomplète",
|
||||||
|
refresh_error != NULL ? refresh_error->message :
|
||||||
|
"La liste des preuves n’a pas pu être actualisée.");
|
||||||
|
}
|
||||||
|
g_clear_error(&refresh_error);
|
||||||
|
}
|
||||||
application_start_graph_loading(application, context->database_path);
|
application_start_graph_loading(application, context->database_path);
|
||||||
}
|
}
|
||||||
static void application_on_person_completed(
|
static void application_on_person_completed(
|
||||||
|
|
@ -5465,7 +6043,8 @@ static void application_on_person_completed(
|
||||||
application->session_generation, active_root, active_database))
|
application->session_generation, active_root, active_database))
|
||||||
goto changed_session;
|
goto changed_session;
|
||||||
request = person_creation_task_request_new(active_database, active_root,
|
request = person_creation_task_request_new(active_database, active_root,
|
||||||
input, create_person_dialog_result_get_evidence_selection(result));
|
input, create_person_dialog_result_get_evidence_selection(result),
|
||||||
|
create_person_dialog_result_get_ocr_runs(result));
|
||||||
task_context = g_new0(ApplicationPersonCreationTaskContext, 1);
|
task_context = g_new0(ApplicationPersonCreationTaskContext, 1);
|
||||||
task_context->application = application;
|
task_context->application = application;
|
||||||
task_context->generation = application->session_generation;
|
task_context->generation = application->session_generation;
|
||||||
|
|
@ -5532,9 +6111,13 @@ static void application_on_add_person_requested(gpointer user_data)
|
||||||
application->session_generation,
|
application->session_generation,
|
||||||
investigation_project_get_root_path(project),
|
investigation_project_get_root_path(project),
|
||||||
investigation_project_get_database_path(project));
|
investigation_project_get_database_path(project));
|
||||||
|
const ToolRegistry *registry =
|
||||||
|
tool_initializer_get_registry(application->tool_initializer);
|
||||||
|
const ToolInfo *tesseract =
|
||||||
|
tool_registry_find(registry, "ocr.tesseract");
|
||||||
create_person_dialog_present(main_window_get_window(application->main_window),
|
create_person_dialog_present(main_window_get_window(application->main_window),
|
||||||
records, investigation_project_get_root_path(project),
|
records, investigation_project_get_root_path(project),
|
||||||
application->task_manager,
|
application->task_manager, tesseract,
|
||||||
application_person_dialog_session_matches,
|
application_person_dialog_session_matches,
|
||||||
application_on_person_completed, context,
|
application_on_person_completed, context,
|
||||||
(GDestroyNotify) application_person_dialog_context_free);
|
(GDestroyNotify) application_person_dialog_context_free);
|
||||||
|
|
@ -5828,6 +6411,45 @@ static void application_on_evidence_selected(
|
||||||
project = investigation_session_get_project(application->session);
|
project = investigation_session_get_project(application->session);
|
||||||
investigation_root = project != NULL
|
investigation_root = project != NULL
|
||||||
? investigation_project_get_root_path(project) : NULL;
|
? investigation_project_get_root_path(project) : NULL;
|
||||||
|
if (investigation_root != NULL) {
|
||||||
|
IdentityOcrDao *ocr_dao = identity_ocr_dao_new(database);
|
||||||
|
GPtrArray *run_records =
|
||||||
|
identity_ocr_dao_list_runs_by_evidence(
|
||||||
|
ocr_dao, evidence_identifier, &error);
|
||||||
|
GPtrArray *workspace_records = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) workspace_identity_ocr_record_free);
|
||||||
|
for (guint index = 0;
|
||||||
|
error == NULL && run_records != NULL &&
|
||||||
|
index < run_records->len; index++) {
|
||||||
|
IdentityOcrRunRecord *record =
|
||||||
|
g_ptr_array_index(run_records, index);
|
||||||
|
char *person_identifier = NULL;
|
||||||
|
IdentityOcrRun *run = identity_ocr_dao_load_run(
|
||||||
|
ocr_dao, investigation_root, record->id,
|
||||||
|
&person_identifier, &error);
|
||||||
|
if (run != NULL) {
|
||||||
|
WorkspaceIdentityOcrRecord *workspace_record =
|
||||||
|
workspace_identity_ocr_record_new(
|
||||||
|
run, person_identifier, record->executed_at,
|
||||||
|
record->text_relative_path, record->text_sha256,
|
||||||
|
record->tsv_relative_path, record->tsv_sha256);
|
||||||
|
if (workspace_record != NULL)
|
||||||
|
g_ptr_array_add(workspace_records, workspace_record);
|
||||||
|
else
|
||||||
|
identity_ocr_run_free(run);
|
||||||
|
}
|
||||||
|
g_free(person_identifier);
|
||||||
|
}
|
||||||
|
if (error != NULL) {
|
||||||
|
g_warning("Analyse OCR de la preuve indisponible : %s",
|
||||||
|
error->message);
|
||||||
|
g_clear_error(&error);
|
||||||
|
}
|
||||||
|
main_window_set_identity_ocr_runs(application->main_window,
|
||||||
|
workspace_records);
|
||||||
|
g_clear_pointer(&run_records, g_ptr_array_unref);
|
||||||
|
identity_ocr_dao_free(ocr_dao);
|
||||||
|
}
|
||||||
gboolean eml_analysis_available = FALSE;
|
gboolean eml_analysis_available = FALSE;
|
||||||
if (investigation_root != NULL) {
|
if (investigation_root != NULL) {
|
||||||
const char *selected_name =
|
const char *selected_name =
|
||||||
|
|
@ -6527,6 +7149,25 @@ cleanup:
|
||||||
evidence_entity_dao_free(dao);
|
evidence_entity_dao_free(dao);
|
||||||
application_person_evidence_context_free(context);
|
application_person_evidence_context_free(context);
|
||||||
}
|
}
|
||||||
|
/** @brief Lance l'import normal avec la personne de la fiche présélectionnée. */
|
||||||
|
static void application_on_person_evidence_import_requested(gpointer user_data)
|
||||||
|
{
|
||||||
|
ApplicationPersonEvidenceContext *context = user_data;
|
||||||
|
Application *application =
|
||||||
|
context != NULL ? context->application : NULL;
|
||||||
|
if (application == NULL || application->main_window == NULL ||
|
||||||
|
application->session == NULL) {
|
||||||
|
application_person_evidence_context_free(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_free(application->pending_evidence_import_person_identifier);
|
||||||
|
application->pending_evidence_import_person_identifier =
|
||||||
|
g_strdup(context->entity_identifier);
|
||||||
|
application_person_evidence_context_free(context);
|
||||||
|
file_dialog_select_files(
|
||||||
|
main_window_get_window(application->main_window),
|
||||||
|
application_on_evidence_files_selected, application);
|
||||||
|
}
|
||||||
/** @brief Ouvre le gestionnaire des preuves de la personne sélectionnée. */
|
/** @brief Ouvre le gestionnaire des preuves de la personne sélectionnée. */
|
||||||
static void application_on_person_evidence_requested(
|
static void application_on_person_evidence_requested(
|
||||||
const char *entity_identifier, gpointer user_data)
|
const char *entity_identifier, gpointer user_data)
|
||||||
|
|
@ -6553,7 +7194,9 @@ static void application_on_person_evidence_requested(
|
||||||
!manage_entity_evidence_dialog_present(main_window_get_window(
|
!manage_entity_evidence_dialog_present(main_window_get_window(
|
||||||
application->main_window), records, selected,
|
application->main_window), records, selected,
|
||||||
investigation_project_get_root_path(project),
|
investigation_project_get_root_path(project),
|
||||||
application_on_person_evidences_selected, context, &error))
|
application->task_manager,
|
||||||
|
application_on_person_evidences_selected, context,
|
||||||
|
application_on_person_evidence_import_requested, &error))
|
||||||
goto failure;
|
goto failure;
|
||||||
context = NULL; goto cleanup;
|
context = NULL; goto cleanup;
|
||||||
failure:
|
failure:
|
||||||
|
|
@ -8306,6 +8949,8 @@ static void application_on_activate(
|
||||||
application_on_extract_metadata_requested, application);
|
application_on_extract_metadata_requested, application);
|
||||||
main_window_set_recover_pdf_password_callback(application->main_window,
|
main_window_set_recover_pdf_password_callback(application->main_window,
|
||||||
application_on_recover_pdf_password_requested, application);
|
application_on_recover_pdf_password_requested, application);
|
||||||
|
main_window_set_identity_ocr_callback(application->main_window,
|
||||||
|
application_on_workspace_identity_ocr, application);
|
||||||
|
|
||||||
main_window_set_graph_node_moved_callback(
|
main_window_set_graph_node_moved_callback(
|
||||||
application->main_window,
|
application->main_window,
|
||||||
|
|
@ -8670,6 +9315,8 @@ void application_free(
|
||||||
g_clear_pointer(&application->pending_relation_selection_identifier,
|
g_clear_pointer(&application->pending_relation_selection_identifier,
|
||||||
g_free);
|
g_free);
|
||||||
g_clear_pointer(&application->pending_entity_selection_identifier, g_free);
|
g_clear_pointer(&application->pending_entity_selection_identifier, g_free);
|
||||||
|
g_clear_pointer(
|
||||||
|
&application->pending_evidence_import_person_identifier, g_free);
|
||||||
|
|
||||||
g_free(
|
g_free(
|
||||||
application
|
application
|
||||||
|
|
|
||||||
349
src/core/evidence_identity_import_task.c
Normal file
349
src/core/evidence_identity_import_task.c
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
#include "core/evidence_identity_import_task.h"
|
||||||
|
|
||||||
|
#include "core/evidence_staging.h"
|
||||||
|
#include "core/person_creation_coordinator.h"
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "dao/evidence_dao.h"
|
||||||
|
#include "dao/identity_ocr_dao.h"
|
||||||
|
#include "models/person_evidence_selection.h"
|
||||||
|
|
||||||
|
struct EvidenceIdentityImportTaskRequest {
|
||||||
|
char *database_path;
|
||||||
|
char *root;
|
||||||
|
char *source_path;
|
||||||
|
char *person_identifier;
|
||||||
|
char *type_identifier;
|
||||||
|
char *collected_at;
|
||||||
|
char *source;
|
||||||
|
char *description;
|
||||||
|
IdentityOcrRun *run;
|
||||||
|
char *evidence_identifier;
|
||||||
|
gboolean review;
|
||||||
|
PersonCreationSessionCheck session_check;
|
||||||
|
gpointer session_check_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
EvidenceIdentityImportTaskRequest *
|
||||||
|
evidence_identity_import_task_request_new(
|
||||||
|
const char *database_path, const char *root,
|
||||||
|
const char *source_path, const char *person_identifier,
|
||||||
|
const char *type_identifier, const char *collected_at,
|
||||||
|
const char *source, const char *description,
|
||||||
|
const IdentityOcrRun *run)
|
||||||
|
{
|
||||||
|
if (database_path == NULL || root == NULL || source_path == NULL ||
|
||||||
|
person_identifier == NULL || type_identifier == NULL ||
|
||||||
|
collected_at == NULL || run == NULL) return NULL;
|
||||||
|
EvidenceIdentityImportTaskRequest *request =
|
||||||
|
g_new0(EvidenceIdentityImportTaskRequest, 1);
|
||||||
|
request->database_path = g_strdup(database_path);
|
||||||
|
request->root = g_strdup(root);
|
||||||
|
request->source_path = g_strdup(source_path);
|
||||||
|
request->person_identifier = g_strdup(person_identifier);
|
||||||
|
request->type_identifier = g_strdup(type_identifier);
|
||||||
|
request->collected_at = g_strdup(collected_at);
|
||||||
|
request->source = g_strdup(source);
|
||||||
|
request->description = g_strdup(description);
|
||||||
|
request->run = identity_ocr_run_copy(run);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
EvidenceIdentityImportTaskRequest *
|
||||||
|
evidence_identity_import_task_request_new_review(
|
||||||
|
const char *database_path, const char *root,
|
||||||
|
const char *evidence_identifier, const IdentityOcrRun *run)
|
||||||
|
{
|
||||||
|
if (database_path == NULL || root == NULL ||
|
||||||
|
evidence_identifier == NULL || run == NULL) return NULL;
|
||||||
|
EvidenceIdentityImportTaskRequest *request =
|
||||||
|
g_new0(EvidenceIdentityImportTaskRequest, 1);
|
||||||
|
request->database_path = g_strdup(database_path);
|
||||||
|
request->root = g_strdup(root);
|
||||||
|
request->evidence_identifier = g_strdup(evidence_identifier);
|
||||||
|
request->run = identity_ocr_run_copy(run);
|
||||||
|
request->review = TRUE;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
EvidenceIdentityImportTaskRequest *
|
||||||
|
evidence_identity_import_task_request_new_existing(
|
||||||
|
const char *database_path, const char *root,
|
||||||
|
const char *evidence_identifier, const char *person_identifier,
|
||||||
|
const IdentityOcrRun *run)
|
||||||
|
{
|
||||||
|
if (database_path == NULL || root == NULL ||
|
||||||
|
evidence_identifier == NULL || person_identifier == NULL ||
|
||||||
|
run == NULL) return NULL;
|
||||||
|
EvidenceIdentityImportTaskRequest *request =
|
||||||
|
g_new0(EvidenceIdentityImportTaskRequest, 1);
|
||||||
|
request->database_path = g_strdup(database_path);
|
||||||
|
request->root = g_strdup(root);
|
||||||
|
request->evidence_identifier = g_strdup(evidence_identifier);
|
||||||
|
request->person_identifier = g_strdup(person_identifier);
|
||||||
|
request->run = identity_ocr_run_copy(run);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
void evidence_identity_import_task_request_set_session_check(
|
||||||
|
EvidenceIdentityImportTaskRequest *request,
|
||||||
|
PersonCreationSessionCheck session_check,
|
||||||
|
gpointer session_check_data)
|
||||||
|
{
|
||||||
|
if (request == NULL) return;
|
||||||
|
request->session_check = session_check;
|
||||||
|
request->session_check_data = session_check_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void evidence_identity_import_task_request_free(
|
||||||
|
EvidenceIdentityImportTaskRequest *request)
|
||||||
|
{
|
||||||
|
if (request == NULL) return;
|
||||||
|
g_free(request->database_path); g_free(request->root);
|
||||||
|
g_free(request->source_path); g_free(request->person_identifier);
|
||||||
|
g_free(request->type_identifier); g_free(request->collected_at);
|
||||||
|
g_free(request->source); g_free(request->description);
|
||||||
|
g_free(request->evidence_identifier);
|
||||||
|
identity_ocr_run_free(request->run);
|
||||||
|
g_free(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean evidence_identity_import_worker(
|
||||||
|
BackgroundTask *task, GCancellable *cancellable,
|
||||||
|
gpointer data, gpointer *result, GError **error)
|
||||||
|
{
|
||||||
|
EvidenceIdentityImportTaskRequest *request = data;
|
||||||
|
EvidenceStaging *staging = NULL;
|
||||||
|
EvidenceStagingResult *prepared = NULL;
|
||||||
|
PersonEvidenceSelection *selection = NULL;
|
||||||
|
GPtrArray *runs = NULL;
|
||||||
|
Database *database = NULL;
|
||||||
|
PersonCreationCoordinatorResult *coordinator_result = NULL;
|
||||||
|
PersonCreationCoordinatorEvidenceMetadata metadata = {
|
||||||
|
.collected_at = request->collected_at,
|
||||||
|
.source = request->source,
|
||||||
|
.description = request->description,
|
||||||
|
.type_identifier = request->type_identifier
|
||||||
|
};
|
||||||
|
PersonCreationCoordinatorOptions options = {
|
||||||
|
.session_check = request->session_check,
|
||||||
|
.session_check_data = request->session_check_data
|
||||||
|
};
|
||||||
|
(void) task;
|
||||||
|
*result = NULL;
|
||||||
|
if (request->review) {
|
||||||
|
IdentityOcrDao *dao = NULL;
|
||||||
|
IdentityOcrRun *verified = NULL;
|
||||||
|
GDateTime *now = NULL;
|
||||||
|
char *timestamp = NULL;
|
||||||
|
if (g_cancellable_set_error_if_cancelled(cancellable, error))
|
||||||
|
goto cleanup;
|
||||||
|
if (request->session_check != NULL &&
|
||||||
|
!request->session_check(request->session_check_data)) {
|
||||||
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
|
||||||
|
"La session d’enquête a changé avant la révision.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
database = database_open(request->database_path);
|
||||||
|
if (database == NULL) {
|
||||||
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
||||||
|
"Impossible d’ouvrir la base de l’enquête.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
dao = identity_ocr_dao_new(database);
|
||||||
|
now = g_date_time_new_now_utc();
|
||||||
|
timestamp = g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ");
|
||||||
|
if (dao == NULL || timestamp == NULL ||
|
||||||
|
!identity_ocr_dao_update_review(
|
||||||
|
dao, request->run, timestamp, error))
|
||||||
|
goto review_cleanup;
|
||||||
|
database_close(database);
|
||||||
|
database = database_open(request->database_path);
|
||||||
|
identity_ocr_dao_free(dao);
|
||||||
|
dao = identity_ocr_dao_new(database);
|
||||||
|
if (database == NULL || dao == NULL ||
|
||||||
|
(verified = identity_ocr_dao_load_run(
|
||||||
|
dao, request->root,
|
||||||
|
identity_ocr_run_get_identifier(request->run),
|
||||||
|
NULL, error)) == NULL ||
|
||||||
|
g_strcmp0(identity_ocr_run_get_identifier(verified),
|
||||||
|
identity_ocr_run_get_identifier(request->run)) != 0 ||
|
||||||
|
g_strcmp0(identity_ocr_run_get_raw_text(verified),
|
||||||
|
identity_ocr_run_get_raw_text(request->run)) != 0) {
|
||||||
|
if (error != NULL && *error == NULL)
|
||||||
|
g_set_error_literal(error, G_IO_ERROR,
|
||||||
|
G_IO_ERROR_INVALID_DATA,
|
||||||
|
"La révision OCR n’est pas relisible après validation.");
|
||||||
|
identity_ocr_run_free(verified);
|
||||||
|
verified = NULL;
|
||||||
|
}
|
||||||
|
review_cleanup:
|
||||||
|
g_free(timestamp);
|
||||||
|
if (now != NULL) g_date_time_unref(now);
|
||||||
|
identity_ocr_dao_free(dao);
|
||||||
|
if (verified != NULL) {
|
||||||
|
*result = verified;
|
||||||
|
database_close(database);
|
||||||
|
database = NULL;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (request->evidence_identifier != NULL) {
|
||||||
|
EvidenceDao *evidence_dao = NULL;
|
||||||
|
EvidenceRecord *record = NULL;
|
||||||
|
database = database_open(request->database_path);
|
||||||
|
if (database == NULL) {
|
||||||
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
||||||
|
"Impossible d’ouvrir la base temporaire de l’enquête.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
evidence_dao = evidence_dao_new(database, error);
|
||||||
|
if (evidence_dao != NULL)
|
||||||
|
record = evidence_dao_find_by_identifier(evidence_dao,
|
||||||
|
request->evidence_identifier, error);
|
||||||
|
if (record == NULL ||
|
||||||
|
!person_evidence_selection_add_existing(selection =
|
||||||
|
person_evidence_selection_new(), record, error) ||
|
||||||
|
!identity_ocr_run_replace_evidence_id(request->run,
|
||||||
|
request->evidence_identifier))
|
||||||
|
{
|
||||||
|
if (error != NULL && *error == NULL)
|
||||||
|
g_set_error_literal(error, G_IO_ERROR,
|
||||||
|
G_IO_ERROR_INVALID_DATA,
|
||||||
|
"La preuve existante ne peut pas recevoir l’analyse OCR.");
|
||||||
|
evidence_record_free(record);
|
||||||
|
evidence_dao_free(evidence_dao);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
runs = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) identity_ocr_run_free);
|
||||||
|
g_ptr_array_add(runs, identity_ocr_run_copy(request->run));
|
||||||
|
coordinator_result =
|
||||||
|
person_creation_coordinator_attach_to_existing_person(
|
||||||
|
database, request->root, request->person_identifier,
|
||||||
|
selection, runs, NULL, &options, cancellable, error);
|
||||||
|
evidence_record_free(record);
|
||||||
|
evidence_dao_free(evidence_dao);
|
||||||
|
if (coordinator_result == NULL) goto cleanup;
|
||||||
|
database_close(database);
|
||||||
|
database = database_open(request->database_path);
|
||||||
|
IdentityOcrDao *verification_dao =
|
||||||
|
identity_ocr_dao_new(database);
|
||||||
|
IdentityOcrRunRecord *persisted =
|
||||||
|
verification_dao != NULL
|
||||||
|
? identity_ocr_dao_find_run(verification_dao,
|
||||||
|
identity_ocr_run_get_identifier(request->run), error)
|
||||||
|
: NULL;
|
||||||
|
if (persisted == NULL ||
|
||||||
|
g_strcmp0(persisted->evidence_id,
|
||||||
|
request->evidence_identifier) != 0 ||
|
||||||
|
g_strcmp0(persisted->expected_sha256,
|
||||||
|
identity_ocr_run_get_expected_sha256(
|
||||||
|
request->run)) != 0) {
|
||||||
|
if (error != NULL && *error == NULL)
|
||||||
|
g_set_error_literal(error, G_IO_ERROR,
|
||||||
|
G_IO_ERROR_INVALID_DATA,
|
||||||
|
"L’analyse OCR n’est pas relisible après réouverture.");
|
||||||
|
identity_ocr_run_record_free(persisted);
|
||||||
|
identity_ocr_dao_free(verification_dao);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
identity_ocr_run_record_free(persisted);
|
||||||
|
identity_ocr_dao_free(verification_dao);
|
||||||
|
*result = coordinator_result;
|
||||||
|
coordinator_result = NULL;
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
staging = evidence_staging_new(error);
|
||||||
|
if (staging == NULL) goto cleanup;
|
||||||
|
prepared = evidence_staging_prepare(
|
||||||
|
staging, request->source_path, cancellable, error);
|
||||||
|
if (prepared == NULL) goto cleanup;
|
||||||
|
selection = person_evidence_selection_new();
|
||||||
|
if (!person_evidence_selection_add_staged(selection,
|
||||||
|
prepared->source_path, prepared->staging_path,
|
||||||
|
prepared->original_name, prepared->mime_type,
|
||||||
|
request->type_identifier, prepared->size_bytes,
|
||||||
|
prepared->sha256, request->description,
|
||||||
|
prepared->prepared_at, error))
|
||||||
|
goto cleanup;
|
||||||
|
const PersonEvidenceSelectionItem *item =
|
||||||
|
person_evidence_selection_get(selection, 0);
|
||||||
|
if (!identity_ocr_run_replace_evidence_id(request->run,
|
||||||
|
person_evidence_selection_item_get_identifier(item))) {
|
||||||
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
|
||||||
|
"L’identifiant temporaire OCR ne peut pas être remplacé.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
runs = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) identity_ocr_run_free);
|
||||||
|
g_ptr_array_add(runs, identity_ocr_run_copy(request->run));
|
||||||
|
database = database_open(request->database_path);
|
||||||
|
if (database == NULL) {
|
||||||
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
||||||
|
"Impossible d’ouvrir la base temporaire de l’enquête.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
coordinator_result =
|
||||||
|
person_creation_coordinator_attach_to_existing_person(
|
||||||
|
database, request->root, request->person_identifier,
|
||||||
|
selection, runs, &metadata, &options, cancellable, error);
|
||||||
|
if (coordinator_result == NULL) goto cleanup;
|
||||||
|
*result = coordinator_result;
|
||||||
|
coordinator_result = NULL;
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
person_creation_coordinator_result_free(coordinator_result);
|
||||||
|
database_close(database);
|
||||||
|
g_clear_pointer(&runs, g_ptr_array_unref);
|
||||||
|
person_evidence_selection_free(selection);
|
||||||
|
evidence_staging_result_free(prepared);
|
||||||
|
if (*result == NULL) evidence_staging_cleanup(staging, NULL);
|
||||||
|
evidence_staging_free(staging);
|
||||||
|
return *result != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackgroundTask *evidence_identity_import_task_start(
|
||||||
|
TaskManager *manager,
|
||||||
|
const EvidenceIdentityImportTaskRequest *request,
|
||||||
|
BackgroundTaskCompletionCallback callback,
|
||||||
|
gpointer callback_data, GDestroyNotify callback_data_destroy,
|
||||||
|
GError **error)
|
||||||
|
{
|
||||||
|
if (manager == NULL || request == NULL) return NULL;
|
||||||
|
EvidenceIdentityImportTaskRequest *copy =
|
||||||
|
request->review
|
||||||
|
? evidence_identity_import_task_request_new_review(
|
||||||
|
request->database_path, request->root,
|
||||||
|
request->evidence_identifier, request->run)
|
||||||
|
: request->evidence_identifier != NULL
|
||||||
|
? evidence_identity_import_task_request_new_existing(
|
||||||
|
request->database_path, request->root,
|
||||||
|
request->evidence_identifier, request->person_identifier,
|
||||||
|
request->run)
|
||||||
|
: evidence_identity_import_task_request_new(
|
||||||
|
request->database_path, request->root, request->source_path,
|
||||||
|
request->person_identifier, request->type_identifier,
|
||||||
|
request->collected_at, request->source,
|
||||||
|
request->description, request->run);
|
||||||
|
if (copy != NULL)
|
||||||
|
evidence_identity_import_task_request_set_session_check(
|
||||||
|
copy, request->session_check, request->session_check_data);
|
||||||
|
BackgroundTask *task = background_task_new(request->review
|
||||||
|
? "Enregistrement de la révision OCR"
|
||||||
|
: "Import de la preuve et conservation OCR");
|
||||||
|
if (copy == NULL || task == NULL ||
|
||||||
|
!task_manager_add(manager, task, error) ||
|
||||||
|
!background_task_start(task, evidence_identity_import_worker,
|
||||||
|
copy,
|
||||||
|
(GDestroyNotify) evidence_identity_import_task_request_free,
|
||||||
|
request->review
|
||||||
|
? (GDestroyNotify) identity_ocr_run_free
|
||||||
|
: (GDestroyNotify) person_creation_coordinator_result_free,
|
||||||
|
callback, callback_data, callback_data_destroy, error)) {
|
||||||
|
evidence_identity_import_task_request_free(copy);
|
||||||
|
background_task_unref(task);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
@ -142,6 +142,48 @@ static gboolean valid_dimensions(gint width, gint height, GError **error)
|
||||||
}
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
static guint16 read_u16(const guint8*p,gboolean little)
|
||||||
|
{return little?(guint16)(p[0]|p[1]<<8):(guint16)(p[0]<<8|p[1]);}
|
||||||
|
static guint32 read_u32(const guint8*p,gboolean little)
|
||||||
|
{return little?((guint32)p[0]|(guint32)p[1]<<8|(guint32)p[2]<<16|(guint32)p[3]<<24):
|
||||||
|
((guint32)p[0]<<24|(guint32)p[1]<<16|(guint32)p[2]<<8|p[3]);}
|
||||||
|
static gint jpeg_exif_orientation(const char*path)
|
||||||
|
{
|
||||||
|
char*raw=NULL;gsize size=0;if(!g_file_get_contents(path,&raw,&size,NULL))return 1;
|
||||||
|
const guint8*d=(const guint8*)raw;gint orientation=1;
|
||||||
|
for(gsize p=2;p+4<size&&d[p]==0xff;){
|
||||||
|
guint8 marker=d[p+1];guint16 length=(guint16)(d[p+2]<<8|d[p+3]);
|
||||||
|
if(length<2||p+2+length>size)break;
|
||||||
|
if(marker==0xe1&&length>=16&&memcmp(d+p+4,"Exif\0\0",6)==0){
|
||||||
|
const guint8*t=d+p+10;gsize available=p+2+length-(p+10);
|
||||||
|
gboolean little=available>=8&&t[0]=='I'&&t[1]=='I';
|
||||||
|
if((little||(t[0]=='M'&&t[1]=='M'))&&read_u16(t+2,little)==42){
|
||||||
|
guint32 offset=read_u32(t+4,little);
|
||||||
|
if(offset+2<=available){guint16 count=read_u16(t+offset,little);
|
||||||
|
for(guint i=0;i<count&&offset+2+(i+1)*12<=available;i++){
|
||||||
|
const guint8*entry=t+offset+2+i*12;
|
||||||
|
if(read_u16(entry,little)==0x0112){
|
||||||
|
guint16 value=read_u16(entry+8,little);
|
||||||
|
if(value==1||value==3||value==6||value==8)orientation=value;
|
||||||
|
}}}
|
||||||
|
}break;
|
||||||
|
}p+=2+length;
|
||||||
|
}g_free(raw);return orientation;
|
||||||
|
}
|
||||||
|
static cairo_surface_t *orient_surface(cairo_surface_t*source,gint orientation,
|
||||||
|
gint width,gint height,gint*out_width,gint*out_height)
|
||||||
|
{
|
||||||
|
if(orientation==1){*out_width=width;*out_height=height;return source;}
|
||||||
|
gboolean quarter=orientation==6||orientation==8;
|
||||||
|
*out_width=quarter?height:width;*out_height=quarter?width:height;
|
||||||
|
cairo_surface_t*out=cairo_image_surface_create(CAIRO_FORMAT_RGB24,
|
||||||
|
*out_width,*out_height);cairo_t*cr=cairo_create(out);
|
||||||
|
if(orientation==3){cairo_translate(cr,width,height);cairo_rotate(cr,G_PI);}
|
||||||
|
else if(orientation==6){cairo_translate(cr,height,0);cairo_rotate(cr,G_PI_2);}
|
||||||
|
else if(orientation==8){cairo_translate(cr,0,width);cairo_rotate(cr,-G_PI_2);}
|
||||||
|
cairo_set_source_surface(cr,source,0,0);cairo_paint(cr);cairo_destroy(cr);
|
||||||
|
cairo_surface_destroy(source);return out;
|
||||||
|
}
|
||||||
gboolean evidence_preview_dimensions_are_safe(gint width, gint height)
|
gboolean evidence_preview_dimensions_are_safe(gint width, gint height)
|
||||||
{
|
{
|
||||||
return width > 0 && height > 0 &&
|
return width > 0 && height > 0 &&
|
||||||
|
|
@ -204,6 +246,8 @@ static gboolean load_jpeg(const char *path, EvidencePreviewResult *result,
|
||||||
if (cancelled(cancellable, error)) {
|
if (cancelled(cancellable, error)) {
|
||||||
cairo_surface_destroy(surface); return FALSE;
|
cairo_surface_destroy(surface); return FALSE;
|
||||||
}
|
}
|
||||||
|
surface=orient_surface(surface,jpeg_exif_orientation(path),width,height,
|
||||||
|
&width,&height);
|
||||||
gboolean ok = surface_to_result(surface, width, height, result, error);
|
gboolean ok = surface_to_result(surface, width, height, result, error);
|
||||||
cairo_surface_destroy(surface); return ok;
|
cairo_surface_destroy(surface); return ok;
|
||||||
invalid:
|
invalid:
|
||||||
|
|
|
||||||
44
src/core/identity_field_extractor.c
Normal file
44
src/core/identity_field_extractor.c
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#include "core/identity_field_extractor.h"
|
||||||
|
typedef struct { char *text; gint x,y,w,h; double confidence; } Word;
|
||||||
|
static void word_free(gpointer p){Word*w=p;if(w){g_free(w->text);g_free(w);}}
|
||||||
|
static GPtrArray *parse_tsv(const char *tsv)
|
||||||
|
{
|
||||||
|
GPtrArray*a=g_ptr_array_new_with_free_func(word_free);
|
||||||
|
char **lines=g_strsplit(tsv!=NULL?tsv:"","\n",-1);
|
||||||
|
for(guint i=1;lines[i]!=NULL;i++){char **c=g_strsplit(lines[i],"\t",12);
|
||||||
|
if(g_strv_length(c)>=12&&c[11][0]!='\0'){Word*w=g_new0(Word,1);
|
||||||
|
w->x=(gint)g_ascii_strtoll(c[6],NULL,10);w->y=(gint)g_ascii_strtoll(c[7],NULL,10);
|
||||||
|
w->w=(gint)g_ascii_strtoll(c[8],NULL,10);w->h=(gint)g_ascii_strtoll(c[9],NULL,10);
|
||||||
|
w->confidence=g_ascii_strtod(c[10],NULL);w->text=g_strdup(c[11]);
|
||||||
|
g_ptr_array_add(a,w);}g_strfreev(c);}g_strfreev(lines);return a;
|
||||||
|
}
|
||||||
|
typedef struct {const char*label;const char*code;} Rule;
|
||||||
|
static const Rule rules[]={
|
||||||
|
{"NOM :","surname"},{"NOM DE NAISSANCE :","birth_name"},
|
||||||
|
{"PRÉNOMS :","given_names"},{"PRENOMS :","given_names"},
|
||||||
|
{"DATE DE NAISSANCE :","birth_date"},{"LIEU DE NAISSANCE :","birth_place"},
|
||||||
|
{"NATIONALITÉ :","nationality"},{"N° DU DOCUMENT :","document_number"},
|
||||||
|
{"NUMÉRO DU DOCUMENT :","document_number"},{"DÉLIVRÉ LE :","issue_date"},
|
||||||
|
{"EXPIRE LE :","expiry_date"},{"AUTORITÉ :","issuing_authority"},
|
||||||
|
{"ADRESSE :","address_as_printed"}};
|
||||||
|
GPtrArray *identity_field_extractor_extract(const char *text,const char *tsv,
|
||||||
|
gint iw,gint ih,GError **error)
|
||||||
|
{
|
||||||
|
(void)error;GPtrArray*out=g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)identity_field_observation_free);
|
||||||
|
GPtrArray*words=parse_tsv(tsv);char**lines=g_strsplit(text!=NULL?text:"","\n",-1);
|
||||||
|
guint order=0;
|
||||||
|
for(guint i=0;lines[i]!=NULL;i++){char*line=g_strstrip(lines[i]);
|
||||||
|
for(guint r=0;r<G_N_ELEMENTS(rules);r++)if(g_str_has_prefix(line,rules[r].label)){
|
||||||
|
char*value=g_strdup(line+strlen(rules[r].label));g_strstrip(value);
|
||||||
|
if(value[0]!='\0'){IdentitySourceBox box={.page=1,.image_width=iw,.image_height=ih};
|
||||||
|
double confidence=-1.0;for(guint w=0;w<words->len;w++){Word*word=g_ptr_array_index(words,w);
|
||||||
|
if(g_strstr_len(value,-1,word->text)!=NULL){if(!box.available){box.x=word->x;box.y=word->y;
|
||||||
|
box.width=word->w;box.height=word->h;box.available=TRUE;}else{
|
||||||
|
gint right=MAX(box.x+box.width,word->x+word->w);gint bottom=MAX(box.y+box.height,word->y+word->h);
|
||||||
|
box.x=MIN(box.x,word->x);box.y=MIN(box.y,word->y);box.width=right-box.x;box.height=bottom-box.y;}
|
||||||
|
confidence=MAX(confidence,word->confidence);}}
|
||||||
|
IdentityFieldObservation*f=identity_field_observation_new(rules[r].code,
|
||||||
|
value,confidence,&box,order++);if(f!=NULL)g_ptr_array_add(out,f);}g_free(value);break;}}
|
||||||
|
g_strfreev(lines);g_ptr_array_unref(words);return out;
|
||||||
|
}
|
||||||
124
src/core/identity_ocr_preprocessor.c
Normal file
124
src/core/identity_ocr_preprocessor.c
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
#include "core/identity_ocr_preprocessor.h"
|
||||||
|
#include "core/file_hash.h"
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <poppler.h>
|
||||||
|
|
||||||
|
gboolean identity_ocr_preprocessor_dimensions_are_safe(gint width,gint height,
|
||||||
|
IdentityOcrPreprocessProfile profile)
|
||||||
|
{
|
||||||
|
if(width<=0||height<=0||width>12000||height>12000||
|
||||||
|
(guint64)width*(guint64)height>40000000U||
|
||||||
|
profile>IDENTITY_OCR_PREPROCESS_UPSCALE)return FALSE;
|
||||||
|
if(profile==IDENTITY_OCR_PREPROCESS_UPSCALE&&(width>6000||height>6000||
|
||||||
|
(guint64)width*(guint64)height>10000000U))return FALSE;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean apply_profile(const char *path,
|
||||||
|
IdentityOcrPreprocessProfile profile, GError **error)
|
||||||
|
{
|
||||||
|
if (profile == IDENTITY_OCR_PREPROCESS_NONE ||
|
||||||
|
profile == IDENTITY_OCR_PREPROCESS_ORIENTATION) return TRUE;
|
||||||
|
cairo_surface_t *source = cairo_image_surface_create_from_png(path);
|
||||||
|
if (cairo_surface_status(source) != CAIRO_STATUS_SUCCESS) {
|
||||||
|
cairo_surface_destroy(source);
|
||||||
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
|
||||||
|
"L’image de travail OCR est illisible.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
gint width = cairo_image_surface_get_width(source);
|
||||||
|
gint height = cairo_image_surface_get_height(source);
|
||||||
|
cairo_surface_t *output = source;
|
||||||
|
if (profile == IDENTITY_OCR_PREPROCESS_GRAYSCALE) {
|
||||||
|
cairo_surface_flush(source);
|
||||||
|
guint8 *pixels = cairo_image_surface_get_data(source);
|
||||||
|
gint stride = cairo_image_surface_get_stride(source);
|
||||||
|
for (gint y = 0; y < height; y++)
|
||||||
|
for (gint x = 0; x < width; x++) {
|
||||||
|
guint8 *pixel = pixels + y * stride + x * 4;
|
||||||
|
guint8 gray = (guint8) ((pixel[0] * 11U +
|
||||||
|
pixel[1] * 59U + pixel[2] * 30U) / 100U);
|
||||||
|
pixel[0] = gray; pixel[1] = gray; pixel[2] = gray;
|
||||||
|
}
|
||||||
|
cairo_surface_mark_dirty(source);
|
||||||
|
} else if (profile == IDENTITY_OCR_PREPROCESS_UPSCALE &&
|
||||||
|
identity_ocr_preprocessor_dimensions_are_safe(width,height,
|
||||||
|
profile)) {
|
||||||
|
output = cairo_image_surface_create(
|
||||||
|
CAIRO_FORMAT_ARGB32, width * 2, height * 2);
|
||||||
|
cairo_t *cr = cairo_create(output);
|
||||||
|
cairo_scale(cr, 2.0, 2.0);
|
||||||
|
cairo_set_source_surface(cr, source, 0, 0);
|
||||||
|
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR);
|
||||||
|
cairo_paint(cr); cairo_destroy(cr);
|
||||||
|
}
|
||||||
|
cairo_status_t status = cairo_surface_write_to_png(output, path);
|
||||||
|
if (output != source) cairo_surface_destroy(output);
|
||||||
|
cairo_surface_destroy(source);
|
||||||
|
if (status != CAIRO_STATUS_SUCCESS) {
|
||||||
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
||||||
|
"Le prétraitement OCR a échoué.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
IdentityOcrWorkImage *identity_ocr_preprocessor_prepare(
|
||||||
|
const EvidencePreviewRequest *request,guint pdf_page,
|
||||||
|
IdentityOcrPreprocessProfile profile,GCancellable*cancellable,GError**error)
|
||||||
|
{
|
||||||
|
if(request==NULL||pdf_page==0||profile>IDENTITY_OCR_PREPROCESS_UPSCALE){
|
||||||
|
g_set_error_literal(error,G_IO_ERROR,G_IO_ERROR_INVALID_ARGUMENT,
|
||||||
|
"Paramètres de prétraitement OCR invalides.");return NULL;}
|
||||||
|
EvidencePreviewResult*r=evidence_preview_load(request,cancellable,error);
|
||||||
|
if(r==NULL)return NULL;
|
||||||
|
if((r->kind!=EVIDENCE_PREVIEW_KIND_IMAGE&&r->kind!=EVIDENCE_PREVIEW_KIND_PDF)||
|
||||||
|
r->png_bytes==NULL){evidence_preview_result_free(r);
|
||||||
|
g_set_error_literal(error,G_IO_ERROR,G_IO_ERROR_NOT_SUPPORTED,
|
||||||
|
"Cette preuve ne peut pas être préparée pour l’OCR.");return NULL;}
|
||||||
|
if(!identity_ocr_preprocessor_dimensions_are_safe(r->width,r->height,profile)){
|
||||||
|
evidence_preview_result_free(r);g_set_error_literal(error,G_IO_ERROR,
|
||||||
|
G_IO_ERROR_NO_SPACE,"Dimensions OCR hors limites.");return NULL;}
|
||||||
|
gchar*path=NULL;gint fd=g_file_open_tmp("labfy-identity-ocr-XXXXXX.png",&path,error);
|
||||||
|
if(fd<0){evidence_preview_result_free(r);return NULL;}close(fd);
|
||||||
|
if(r->kind==EVIDENCE_PREVIEW_KIND_PDF){
|
||||||
|
char*uri=g_filename_to_uri(r->controlled_path,NULL,error);
|
||||||
|
PopplerDocument*document=uri!=NULL?
|
||||||
|
poppler_document_new_from_file(uri,NULL,error):NULL;g_free(uri);
|
||||||
|
gint pages=document!=NULL?poppler_document_get_n_pages(document):0;
|
||||||
|
if(document==NULL||pdf_page>(guint)pages){g_clear_object(&document);
|
||||||
|
g_unlink(path);g_free(path);evidence_preview_result_free(r);
|
||||||
|
if(error!=NULL&&*error==NULL)g_set_error_literal(error,G_IO_ERROR,
|
||||||
|
G_IO_ERROR_INVALID_ARGUMENT,"La page PDF choisie n’existe pas.");return NULL;}
|
||||||
|
PopplerPage*page=poppler_document_get_page(document,(gint)pdf_page-1);
|
||||||
|
double pw=0,ph=0;poppler_page_get_size(page,&pw,&ph);
|
||||||
|
if(pw<=0||ph<=0||pw>12000||ph>12000||pw*ph>40000000){
|
||||||
|
g_object_unref(page);g_object_unref(document);g_unlink(path);g_free(path);
|
||||||
|
evidence_preview_result_free(r);g_set_error_literal(error,G_IO_ERROR,
|
||||||
|
G_IO_ERROR_NO_SPACE,"Dimensions PDF hors limites.");return NULL;}
|
||||||
|
double scale=MIN(2.0,MIN(2000.0/pw,2000.0/ph));
|
||||||
|
gint width=MAX(1,(gint)(pw*scale)),height=MAX(1,(gint)(ph*scale));
|
||||||
|
cairo_surface_t*surface=cairo_image_surface_create(CAIRO_FORMAT_ARGB32,width,height);
|
||||||
|
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_destroy(cr);
|
||||||
|
cairo_status_t status=cairo_surface_write_to_png(surface,path);
|
||||||
|
cairo_surface_destroy(surface);g_object_unref(page);g_object_unref(document);
|
||||||
|
if(status!=CAIRO_STATUS_SUCCESS){g_unlink(path);g_free(path);
|
||||||
|
evidence_preview_result_free(r);g_set_error_literal(error,G_IO_ERROR,
|
||||||
|
G_IO_ERROR_FAILED,"Rendu de la page PDF impossible.");return NULL;}
|
||||||
|
r->width=width;r->height=height;
|
||||||
|
}else{
|
||||||
|
gsize length=0;const guint8*data=g_bytes_get_data(r->png_bytes,&length);
|
||||||
|
if(!g_file_set_contents(path,(const char*)data,(gssize)length,error)){
|
||||||
|
g_unlink(path);g_free(path);evidence_preview_result_free(r);return NULL;}
|
||||||
|
}
|
||||||
|
if(!apply_profile(path,profile,error)){g_unlink(path);g_free(path);
|
||||||
|
evidence_preview_result_free(r);return NULL;}
|
||||||
|
IdentityOcrWorkImage*out=g_new0(IdentityOcrWorkImage,1);out->path=path;
|
||||||
|
out->width=r->width;out->height=r->height;out->page=pdf_page;
|
||||||
|
guint64 size=0;if(!file_hash_compute_sha256(path,cancellable,&out->sha256,
|
||||||
|
&size,error)){identity_ocr_work_image_free(out);out=NULL;}
|
||||||
|
evidence_preview_result_free(r);return out;
|
||||||
|
}
|
||||||
|
void identity_ocr_work_image_free(IdentityOcrWorkImage*i)
|
||||||
|
{if(!i)return;if(i->path)g_unlink(i->path);g_free(i->path);g_free(i->sha256);g_free(i);}
|
||||||
98
src/core/identity_ocr_workflow.c
Normal file
98
src/core/identity_ocr_workflow.c
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
#include "core/identity_ocr_workflow.h"
|
||||||
|
|
||||||
|
#include "core/evidence_preview.h"
|
||||||
|
#include "core/identity_field_extractor.h"
|
||||||
|
#include "core/ocr_analysis.h"
|
||||||
|
|
||||||
|
gboolean identity_ocr_workflow_request_is_valid(
|
||||||
|
const IdentityOcrWorkflowRequest *request)
|
||||||
|
{
|
||||||
|
return 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(
|
||||||
|
const IdentityOcrWorkflowRequest *request,
|
||||||
|
GCancellable *cancellable,
|
||||||
|
GError **error)
|
||||||
|
{
|
||||||
|
EvidencePreviewRequest *preview_request = NULL;
|
||||||
|
IdentityOcrWorkImage *image = NULL;
|
||||||
|
OcrAnalysisResult *ocr = NULL;
|
||||||
|
IdentityOcrRun *run = NULL;
|
||||||
|
GPtrArray *fields = NULL;
|
||||||
|
char *available = NULL;
|
||||||
|
char *preview_data = NULL;
|
||||||
|
gsize preview_length = 0;
|
||||||
|
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
||||||
|
if (!identity_ocr_workflow_request_is_valid(request)) {
|
||||||
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||||||
|
"La demande du parcours OCR d’identité est invalide.");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
preview_request = evidence_preview_request_new(request->root_path,
|
||||||
|
request->evidence_identifier, request->relative_path,
|
||||||
|
request->expected_sha256, request->mime_type, request->generation);
|
||||||
|
if (preview_request != NULL)
|
||||||
|
image = identity_ocr_preprocessor_prepare(preview_request,
|
||||||
|
request->page_number, request->profile, cancellable, error);
|
||||||
|
evidence_preview_request_free(preview_request);
|
||||||
|
if (image == NULL) goto cleanup;
|
||||||
|
available = ocr_analysis_list_languages(
|
||||||
|
request->tesseract_executable, cancellable, error);
|
||||||
|
if (available == NULL) goto cleanup;
|
||||||
|
ocr = ocr_analysis_run(request->tesseract_executable, image->path,
|
||||||
|
request->languages, cancellable, error);
|
||||||
|
if (ocr == NULL) goto cleanup;
|
||||||
|
run = identity_ocr_run_new(request->evidence_identifier,
|
||||||
|
request->expected_sha256, request->document_type,
|
||||||
|
request->document_side, request->page_number, request->languages,
|
||||||
|
request->preprocessing_profile);
|
||||||
|
if (run == NULL) {
|
||||||
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
|
||||||
|
"Le résultat OCR ne peut pas être représenté.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
identity_ocr_run_set_outputs(run,
|
||||||
|
ocr->execution != NULL ? ocr->execution->version :
|
||||||
|
request->tesseract_version,
|
||||||
|
available, "tesseract stdout -l LANG ; tesseract stdout -l LANG tsv",
|
||||||
|
ocr->text, ocr->tsv);
|
||||||
|
if (g_file_get_contents(image->path, &preview_data, &preview_length,
|
||||||
|
NULL)) {
|
||||||
|
GBytes *preview = g_bytes_new_take(preview_data, preview_length);
|
||||||
|
preview_data = NULL;
|
||||||
|
identity_ocr_run_set_preview(run, preview);
|
||||||
|
g_bytes_unref(preview);
|
||||||
|
}
|
||||||
|
fields = identity_field_extractor_extract(ocr->text, ocr->tsv,
|
||||||
|
image->width, image->height, error);
|
||||||
|
if (fields == NULL && error != NULL && *error != NULL) {
|
||||||
|
identity_ocr_run_free(run);
|
||||||
|
run = NULL;
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
while (fields != NULL && fields->len > 0)
|
||||||
|
identity_ocr_run_add_field(run,
|
||||||
|
g_ptr_array_steal_index(fields, 0));
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
g_free(preview_data);
|
||||||
|
g_clear_pointer(&fields, g_ptr_array_unref);
|
||||||
|
identity_ocr_work_image_free(image);
|
||||||
|
ocr_analysis_result_free(ocr);
|
||||||
|
g_free(available);
|
||||||
|
return run;
|
||||||
|
}
|
||||||
31
src/core/mrz_parser.c
Normal file
31
src/core/mrz_parser.c
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#include "core/mrz_parser.h"
|
||||||
|
static gint value(gchar c)
|
||||||
|
{
|
||||||
|
if(c>='0'&&c<='9')return c-'0';
|
||||||
|
if(c>='A'&&c<='Z')return c-'A'+10;
|
||||||
|
if(c=='<')return 0;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
gint mrz_parser_check_digit(const char*s){static const gint weights[]={7,3,1};
|
||||||
|
gint sum=0;if(s==NULL)return -1;for(gsize i=0;s[i];i++){gint v=value(s[i]);
|
||||||
|
if(v<0)return -1;
|
||||||
|
sum+=v*weights[i%3];}return sum%10;}
|
||||||
|
MrzParseResult *mrz_parser_parse(const char*text)
|
||||||
|
{
|
||||||
|
MrzParseResult*r=g_new0(MrzParseResult,1);r->warnings=g_ptr_array_new_with_free_func(g_free);
|
||||||
|
char**all=g_strsplit(text!=NULL?text:"","\n",-1);GPtrArray*l=g_ptr_array_new();
|
||||||
|
for(guint i=0;all[i]!=NULL;i++){g_strstrip(all[i]);if(strlen(all[i])==44)g_ptr_array_add(l,all[i]);}
|
||||||
|
if(l->len>=2){const char*a=g_ptr_array_index(l,0),*b=g_ptr_array_index(l,1);
|
||||||
|
r->structure_valid=TRUE;char*num=g_strndup(b,9);r->document_number=g_strdup(num);
|
||||||
|
gint expected=value(b[9]);r->check_digits_valid=expected>=0&&mrz_parser_check_digit(num)==expected;
|
||||||
|
r->nationality=g_strndup(b+10,3);const char*names=a+5;char**parts=g_strsplit(names,"<<",2);
|
||||||
|
r->surname=g_strdup(parts[0]);r->given_names=g_strdup(parts[1]!=NULL?parts[1]:"");
|
||||||
|
g_strdelimit(r->surname,"<",' ');g_strdelimit(r->given_names,"<",' ');
|
||||||
|
g_strfreev(parts);g_free(num);if(!r->check_digits_valid)g_ptr_array_add(r->warnings,
|
||||||
|
g_strdup("Chiffre de contrôle MRZ incohérent ; aucun verdict d’authenticité."));}
|
||||||
|
else g_ptr_array_add(r->warnings,g_strdup("Structure MRZ non reconnue."));
|
||||||
|
g_ptr_array_unref(l);g_strfreev(all);return r;
|
||||||
|
}
|
||||||
|
void mrz_parse_result_free(MrzParseResult*r){if(!r)return;g_free(r->document_number);
|
||||||
|
g_free(r->nationality);g_free(r->surname);g_free(r->given_names);
|
||||||
|
g_ptr_array_unref(r->warnings);g_free(r);}
|
||||||
|
|
@ -5,6 +5,12 @@
|
||||||
#include "core/ocr_analysis.h"
|
#include "core/ocr_analysis.h"
|
||||||
#include "core/document_tool_runner.h"
|
#include "core/document_tool_runner.h"
|
||||||
|
|
||||||
|
static gint compare_language_codes(gconstpointer left, gconstpointer right)
|
||||||
|
{
|
||||||
|
return g_strcmp0(*(const char *const *) left,
|
||||||
|
*(const char *const *) right);
|
||||||
|
}
|
||||||
|
|
||||||
gboolean ocr_analysis_mime_is_compatible(const char *mime_type)
|
gboolean ocr_analysis_mime_is_compatible(const char *mime_type)
|
||||||
{
|
{
|
||||||
return g_strcmp0(mime_type, "image/png") == 0 ||
|
return g_strcmp0(mime_type, "image/png") == 0 ||
|
||||||
|
|
@ -18,7 +24,9 @@ void ocr_analysis_result_free(OcrAnalysisResult *result)
|
||||||
return;
|
return;
|
||||||
document_tool_execution_free(result->execution);
|
document_tool_execution_free(result->execution);
|
||||||
g_free(result->requested_languages);
|
g_free(result->requested_languages);
|
||||||
|
g_free(result->available_languages);
|
||||||
g_free(result->text);
|
g_free(result->text);
|
||||||
|
g_free(result->tsv);
|
||||||
g_free(result);
|
g_free(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,9 +57,7 @@ OcrAnalysisResult *ocr_analysis_run_with_limits(
|
||||||
{
|
{
|
||||||
if (executable == NULL || image_path == NULL || languages == NULL ||
|
if (executable == NULL || image_path == NULL || languages == NULL ||
|
||||||
languages[0] == '\0' ||
|
languages[0] == '\0' ||
|
||||||
(!g_str_equal(languages, "fra") &&
|
strpbrk(languages, " \t\r\n/\\") != NULL)
|
||||||
!g_str_equal(languages, "eng") &&
|
|
||||||
!g_str_equal(languages, "fra+eng")))
|
|
||||||
{
|
{
|
||||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||||||
"Les paramètres OCR sont invalides.");
|
"Les paramètres OCR sont invalides.");
|
||||||
|
|
@ -60,6 +66,9 @@ OcrAnalysisResult *ocr_analysis_run_with_limits(
|
||||||
const char *arguments[] = {
|
const char *arguments[] = {
|
||||||
image_path, "stdout", "-l", languages, NULL
|
image_path, "stdout", "-l", languages, NULL
|
||||||
};
|
};
|
||||||
|
const char *tsv_arguments[] = {
|
||||||
|
image_path, "stdout", "-l", languages, "tsv", NULL
|
||||||
|
};
|
||||||
const char *version_arguments[] = { "--version", NULL };
|
const char *version_arguments[] = { "--version", NULL };
|
||||||
DocumentToolExecution *execution = NULL;
|
DocumentToolExecution *execution = NULL;
|
||||||
if (!document_tool_runner_run_with_limits("tesseract", executable,
|
if (!document_tool_runner_run_with_limits("tesseract", executable,
|
||||||
|
|
@ -94,5 +103,67 @@ OcrAnalysisResult *ocr_analysis_run_with_limits(
|
||||||
g_ptr_array_add(execution->warnings,
|
g_ptr_array_add(execution->warnings,
|
||||||
g_strdup("Tesseract n'a produit aucun texte."));
|
g_strdup("Tesseract n'a produit aucun texte."));
|
||||||
}
|
}
|
||||||
|
DocumentToolExecution *tsv_execution = NULL;
|
||||||
|
if (!document_tool_runner_run_with_limits("tesseract", executable,
|
||||||
|
tsv_arguments, image_path, limits, cancellable,
|
||||||
|
&tsv_execution, error)) {
|
||||||
|
ocr_analysis_result_free(result);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
result->tsv = g_strdup(tsv_execution->raw_stdout);
|
||||||
|
document_tool_execution_free(tsv_execution);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GPtrArray *ocr_analysis_parse_languages(const char *output)
|
||||||
|
{
|
||||||
|
GPtrArray *items = g_ptr_array_new_with_free_func(g_free);
|
||||||
|
if (output == NULL) return items;
|
||||||
|
char **lines = g_strsplit(output, "\n", -1);
|
||||||
|
for (guint i = 0; lines[i] != NULL; i++) {
|
||||||
|
char *code = g_strstrip(lines[i]);
|
||||||
|
if (*code == '\0' || g_str_has_prefix(code,
|
||||||
|
"List of available languages")) continue;
|
||||||
|
gboolean valid = TRUE;
|
||||||
|
for (const char *p = code; *p != '\0'; p++)
|
||||||
|
if (!g_ascii_isalnum(*p) && *p != '_' && *p != '-') valid = FALSE;
|
||||||
|
gboolean duplicate = FALSE;
|
||||||
|
for (guint j = 0; valid && j < items->len; j++)
|
||||||
|
if (g_str_equal(code, g_ptr_array_index(items, j)))
|
||||||
|
duplicate = TRUE;
|
||||||
|
if (valid && !duplicate) g_ptr_array_add(items, g_strdup(code));
|
||||||
|
}
|
||||||
|
g_strfreev(lines);
|
||||||
|
g_ptr_array_sort(items, compare_language_codes);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
GPtrArray *ocr_analysis_build_language_choices(const GPtrArray *languages)
|
||||||
|
{
|
||||||
|
GPtrArray *choices = g_ptr_array_new_with_free_func(g_free);
|
||||||
|
gboolean fra = FALSE, eng = FALSE;
|
||||||
|
for (guint i = 0; languages != NULL && i < languages->len; i++) {
|
||||||
|
const char *code = g_ptr_array_index((GPtrArray *) languages, i);
|
||||||
|
g_ptr_array_add(choices, g_strdup(code));
|
||||||
|
fra |= g_str_equal(code, "fra");
|
||||||
|
eng |= g_str_equal(code, "eng");
|
||||||
|
}
|
||||||
|
if (fra && eng) g_ptr_array_add(choices, g_strdup("fra+eng"));
|
||||||
|
return choices;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *ocr_analysis_list_languages(const char *executable,
|
||||||
|
GCancellable *cancellable, GError **error)
|
||||||
|
{
|
||||||
|
const char *arguments[] = {"--list-langs", NULL};
|
||||||
|
const DocumentToolRunnerLimits limits = {65536U, 16384U};
|
||||||
|
DocumentToolExecution *execution = NULL;
|
||||||
|
char *languages = NULL;
|
||||||
|
if (executable == NULL ||
|
||||||
|
!document_tool_runner_run_with_limits("tesseract", executable,
|
||||||
|
arguments, executable, &limits, cancellable, &execution, error))
|
||||||
|
return NULL;
|
||||||
|
languages = g_strdup(execution->raw_stdout);
|
||||||
|
document_tool_execution_free(execution);
|
||||||
|
return languages;
|
||||||
|
}
|
||||||
|
|
|
||||||
26
src/core/ocr_region_geometry.c
Normal file
26
src/core/ocr_region_geometry.c
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
#include "core/ocr_region_geometry.h"
|
||||||
|
|
||||||
|
gboolean ocr_region_geometry_transform(gint sw, gint sh, gint x, gint y,
|
||||||
|
gint width, gint height, double dw, double dh, OcrRegionFit fit,
|
||||||
|
OcrDisplayRegion *out)
|
||||||
|
{
|
||||||
|
if (out == NULL || sw <= 0 || sh <= 0 || width <= 0 || height <= 0 ||
|
||||||
|
dw <= 0 || dh <= 0 || fit > OCR_REGION_FIT_FILL)
|
||||||
|
return FALSE;
|
||||||
|
gint x1 = CLAMP(x, 0, sw);
|
||||||
|
gint y1 = CLAMP(y, 0, sh);
|
||||||
|
gint x2 = (gint) CLAMP((gint64) x + width, 0, (gint64) sw);
|
||||||
|
gint y2 = (gint) CLAMP((gint64) y + height, 0, (gint64) sh);
|
||||||
|
if (x2 <= x1 || y2 <= y1) return FALSE;
|
||||||
|
double sx = dw / sw, sy = dh / sh, ox = 0, oy = 0;
|
||||||
|
if (fit == OCR_REGION_FIT_CONTAIN) {
|
||||||
|
sx = sy = MIN(sx, sy);
|
||||||
|
ox = (dw - sw * sx) / 2.0;
|
||||||
|
oy = (dh - sh * sy) / 2.0;
|
||||||
|
}
|
||||||
|
out->x = ox + x1 * sx;
|
||||||
|
out->y = oy + y1 * sy;
|
||||||
|
out->width = (x2 - x1) * sx;
|
||||||
|
out->height = (y2 - y1) * sy;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "dao/evidence_dao.h"
|
#include "dao/evidence_dao.h"
|
||||||
#include "dao/evidence_entity_dao.h"
|
#include "dao/evidence_entity_dao.h"
|
||||||
#include "dao/person_role_assignment_dao.h"
|
#include "dao/person_role_assignment_dao.h"
|
||||||
|
#include "dao/identity_ocr_dao.h"
|
||||||
#include "database/transaction.h"
|
#include "database/transaction.h"
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <glib/gstdio.h>
|
#include <glib/gstdio.h>
|
||||||
|
|
@ -13,6 +14,29 @@ static GQuark coordinator_error(void)
|
||||||
{
|
{
|
||||||
return g_quark_from_static_string("person-creation-coordinator-error");
|
return g_quark_from_static_string("person-creation-coordinator-error");
|
||||||
}
|
}
|
||||||
|
typedef struct {
|
||||||
|
const PersonCreationCoordinatorOptions *options;
|
||||||
|
guint occurrences[PERSON_CREATION_FAILURE_COMPENSATION + 1];
|
||||||
|
} FailureControl;
|
||||||
|
static gboolean fail_at(FailureControl *control,
|
||||||
|
PersonCreationFailurePoint point, GError **error)
|
||||||
|
{
|
||||||
|
guint occurrence;
|
||||||
|
if (control == NULL || control->options == NULL ||
|
||||||
|
control->options->failure_point != point) return FALSE;
|
||||||
|
occurrence = control->occurrences[point]++;
|
||||||
|
if (occurrence != control->options->failure_occurrence) return FALSE;
|
||||||
|
if (error != NULL && *error == NULL)
|
||||||
|
g_set_error(error, coordinator_error(), 100 + point,
|
||||||
|
"Échec injecté au point %u, occurrence %u.",
|
||||||
|
(guint) point, occurrence);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
static gboolean session_valid(const PersonCreationCoordinatorOptions *options)
|
||||||
|
{
|
||||||
|
return options == NULL || options->session_check == NULL ||
|
||||||
|
options->session_check(options->session_check_data);
|
||||||
|
}
|
||||||
static gboolean cancelled(GCancellable *cancellable, GError **error)
|
static gboolean cancelled(GCancellable *cancellable, GError **error)
|
||||||
{
|
{
|
||||||
return cancellable != NULL &&
|
return cancellable != NULL &&
|
||||||
|
|
@ -140,9 +164,110 @@ static gboolean validate_selection_files(const char *root,
|
||||||
}
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
PersonCreationCoordinatorResult *person_creation_coordinator_execute(
|
|
||||||
|
static gboolean persist_ocr_runs(IdentityOcrDao *dao, const char *root,
|
||||||
|
const char *person_identifier, const PersonEvidenceSelection *selection,
|
||||||
|
const GPtrArray *evidence_identifiers, const GPtrArray *runs,
|
||||||
|
const char *timestamp, GPtrArray *created_paths,
|
||||||
|
GPtrArray *created_directories,
|
||||||
|
FailureControl *failure,
|
||||||
|
GCancellable *cancellable, GError **error)
|
||||||
|
{
|
||||||
|
for (guint i = 0; runs != NULL && i < runs->len; i++) {
|
||||||
|
IdentityOcrRun *run = g_ptr_array_index((GPtrArray *) runs, i);
|
||||||
|
const char *evidence_identifier = NULL;
|
||||||
|
for (guint j = 0; j < person_evidence_selection_get_count(selection);
|
||||||
|
j++) {
|
||||||
|
const PersonEvidenceSelectionItem *item =
|
||||||
|
person_evidence_selection_get(selection, j);
|
||||||
|
if (g_strcmp0(person_evidence_selection_item_get_identifier(item),
|
||||||
|
identity_ocr_run_get_evidence_id(run)) == 0) {
|
||||||
|
evidence_identifier = g_ptr_array_index(
|
||||||
|
(GPtrArray *) evidence_identifiers, j);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (evidence_identifier == NULL) {
|
||||||
|
g_set_error_literal(error, coordinator_error(), 4,
|
||||||
|
"La preuve source de l’OCR n’est plus retenue.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
char *parent = g_build_filename(root, "02_Preuves_Traitees",
|
||||||
|
"OCR", NULL);
|
||||||
|
char *directory = g_build_filename(parent,
|
||||||
|
identity_ocr_run_get_identifier(run), NULL);
|
||||||
|
gboolean parent_existed = g_file_test(parent, G_FILE_TEST_IS_DIR);
|
||||||
|
gboolean directory_existed =
|
||||||
|
g_file_test(directory, G_FILE_TEST_IS_DIR);
|
||||||
|
char *text_absolute = g_build_filename(directory, "ocr.txt", NULL);
|
||||||
|
char *tsv_absolute = g_build_filename(directory, "ocr.tsv", NULL);
|
||||||
|
char *text_relative = g_build_filename("02_Preuves_Traitees", "OCR",
|
||||||
|
identity_ocr_run_get_identifier(run), "ocr.txt", NULL);
|
||||||
|
char *tsv_relative = g_build_filename("02_Preuves_Traitees", "OCR",
|
||||||
|
identity_ocr_run_get_identifier(run), "ocr.tsv", NULL);
|
||||||
|
char *text_sha = NULL, *tsv_sha = NULL;
|
||||||
|
guint64 artifact_size = 0;
|
||||||
|
gboolean ok =
|
||||||
|
!fail_at(failure,
|
||||||
|
PERSON_CREATION_FAILURE_CREATE_OCR_DIRECTORY, error) &&
|
||||||
|
g_mkdir_with_parents(directory, 0700) == 0 &&
|
||||||
|
!fail_at(failure, PERSON_CREATION_FAILURE_COPY_OCR_TEXT, error) &&
|
||||||
|
g_file_set_contents(text_absolute,
|
||||||
|
identity_ocr_run_get_raw_text(run), -1, error) &&
|
||||||
|
!fail_at(failure, PERSON_CREATION_FAILURE_COPY_OCR_TSV, error) &&
|
||||||
|
g_file_set_contents(tsv_absolute,
|
||||||
|
identity_ocr_run_get_tsv(run), -1, error) &&
|
||||||
|
!fail_at(failure, PERSON_CREATION_FAILURE_HASH_OCR_TEXT, error) &&
|
||||||
|
file_hash_compute_sha256(text_absolute, cancellable,
|
||||||
|
&text_sha, &artifact_size, error) &&
|
||||||
|
!fail_at(failure, PERSON_CREATION_FAILURE_HASH_OCR_TSV, error) &&
|
||||||
|
file_hash_compute_sha256(tsv_absolute, cancellable,
|
||||||
|
&tsv_sha, &artifact_size, error);
|
||||||
|
const GPtrArray *fields = identity_ocr_run_get_fields(run);
|
||||||
|
ok = ok && !fail_at(failure,
|
||||||
|
PERSON_CREATION_FAILURE_INSERT_OCR_RUN, error) &&
|
||||||
|
!fail_at(failure,
|
||||||
|
PERSON_CREATION_FAILURE_INSERT_DOCUMENT_OBSERVATION, error);
|
||||||
|
for (guint field_index = 0; ok && fields != NULL &&
|
||||||
|
field_index < fields->len; field_index++) {
|
||||||
|
IdentityFieldObservation *field =
|
||||||
|
g_ptr_array_index((GPtrArray *) fields, field_index);
|
||||||
|
IdentityReviewStatus status =
|
||||||
|
identity_field_observation_get_status(field);
|
||||||
|
if (status != IDENTITY_REVIEW_ACCEPTED &&
|
||||||
|
status != IDENTITY_REVIEW_MODIFIED) continue;
|
||||||
|
ok = !fail_at(failure,
|
||||||
|
PERSON_CREATION_FAILURE_INSERT_FIELD, error) &&
|
||||||
|
!fail_at(failure,
|
||||||
|
PERSON_CREATION_FAILURE_CREATE_SOURCE, error);
|
||||||
|
}
|
||||||
|
ok = ok &&
|
||||||
|
identity_ocr_dao_insert(dao, person_identifier,
|
||||||
|
evidence_identifier, run, text_relative, text_sha,
|
||||||
|
tsv_relative, tsv_sha, timestamp, error);
|
||||||
|
if (g_file_test(text_absolute, G_FILE_TEST_IS_REGULAR))
|
||||||
|
g_ptr_array_add(created_paths, g_strdup(text_absolute));
|
||||||
|
if (g_file_test(tsv_absolute, G_FILE_TEST_IS_REGULAR))
|
||||||
|
g_ptr_array_add(created_paths, g_strdup(tsv_absolute));
|
||||||
|
if (!parent_existed && g_file_test(parent, G_FILE_TEST_IS_DIR))
|
||||||
|
g_ptr_array_add(created_directories, g_strdup(parent));
|
||||||
|
if (!directory_existed && g_file_test(directory, G_FILE_TEST_IS_DIR))
|
||||||
|
g_ptr_array_add(created_directories, g_strdup(directory));
|
||||||
|
g_free(parent); g_free(directory); g_free(text_absolute); g_free(tsv_absolute);
|
||||||
|
g_free(text_relative); g_free(tsv_relative);
|
||||||
|
g_free(text_sha); g_free(tsv_sha);
|
||||||
|
if (!ok) return FALSE;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PersonCreationCoordinatorResult *person_creation_coordinator_execute_internal(
|
||||||
Database *database, const char *root, const PersonEntityInput *person,
|
Database *database, const char *root, const PersonEntityInput *person,
|
||||||
const PersonEvidenceSelection *selection, GCancellable *cancellable,
|
const char *existing_person_identifier,
|
||||||
|
const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs,
|
||||||
|
const PersonCreationCoordinatorEvidenceMetadata *metadata,
|
||||||
|
const PersonCreationCoordinatorOptions *options,
|
||||||
|
GCancellable *cancellable,
|
||||||
GError **error)
|
GError **error)
|
||||||
{
|
{
|
||||||
PersonCreationCoordinatorResult *result = NULL;
|
PersonCreationCoordinatorResult *result = NULL;
|
||||||
|
|
@ -150,38 +275,71 @@ PersonCreationCoordinatorResult *person_creation_coordinator_execute(
|
||||||
EvidenceDao *evidence_dao = NULL;
|
EvidenceDao *evidence_dao = NULL;
|
||||||
EvidenceEntityDao *link_dao = NULL;
|
EvidenceEntityDao *link_dao = NULL;
|
||||||
PersonRoleAssignmentDao *role_dao = NULL;
|
PersonRoleAssignmentDao *role_dao = NULL;
|
||||||
|
IdentityOcrDao *ocr_dao = NULL;
|
||||||
EntityRecord *person_record = NULL;
|
EntityRecord *person_record = NULL;
|
||||||
GPtrArray *created_paths = g_ptr_array_new_with_free_func(g_free);
|
GPtrArray *created_paths = g_ptr_array_new_with_free_func(g_free);
|
||||||
|
GPtrArray *created_directories = g_ptr_array_new_with_free_func(g_free);
|
||||||
char *timestamp = NULL;
|
char *timestamp = NULL;
|
||||||
gboolean transaction_active = FALSE, success = FALSE;
|
gboolean transaction_active = FALSE, success = FALSE;
|
||||||
if (database == NULL || root == NULL || person == NULL ||
|
gboolean create_person = existing_person_identifier == NULL;
|
||||||
person->designation == NULL || person->designation[0] == '\0' ||
|
FailureControl failure = {.options = options};
|
||||||
|
if (fail_at(&failure, PERSON_CREATION_FAILURE_VALIDATE, error))
|
||||||
|
goto cleanup;
|
||||||
|
if (database == NULL || root == NULL ||
|
||||||
|
(create_person && (person == NULL ||
|
||||||
|
person->designation == NULL || person->designation[0] == '\0')) ||
|
||||||
|
(!create_person && (existing_person_identifier[0] == '\0' ||
|
||||||
|
!g_uuid_string_is_valid(existing_person_identifier))) ||
|
||||||
!person_evidence_selection_is_confirmable(selection)) {
|
!person_evidence_selection_is_confirmable(selection)) {
|
||||||
g_set_error_literal(error, coordinator_error(), 1,
|
g_set_error_literal(error, coordinator_error(), 1,
|
||||||
"La création de personne ou la sélection est invalide.");
|
"La création de personne ou la sélection est invalide.");
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
if (!session_valid(options) ||
|
||||||
|
fail_at(&failure, PERSON_CREATION_FAILURE_SESSION_BEFORE_START,
|
||||||
|
error)) {
|
||||||
|
if (error != NULL && *error == NULL)
|
||||||
|
g_set_error_literal(error, coordinator_error(), 6,
|
||||||
|
"La session d’enquête a changé avant la création.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
if (cancelled(cancellable, error)) goto cleanup;
|
if (cancelled(cancellable, error)) goto cleanup;
|
||||||
|
if (fail_at(&failure, PERSON_CREATION_FAILURE_SOURCE_HASH, error))
|
||||||
|
goto cleanup;
|
||||||
if (!validate_selection_files(root, selection, cancellable, error))
|
if (!validate_selection_files(root, selection, cancellable, error))
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
timestamp = timestamp_now();
|
timestamp = timestamp_now();
|
||||||
result = g_new0(PersonCreationCoordinatorResult, 1);
|
result = g_new0(PersonCreationCoordinatorResult, 1);
|
||||||
result->person_identifier = g_uuid_string_random();
|
result->person_identifier = create_person
|
||||||
|
? g_uuid_string_random() : g_strdup(existing_person_identifier);
|
||||||
result->evidence_identifiers =
|
result->evidence_identifiers =
|
||||||
g_ptr_array_new_with_free_func(g_free);
|
g_ptr_array_new_with_free_func(g_free);
|
||||||
entity_dao = entity_dao_new(database, error);
|
entity_dao = entity_dao_new(database, error);
|
||||||
evidence_dao = evidence_dao_new(database, error);
|
evidence_dao = evidence_dao_new(database, error);
|
||||||
link_dao = evidence_entity_dao_new(database, error);
|
link_dao = evidence_entity_dao_new(database, error);
|
||||||
role_dao = person_role_assignment_dao_new(database, error);
|
role_dao = person_role_assignment_dao_new(database, error);
|
||||||
person_record = build_person(person, result->person_identifier,
|
ocr_dao = identity_ocr_dao_new(database);
|
||||||
timestamp, error);
|
person_record = entity_dao == NULL ? NULL : create_person
|
||||||
|
? build_person(person, result->person_identifier, timestamp, error)
|
||||||
|
: entity_dao_find_by_identifier(
|
||||||
|
entity_dao, result->person_identifier, error);
|
||||||
if (timestamp == NULL || entity_dao == NULL || evidence_dao == NULL ||
|
if (timestamp == NULL || entity_dao == NULL || evidence_dao == NULL ||
|
||||||
link_dao == NULL || role_dao == NULL || person_record == NULL ||
|
link_dao == NULL || role_dao == NULL || ocr_dao == NULL ||
|
||||||
|
person_record == NULL ||
|
||||||
|
g_strcmp0(entity_record_get_type_identifier(person_record),
|
||||||
|
"person") != 0 ||
|
||||||
!database_transaction_begin(database)) goto cleanup;
|
!database_transaction_begin(database)) goto cleanup;
|
||||||
transaction_active = TRUE;
|
transaction_active = TRUE;
|
||||||
if (!entity_dao_insert(entity_dao, person_record, error) ||
|
if (create_person &&
|
||||||
!person_role_assignment_dao_insert_all(role_dao,
|
(fail_at(&failure, PERSON_CREATION_FAILURE_CREATE_PERSON, error) ||
|
||||||
result->person_identifier, person->role_assignments, error))
|
!entity_dao_insert(entity_dao, person_record, error)))
|
||||||
|
goto cleanup;
|
||||||
|
for (guint i = 0; create_person && person->role_assignments != NULL &&
|
||||||
|
i < person->role_assignments->len; i++)
|
||||||
|
if (fail_at(&failure, PERSON_CREATION_FAILURE_CREATE_ROLE, error) ||
|
||||||
|
!person_role_assignment_dao_insert(role_dao,
|
||||||
|
result->person_identifier,
|
||||||
|
g_ptr_array_index(person->role_assignments, i), error))
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
for (guint i = 0; i < person_evidence_selection_get_count(selection);
|
for (guint i = 0; i < person_evidence_selection_get_count(selection);
|
||||||
i++) {
|
i++) {
|
||||||
|
|
@ -193,6 +351,8 @@ PersonCreationCoordinatorResult *person_creation_coordinator_execute(
|
||||||
char *absolute = NULL, *duplicate_identifier = NULL;
|
char *absolute = NULL, *duplicate_identifier = NULL;
|
||||||
EvidenceRecord *record = NULL;
|
EvidenceRecord *record = NULL;
|
||||||
if (cancelled(cancellable, error)) goto cleanup;
|
if (cancelled(cancellable, error)) goto cleanup;
|
||||||
|
if (fail_at(&failure, PERSON_CREATION_FAILURE_IMPORT_EVIDENCE,
|
||||||
|
error)) goto cleanup;
|
||||||
if (person_evidence_selection_item_get_origin(item) ==
|
if (person_evidence_selection_item_get_origin(item) ==
|
||||||
PERSON_EVIDENCE_ORIGIN_STAGED) {
|
PERSON_EVIDENCE_ORIGIN_STAGED) {
|
||||||
duplicate_identifier = evidence_dao_find_identifier_by_sha256(
|
duplicate_identifier = evidence_dao_find_identifier_by_sha256(
|
||||||
|
|
@ -215,11 +375,19 @@ PersonCreationCoordinatorResult *person_creation_coordinator_execute(
|
||||||
record = evidence_record_new(identifier,
|
record = evidence_record_new(identifier,
|
||||||
person_evidence_selection_item_get_original_name(item),
|
person_evidence_selection_item_get_original_name(item),
|
||||||
internal, relative,
|
internal, relative,
|
||||||
person_evidence_selection_item_get_type_identifier(item),
|
metadata != NULL && metadata->type_identifier != NULL
|
||||||
|
? metadata->type_identifier
|
||||||
|
: person_evidence_selection_item_get_type_identifier(item),
|
||||||
person_evidence_selection_item_get_size_bytes(item),
|
person_evidence_selection_item_get_size_bytes(item),
|
||||||
person_evidence_selection_item_get_sha256(item), timestamp,
|
person_evidence_selection_item_get_sha256(item),
|
||||||
NULL, "Import depuis l’assistant de création d’une personne",
|
metadata != NULL && metadata->collected_at != NULL
|
||||||
person_evidence_selection_item_get_description(item),
|
? metadata->collected_at : timestamp,
|
||||||
|
metadata != NULL ? metadata->source : NULL, create_person
|
||||||
|
? "Import depuis l’assistant de création d’une personne"
|
||||||
|
: "Import OCR rattaché à une personne existante",
|
||||||
|
metadata != NULL && metadata->description != NULL
|
||||||
|
? metadata->description
|
||||||
|
: person_evidence_selection_item_get_description(item),
|
||||||
EVIDENCE_INTEGRITY_STATUS_VALID, error);
|
EVIDENCE_INTEGRITY_STATUS_VALID, error);
|
||||||
if (record == NULL ||
|
if (record == NULL ||
|
||||||
!evidence_dao_insert(evidence_dao, record, error)) {
|
!evidence_dao_insert(evidence_dao, record, error)) {
|
||||||
|
|
@ -230,7 +398,9 @@ PersonCreationCoordinatorResult *person_creation_coordinator_execute(
|
||||||
evidence_identifier = identifier;
|
evidence_identifier = identifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!evidence_entity_dao_link(link_dao, evidence_identifier,
|
if (fail_at(&failure, PERSON_CREATION_FAILURE_LINK_EVIDENCE,
|
||||||
|
error) ||
|
||||||
|
!evidence_entity_dao_link(link_dao, evidence_identifier,
|
||||||
result->person_identifier, error)) {
|
result->person_identifier, error)) {
|
||||||
evidence_record_free(record); g_free(identifier);
|
evidence_record_free(record); g_free(identifier);
|
||||||
g_free(duplicate_identifier);
|
g_free(duplicate_identifier);
|
||||||
|
|
@ -244,7 +414,61 @@ PersonCreationCoordinatorResult *person_creation_coordinator_execute(
|
||||||
g_free(identifier); g_free(relative); g_free(internal);
|
g_free(identifier); g_free(relative); g_free(internal);
|
||||||
g_free(absolute);
|
g_free(absolute);
|
||||||
}
|
}
|
||||||
if (!database_transaction_commit(database)) goto cleanup;
|
if (!persist_ocr_runs(ocr_dao, root, result->person_identifier,
|
||||||
|
selection, result->evidence_identifiers, ocr_runs, timestamp,
|
||||||
|
created_paths, created_directories, &failure,
|
||||||
|
cancellable, error))
|
||||||
|
goto cleanup;
|
||||||
|
for (guint i = 0; ocr_runs != NULL && i < ocr_runs->len; i++) {
|
||||||
|
IdentityOcrRun *run = g_ptr_array_index((GPtrArray *) ocr_runs, i);
|
||||||
|
char *directory = g_build_filename(root, "02_Preuves_Traitees",
|
||||||
|
"OCR", identity_ocr_run_get_identifier(run), NULL);
|
||||||
|
char *text_path = g_build_filename(directory, "ocr.txt", NULL);
|
||||||
|
char *tsv_path = g_build_filename(directory, "ocr.tsv", NULL);
|
||||||
|
gboolean alter_text = fail_at(&failure,
|
||||||
|
PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED, error);
|
||||||
|
gboolean alter_tsv = fail_at(&failure,
|
||||||
|
PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED, error);
|
||||||
|
if (alter_text || alter_tsv) {
|
||||||
|
g_clear_error(error);
|
||||||
|
if (!g_file_set_contents(alter_text ? text_path : tsv_path,
|
||||||
|
"SPECIMEN MODIFIÉ APRÈS COPIE", -1, error)) {
|
||||||
|
g_free(directory); g_free(text_path); g_free(tsv_path);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char *text_sha = NULL, *tsv_sha = NULL;
|
||||||
|
guint64 size = 0;
|
||||||
|
char *expected_text = g_compute_checksum_for_string(
|
||||||
|
G_CHECKSUM_SHA256, identity_ocr_run_get_raw_text(run), -1);
|
||||||
|
char *expected_tsv = g_compute_checksum_for_string(
|
||||||
|
G_CHECKSUM_SHA256, identity_ocr_run_get_tsv(run), -1);
|
||||||
|
gboolean verified = file_hash_compute_sha256(text_path, cancellable,
|
||||||
|
&text_sha, &size, error) &&
|
||||||
|
file_hash_compute_sha256(tsv_path, cancellable,
|
||||||
|
&tsv_sha, &size, error) &&
|
||||||
|
g_strcmp0(text_sha, expected_text) == 0 &&
|
||||||
|
g_strcmp0(tsv_sha, expected_tsv) == 0;
|
||||||
|
g_free(directory); g_free(text_path); g_free(tsv_path);
|
||||||
|
g_free(text_sha); g_free(tsv_sha);
|
||||||
|
g_free(expected_text); g_free(expected_tsv);
|
||||||
|
if (!verified) {
|
||||||
|
if (error != NULL && *error == NULL)
|
||||||
|
g_set_error_literal(error, coordinator_error(), 7,
|
||||||
|
"Un artefact OCR a changé avant la validation.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!session_valid(options) ||
|
||||||
|
fail_at(&failure, PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,
|
||||||
|
error)) {
|
||||||
|
if (error != NULL && *error == NULL)
|
||||||
|
g_set_error_literal(error, coordinator_error(), 8,
|
||||||
|
"La session d’enquête a changé avant la validation.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (fail_at(&failure, PERSON_CREATION_FAILURE_COMMIT, error) ||
|
||||||
|
!database_transaction_commit(database)) goto cleanup;
|
||||||
transaction_active = FALSE;
|
transaction_active = FALSE;
|
||||||
success = TRUE;
|
success = TRUE;
|
||||||
cleanup:
|
cleanup:
|
||||||
|
|
@ -253,12 +477,42 @@ cleanup:
|
||||||
if (!success)
|
if (!success)
|
||||||
for (guint i = 0; i < created_paths->len; i++)
|
for (guint i = 0; i < created_paths->len; i++)
|
||||||
g_unlink(g_ptr_array_index(created_paths, i));
|
g_unlink(g_ptr_array_index(created_paths, i));
|
||||||
|
if (!success)
|
||||||
|
for (guint i = created_directories->len; i > 0; i--) {
|
||||||
|
const char *directory =
|
||||||
|
g_ptr_array_index(created_directories, i - 1);
|
||||||
|
gboolean compensation_failure =
|
||||||
|
failure.options != NULL &&
|
||||||
|
failure.options->inject_compensation_failure &&
|
||||||
|
failure.occurrences[PERSON_CREATION_FAILURE_COMPENSATION]++ == 0;
|
||||||
|
if (compensation_failure ||
|
||||||
|
fail_at(&failure, PERSON_CREATION_FAILURE_COMPENSATION,
|
||||||
|
error)) {
|
||||||
|
if (error != NULL && *error != NULL) {
|
||||||
|
char *message = g_strdup_printf(
|
||||||
|
"%s Compensation incomplète : suppression injectée "
|
||||||
|
"du dossier « %s ».", (*error)->message, directory);
|
||||||
|
g_clear_error(error);
|
||||||
|
g_set_error_literal(error, coordinator_error(), 9,
|
||||||
|
message);
|
||||||
|
g_free(message);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (g_rmdir(directory) != 0 && errno != ENOENT &&
|
||||||
|
errno != ENOTEMPTY && error != NULL && *error == NULL)
|
||||||
|
g_set_error(error, coordinator_error(), 5,
|
||||||
|
"Impossible de nettoyer le dossier OCR : %s.",
|
||||||
|
g_strerror(errno));
|
||||||
|
}
|
||||||
g_ptr_array_unref(created_paths);
|
g_ptr_array_unref(created_paths);
|
||||||
|
g_ptr_array_unref(created_directories);
|
||||||
entity_record_free(person_record);
|
entity_record_free(person_record);
|
||||||
entity_dao_free(entity_dao);
|
entity_dao_free(entity_dao);
|
||||||
evidence_dao_free(evidence_dao);
|
evidence_dao_free(evidence_dao);
|
||||||
evidence_entity_dao_free(link_dao);
|
evidence_entity_dao_free(link_dao);
|
||||||
person_role_assignment_dao_free(role_dao);
|
person_role_assignment_dao_free(role_dao);
|
||||||
|
identity_ocr_dao_free(ocr_dao);
|
||||||
g_free(timestamp);
|
g_free(timestamp);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
person_creation_coordinator_result_free(result);
|
person_creation_coordinator_result_free(result);
|
||||||
|
|
@ -266,6 +520,36 @@ cleanup:
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
PersonCreationCoordinatorResult *person_creation_coordinator_execute_with_options(
|
||||||
|
Database *database, const char *root, const PersonEntityInput *person,
|
||||||
|
const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs,
|
||||||
|
const PersonCreationCoordinatorOptions *options,
|
||||||
|
GCancellable *cancellable, GError **error)
|
||||||
|
{
|
||||||
|
return person_creation_coordinator_execute_internal(database, root,
|
||||||
|
person, NULL, selection, ocr_runs, NULL, options,
|
||||||
|
cancellable, error);
|
||||||
|
}
|
||||||
|
PersonCreationCoordinatorResult *
|
||||||
|
person_creation_coordinator_attach_to_existing_person(
|
||||||
|
Database *database, const char *root, const char *person_identifier,
|
||||||
|
const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs,
|
||||||
|
const PersonCreationCoordinatorEvidenceMetadata *metadata,
|
||||||
|
const PersonCreationCoordinatorOptions *options,
|
||||||
|
GCancellable *cancellable, GError **error)
|
||||||
|
{
|
||||||
|
return person_creation_coordinator_execute_internal(database, root,
|
||||||
|
NULL, person_identifier, selection, ocr_runs, metadata, options,
|
||||||
|
cancellable, error);
|
||||||
|
}
|
||||||
|
PersonCreationCoordinatorResult *person_creation_coordinator_execute(
|
||||||
|
Database *database, const char *root, const PersonEntityInput *person,
|
||||||
|
const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs,
|
||||||
|
GCancellable *cancellable, GError **error)
|
||||||
|
{
|
||||||
|
return person_creation_coordinator_execute_with_options(database, root,
|
||||||
|
person, selection, ocr_runs, NULL, cancellable, error);
|
||||||
|
}
|
||||||
void person_creation_coordinator_result_free(
|
void person_creation_coordinator_result_free(
|
||||||
PersonCreationCoordinatorResult *result)
|
PersonCreationCoordinatorResult *result)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,12 @@ struct PersonCreationTaskRequest {
|
||||||
char *notes;
|
char *notes;
|
||||||
GPtrArray *roles;
|
GPtrArray *roles;
|
||||||
PersonEvidenceSelection *selection;
|
PersonEvidenceSelection *selection;
|
||||||
|
GPtrArray *ocr_runs;
|
||||||
};
|
};
|
||||||
PersonCreationTaskRequest *person_creation_task_request_new(
|
PersonCreationTaskRequest *person_creation_task_request_new(
|
||||||
const char *database_path, const char *root,
|
const char *database_path, const char *root,
|
||||||
const PersonEntityInput *person,
|
const PersonEntityInput *person,
|
||||||
const PersonEvidenceSelection *selection)
|
const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs)
|
||||||
{
|
{
|
||||||
PersonCreationTaskRequest *request;
|
PersonCreationTaskRequest *request;
|
||||||
if (database_path == NULL || root == NULL || person == NULL) return NULL;
|
if (database_path == NULL || root == NULL || person == NULL) return NULL;
|
||||||
|
|
@ -43,6 +44,11 @@ PersonCreationTaskRequest *person_creation_task_request_new(
|
||||||
request->person.evidence_identifier = NULL;
|
request->person.evidence_identifier = NULL;
|
||||||
request->person.role_assignments = request->roles;
|
request->person.role_assignments = request->roles;
|
||||||
request->selection = person_evidence_selection_copy(selection);
|
request->selection = person_evidence_selection_copy(selection);
|
||||||
|
request->ocr_runs = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) identity_ocr_run_free);
|
||||||
|
for (guint i = 0; ocr_runs != NULL && i < ocr_runs->len; i++)
|
||||||
|
g_ptr_array_add(request->ocr_runs,
|
||||||
|
identity_ocr_run_copy(g_ptr_array_index((GPtrArray *) ocr_runs,i)));
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
void person_creation_task_request_free(PersonCreationTaskRequest *request)
|
void person_creation_task_request_free(PersonCreationTaskRequest *request)
|
||||||
|
|
@ -53,6 +59,7 @@ void person_creation_task_request_free(PersonCreationTaskRequest *request)
|
||||||
g_free(request->pseudonym); g_free(request->status);
|
g_free(request->pseudonym); g_free(request->status);
|
||||||
g_free(request->notes); g_ptr_array_unref(request->roles);
|
g_free(request->notes); g_ptr_array_unref(request->roles);
|
||||||
person_evidence_selection_free(request->selection);
|
person_evidence_selection_free(request->selection);
|
||||||
|
g_ptr_array_unref(request->ocr_runs);
|
||||||
g_free(request);
|
g_free(request);
|
||||||
}
|
}
|
||||||
static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
|
static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
|
||||||
|
|
@ -69,7 +76,8 @@ static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
*result = person_creation_coordinator_execute(database, request->root,
|
*result = person_creation_coordinator_execute(database, request->root,
|
||||||
&request->person, request->selection, cancellable, error);
|
&request->person, request->selection, request->ocr_runs,
|
||||||
|
cancellable, error);
|
||||||
database_close(database);
|
database_close(database);
|
||||||
return *result != NULL;
|
return *result != NULL;
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +91,8 @@ BackgroundTask *person_creation_task_start(TaskManager *manager,
|
||||||
if (manager == NULL || request == NULL) return NULL;
|
if (manager == NULL || request == NULL) return NULL;
|
||||||
task = background_task_new("Création de la personne et import des preuves");
|
task = background_task_new("Création de la personne et import des preuves");
|
||||||
copy = person_creation_task_request_new(request->database_path,
|
copy = person_creation_task_request_new(request->database_path,
|
||||||
request->root, &request->person, request->selection);
|
request->root, &request->person, request->selection,
|
||||||
|
request->ocr_runs);
|
||||||
if (task == NULL || copy == NULL ||
|
if (task == NULL || copy == NULL ||
|
||||||
!task_manager_add(manager, task, error) ||
|
!task_manager_add(manager, task, error) ||
|
||||||
!background_task_start(task, worker, copy,
|
!background_task_start(task, worker, copy,
|
||||||
|
|
|
||||||
437
src/dao/identity_ocr_dao.c
Normal file
437
src/dao/identity_ocr_dao.c
Normal file
|
|
@ -0,0 +1,437 @@
|
||||||
|
#include "dao/identity_ocr_dao.h"
|
||||||
|
#include "database/statement.h"
|
||||||
|
#include "database/transaction.h"
|
||||||
|
struct IdentityOcrDao{Database*database;};
|
||||||
|
static const char *review_status_text(IdentityReviewStatus status)
|
||||||
|
{
|
||||||
|
if(status==IDENTITY_REVIEW_ACCEPTED)return "accepted";
|
||||||
|
if(status==IDENTITY_REVIEW_MODIFIED)return "modified";
|
||||||
|
if(status==IDENTITY_REVIEW_REJECTED)return "rejected";
|
||||||
|
if(status==IDENTITY_REVIEW_CONFLICT)return "conflict";
|
||||||
|
return "proposed";
|
||||||
|
}
|
||||||
|
static gboolean bind_text(DatabaseStatement*s,int i,const char*v)
|
||||||
|
{return v!=NULL?database_statement_bind_text(s,i,v):
|
||||||
|
database_statement_bind_null(s,i);}
|
||||||
|
IdentityOcrDao *identity_ocr_dao_new(Database*d)
|
||||||
|
{if(!d)return NULL;IdentityOcrDao*dao=g_new0(IdentityOcrDao,1);dao->database=d;return dao;}
|
||||||
|
void identity_ocr_dao_free(IdentityOcrDao*d){g_free(d);}
|
||||||
|
gboolean identity_ocr_dao_insert(IdentityOcrDao*d,const char*person,
|
||||||
|
const char*evidence,const IdentityOcrRun*r,const char*text_path,
|
||||||
|
const char*text_sha,const char*tsv_path,const char*tsv_sha,
|
||||||
|
const char*timestamp,GError**error)
|
||||||
|
{
|
||||||
|
const char*sql="INSERT INTO identity_ocr_runs(id,evidence_id,expected_sha256,"
|
||||||
|
"page_number,document_type,document_side,engine,engine_version,"
|
||||||
|
"requested_languages,available_languages,parameters,preprocessing_profile,"
|
||||||
|
"executed_at,status,text_relative_path,text_sha256,tsv_relative_path,"
|
||||||
|
"tsv_sha256,corrected_transcription,transcription_is_human,"
|
||||||
|
"transcription_corrected_at,transcription_origin)"
|
||||||
|
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,'success',?,?,?,?,?,?,?,?);";
|
||||||
|
DatabaseStatement*s=database_statement_prepare(d->database,sql);
|
||||||
|
gboolean ok=s&&bind_text(s,1,identity_ocr_run_get_identifier(r))&&
|
||||||
|
bind_text(s,2,evidence)&&bind_text(s,3,identity_ocr_run_get_expected_sha256(r))&&
|
||||||
|
database_statement_bind_int64(s,4,identity_ocr_run_get_page(r))&&
|
||||||
|
bind_text(s,5,identity_ocr_run_get_document_type(r))&&
|
||||||
|
bind_text(s,6,identity_ocr_run_get_document_side(r))&&bind_text(s,7,"tesseract")&&
|
||||||
|
bind_text(s,8,identity_ocr_run_get_version(r))&&
|
||||||
|
bind_text(s,9,identity_ocr_run_get_languages(r))&&
|
||||||
|
bind_text(s,10,identity_ocr_run_get_available_languages(r))&&
|
||||||
|
bind_text(s,11,"GSubprocess sans shell; sorties texte et TSV")&&
|
||||||
|
bind_text(s,12,identity_ocr_run_get_profile(r))&&bind_text(s,13,timestamp)&&
|
||||||
|
bind_text(s,14,text_path)&&bind_text(s,15,text_sha)&&bind_text(s,16,tsv_path)&&
|
||||||
|
bind_text(s,17,tsv_sha)&&
|
||||||
|
bind_text(s,18,identity_ocr_run_get_corrected_transcription(r))&&
|
||||||
|
database_statement_bind_int64(s,19,
|
||||||
|
identity_ocr_run_has_human_transcription(r)?1:0)&&
|
||||||
|
bind_text(s,20,identity_ocr_run_get_transcription_corrected_at(r))&&
|
||||||
|
bind_text(s,21,identity_ocr_run_has_human_transcription(r)?"human":NULL)&&
|
||||||
|
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
database_statement_finalize(s);if(!ok)goto fail;
|
||||||
|
char*obs=g_uuid_string_random();s=database_statement_prepare(d->database,
|
||||||
|
"INSERT INTO identity_document_observations(id,person_id,evidence_id,"
|
||||||
|
"ocr_run_id,document_type,document_side,page_number,review_state,observed_at,"
|
||||||
|
"factual_notes) VALUES(?,?,?,?,?,?,?,'accepted',?,?);");
|
||||||
|
ok=s&&bind_text(s,1,obs)&&bind_text(s,2,person)&&bind_text(s,3,evidence)&&
|
||||||
|
bind_text(s,4,identity_ocr_run_get_identifier(r))&&
|
||||||
|
bind_text(s,5,identity_ocr_run_get_document_type(r))&&
|
||||||
|
bind_text(s,6,identity_ocr_run_get_document_side(r))&&
|
||||||
|
database_statement_bind_int64(s,7,identity_ocr_run_get_page(r))&&
|
||||||
|
bind_text(s,8,timestamp)&&
|
||||||
|
bind_text(s,9,identity_ocr_run_get_factual_notes(r))&&
|
||||||
|
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
database_statement_finalize(s);if(!ok){g_free(obs);goto fail;}
|
||||||
|
const GPtrArray*fields=identity_ocr_run_get_fields(r);
|
||||||
|
for(guint i=0;fields&&i<fields->len;i++){IdentityFieldObservation*f=g_ptr_array_index((GPtrArray*)fields,i);
|
||||||
|
IdentityReviewStatus status=identity_field_observation_get_status(f);
|
||||||
|
const IdentitySourceBox*b=identity_field_observation_get_box(f);char*id=g_uuid_string_random();
|
||||||
|
s=database_statement_prepare(d->database,"INSERT INTO identity_field_observations("
|
||||||
|
"id,observation_id,field_code,raw_value,corrected_value,confidence,review_status,"
|
||||||
|
"origin,evidence_id,ocr_run_id,page_number,source_x,source_y,source_width,"
|
||||||
|
"source_height,source_image_width,source_image_height,display_order,reviewed_at)"
|
||||||
|
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
|
||||||
|
ok=s&&bind_text(s,1,id)&&bind_text(s,2,obs)&&bind_text(s,3,identity_field_observation_get_code(f))&&
|
||||||
|
bind_text(s,4,identity_field_observation_get_raw_value(f))&&
|
||||||
|
bind_text(s,5,identity_field_observation_get_corrected_value(f))&&
|
||||||
|
(identity_field_observation_get_confidence(f)>=0?
|
||||||
|
database_statement_bind_double(s,6,identity_field_observation_get_confidence(f)):
|
||||||
|
database_statement_bind_null(s,6))&&bind_text(s,7,review_status_text(status))&&
|
||||||
|
bind_text(s,8,identity_field_observation_get_origin(f))&&bind_text(s,9,evidence)&&
|
||||||
|
bind_text(s,10,identity_ocr_run_get_identifier(r))&&database_statement_bind_int64(s,11,identity_ocr_run_get_page(r));
|
||||||
|
for(int column=12;ok&&column<=17;column++){gint value=0;if(b&&b->available){
|
||||||
|
const gint values[]={b->x,b->y,b->width,b->height,b->image_width,b->image_height};
|
||||||
|
value=values[column-12];ok=database_statement_bind_int64(s,column,value);
|
||||||
|
}else ok=database_statement_bind_null(s,column);}
|
||||||
|
ok=ok&&database_statement_bind_int64(s,18,identity_field_observation_get_order(f))&&
|
||||||
|
bind_text(s,19,timestamp)&&database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
database_statement_finalize(s);g_free(id);if(!ok){g_free(obs);goto fail;}}
|
||||||
|
g_free(obs);return TRUE;
|
||||||
|
fail:g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),1,
|
||||||
|
"Impossible de conserver l’OCR d’identité.");return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define FREE_FIELD(record,field) g_free((record)->field)
|
||||||
|
void identity_ocr_run_record_free(IdentityOcrRunRecord*r)
|
||||||
|
{if(!r)return;FREE_FIELD(r,id);FREE_FIELD(r,evidence_id);FREE_FIELD(r,expected_sha256);
|
||||||
|
FREE_FIELD(r,document_type);FREE_FIELD(r,document_side);FREE_FIELD(r,engine);
|
||||||
|
FREE_FIELD(r,engine_version);FREE_FIELD(r,requested_languages);
|
||||||
|
FREE_FIELD(r,available_languages);FREE_FIELD(r,parameters);
|
||||||
|
FREE_FIELD(r,preprocessing_profile);FREE_FIELD(r,executed_at);FREE_FIELD(r,status);
|
||||||
|
FREE_FIELD(r,error_message);FREE_FIELD(r,text_relative_path);FREE_FIELD(r,text_sha256);
|
||||||
|
FREE_FIELD(r,tsv_relative_path);FREE_FIELD(r,tsv_sha256);
|
||||||
|
FREE_FIELD(r,work_image_relative_path);FREE_FIELD(r,work_image_sha256);
|
||||||
|
FREE_FIELD(r,corrected_transcription);
|
||||||
|
FREE_FIELD(r,transcription_corrected_at);
|
||||||
|
FREE_FIELD(r,transcription_origin);g_free(r);}
|
||||||
|
void identity_document_observation_record_free(IdentityDocumentObservationRecord*r)
|
||||||
|
{if(!r)return;FREE_FIELD(r,id);FREE_FIELD(r,person_id);FREE_FIELD(r,evidence_id);
|
||||||
|
FREE_FIELD(r,ocr_run_id);FREE_FIELD(r,document_type);
|
||||||
|
FREE_FIELD(r,issuing_country_declared);FREE_FIELD(r,document_side);
|
||||||
|
FREE_FIELD(r,review_state);FREE_FIELD(r,observed_at);FREE_FIELD(r,factual_notes);g_free(r);}
|
||||||
|
void identity_field_observation_record_free(IdentityFieldObservationRecord*r)
|
||||||
|
{if(!r)return;FREE_FIELD(r,id);FREE_FIELD(r,observation_id);FREE_FIELD(r,field_code);
|
||||||
|
FREE_FIELD(r,raw_value);FREE_FIELD(r,corrected_value);FREE_FIELD(r,normalized_value);
|
||||||
|
FREE_FIELD(r,review_status);FREE_FIELD(r,origin);FREE_FIELD(r,evidence_id);
|
||||||
|
FREE_FIELD(r,ocr_run_id);FREE_FIELD(r,reviewed_at);FREE_FIELD(r,review_note);g_free(r);}
|
||||||
|
|
||||||
|
static IdentityOcrRunRecord *read_run(DatabaseStatement*s)
|
||||||
|
{
|
||||||
|
gint64 human=0;
|
||||||
|
IdentityOcrRunRecord*r=g_new0(IdentityOcrRunRecord,1);gboolean ok=
|
||||||
|
database_statement_column_text(s,0,&r->id)&&
|
||||||
|
database_statement_column_text(s,1,&r->evidence_id)&&
|
||||||
|
database_statement_column_text(s,2,&r->expected_sha256)&&
|
||||||
|
database_statement_column_int64(s,3,&r->page_number)&&
|
||||||
|
database_statement_column_text(s,4,&r->document_type)&&
|
||||||
|
database_statement_column_text(s,5,&r->document_side)&&
|
||||||
|
database_statement_column_text(s,6,&r->engine)&&
|
||||||
|
database_statement_column_text(s,7,&r->engine_version)&&
|
||||||
|
database_statement_column_text(s,8,&r->requested_languages)&&
|
||||||
|
database_statement_column_text(s,9,&r->available_languages)&&
|
||||||
|
database_statement_column_text(s,10,&r->parameters)&&
|
||||||
|
database_statement_column_text(s,11,&r->preprocessing_profile)&&
|
||||||
|
database_statement_column_text(s,12,&r->executed_at)&&
|
||||||
|
database_statement_column_text(s,13,&r->status)&&
|
||||||
|
database_statement_column_text(s,14,&r->error_message)&&
|
||||||
|
database_statement_column_text(s,15,&r->text_relative_path)&&
|
||||||
|
database_statement_column_text(s,16,&r->text_sha256)&&
|
||||||
|
database_statement_column_text(s,17,&r->tsv_relative_path)&&
|
||||||
|
database_statement_column_text(s,18,&r->tsv_sha256)&&
|
||||||
|
database_statement_column_text(s,19,&r->work_image_relative_path)&&
|
||||||
|
database_statement_column_text(s,20,&r->work_image_sha256)&&
|
||||||
|
database_statement_column_text(s,21,&r->corrected_transcription)&&
|
||||||
|
database_statement_column_int64(s,22,&human)&&
|
||||||
|
database_statement_column_text(s,23,&r->transcription_corrected_at)&&
|
||||||
|
database_statement_column_text(s,24,&r->transcription_origin);
|
||||||
|
if(!ok){identity_ocr_run_record_free(r);return NULL;}
|
||||||
|
r->transcription_is_human=human!=0;return r;
|
||||||
|
}
|
||||||
|
static IdentityDocumentObservationRecord *read_document(DatabaseStatement*s)
|
||||||
|
{
|
||||||
|
IdentityDocumentObservationRecord*r=g_new0(IdentityDocumentObservationRecord,1);
|
||||||
|
gboolean ok=database_statement_column_text(s,0,&r->id)&&
|
||||||
|
database_statement_column_text(s,1,&r->person_id)&&
|
||||||
|
database_statement_column_text(s,2,&r->evidence_id)&&
|
||||||
|
database_statement_column_text(s,3,&r->ocr_run_id)&&
|
||||||
|
database_statement_column_text(s,4,&r->document_type)&&
|
||||||
|
database_statement_column_text(s,5,&r->issuing_country_declared)&&
|
||||||
|
database_statement_column_text(s,6,&r->document_side)&&
|
||||||
|
database_statement_column_int64(s,7,&r->page_number)&&
|
||||||
|
database_statement_column_text(s,8,&r->review_state)&&
|
||||||
|
database_statement_column_text(s,9,&r->observed_at)&&
|
||||||
|
database_statement_column_text(s,10,&r->factual_notes);
|
||||||
|
if(!ok){identity_document_observation_record_free(r);return NULL;}return r;
|
||||||
|
}
|
||||||
|
static IdentityFieldObservationRecord *read_field(DatabaseStatement*s)
|
||||||
|
{
|
||||||
|
IdentityFieldObservationRecord*r=g_new0(IdentityFieldObservationRecord,1);
|
||||||
|
gboolean ok=database_statement_column_text(s,0,&r->id)&&
|
||||||
|
database_statement_column_text(s,1,&r->observation_id)&&
|
||||||
|
database_statement_column_text(s,2,&r->field_code)&&
|
||||||
|
database_statement_column_text(s,3,&r->raw_value)&&
|
||||||
|
database_statement_column_text(s,4,&r->corrected_value)&&
|
||||||
|
database_statement_column_text(s,5,&r->normalized_value);
|
||||||
|
bool is_null=FALSE;
|
||||||
|
ok=ok&&database_statement_column_is_null(s,6,&is_null);
|
||||||
|
r->has_confidence=!is_null;
|
||||||
|
ok=ok&&(!r->has_confidence||database_statement_column_double(s,6,&r->confidence))&&
|
||||||
|
database_statement_column_text(s,7,&r->review_status)&&
|
||||||
|
database_statement_column_text(s,8,&r->origin)&&
|
||||||
|
database_statement_column_text(s,9,&r->evidence_id)&&
|
||||||
|
database_statement_column_text(s,10,&r->ocr_run_id)&&
|
||||||
|
database_statement_column_int64(s,11,&r->page_number);
|
||||||
|
ok=ok&&database_statement_column_is_null(s,12,&is_null);
|
||||||
|
r->has_source_box=!is_null;
|
||||||
|
gint64*values[]={&r->source_x,&r->source_y,&r->source_width,&r->source_height,
|
||||||
|
&r->source_image_width,&r->source_image_height};
|
||||||
|
for(guint i=0;ok&&r->has_source_box&&i<G_N_ELEMENTS(values);i++)
|
||||||
|
ok=database_statement_column_int64(s,12+(int)i,values[i]);
|
||||||
|
ok=ok&&database_statement_column_int64(s,18,&r->display_order)&&
|
||||||
|
database_statement_column_text(s,19,&r->reviewed_at)&&
|
||||||
|
database_statement_column_text(s,20,&r->review_note);
|
||||||
|
if(!ok){identity_field_observation_record_free(r);return NULL;}return r;
|
||||||
|
}
|
||||||
|
static const char run_columns[]="id,evidence_id,expected_sha256,page_number,"
|
||||||
|
"document_type,document_side,engine,engine_version,requested_languages,"
|
||||||
|
"available_languages,parameters,preprocessing_profile,executed_at,status,"
|
||||||
|
"error_message,text_relative_path,text_sha256,tsv_relative_path,tsv_sha256,"
|
||||||
|
"work_image_relative_path,work_image_sha256,corrected_transcription,"
|
||||||
|
"transcription_is_human,transcription_corrected_at,transcription_origin";
|
||||||
|
static const char document_columns[]="id,person_id,evidence_id,ocr_run_id,"
|
||||||
|
"document_type,issuing_country_declared,document_side,page_number,"
|
||||||
|
"review_state,observed_at,factual_notes";
|
||||||
|
static const char field_columns[]="id,observation_id,field_code,raw_value,"
|
||||||
|
"corrected_value,normalized_value,confidence,review_status,origin,evidence_id,"
|
||||||
|
"ocr_run_id,page_number,source_x,source_y,source_width,source_height,"
|
||||||
|
"source_image_width,source_image_height,display_order,reviewed_at,review_note";
|
||||||
|
|
||||||
|
typedef gpointer(*ReadRecord)(DatabaseStatement*);
|
||||||
|
static gpointer read_run_record(DatabaseStatement*s){return read_run(s);}
|
||||||
|
static gpointer read_document_record(DatabaseStatement*s){return read_document(s);}
|
||||||
|
static gpointer read_field_record(DatabaseStatement*s){return read_field(s);}
|
||||||
|
static gpointer find_one(IdentityOcrDao*d,const char*table,const char*columns,
|
||||||
|
const char*id,ReadRecord read,GError**error)
|
||||||
|
{
|
||||||
|
if(!d||!id){g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),2,
|
||||||
|
"Lecture OCR invalide.");return NULL;}
|
||||||
|
char*sql=g_strdup_printf("SELECT %s FROM %s WHERE id=?;",columns,table);
|
||||||
|
DatabaseStatement*s=database_statement_prepare(d->database,sql);g_free(sql);
|
||||||
|
if(!s||!database_statement_bind_text(s,1,id)){database_statement_finalize(s);
|
||||||
|
g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),3,
|
||||||
|
"Impossible de préparer la lecture OCR.");return NULL;}
|
||||||
|
DatabaseStatementStepResult step=database_statement_step(s);
|
||||||
|
gpointer result=step==DATABASE_STATEMENT_STEP_ROW?read(s):NULL;
|
||||||
|
if(step==DATABASE_STATEMENT_STEP_ERROR||(step==DATABASE_STATEMENT_STEP_ROW&&!result))
|
||||||
|
g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),4,
|
||||||
|
"Impossible de lire l’enregistrement OCR.");
|
||||||
|
database_statement_finalize(s);return result;
|
||||||
|
}
|
||||||
|
static GPtrArray *list_records(IdentityOcrDao*d,const char*table,
|
||||||
|
const char*columns,const char*foreign_column,const char*id,ReadRecord read,
|
||||||
|
GDestroyNotify destroy,GError**error)
|
||||||
|
{
|
||||||
|
if(!d||!id){g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),2,
|
||||||
|
"Lecture OCR invalide.");return NULL;}
|
||||||
|
char*sql=g_strdup_printf("SELECT %s FROM %s WHERE %s=? ORDER BY rowid;",
|
||||||
|
columns,table,foreign_column);
|
||||||
|
DatabaseStatement*s=database_statement_prepare(d->database,sql);g_free(sql);
|
||||||
|
GPtrArray*a=g_ptr_array_new_with_free_func(destroy);
|
||||||
|
if(!s||!database_statement_bind_text(s,1,id))goto failed;
|
||||||
|
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
|
||||||
|
if(step==DATABASE_STATEMENT_STEP_DONE)break;
|
||||||
|
if(step!=DATABASE_STATEMENT_STEP_ROW)goto failed;
|
||||||
|
gpointer record=read(s);if(!record)goto failed;g_ptr_array_add(a,record);}
|
||||||
|
database_statement_finalize(s);return a;
|
||||||
|
failed:database_statement_finalize(s);g_ptr_array_unref(a);
|
||||||
|
g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),4,
|
||||||
|
"Impossible de lire les enregistrements OCR.");return NULL;
|
||||||
|
}
|
||||||
|
IdentityOcrRunRecord *identity_ocr_dao_find_run(IdentityOcrDao*d,const char*i,GError**e)
|
||||||
|
{return find_one(d,"identity_ocr_runs",run_columns,i,read_run_record,e);}
|
||||||
|
GPtrArray *identity_ocr_dao_list_runs_by_evidence(IdentityOcrDao*d,const char*i,GError**e)
|
||||||
|
{return list_records(d,"identity_ocr_runs",run_columns,"evidence_id",i,
|
||||||
|
read_run_record,(GDestroyNotify)identity_ocr_run_record_free,e);}
|
||||||
|
IdentityDocumentObservationRecord *identity_ocr_dao_find_document(
|
||||||
|
IdentityOcrDao*d,const char*i,GError**e)
|
||||||
|
{return find_one(d,"identity_document_observations",document_columns,i,
|
||||||
|
read_document_record,e);}
|
||||||
|
GPtrArray *identity_ocr_dao_list_documents_by_person(IdentityOcrDao*d,const char*i,GError**e)
|
||||||
|
{return list_records(d,"identity_document_observations",document_columns,
|
||||||
|
"person_id",i,read_document_record,
|
||||||
|
(GDestroyNotify)identity_document_observation_record_free,e);}
|
||||||
|
GPtrArray *identity_ocr_dao_list_documents_by_evidence(IdentityOcrDao*d,
|
||||||
|
const char*i,GError**e)
|
||||||
|
{return list_records(d,"identity_document_observations",document_columns,
|
||||||
|
"evidence_id",i,read_document_record,
|
||||||
|
(GDestroyNotify)identity_document_observation_record_free,e);}
|
||||||
|
IdentityFieldObservationRecord *identity_ocr_dao_find_field(IdentityOcrDao*d,
|
||||||
|
const char*i,GError**e)
|
||||||
|
{return find_one(d,"identity_field_observations",field_columns,i,
|
||||||
|
read_field_record,e);}
|
||||||
|
GPtrArray *identity_ocr_dao_list_fields_by_document(IdentityOcrDao*d,const char*i,GError**e)
|
||||||
|
{return list_records(d,"identity_field_observations",field_columns,
|
||||||
|
"observation_id",i,read_field_record,
|
||||||
|
(GDestroyNotify)identity_field_observation_record_free,e);}
|
||||||
|
|
||||||
|
IdentityOcrRun *identity_ocr_dao_load_run(
|
||||||
|
IdentityOcrDao*d,const char*root,const char*identifier,
|
||||||
|
char**person_identifier,GError**error)
|
||||||
|
{
|
||||||
|
IdentityOcrRunRecord*record=identity_ocr_dao_find_run(d,identifier,error);
|
||||||
|
IdentityOcrRun*run=NULL;GPtrArray*documents=NULL;char*raw=NULL;char*tsv=NULL;
|
||||||
|
if(person_identifier!=NULL)*person_identifier=NULL;
|
||||||
|
if(record==NULL||root==NULL)goto cleanup;
|
||||||
|
char*raw_path=g_build_filename(root,record->text_relative_path,NULL);
|
||||||
|
char*tsv_path=g_build_filename(root,record->tsv_relative_path,NULL);
|
||||||
|
if(!g_file_get_contents(raw_path,&raw,NULL,error)||
|
||||||
|
!g_file_get_contents(tsv_path,&tsv,NULL,error)){
|
||||||
|
g_free(raw_path);g_free(tsv_path);goto cleanup;}
|
||||||
|
g_free(raw_path);g_free(tsv_path);
|
||||||
|
run=identity_ocr_run_new(record->evidence_id,record->expected_sha256,
|
||||||
|
record->document_type,record->document_side,(guint)record->page_number,
|
||||||
|
record->requested_languages,record->preprocessing_profile);
|
||||||
|
if(run==NULL||!identity_ocr_run_replace_identifier(run,record->id))
|
||||||
|
goto cleanup;
|
||||||
|
identity_ocr_run_set_outputs(run,record->engine_version,
|
||||||
|
record->available_languages,record->parameters,raw,tsv);
|
||||||
|
if(record->transcription_is_human)
|
||||||
|
identity_ocr_run_set_corrected_transcription(run,
|
||||||
|
record->corrected_transcription,record->transcription_corrected_at);
|
||||||
|
documents=identity_ocr_dao_list_documents_by_evidence(
|
||||||
|
d,record->evidence_id,error);
|
||||||
|
for(guint i=0;documents!=NULL&&i<documents->len;i++){
|
||||||
|
IdentityDocumentObservationRecord*document=g_ptr_array_index(documents,i);
|
||||||
|
if(g_strcmp0(document->ocr_run_id,identifier)!=0)continue;
|
||||||
|
identity_ocr_run_set_factual_notes(run,document->factual_notes);
|
||||||
|
if(person_identifier!=NULL)*person_identifier=g_strdup(document->person_id);
|
||||||
|
GPtrArray*fields=identity_ocr_dao_list_fields_by_document(
|
||||||
|
d,document->id,error);
|
||||||
|
for(guint j=0;fields!=NULL&&j<fields->len;j++){
|
||||||
|
IdentityFieldObservationRecord*f=g_ptr_array_index(fields,j);
|
||||||
|
IdentitySourceBox box={.page=(gint)f->page_number,
|
||||||
|
.x=(gint)f->source_x,.y=(gint)f->source_y,
|
||||||
|
.width=(gint)f->source_width,.height=(gint)f->source_height,
|
||||||
|
.image_width=(gint)f->source_image_width,
|
||||||
|
.image_height=(gint)f->source_image_height,
|
||||||
|
.available=f->has_source_box};
|
||||||
|
IdentityFieldObservation*field=f->raw_value!=NULL
|
||||||
|
?identity_field_observation_new(f->field_code,f->raw_value,
|
||||||
|
f->has_confidence?f->confidence:-1.0,&box,(guint)f->display_order)
|
||||||
|
:identity_field_observation_new_manual(f->field_code,
|
||||||
|
f->corrected_value,(guint)f->display_order);
|
||||||
|
if(field==NULL)continue;
|
||||||
|
if(f->corrected_value!=NULL)
|
||||||
|
identity_field_observation_modify(field,f->corrected_value,f->review_note);
|
||||||
|
if(g_strcmp0(f->review_status,"accepted")==0)
|
||||||
|
identity_field_observation_accept(field);
|
||||||
|
else if(g_strcmp0(f->review_status,"rejected")==0)
|
||||||
|
identity_field_observation_reject(field);
|
||||||
|
else if(g_strcmp0(f->review_status,"conflict")==0)
|
||||||
|
identity_field_observation_mark_conflict(field);
|
||||||
|
identity_field_observation_set_origin(field,f->origin);
|
||||||
|
identity_ocr_run_add_field(run,field);
|
||||||
|
}
|
||||||
|
g_clear_pointer(&fields,g_ptr_array_unref);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cleanup:
|
||||||
|
if(record==NULL||run==NULL||documents==NULL){
|
||||||
|
identity_ocr_run_free(run);run=NULL;
|
||||||
|
if(error!=NULL&&*error==NULL)g_set_error_literal(error,
|
||||||
|
g_quark_from_static_string("identity-ocr-dao"),5,
|
||||||
|
"Impossible de recharger l’analyse OCR persistée.");}
|
||||||
|
g_free(raw);g_free(tsv);g_clear_pointer(&documents,g_ptr_array_unref);
|
||||||
|
identity_ocr_run_record_free(record);return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean update_review_fields(IdentityOcrDao*d,const char*observation,
|
||||||
|
const IdentityOcrRun*run,const char*timestamp)
|
||||||
|
{
|
||||||
|
const GPtrArray*fields=identity_ocr_run_get_fields(run);
|
||||||
|
for(guint i=0;fields!=NULL&&i<fields->len;i++){
|
||||||
|
IdentityFieldObservation*f=g_ptr_array_index((GPtrArray*)fields,i);
|
||||||
|
const IdentitySourceBox*b=identity_field_observation_get_box(f);
|
||||||
|
char*id=g_uuid_string_random();
|
||||||
|
DatabaseStatement*s=database_statement_prepare(d->database,
|
||||||
|
"INSERT INTO identity_field_observations(id,observation_id,field_code,"
|
||||||
|
"raw_value,corrected_value,confidence,review_status,origin,evidence_id,"
|
||||||
|
"ocr_run_id,page_number,source_x,source_y,source_width,source_height,"
|
||||||
|
"source_image_width,source_image_height,display_order,reviewed_at)"
|
||||||
|
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
|
||||||
|
gboolean ok=s&&bind_text(s,1,id)&&bind_text(s,2,observation)&&
|
||||||
|
bind_text(s,3,identity_field_observation_get_code(f))&&
|
||||||
|
bind_text(s,4,identity_field_observation_get_raw_value(f))&&
|
||||||
|
bind_text(s,5,identity_field_observation_get_corrected_value(f))&&
|
||||||
|
(identity_field_observation_get_confidence(f)>=0
|
||||||
|
?database_statement_bind_double(s,6,
|
||||||
|
identity_field_observation_get_confidence(f))
|
||||||
|
:database_statement_bind_null(s,6))&&
|
||||||
|
bind_text(s,7,review_status_text(
|
||||||
|
identity_field_observation_get_status(f)))&&
|
||||||
|
bind_text(s,8,identity_field_observation_get_origin(f))&&
|
||||||
|
bind_text(s,9,identity_ocr_run_get_evidence_id(run))&&
|
||||||
|
bind_text(s,10,identity_ocr_run_get_identifier(run))&&
|
||||||
|
database_statement_bind_int64(s,11,identity_ocr_run_get_page(run));
|
||||||
|
const gint values[]={b!=NULL?b->x:0,b!=NULL?b->y:0,
|
||||||
|
b!=NULL?b->width:0,b!=NULL?b->height:0,
|
||||||
|
b!=NULL?b->image_width:0,b!=NULL?b->image_height:0};
|
||||||
|
for(gint column=12;ok&&column<=17;column++)
|
||||||
|
ok=b!=NULL&&b->available
|
||||||
|
?database_statement_bind_int64(s,column,values[column-12])
|
||||||
|
:database_statement_bind_null(s,column);
|
||||||
|
ok=ok&&database_statement_bind_int64(s,18,
|
||||||
|
identity_field_observation_get_order(f))&&
|
||||||
|
bind_text(s,19,timestamp)&&
|
||||||
|
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
database_statement_finalize(s);g_free(id);if(!ok)return FALSE;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean identity_ocr_dao_update_review(IdentityOcrDao*d,
|
||||||
|
const IdentityOcrRun*run,const char*timestamp,GError**error)
|
||||||
|
{
|
||||||
|
if(d==NULL||run==NULL||timestamp==NULL||
|
||||||
|
!database_transaction_begin(d->database))goto fail;
|
||||||
|
DatabaseStatement*s=database_statement_prepare(d->database,
|
||||||
|
"UPDATE identity_ocr_runs SET corrected_transcription=?,"
|
||||||
|
"transcription_is_human=?,transcription_corrected_at=?,"
|
||||||
|
"transcription_origin=? WHERE id=? AND evidence_id=?;");
|
||||||
|
gboolean ok=s&&bind_text(s,1,
|
||||||
|
identity_ocr_run_get_corrected_transcription(run))&&
|
||||||
|
database_statement_bind_int64(s,2,
|
||||||
|
identity_ocr_run_has_human_transcription(run)?1:0)&&
|
||||||
|
bind_text(s,3,identity_ocr_run_get_transcription_corrected_at(run))&&
|
||||||
|
bind_text(s,4,identity_ocr_run_has_human_transcription(run)?"human":NULL)&&
|
||||||
|
bind_text(s,5,identity_ocr_run_get_identifier(run))&&
|
||||||
|
bind_text(s,6,identity_ocr_run_get_evidence_id(run))&&
|
||||||
|
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
database_statement_finalize(s);
|
||||||
|
IdentityDocumentObservationRecord*document=NULL;
|
||||||
|
GPtrArray*documents=ok?identity_ocr_dao_list_documents_by_evidence(
|
||||||
|
d,identity_ocr_run_get_evidence_id(run),error):NULL;
|
||||||
|
for(guint i=0;documents!=NULL&&i<documents->len;i++){
|
||||||
|
IdentityDocumentObservationRecord*candidate=g_ptr_array_index(documents,i);
|
||||||
|
if(g_strcmp0(candidate->ocr_run_id,
|
||||||
|
identity_ocr_run_get_identifier(run))==0){document=candidate;break;}}
|
||||||
|
if(document==NULL)ok=FALSE;
|
||||||
|
if(ok){s=database_statement_prepare(d->database,
|
||||||
|
"UPDATE identity_document_observations SET factual_notes=?,"
|
||||||
|
"review_state='accepted',observed_at=? WHERE id=?;");
|
||||||
|
ok=s&&bind_text(s,1,identity_ocr_run_get_factual_notes(run))&&
|
||||||
|
bind_text(s,2,timestamp)&&bind_text(s,3,document->id)&&
|
||||||
|
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
database_statement_finalize(s);}
|
||||||
|
if(ok){s=database_statement_prepare(d->database,
|
||||||
|
"DELETE FROM identity_field_observations WHERE observation_id=?;");
|
||||||
|
ok=s&&bind_text(s,1,document->id)&&
|
||||||
|
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
database_statement_finalize(s);}
|
||||||
|
if(ok)ok=update_review_fields(d,document->id,run,timestamp);
|
||||||
|
g_clear_pointer(&documents,g_ptr_array_unref);
|
||||||
|
if(ok&&database_transaction_commit(d->database))return TRUE;
|
||||||
|
database_transaction_rollback(d->database);
|
||||||
|
fail:
|
||||||
|
g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),6,
|
||||||
|
"Impossible d’enregistrer la révision OCR.");return FALSE;
|
||||||
|
}
|
||||||
|
|
@ -16,12 +16,12 @@
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle du schéma SQLite.
|
* @brief Version actuelle du schéma SQLite.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT 14
|
#define DATABASE_SCHEMA_VERSION_CURRENT 17
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "14"
|
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "17"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||||
|
|
@ -872,6 +872,51 @@ rollback:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool database_migrate_v14_to_v15(Database *database)
|
||||||
|
{
|
||||||
|
bool transaction_started = false;
|
||||||
|
if (database == NULL || !database_transaction_begin(database)) return false;
|
||||||
|
transaction_started = true;
|
||||||
|
if (!schema_install_v15(database) ||
|
||||||
|
!database_update_schema_version(database, "15") ||
|
||||||
|
!database_transaction_commit(database)) goto rollback;
|
||||||
|
return true;
|
||||||
|
rollback:
|
||||||
|
if (transaction_started && !database_transaction_rollback(database))
|
||||||
|
g_warning("Impossible d’annuler la migration SQLite V14 vers V15.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool database_migrate_v15_to_v16(Database *database)
|
||||||
|
{
|
||||||
|
bool transaction_started = false;
|
||||||
|
if (database == NULL || !database_transaction_begin(database)) return false;
|
||||||
|
transaction_started = true;
|
||||||
|
if (!schema_install_v16(database) ||
|
||||||
|
!database_update_schema_version(database, "16") ||
|
||||||
|
!database_transaction_commit(database)) goto rollback;
|
||||||
|
return true;
|
||||||
|
rollback:
|
||||||
|
if (transaction_started && !database_transaction_rollback(database))
|
||||||
|
g_warning("Impossible d’annuler la migration SQLite V15 vers V16.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool database_migrate_v16_to_v17(Database *database)
|
||||||
|
{
|
||||||
|
bool transaction_started = false;
|
||||||
|
if (database == NULL || !database_transaction_begin(database)) return false;
|
||||||
|
transaction_started = true;
|
||||||
|
if (!schema_install_v17(database) ||
|
||||||
|
!database_update_schema_version(database, "17") ||
|
||||||
|
!database_transaction_commit(database)) goto rollback;
|
||||||
|
return true;
|
||||||
|
rollback:
|
||||||
|
if (transaction_started && !database_transaction_rollback(database))
|
||||||
|
g_warning("Impossible d’annuler la migration SQLite V16 vers V17.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1117,6 +1162,18 @@ bool database_migrate_to_latest(
|
||||||
if (!database_migrate_v13_to_v14(database)) return false;
|
if (!database_migrate_v13_to_v14(database)) return false;
|
||||||
schema_version = 14;
|
schema_version = 14;
|
||||||
break;
|
break;
|
||||||
|
case 14:
|
||||||
|
if (!database_migrate_v14_to_v15(database)) return false;
|
||||||
|
schema_version = 15;
|
||||||
|
break;
|
||||||
|
case 15:
|
||||||
|
if (!database_migrate_v15_to_v16(database)) return false;
|
||||||
|
schema_version = 16;
|
||||||
|
break;
|
||||||
|
case 16:
|
||||||
|
if (!database_migrate_v16_to_v17(database)) return false;
|
||||||
|
schema_version = 17;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
database_set_error(
|
database_set_error(
|
||||||
|
|
|
||||||
|
|
@ -377,6 +377,36 @@ bool schema_install_v14(Database *database)
|
||||||
"la migration SQLite V14");
|
"la migration SQLite V14");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool schema_install_v15(Database *database)
|
||||||
|
{
|
||||||
|
return schema_execute_file(database, "database/schema_v15.sql",
|
||||||
|
"la migration SQLite V15");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool schema_install_v16(Database *database)
|
||||||
|
{
|
||||||
|
return schema_execute_file(database, "database/schema_v16.sql",
|
||||||
|
"la migration SQLite V16");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool schema_install_v17(Database *database)
|
||||||
|
{
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
sqlite3 *handle = database_get_handle(database);
|
||||||
|
if (handle != NULL &&
|
||||||
|
sqlite3_prepare_v2(handle,
|
||||||
|
"SELECT corrected_transcription, transcription_is_human, "
|
||||||
|
"transcription_corrected_at, transcription_origin "
|
||||||
|
"FROM identity_ocr_runs LIMIT 0",
|
||||||
|
-1, &statement, NULL) == SQLITE_OK) {
|
||||||
|
sqlite3_finalize(statement);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(statement);
|
||||||
|
return schema_execute_file(database, "database/schema_v17.sql",
|
||||||
|
"la migration SQLite V17");
|
||||||
|
}
|
||||||
|
|
||||||
bool schema_ensure_current(
|
bool schema_ensure_current(
|
||||||
Database *database
|
Database *database
|
||||||
)
|
)
|
||||||
|
|
|
||||||
242
src/models/identity_ocr.c
Normal file
242
src/models/identity_ocr.c
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
|
struct IdentityFieldObservation {
|
||||||
|
char *code, *raw_value, *corrected_value, *normalized_value, *note;
|
||||||
|
char *origin;
|
||||||
|
double confidence;
|
||||||
|
IdentityReviewStatus status;
|
||||||
|
IdentitySourceBox box;
|
||||||
|
guint order;
|
||||||
|
};
|
||||||
|
struct IdentityOcrRun {
|
||||||
|
char *id, *evidence_id, *expected_sha256, *document_type, *document_side;
|
||||||
|
char *languages, *profile, *version, *available_languages, *parameters;
|
||||||
|
char *raw_text, *tsv, *factual_notes;
|
||||||
|
char *corrected_transcription, *transcription_corrected_at;
|
||||||
|
gboolean human_transcription;
|
||||||
|
guint page;
|
||||||
|
GPtrArray *fields;
|
||||||
|
GBytes *preview;
|
||||||
|
};
|
||||||
|
static const char *const document_types[] = {
|
||||||
|
"identity_card","passport","driving_licence","residence_permit","other"};
|
||||||
|
static const char *const document_sides[] = {
|
||||||
|
"front","back","identity_page","other_page"};
|
||||||
|
static const char *const field_codes[] = {
|
||||||
|
"document_type","issuing_country","issuing_authority","document_number",
|
||||||
|
"surname","birth_name","given_names","sex_as_printed","nationality",
|
||||||
|
"birth_date","birth_place","issue_date","expiry_date",
|
||||||
|
"address_as_printed","mrz_line_1","mrz_line_2","mrz_line_3"};
|
||||||
|
static gboolean in_vocab(const char *value, const char *const *values,
|
||||||
|
guint count)
|
||||||
|
{
|
||||||
|
for (guint i=0; value != NULL && i<count; i++)
|
||||||
|
if (g_str_equal(value, values[i])) return TRUE;
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
gboolean identity_ocr_document_type_is_valid(const char *v)
|
||||||
|
{ return in_vocab(v, document_types, G_N_ELEMENTS(document_types)); }
|
||||||
|
gboolean identity_ocr_document_side_is_valid(const char *v)
|
||||||
|
{ return in_vocab(v, document_sides, G_N_ELEMENTS(document_sides)); }
|
||||||
|
gboolean identity_ocr_field_code_is_valid(const char *v)
|
||||||
|
{ return in_vocab(v, field_codes, G_N_ELEMENTS(field_codes)); }
|
||||||
|
IdentityFieldObservation *identity_field_observation_new(
|
||||||
|
const char *code, const char *raw, double confidence,
|
||||||
|
const IdentitySourceBox *box, guint order)
|
||||||
|
{
|
||||||
|
if (!identity_ocr_field_code_is_valid(code) || raw == NULL ||
|
||||||
|
raw[0]=='\0' || confidence < -1.0 || confidence > 100.0) return NULL;
|
||||||
|
IdentityFieldObservation *f=g_new0(IdentityFieldObservation,1);
|
||||||
|
f->code=g_strdup(code); f->raw_value=g_strdup(raw);
|
||||||
|
f->confidence=confidence; f->status=IDENTITY_REVIEW_PROPOSED;
|
||||||
|
f->origin=g_strdup("ocr"); f->order=order;
|
||||||
|
if (box != NULL) f->box=*box;
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
IdentityFieldObservation *identity_field_observation_new_manual(
|
||||||
|
const char *code, const char *value, guint order)
|
||||||
|
{
|
||||||
|
if (!identity_ocr_field_code_is_valid(code) || value == NULL ||
|
||||||
|
value[0] == '\0') return NULL;
|
||||||
|
IdentityFieldObservation *field =
|
||||||
|
g_new0(IdentityFieldObservation, 1);
|
||||||
|
field->code = g_strdup(code);
|
||||||
|
field->corrected_value = g_strdup(value);
|
||||||
|
field->confidence = -1.0;
|
||||||
|
field->status = IDENTITY_REVIEW_PROPOSED;
|
||||||
|
field->origin = g_strdup("manual_entry");
|
||||||
|
field->order = order;
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
IdentityFieldObservation *identity_field_observation_copy(
|
||||||
|
const IdentityFieldObservation *f)
|
||||||
|
{
|
||||||
|
if (f==NULL) return NULL;
|
||||||
|
IdentityFieldObservation *c=f->raw_value != NULL
|
||||||
|
? identity_field_observation_new(
|
||||||
|
f->code,f->raw_value,f->confidence,&f->box,f->order)
|
||||||
|
: identity_field_observation_new_manual(
|
||||||
|
f->code,f->corrected_value,f->order);
|
||||||
|
c->corrected_value=g_strdup(f->corrected_value);
|
||||||
|
c->normalized_value=g_strdup(f->normalized_value);
|
||||||
|
c->note=g_strdup(f->note); c->status=f->status;
|
||||||
|
g_free(c->origin); c->origin=g_strdup(f->origin); return c;
|
||||||
|
}
|
||||||
|
void identity_field_observation_free(IdentityFieldObservation *f)
|
||||||
|
{
|
||||||
|
if(f==NULL)return;
|
||||||
|
g_free(f->code);g_free(f->raw_value);
|
||||||
|
g_free(f->corrected_value);g_free(f->normalized_value);g_free(f->note);
|
||||||
|
g_free(f->origin);g_free(f);
|
||||||
|
}
|
||||||
|
gboolean identity_field_observation_accept(IdentityFieldObservation *f)
|
||||||
|
{ if(f==NULL)return FALSE;f->status=IDENTITY_REVIEW_ACCEPTED;return TRUE; }
|
||||||
|
gboolean identity_field_observation_modify(IdentityFieldObservation *f,
|
||||||
|
const char *value,const char *note)
|
||||||
|
{
|
||||||
|
if(f==NULL||value==NULL||value[0]=='\0')return FALSE;
|
||||||
|
g_free(f->corrected_value);f->corrected_value=g_strdup(value);
|
||||||
|
g_free(f->note);f->note=g_strdup(note);g_free(f->origin);
|
||||||
|
f->origin=g_strdup(f->raw_value == NULL
|
||||||
|
? "manual_entry" : "manual_override");
|
||||||
|
f->status=IDENTITY_REVIEW_MODIFIED;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
gboolean identity_field_observation_restore_raw(IdentityFieldObservation *f)
|
||||||
|
{
|
||||||
|
if (f == NULL || f->raw_value == NULL) return FALSE;
|
||||||
|
g_clear_pointer(&f->corrected_value, g_free);
|
||||||
|
g_clear_pointer(&f->note, g_free);
|
||||||
|
g_free(f->origin);
|
||||||
|
f->origin = g_strdup("ocr");
|
||||||
|
f->status = IDENTITY_REVIEW_PROPOSED;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
void identity_field_observation_reject(IdentityFieldObservation *f)
|
||||||
|
{if(f!=NULL)f->status=IDENTITY_REVIEW_REJECTED;}
|
||||||
|
gboolean identity_field_observation_set_origin(IdentityFieldObservation*f,
|
||||||
|
const char*origin)
|
||||||
|
{if(f==NULL||(!g_str_equal(origin,"ocr")&&!g_str_equal(origin,"mrz")&&
|
||||||
|
!g_str_equal(origin,"manual_override")&&!g_str_equal(origin,"manual_entry")))
|
||||||
|
return FALSE;
|
||||||
|
g_free(f->origin);f->origin=g_strdup(origin);return TRUE;}
|
||||||
|
void identity_field_observation_mark_conflict(IdentityFieldObservation*f)
|
||||||
|
{if(f!=NULL)f->status=IDENTITY_REVIEW_CONFLICT;}
|
||||||
|
#define FG(name,type,field,zero) type identity_field_observation_get_##name(\
|
||||||
|
const IdentityFieldObservation*f){return f!=NULL?f->field:zero;}
|
||||||
|
FG(code,const char*,code,NULL) FG(raw_value,const char*,raw_value,NULL)
|
||||||
|
FG(corrected_value,const char*,corrected_value,NULL)
|
||||||
|
FG(origin,const char*,origin,NULL)
|
||||||
|
FG(status,IdentityReviewStatus,status,IDENTITY_REVIEW_PROPOSED)
|
||||||
|
FG(confidence,double,confidence,-1.0)
|
||||||
|
FG(order,guint,order,0)
|
||||||
|
const IdentitySourceBox *identity_field_observation_get_box(
|
||||||
|
const IdentityFieldObservation*f){return f!=NULL?&f->box:NULL;}
|
||||||
|
IdentityOcrRun *identity_ocr_run_new(const char *evidence_id,
|
||||||
|
const char *sha,const char *type,const char *side,guint page,
|
||||||
|
const char *languages,const char *profile)
|
||||||
|
{
|
||||||
|
if(evidence_id==NULL||sha==NULL||strlen(sha)!=64||
|
||||||
|
!identity_ocr_document_type_is_valid(type)||
|
||||||
|
!identity_ocr_document_side_is_valid(side)||page==0||
|
||||||
|
languages==NULL||profile==NULL)return NULL;
|
||||||
|
IdentityOcrRun*r=g_new0(IdentityOcrRun,1);r->id=g_uuid_string_random();
|
||||||
|
r->evidence_id=g_strdup(evidence_id);r->expected_sha256=g_strdup(sha);
|
||||||
|
r->document_type=g_strdup(type);r->document_side=g_strdup(side);
|
||||||
|
r->page=page;r->languages=g_strdup(languages);r->profile=g_strdup(profile);
|
||||||
|
r->fields=g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)identity_field_observation_free);return r;
|
||||||
|
}
|
||||||
|
IdentityOcrRun *identity_ocr_run_copy(const IdentityOcrRun*r)
|
||||||
|
{
|
||||||
|
if(r==NULL)return NULL;
|
||||||
|
IdentityOcrRun*c=identity_ocr_run_new(r->evidence_id,
|
||||||
|
r->expected_sha256,r->document_type,r->document_side,r->page,
|
||||||
|
r->languages,r->profile);g_free(c->id);c->id=g_strdup(r->id);
|
||||||
|
identity_ocr_run_set_outputs(c,r->version,r->available_languages,
|
||||||
|
r->parameters,r->raw_text,r->tsv);
|
||||||
|
identity_ocr_run_set_preview(c,r->preview);
|
||||||
|
identity_ocr_run_set_factual_notes(c,r->factual_notes);
|
||||||
|
if (r->human_transcription)
|
||||||
|
identity_ocr_run_set_corrected_transcription(c,
|
||||||
|
r->corrected_transcription, r->transcription_corrected_at);
|
||||||
|
for(guint i=0;i<r->fields->len;i++)identity_ocr_run_add_field(c,
|
||||||
|
identity_field_observation_copy(g_ptr_array_index(r->fields,i)));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
void identity_ocr_run_free(IdentityOcrRun*r)
|
||||||
|
{
|
||||||
|
if(r==NULL)return;
|
||||||
|
g_free(r->id);g_free(r->evidence_id);g_free(r->expected_sha256);
|
||||||
|
g_free(r->document_type);g_free(r->document_side);g_free(r->languages);
|
||||||
|
g_free(r->profile);g_free(r->version);g_free(r->available_languages);
|
||||||
|
g_free(r->parameters);g_free(r->raw_text);g_free(r->tsv);
|
||||||
|
g_free(r->factual_notes);
|
||||||
|
g_free(r->corrected_transcription);g_free(r->transcription_corrected_at);
|
||||||
|
g_clear_pointer(&r->preview,g_bytes_unref);
|
||||||
|
g_ptr_array_unref(r->fields);g_free(r);
|
||||||
|
}
|
||||||
|
void identity_ocr_run_set_outputs(IdentityOcrRun*r,const char*v,
|
||||||
|
const char*a,const char*p,const char*t,const char*tsv)
|
||||||
|
{if(r==NULL)return;r->version=g_strdup(v);r->available_languages=g_strdup(a);
|
||||||
|
r->parameters=g_strdup(p);r->raw_text=g_strdup(t);r->tsv=g_strdup(tsv);}
|
||||||
|
void identity_ocr_run_add_field(IdentityOcrRun*r,IdentityFieldObservation*f)
|
||||||
|
{if(r!=NULL&&f!=NULL)g_ptr_array_add(r->fields,f);}
|
||||||
|
gboolean identity_ocr_run_replace_evidence_id(
|
||||||
|
IdentityOcrRun *run, const char *evidence_identifier)
|
||||||
|
{
|
||||||
|
if (run == NULL || evidence_identifier == NULL ||
|
||||||
|
evidence_identifier[0] == '\0') return FALSE;
|
||||||
|
g_free(run->evidence_id);
|
||||||
|
run->evidence_id = g_strdup(evidence_identifier);
|
||||||
|
return run->evidence_id != NULL;
|
||||||
|
}
|
||||||
|
gboolean identity_ocr_run_replace_identifier(
|
||||||
|
IdentityOcrRun *run, const char *identifier)
|
||||||
|
{
|
||||||
|
if (run == NULL || identifier == NULL ||
|
||||||
|
!g_uuid_string_is_valid(identifier)) return FALSE;
|
||||||
|
g_free(run->id);
|
||||||
|
run->id = g_strdup(identifier);
|
||||||
|
return run->id != NULL;
|
||||||
|
}
|
||||||
|
void identity_ocr_run_set_factual_notes(IdentityOcrRun*r,const char*notes)
|
||||||
|
{if(r==NULL)return;g_free(r->factual_notes);r->factual_notes=g_strdup(notes);}
|
||||||
|
const char *identity_ocr_run_get_factual_notes(const IdentityOcrRun*r)
|
||||||
|
{return r!=NULL?r->factual_notes:NULL;}
|
||||||
|
gboolean identity_ocr_run_set_corrected_transcription(IdentityOcrRun*r,
|
||||||
|
const char*text,const char*at)
|
||||||
|
{
|
||||||
|
if(r==NULL||text==NULL||!g_utf8_validate(text,-1,NULL)||
|
||||||
|
strlen(text)>1048576||(r->raw_text!=NULL&&r->raw_text[0]!='\0'&&
|
||||||
|
text[0]=='\0'))return FALSE;
|
||||||
|
g_free(r->corrected_transcription);r->corrected_transcription=g_strdup(text);
|
||||||
|
g_free(r->transcription_corrected_at);
|
||||||
|
r->transcription_corrected_at=g_strdup(at);
|
||||||
|
r->human_transcription=TRUE;return TRUE;
|
||||||
|
}
|
||||||
|
void identity_ocr_run_reset_corrected_transcription(IdentityOcrRun*r)
|
||||||
|
{if(r==NULL)return;g_clear_pointer(&r->corrected_transcription,g_free);
|
||||||
|
g_clear_pointer(&r->transcription_corrected_at,g_free);
|
||||||
|
r->human_transcription=FALSE;}
|
||||||
|
const char *identity_ocr_run_get_corrected_transcription(const IdentityOcrRun*r)
|
||||||
|
{return r!=NULL?r->corrected_transcription:NULL;}
|
||||||
|
const char *identity_ocr_run_get_transcription_corrected_at(const IdentityOcrRun*r)
|
||||||
|
{return r!=NULL?r->transcription_corrected_at:NULL;}
|
||||||
|
gboolean identity_ocr_run_has_human_transcription(const IdentityOcrRun*r)
|
||||||
|
{return r!=NULL&&r->human_transcription;}
|
||||||
|
void identity_ocr_run_set_preview(IdentityOcrRun*r,GBytes*b)
|
||||||
|
{if(r==NULL)return;g_clear_pointer(&r->preview,g_bytes_unref);r->preview=b!=NULL?g_bytes_ref(b):NULL;}
|
||||||
|
GBytes *identity_ocr_run_get_preview(const IdentityOcrRun*r)
|
||||||
|
{return r!=NULL?r->preview:NULL;}
|
||||||
|
#define RG(name,type,field,zero) type identity_ocr_run_get_##name(\
|
||||||
|
const IdentityOcrRun*r){return r!=NULL?r->field:zero;}
|
||||||
|
RG(identifier,const char*,id,NULL) RG(evidence_id,const char*,evidence_id,NULL)
|
||||||
|
RG(raw_text,const char*,raw_text,NULL) RG(tsv,const char*,tsv,NULL)
|
||||||
|
RG(document_type,const char*,document_type,NULL)
|
||||||
|
RG(document_side,const char*,document_side,NULL)
|
||||||
|
RG(languages,const char*,languages,NULL) RG(page,guint,page,0)
|
||||||
|
RG(expected_sha256,const char*,expected_sha256,NULL)
|
||||||
|
RG(profile,const char*,profile,NULL) RG(version,const char*,version,NULL)
|
||||||
|
RG(available_languages,const char*,available_languages,NULL)
|
||||||
|
const GPtrArray *identity_ocr_run_get_fields(const IdentityOcrRun*r)
|
||||||
|
{return r!=NULL?r->fields:NULL;}
|
||||||
|
|
@ -88,11 +88,16 @@ PersonEvidenceSelection *person_evidence_selection_copy(
|
||||||
copy, item->record, NULL))
|
copy, item->record, NULL))
|
||||||
person_evidence_selection_set_type(copy, item->identifier,
|
person_evidence_selection_set_type(copy, item->identifier,
|
||||||
item->type_identifier);
|
item->type_identifier);
|
||||||
} else
|
} else {
|
||||||
person_evidence_selection_add_staged(copy, item->source_path,
|
person_evidence_selection_add_staged(copy, item->source_path,
|
||||||
item->staging_path, item->original_name, item->mime_type,
|
item->staging_path, item->original_name, item->mime_type,
|
||||||
item->type_identifier, item->size_bytes, item->sha256,
|
item->type_identifier, item->size_bytes, item->sha256,
|
||||||
item->description, item->prepared_at, NULL);
|
item->description, item->prepared_at, NULL);
|
||||||
|
PersonEvidenceSelectionItem *copied_item =
|
||||||
|
g_ptr_array_index(copy->items, copy->items->len - 1);
|
||||||
|
g_free(copied_item->identifier);
|
||||||
|
copied_item->identifier = g_strdup(item->identifier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (source != NULL && source->active_identifier != NULL) {
|
if (source != NULL && source->active_identifier != NULL) {
|
||||||
PersonEvidenceSelectionItem *active =
|
PersonEvidenceSelectionItem *active =
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
1043
src/views/evidence_identity_ocr_dialog.c
Normal file
1043
src/views/evidence_identity_ocr_dialog.c
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -5,6 +5,9 @@
|
||||||
|
|
||||||
#include "views/evidence_import_dialog.h"
|
#include "views/evidence_import_dialog.h"
|
||||||
#include "models/evidence_type.h"
|
#include "models/evidence_type.h"
|
||||||
|
#include "core/file_hash.h"
|
||||||
|
#include "core/task_manager.h"
|
||||||
|
#include "widgets/evidence_preview_widget.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
|
@ -33,6 +36,8 @@ typedef struct
|
||||||
GtkEntry *source_entry;
|
GtkEntry *source_entry;
|
||||||
GtkTextView *description_text_view;
|
GtkTextView *description_text_view;
|
||||||
GtkLabel *error_label;
|
GtkLabel *error_label;
|
||||||
|
TaskManager *preview_task_manager;
|
||||||
|
EvidencePreviewWidget *preview;
|
||||||
|
|
||||||
char *file_path;
|
char *file_path;
|
||||||
GPtrArray *type_codes;
|
GPtrArray *type_codes;
|
||||||
|
|
@ -391,6 +396,8 @@ static void evidence_import_dialog_state_free(
|
||||||
g_ptr_array_unref(
|
g_ptr_array_unref(
|
||||||
state->type_codes
|
state->type_codes
|
||||||
);
|
);
|
||||||
|
g_clear_pointer(&state->preview, evidence_preview_widget_free);
|
||||||
|
g_clear_pointer(&state->preview_task_manager, task_manager_free);
|
||||||
|
|
||||||
g_free(
|
g_free(
|
||||||
state
|
state
|
||||||
|
|
@ -752,6 +759,7 @@ gboolean evidence_import_dialog_present(
|
||||||
GtkWidget *button_box = NULL;
|
GtkWidget *button_box = NULL;
|
||||||
GtkWidget *cancel_button = NULL;
|
GtkWidget *cancel_button = NULL;
|
||||||
GtkWidget *import_button = NULL;
|
GtkWidget *import_button = NULL;
|
||||||
|
GtkWidget *paned = NULL;
|
||||||
|
|
||||||
GDateTime *current_date_time = NULL;
|
GDateTime *current_date_time = NULL;
|
||||||
char *current_date_time_iso8601 = NULL;
|
char *current_date_time_iso8601 = NULL;
|
||||||
|
|
@ -971,6 +979,13 @@ gboolean evidence_import_dialog_present(
|
||||||
parent
|
parent
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gtk_window_set_application(
|
||||||
|
state->window,
|
||||||
|
gtk_window_get_application(
|
||||||
|
parent
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
gtk_window_set_modal(
|
gtk_window_set_modal(
|
||||||
state->window,
|
state->window,
|
||||||
TRUE
|
TRUE
|
||||||
|
|
@ -983,8 +998,8 @@ gboolean evidence_import_dialog_present(
|
||||||
|
|
||||||
gtk_window_set_default_size(
|
gtk_window_set_default_size(
|
||||||
state->window,
|
state->window,
|
||||||
620,
|
1200,
|
||||||
520
|
800
|
||||||
);
|
);
|
||||||
|
|
||||||
root_box =
|
root_box =
|
||||||
|
|
@ -1396,10 +1411,24 @@ gboolean evidence_import_dialog_present(
|
||||||
button_box
|
button_box
|
||||||
);
|
);
|
||||||
|
|
||||||
gtk_window_set_child(
|
paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||||
state->window,
|
gtk_widget_set_name(paned, "evidence-import-paned");
|
||||||
root_box
|
gtk_paned_set_start_child(GTK_PANED(paned), root_box);
|
||||||
);
|
gtk_widget_set_size_request(root_box, 320, -1);
|
||||||
|
state->preview_task_manager = task_manager_new();
|
||||||
|
state->preview = evidence_preview_widget_new(
|
||||||
|
state->preview_task_manager, NULL, NULL);
|
||||||
|
if (state->preview == NULL) {
|
||||||
|
gtk_window_destroy(state->window);
|
||||||
|
evidence_import_dialog_state_free(state);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
gtk_paned_set_end_child(GTK_PANED(paned),
|
||||||
|
evidence_preview_widget_get_widget(state->preview));
|
||||||
|
gtk_paned_set_position(GTK_PANED(paned), 420);
|
||||||
|
gtk_paned_set_resize_start_child(GTK_PANED(paned), FALSE);
|
||||||
|
gtk_paned_set_resize_end_child(GTK_PANED(paned), TRUE);
|
||||||
|
gtk_window_set_child(state->window, paned);
|
||||||
|
|
||||||
g_object_set_data_full(
|
g_object_set_data_full(
|
||||||
G_OBJECT(
|
G_OBJECT(
|
||||||
|
|
@ -1419,6 +1448,35 @@ gboolean evidence_import_dialog_present(
|
||||||
state
|
state
|
||||||
);
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
char *directory = g_path_get_dirname(state->file_path);
|
||||||
|
char *basename = g_path_get_basename(state->file_path);
|
||||||
|
char *sha256 = NULL;
|
||||||
|
guint64 size = 0U;
|
||||||
|
gboolean uncertain = FALSE;
|
||||||
|
char *content_type = g_content_type_guess(
|
||||||
|
state->file_path, NULL, 0U, &uncertain);
|
||||||
|
char *mime = content_type != NULL
|
||||||
|
? g_content_type_get_mime_type(content_type) : NULL;
|
||||||
|
if (file_hash_compute_sha256(
|
||||||
|
state->file_path, NULL, &sha256, &size, NULL)) {
|
||||||
|
EvidencePreviewRequest *request = evidence_preview_request_new(
|
||||||
|
directory, "00000000-0000-4000-8000-000000000001",
|
||||||
|
basename, sha256,
|
||||||
|
mime != NULL ? mime : "application/octet-stream", 1U);
|
||||||
|
evidence_preview_widget_show(
|
||||||
|
state->preview, request, "Aperçu SPECIMEN contrôlé");
|
||||||
|
evidence_preview_request_free(request);
|
||||||
|
}
|
||||||
|
g_free(mime);
|
||||||
|
g_free(content_type);
|
||||||
|
g_free(sha256);
|
||||||
|
g_free(basename);
|
||||||
|
g_free(directory);
|
||||||
|
(void) size;
|
||||||
|
(void) uncertain;
|
||||||
|
}
|
||||||
|
|
||||||
g_signal_connect(
|
g_signal_connect(
|
||||||
cancel_button,
|
cancel_button,
|
||||||
"clicked",
|
"clicked",
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@
|
||||||
#include "views/evidence_metadata_dialog.h"
|
#include "views/evidence_metadata_dialog.h"
|
||||||
|
|
||||||
#include "models/evidence_type.h"
|
#include "models/evidence_type.h"
|
||||||
|
#include "dao/identity_ocr_dao.h"
|
||||||
|
#include "views/evidence_identity_ocr_dialog.h"
|
||||||
|
#include "widgets/ocr_provenance_overlay.h"
|
||||||
|
|
||||||
struct EvidenceMetadataDialogResult
|
struct EvidenceMetadataDialogResult
|
||||||
{
|
{
|
||||||
|
|
@ -23,10 +26,45 @@ typedef struct
|
||||||
GtkStringList *type_labels;
|
GtkStringList *type_labels;
|
||||||
GPtrArray *type_identifiers;
|
GPtrArray *type_identifiers;
|
||||||
EvidenceMetadataDialogCallback callback;
|
EvidenceMetadataDialogCallback callback;
|
||||||
|
EvidenceMetadataDialogAnalyzeCallback analyze_callback;
|
||||||
|
char *evidence_identifier;
|
||||||
|
char *investigation_root;
|
||||||
|
GPtrArray *ocr_runs;
|
||||||
|
GPtrArray *ocr_documents;
|
||||||
|
GPtrArray *ocr_run_identifiers;
|
||||||
|
GtkStringList *ocr_run_labels;
|
||||||
|
GtkDropDown *ocr_run_dropdown;
|
||||||
|
GtkLabel *ocr_summary;
|
||||||
|
GtkTextBuffer *ocr_raw_buffer;
|
||||||
|
GtkTextBuffer *ocr_corrected_buffer;
|
||||||
|
GtkBox *ocr_details;
|
||||||
|
GtkButton *ocr_revise_button;
|
||||||
|
OcrProvenanceOverlay *ocr_overlay;
|
||||||
|
gulong ocr_selection_handler;
|
||||||
|
guint64 ocr_generation;
|
||||||
gpointer user_data;
|
gpointer user_data;
|
||||||
gboolean completed;
|
gboolean completed;
|
||||||
} EvidenceMetadataDialogContext;
|
} EvidenceMetadataDialogContext;
|
||||||
|
|
||||||
|
static void evidence_metadata_dialog_complete(
|
||||||
|
EvidenceMetadataDialogContext *context,
|
||||||
|
EvidenceMetadataDialogResult *result
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @brief Ferme le dialogue avec Échap sans enregistrer. */
|
||||||
|
static gboolean evidence_metadata_dialog_on_key_pressed(
|
||||||
|
GtkEventControllerKey *controller, guint keyval, guint keycode,
|
||||||
|
GdkModifierType state, gpointer user_data
|
||||||
|
)
|
||||||
|
{
|
||||||
|
(void) controller;
|
||||||
|
(void) keycode;
|
||||||
|
(void) state;
|
||||||
|
if (keyval != GDK_KEY_Escape) return FALSE;
|
||||||
|
evidence_metadata_dialog_complete(user_data, NULL);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
/** @brief Copie un texte facultatif et transforme une chaîne vide en NULL. */
|
/** @brief Copie un texte facultatif et transforme une chaîne vide en NULL. */
|
||||||
static char *evidence_metadata_dialog_copy_optional(const char *text)
|
static char *evidence_metadata_dialog_copy_optional(const char *text)
|
||||||
{
|
{
|
||||||
|
|
@ -43,9 +81,39 @@ static void evidence_metadata_dialog_context_free(gpointer data)
|
||||||
if (context == NULL) return;
|
if (context == NULL) return;
|
||||||
g_clear_pointer(&context->type_identifiers, g_ptr_array_unref);
|
g_clear_pointer(&context->type_identifiers, g_ptr_array_unref);
|
||||||
g_clear_object(&context->type_labels);
|
g_clear_object(&context->type_labels);
|
||||||
|
g_clear_pointer(&context->ocr_runs, g_ptr_array_unref);
|
||||||
|
g_clear_pointer(&context->ocr_documents, g_ptr_array_unref);
|
||||||
|
g_clear_pointer(&context->ocr_run_identifiers, g_ptr_array_unref);
|
||||||
|
g_clear_object(&context->ocr_run_labels);
|
||||||
|
g_free(context->investigation_root);
|
||||||
|
g_free(context->evidence_identifier);
|
||||||
g_free(context);
|
g_free(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void evidence_metadata_dialog_on_analyze(
|
||||||
|
GtkButton *button, gpointer user_data)
|
||||||
|
{
|
||||||
|
EvidenceMetadataDialogContext *context = user_data;
|
||||||
|
(void) button;
|
||||||
|
if (context != NULL && context->analyze_callback != NULL) {
|
||||||
|
gboolean revise=GPOINTER_TO_INT(g_object_get_data(
|
||||||
|
G_OBJECT(button),"revise-existing"));
|
||||||
|
const char *run_identifier = NULL;
|
||||||
|
if (revise && context->ocr_run_dropdown != NULL) {
|
||||||
|
guint selected = gtk_drop_down_get_selected(
|
||||||
|
context->ocr_run_dropdown);
|
||||||
|
if (selected < context->ocr_run_identifiers->len)
|
||||||
|
run_identifier = g_ptr_array_index(
|
||||||
|
context->ocr_run_identifiers, selected);
|
||||||
|
}
|
||||||
|
if (!revise || run_identifier != NULL)
|
||||||
|
context->analyze_callback(context->window,
|
||||||
|
context->evidence_identifier, revise, run_identifier,
|
||||||
|
context->user_data);
|
||||||
|
evidence_metadata_dialog_complete(context, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @brief Détruit la fenêtre hors de la pile du signal GTK courant. */
|
/** @brief Détruit la fenêtre hors de la pile du signal GTK courant. */
|
||||||
static gboolean evidence_metadata_dialog_destroy_idle(gpointer data)
|
static gboolean evidence_metadata_dialog_destroy_idle(gpointer data)
|
||||||
{
|
{
|
||||||
|
|
@ -61,15 +129,19 @@ static void evidence_metadata_dialog_complete(
|
||||||
EvidenceMetadataDialogResult *result
|
EvidenceMetadataDialogResult *result
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
EvidenceMetadataDialogCallback callback = NULL;
|
||||||
|
gpointer callback_data = NULL;
|
||||||
if (context == NULL || context->completed) return;
|
if (context == NULL || context->completed) return;
|
||||||
context->completed = TRUE;
|
context->completed = TRUE;
|
||||||
if (context->callback != NULL) context->callback(result, context->user_data);
|
callback = context->callback;
|
||||||
else evidence_metadata_dialog_result_free(result);
|
callback_data = context->user_data;
|
||||||
gtk_widget_set_visible(GTK_WIDGET(context->window), FALSE);
|
gtk_widget_set_visible(GTK_WIDGET(context->window), FALSE);
|
||||||
g_idle_add(
|
g_idle_add(
|
||||||
evidence_metadata_dialog_destroy_idle,
|
evidence_metadata_dialog_destroy_idle,
|
||||||
g_object_ref(context->window)
|
g_object_ref(context->window)
|
||||||
);
|
);
|
||||||
|
if (callback != NULL) callback(result, callback_data);
|
||||||
|
else evidence_metadata_dialog_result_free(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Annule la modification. */
|
/** @brief Annule la modification. */
|
||||||
|
|
@ -117,10 +189,281 @@ static void evidence_metadata_dialog_on_save(
|
||||||
evidence_metadata_dialog_complete(context, result);
|
evidence_metadata_dialog_complete(context, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean evidence_metadata_dialog_present(
|
static void evidence_metadata_dialog_clear_box(GtkBox *box)
|
||||||
|
{
|
||||||
|
GtkWidget *child = gtk_widget_get_first_child(GTK_WIDGET(box));
|
||||||
|
while (child != NULL) {
|
||||||
|
GtkWidget *next = gtk_widget_get_next_sibling(child);
|
||||||
|
gtk_box_remove(box, child);
|
||||||
|
child = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static IdentityFieldObservation *evidence_metadata_dialog_field_model(
|
||||||
|
const IdentityFieldObservationRecord *record)
|
||||||
|
{
|
||||||
|
IdentitySourceBox box = {
|
||||||
|
.page = (gint) record->page_number,
|
||||||
|
.x = (gint) record->source_x,
|
||||||
|
.y = (gint) record->source_y,
|
||||||
|
.width = (gint) record->source_width,
|
||||||
|
.height = (gint) record->source_height,
|
||||||
|
.image_width = (gint) record->source_image_width,
|
||||||
|
.image_height = (gint) record->source_image_height,
|
||||||
|
.available = record->has_source_box
|
||||||
|
};
|
||||||
|
IdentityFieldObservation *field = record->raw_value != NULL
|
||||||
|
? identity_field_observation_new(record->field_code,
|
||||||
|
record->raw_value,
|
||||||
|
record->has_confidence ? record->confidence : -1.0,
|
||||||
|
&box, (guint) record->display_order)
|
||||||
|
: identity_field_observation_new_manual(record->field_code,
|
||||||
|
record->corrected_value, (guint) record->display_order);
|
||||||
|
if (field != NULL)
|
||||||
|
identity_field_observation_set_origin(field, record->origin);
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void evidence_metadata_dialog_show_region(
|
||||||
|
GtkButton *button, gpointer user_data)
|
||||||
|
{
|
||||||
|
EvidenceMetadataDialogContext *context = user_data;
|
||||||
|
IdentityFieldObservationRecord *record = g_object_get_data(
|
||||||
|
G_OBJECT(button), "ocr-field-record");
|
||||||
|
IdentityFieldObservation *field =
|
||||||
|
evidence_metadata_dialog_field_model(record);
|
||||||
|
context->ocr_generation++;
|
||||||
|
ocr_provenance_overlay_set_field(
|
||||||
|
context->ocr_overlay, field, context->ocr_generation);
|
||||||
|
identity_field_observation_free(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void evidence_metadata_dialog_render_ocr(
|
||||||
|
EvidenceMetadataDialogContext *context)
|
||||||
|
{
|
||||||
|
guint selected = gtk_drop_down_get_selected(context->ocr_run_dropdown);
|
||||||
|
evidence_metadata_dialog_clear_box(context->ocr_details);
|
||||||
|
context->ocr_generation++;
|
||||||
|
ocr_provenance_overlay_clear(
|
||||||
|
context->ocr_overlay, context->ocr_generation);
|
||||||
|
gtk_text_buffer_set_text(context->ocr_raw_buffer, "", -1);
|
||||||
|
gtk_text_buffer_set_text(context->ocr_corrected_buffer, "", -1);
|
||||||
|
gtk_label_set_text(context->ocr_summary,
|
||||||
|
"Aucune analyse OCR sélectionnée.");
|
||||||
|
if (context->ocr_revise_button != NULL)
|
||||||
|
gtk_widget_set_sensitive(
|
||||||
|
GTK_WIDGET(context->ocr_revise_button), FALSE);
|
||||||
|
if (selected >= context->ocr_runs->len ||
|
||||||
|
selected >= context->ocr_run_identifiers->len) return;
|
||||||
|
|
||||||
|
IdentityOcrRunRecord *run = g_ptr_array_index(
|
||||||
|
context->ocr_runs, selected);
|
||||||
|
const char *selected_identifier = g_ptr_array_index(
|
||||||
|
context->ocr_run_identifiers, selected);
|
||||||
|
if (g_strcmp0(run->id, selected_identifier) != 0) return;
|
||||||
|
char *summary = g_strdup_printf(
|
||||||
|
"Run actif : %s\nUTC : %s\nMoteur : %s %s\n"
|
||||||
|
"Langues : %s\nDocument : %s — %s — page %" G_GINT64_FORMAT
|
||||||
|
"\nStatut : %s\nSHA-256 preuve : %s\n"
|
||||||
|
"Artefact texte : %s\nSHA-256 texte : %s\n"
|
||||||
|
"Artefact TSV : %s\nSHA-256 TSV : %s",
|
||||||
|
run->id, run->executed_at, run->engine, run->engine_version,
|
||||||
|
run->requested_languages, run->document_type, run->document_side,
|
||||||
|
run->page_number, run->status, run->expected_sha256,
|
||||||
|
run->text_relative_path, run->text_sha256,
|
||||||
|
run->tsv_relative_path, run->tsv_sha256);
|
||||||
|
gtk_label_set_text(context->ocr_summary, summary);
|
||||||
|
g_free(summary);
|
||||||
|
char *raw_path = g_build_filename(context->investigation_root,
|
||||||
|
run->text_relative_path, NULL);
|
||||||
|
char *raw = NULL;
|
||||||
|
if (!g_file_get_contents(raw_path, &raw, NULL, NULL))
|
||||||
|
raw = g_strdup("Artefact OCR brut indisponible");
|
||||||
|
gtk_text_buffer_set_text(context->ocr_raw_buffer, raw, -1);
|
||||||
|
gtk_text_buffer_set_text(context->ocr_corrected_buffer,
|
||||||
|
run->corrected_transcription != NULL
|
||||||
|
? run->corrected_transcription
|
||||||
|
: "Aucune transcription corrigée", -1);
|
||||||
|
g_free(raw_path);
|
||||||
|
g_free(raw);
|
||||||
|
|
||||||
|
IdentityDocumentObservationRecord *document = NULL;
|
||||||
|
for (guint index = 0; index < context->ocr_documents->len; index++) {
|
||||||
|
IdentityDocumentObservationRecord *candidate =
|
||||||
|
g_ptr_array_index(context->ocr_documents, index);
|
||||||
|
if (g_strcmp0(candidate->ocr_run_id, run->id) == 0) {
|
||||||
|
document = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (document == NULL) {
|
||||||
|
gtk_box_append(context->ocr_details,
|
||||||
|
gtk_label_new("Observation OCR persistée indisponible."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char *description = g_strdup_printf(
|
||||||
|
"Observation %s\nPersonne liée : %s\nDocument : %s — %s — "
|
||||||
|
"page %" G_GINT64_FORMAT "\nÉtat : %s\nNotes : %s",
|
||||||
|
document->id,
|
||||||
|
document->person_id != NULL ? document->person_id
|
||||||
|
: "Personne indisponible",
|
||||||
|
document->document_type, document->document_side,
|
||||||
|
document->page_number, document->review_state,
|
||||||
|
document->factual_notes != NULL ? document->factual_notes
|
||||||
|
: "Aucune note");
|
||||||
|
GtkWidget *document_label = gtk_label_new(description);
|
||||||
|
gtk_widget_set_name(document_label, "evidence-ocr-observation");
|
||||||
|
gtk_label_set_wrap(GTK_LABEL(document_label), TRUE);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(document_label), 0.0f);
|
||||||
|
gtk_box_append(context->ocr_details, document_label);
|
||||||
|
g_free(description);
|
||||||
|
|
||||||
|
IdentityOcrDao *dao = g_object_get_data(
|
||||||
|
G_OBJECT(context->window), "identity-ocr-dao");
|
||||||
|
GError *error = NULL;
|
||||||
|
GPtrArray *fields = identity_ocr_dao_list_fields_by_document(
|
||||||
|
dao, document->id, &error);
|
||||||
|
for (guint index = 0; fields != NULL && index < fields->len; index++) {
|
||||||
|
IdentityFieldObservationRecord *field =
|
||||||
|
g_ptr_array_index(fields, index);
|
||||||
|
char *field_text = g_strdup_printf(
|
||||||
|
"%s — brut : %s — corrigé : %s — statut : %s — "
|
||||||
|
"origine : %s — page %" G_GINT64_FORMAT,
|
||||||
|
field->field_code,
|
||||||
|
field->raw_value != NULL ? field->raw_value : "absent",
|
||||||
|
field->corrected_value != NULL
|
||||||
|
? field->corrected_value : "absente",
|
||||||
|
field->review_status, field->origin, field->page_number);
|
||||||
|
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
|
GtkWidget *label = gtk_label_new(field_text);
|
||||||
|
gtk_label_set_wrap(GTK_LABEL(label), TRUE);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
|
||||||
|
gtk_widget_set_hexpand(label, TRUE);
|
||||||
|
gtk_box_append(GTK_BOX(row), label);
|
||||||
|
GtkWidget *show = gtk_button_new_with_label("Voir la zone");
|
||||||
|
gtk_widget_set_sensitive(show, field->has_source_box);
|
||||||
|
g_object_set_data(G_OBJECT(show), "ocr-field-record", field);
|
||||||
|
g_signal_connect(show, "clicked",
|
||||||
|
G_CALLBACK(evidence_metadata_dialog_show_region), context);
|
||||||
|
gtk_box_append(GTK_BOX(row), show);
|
||||||
|
gtk_box_append(context->ocr_details, row);
|
||||||
|
g_free(field_text);
|
||||||
|
}
|
||||||
|
g_object_set_data_full(G_OBJECT(context->ocr_details),
|
||||||
|
"active-ocr-fields", fields, fields != NULL
|
||||||
|
? (GDestroyNotify) g_ptr_array_unref : NULL);
|
||||||
|
g_clear_error(&error);
|
||||||
|
if (context->ocr_revise_button != NULL)
|
||||||
|
gtk_widget_set_sensitive(
|
||||||
|
GTK_WIDGET(context->ocr_revise_button), TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void evidence_metadata_dialog_ocr_selected(
|
||||||
|
GObject *object, GParamSpec *spec, gpointer user_data)
|
||||||
|
{
|
||||||
|
(void) object;
|
||||||
|
(void) spec;
|
||||||
|
evidence_metadata_dialog_render_ocr(user_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWidget *evidence_metadata_dialog_build_ocr(
|
||||||
|
EvidenceMetadataDialogContext *context, Database *database,
|
||||||
|
const char *root, const EvidenceRecord *record)
|
||||||
|
{
|
||||||
|
if (database == NULL || root == NULL) return NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
IdentityOcrDao *dao = identity_ocr_dao_new(database);
|
||||||
|
context->ocr_runs = identity_ocr_dao_list_runs_by_evidence(
|
||||||
|
dao, evidence_record_get_identifier(record), &error);
|
||||||
|
context->ocr_documents = identity_ocr_dao_list_documents_by_evidence(
|
||||||
|
dao, evidence_record_get_identifier(record), &error);
|
||||||
|
if (context->ocr_runs == NULL || context->ocr_runs->len == 0 ||
|
||||||
|
context->ocr_documents == NULL) {
|
||||||
|
g_clear_error(&error);
|
||||||
|
identity_ocr_dao_free(dao);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
g_object_set_data_full(G_OBJECT(context->window), "identity-ocr-dao",
|
||||||
|
dao, (GDestroyNotify) identity_ocr_dao_free);
|
||||||
|
context->investigation_root = g_strdup(root);
|
||||||
|
context->ocr_run_identifiers =
|
||||||
|
g_ptr_array_new_with_free_func(g_free);
|
||||||
|
context->ocr_run_labels = gtk_string_list_new(NULL);
|
||||||
|
for (guint index = 0; index < context->ocr_runs->len; index++) {
|
||||||
|
IdentityOcrRunRecord *run =
|
||||||
|
g_ptr_array_index(context->ocr_runs, index);
|
||||||
|
char *short_id = g_strndup(run->id, 8);
|
||||||
|
char *label = g_strdup_printf(
|
||||||
|
"%s — %s — %s — %s/%s — p.%" G_GINT64_FORMAT
|
||||||
|
" — %s — %s",
|
||||||
|
run->executed_at, run->engine, run->requested_languages,
|
||||||
|
run->document_type, run->document_side, run->page_number,
|
||||||
|
run->status, short_id);
|
||||||
|
gtk_string_list_append(context->ocr_run_labels, label);
|
||||||
|
g_ptr_array_add(context->ocr_run_identifiers, g_strdup(run->id));
|
||||||
|
g_free(short_id);
|
||||||
|
g_free(label);
|
||||||
|
}
|
||||||
|
GtkWidget *section = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||||
|
gtk_widget_set_name(section, "evidence-ocr-readonly");
|
||||||
|
gtk_box_append(GTK_BOX(section), gtk_label_new(
|
||||||
|
"Analyse OCR d’identité"));
|
||||||
|
context->ocr_run_dropdown = GTK_DROP_DOWN(gtk_drop_down_new(
|
||||||
|
G_LIST_MODEL(g_object_ref(context->ocr_run_labels)), NULL));
|
||||||
|
gtk_widget_set_name(GTK_WIDGET(context->ocr_run_dropdown),
|
||||||
|
"evidence-ocr-run-selector");
|
||||||
|
gtk_box_append(GTK_BOX(section),
|
||||||
|
GTK_WIDGET(context->ocr_run_dropdown));
|
||||||
|
context->ocr_summary = GTK_LABEL(gtk_label_new(NULL));
|
||||||
|
gtk_widget_set_name(GTK_WIDGET(context->ocr_summary),
|
||||||
|
"evidence-ocr-active-run");
|
||||||
|
gtk_label_set_wrap(context->ocr_summary, TRUE);
|
||||||
|
gtk_label_set_xalign(context->ocr_summary, 0.0f);
|
||||||
|
gtk_box_append(GTK_BOX(section), GTK_WIDGET(context->ocr_summary));
|
||||||
|
GtkWidget *raw = gtk_text_view_new();
|
||||||
|
gtk_widget_set_name(raw, "evidence-ocr-raw");
|
||||||
|
gtk_text_view_set_editable(GTK_TEXT_VIEW(raw), FALSE);
|
||||||
|
context->ocr_raw_buffer =
|
||||||
|
gtk_text_view_get_buffer(GTK_TEXT_VIEW(raw));
|
||||||
|
gtk_box_append(GTK_BOX(section), gtk_label_new("Texte OCR brut"));
|
||||||
|
gtk_box_append(GTK_BOX(section), raw);
|
||||||
|
GtkWidget *corrected = gtk_text_view_new();
|
||||||
|
gtk_widget_set_name(corrected, "evidence-ocr-corrected");
|
||||||
|
gtk_text_view_set_editable(GTK_TEXT_VIEW(corrected), FALSE);
|
||||||
|
context->ocr_corrected_buffer =
|
||||||
|
gtk_text_view_get_buffer(GTK_TEXT_VIEW(corrected));
|
||||||
|
gtk_box_append(GTK_BOX(section),
|
||||||
|
gtk_label_new("Transcription corrigée"));
|
||||||
|
gtk_box_append(GTK_BOX(section), corrected);
|
||||||
|
context->ocr_details = GTK_BOX(gtk_box_new(
|
||||||
|
GTK_ORIENTATION_VERTICAL, 6));
|
||||||
|
gtk_widget_set_name(GTK_WIDGET(context->ocr_details),
|
||||||
|
"evidence-ocr-details");
|
||||||
|
gtk_box_append(GTK_BOX(section), GTK_WIDGET(context->ocr_details));
|
||||||
|
context->ocr_overlay = ocr_provenance_overlay_new();
|
||||||
|
g_object_set_data_full(G_OBJECT(section), "ocr-readonly-overlay",
|
||||||
|
context->ocr_overlay, (GDestroyNotify) ocr_provenance_overlay_free);
|
||||||
|
gtk_box_append(GTK_BOX(section),
|
||||||
|
ocr_provenance_overlay_get_widget(context->ocr_overlay));
|
||||||
|
context->ocr_selection_handler = g_signal_connect(
|
||||||
|
context->ocr_run_dropdown, "notify::selected",
|
||||||
|
G_CALLBACK(evidence_metadata_dialog_ocr_selected), context);
|
||||||
|
/*
|
||||||
|
* Politique d'ouverture et de rafraîchissement : le dernier run inséré
|
||||||
|
* devient actif. La révision, elle, lit toujours l'UUID de la sélection
|
||||||
|
* courante et ne déduit jamais un run de cette politique.
|
||||||
|
*/
|
||||||
|
gtk_drop_down_set_selected(context->ocr_run_dropdown,
|
||||||
|
context->ocr_runs->len - 1);
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean evidence_metadata_dialog_present_with_ocr(
|
||||||
GtkWindow *parent, const EvidenceRecord *record,
|
GtkWindow *parent, const EvidenceRecord *record,
|
||||||
const GPtrArray *evidence_types, EvidenceMetadataDialogCallback callback,
|
const GPtrArray *evidence_types, Database *database,
|
||||||
gpointer user_data
|
const char *investigation_root,
|
||||||
|
EvidenceMetadataDialogAnalyzeCallback analyze_callback,
|
||||||
|
EvidenceMetadataDialogCallback callback, gpointer user_data
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
EvidenceMetadataDialogContext *context = NULL;
|
EvidenceMetadataDialogContext *context = NULL;
|
||||||
|
|
@ -133,6 +476,9 @@ gboolean evidence_metadata_dialog_present(
|
||||||
context = g_new0(EvidenceMetadataDialogContext, 1);
|
context = g_new0(EvidenceMetadataDialogContext, 1);
|
||||||
context->window = GTK_WINDOW(gtk_window_new());
|
context->window = GTK_WINDOW(gtk_window_new());
|
||||||
context->callback = callback; context->user_data = user_data;
|
context->callback = callback; context->user_data = user_data;
|
||||||
|
context->analyze_callback = analyze_callback;
|
||||||
|
context->evidence_identifier =
|
||||||
|
g_strdup(evidence_record_get_identifier(record));
|
||||||
context->type_identifiers = g_ptr_array_new_with_free_func(g_free);
|
context->type_identifiers = g_ptr_array_new_with_free_func(g_free);
|
||||||
labels = gtk_string_list_new(NULL);
|
labels = gtk_string_list_new(NULL);
|
||||||
context->type_labels = labels;
|
context->type_labels = labels;
|
||||||
|
|
@ -149,7 +495,11 @@ gboolean evidence_metadata_dialog_present(
|
||||||
gtk_window_set_default_size(context->window, 560, 420);
|
gtk_window_set_default_size(context->window, 560, 420);
|
||||||
gtk_window_set_modal(context->window, TRUE);
|
gtk_window_set_modal(context->window, TRUE);
|
||||||
gtk_window_set_destroy_with_parent(context->window, TRUE);
|
gtk_window_set_destroy_with_parent(context->window, TRUE);
|
||||||
if (parent != NULL) gtk_window_set_transient_for(context->window, parent);
|
if (parent != NULL) {
|
||||||
|
gtk_window_set_application(context->window,
|
||||||
|
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);
|
||||||
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||||
|
|
@ -158,7 +508,7 @@ gboolean evidence_metadata_dialog_present(
|
||||||
gtk_box_append(GTK_BOX(main_box), gtk_label_new(
|
gtk_box_append(GTK_BOX(main_box), gtk_label_new(
|
||||||
"Le fichier, son UUID, sa taille, sa date d'import et son SHA-256 resteront inchangés."));
|
"Le fichier, son UUID, sa taille, sa date d'import et son SHA-256 resteront inchangés."));
|
||||||
context->type_dropdown = GTK_DROP_DOWN(gtk_drop_down_new(
|
context->type_dropdown = GTK_DROP_DOWN(gtk_drop_down_new(
|
||||||
G_LIST_MODEL(labels), NULL));
|
G_LIST_MODEL(g_object_ref(labels)), NULL));
|
||||||
gtk_drop_down_set_selected(context->type_dropdown, selected);
|
gtk_drop_down_set_selected(context->type_dropdown, selected);
|
||||||
gtk_box_append(GTK_BOX(main_box), gtk_label_new("Type de preuve"));
|
gtk_box_append(GTK_BOX(main_box), gtk_label_new("Type de preuve"));
|
||||||
gtk_box_append(GTK_BOX(main_box), GTK_WIDGET(context->type_dropdown));
|
gtk_box_append(GTK_BOX(main_box), GTK_WIDGET(context->type_dropdown));
|
||||||
|
|
@ -179,15 +529,51 @@ gboolean evidence_metadata_dialog_present(
|
||||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(description_scroll), description_view);
|
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(description_scroll), description_view);
|
||||||
gtk_box_append(GTK_BOX(main_box), gtk_label_new("Description"));
|
gtk_box_append(GTK_BOX(main_box), gtk_label_new("Description"));
|
||||||
gtk_box_append(GTK_BOX(main_box), description_scroll);
|
gtk_box_append(GTK_BOX(main_box), description_scroll);
|
||||||
|
GtkWidget *ocr = evidence_metadata_dialog_build_ocr(
|
||||||
|
context, database, investigation_root, record);
|
||||||
|
if (ocr != NULL) {
|
||||||
|
GtkWidget *ocr_scroll = gtk_scrolled_window_new();
|
||||||
|
gtk_widget_set_size_request(ocr_scroll, -1, 260);
|
||||||
|
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(ocr_scroll), ocr);
|
||||||
|
gtk_box_append(GTK_BOX(main_box), ocr_scroll);
|
||||||
|
}
|
||||||
buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||||
gtk_widget_set_halign(buttons, GTK_ALIGN_END);
|
gtk_widget_set_halign(buttons, GTK_ALIGN_END);
|
||||||
cancel = gtk_button_new_with_label("Annuler");
|
cancel = gtk_button_new_with_label("Annuler");
|
||||||
save = gtk_button_new_with_label("Enregistrer");
|
save = gtk_button_new_with_label("Enregistrer");
|
||||||
|
if (analyze_callback != NULL && ocr != NULL) {
|
||||||
|
GtkWidget *revise = gtk_button_new_with_label(
|
||||||
|
"Réviser l’analyse OCR");
|
||||||
|
context->ocr_revise_button = GTK_BUTTON(revise);
|
||||||
|
gtk_widget_set_name(revise, "revise-existing-identity-ocr");
|
||||||
|
g_object_set_data(G_OBJECT(revise),"revise-existing",
|
||||||
|
GINT_TO_POINTER(TRUE));
|
||||||
|
g_signal_connect(revise, "clicked",
|
||||||
|
G_CALLBACK(evidence_metadata_dialog_on_analyze), context);
|
||||||
|
gtk_box_append(GTK_BOX(buttons), revise);
|
||||||
|
evidence_metadata_dialog_render_ocr(context);
|
||||||
|
}
|
||||||
|
if (analyze_callback != NULL &&
|
||||||
|
evidence_identity_ocr_dialog_file_is_compatible(
|
||||||
|
evidence_record_get_original_name(record))) {
|
||||||
|
GtkWidget *analyze = gtk_button_new_with_label(
|
||||||
|
"Relancer une nouvelle analyse OCR");
|
||||||
|
gtk_widget_set_name(analyze, "analyze-existing-identity");
|
||||||
|
g_signal_connect(analyze, "clicked",
|
||||||
|
G_CALLBACK(evidence_metadata_dialog_on_analyze), context);
|
||||||
|
gtk_box_append(GTK_BOX(buttons), analyze);
|
||||||
|
}
|
||||||
gtk_widget_add_css_class(save, "suggested-action");
|
gtk_widget_add_css_class(save, "suggested-action");
|
||||||
g_signal_connect(cancel, "clicked", G_CALLBACK(evidence_metadata_dialog_on_cancel), context);
|
g_signal_connect(cancel, "clicked", G_CALLBACK(evidence_metadata_dialog_on_cancel), context);
|
||||||
g_signal_connect(save, "clicked", G_CALLBACK(evidence_metadata_dialog_on_save), context);
|
g_signal_connect(save, "clicked", G_CALLBACK(evidence_metadata_dialog_on_save), context);
|
||||||
g_signal_connect(context->window, "close-request",
|
g_signal_connect(context->window, "close-request",
|
||||||
G_CALLBACK(evidence_metadata_dialog_on_close_request), context);
|
G_CALLBACK(evidence_metadata_dialog_on_close_request), context);
|
||||||
|
{
|
||||||
|
GtkEventController *keys = gtk_event_controller_key_new();
|
||||||
|
g_signal_connect(keys, "key-pressed",
|
||||||
|
G_CALLBACK(evidence_metadata_dialog_on_key_pressed), context);
|
||||||
|
gtk_widget_add_controller(GTK_WIDGET(context->window), keys);
|
||||||
|
}
|
||||||
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);
|
||||||
|
|
@ -195,6 +581,16 @@ gboolean evidence_metadata_dialog_present(
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gboolean evidence_metadata_dialog_present(
|
||||||
|
GtkWindow *parent, const EvidenceRecord *record,
|
||||||
|
const GPtrArray *evidence_types, EvidenceMetadataDialogCallback callback,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
return evidence_metadata_dialog_present_with_ocr(
|
||||||
|
parent, record, evidence_types, NULL, NULL, NULL,
|
||||||
|
callback, user_data);
|
||||||
|
}
|
||||||
|
|
||||||
void evidence_metadata_dialog_result_free(EvidenceMetadataDialogResult *result)
|
void evidence_metadata_dialog_result_free(EvidenceMetadataDialogResult *result)
|
||||||
{
|
{
|
||||||
if (result == NULL) return;
|
if (result == NULL) return;
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,8 @@ struct MainWindow
|
||||||
gpointer extract_metadata_user_data;
|
gpointer extract_metadata_user_data;
|
||||||
MainWindowRecoverPdfPasswordCallback recover_pdf_password_callback;
|
MainWindowRecoverPdfPasswordCallback recover_pdf_password_callback;
|
||||||
gpointer recover_pdf_password_user_data;
|
gpointer recover_pdf_password_user_data;
|
||||||
|
MainWindowIdentityOcrCallback identity_ocr_callback;
|
||||||
|
gpointer identity_ocr_user_data;
|
||||||
|
|
||||||
MainWindowGraphNodeMovedCallback
|
MainWindowGraphNodeMovedCallback
|
||||||
graph_node_moved_callback;
|
graph_node_moved_callback;
|
||||||
|
|
@ -242,6 +244,15 @@ static void main_window_on_recover_pdf_password_requested(
|
||||||
window->recover_pdf_password_callback(identifier,
|
window->recover_pdf_password_callback(identifier,
|
||||||
window->recover_pdf_password_user_data);
|
window->recover_pdf_password_user_data);
|
||||||
}
|
}
|
||||||
|
static void main_window_on_identity_ocr_requested(
|
||||||
|
const char *identifier, gboolean revise_existing,
|
||||||
|
const char *run_identifier, gpointer data)
|
||||||
|
{
|
||||||
|
MainWindow *window = data;
|
||||||
|
if (window != NULL && window->identity_ocr_callback != NULL)
|
||||||
|
window->identity_ocr_callback(identifier, revise_existing,
|
||||||
|
run_identifier, window->identity_ocr_user_data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Ouvre dans le workspace l'entité choisie dans la sidebar.
|
* @brief Ouvre dans le workspace l'entité choisie dans la sidebar.
|
||||||
|
|
@ -1043,6 +1054,8 @@ MainWindow *main_window_new(
|
||||||
main_window_on_extract_metadata_requested, main_window);
|
main_window_on_extract_metadata_requested, main_window);
|
||||||
workspace_set_recover_pdf_password_callback(main_window->workspace,
|
workspace_set_recover_pdf_password_callback(main_window->workspace,
|
||||||
main_window_on_recover_pdf_password_requested, main_window);
|
main_window_on_recover_pdf_password_requested, main_window);
|
||||||
|
workspace_set_identity_ocr_callback(main_window->workspace,
|
||||||
|
main_window_on_identity_ocr_requested, main_window);
|
||||||
|
|
||||||
workspace_widget = workspace_get_widget(
|
workspace_widget = workspace_get_widget(
|
||||||
main_window->workspace
|
main_window->workspace
|
||||||
|
|
@ -1628,6 +1641,14 @@ void main_window_set_recover_pdf_password_callback(MainWindow *main_window,
|
||||||
main_window->recover_pdf_password_callback = callback;
|
main_window->recover_pdf_password_callback = callback;
|
||||||
main_window->recover_pdf_password_user_data = user_data;
|
main_window->recover_pdf_password_user_data = user_data;
|
||||||
}
|
}
|
||||||
|
void main_window_set_identity_ocr_callback(
|
||||||
|
MainWindow *main_window, MainWindowIdentityOcrCallback callback,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
if (main_window == NULL) return;
|
||||||
|
main_window->identity_ocr_callback = callback;
|
||||||
|
main_window->identity_ocr_user_data = user_data;
|
||||||
|
}
|
||||||
|
|
||||||
void main_window_set_tree_selection_callback(
|
void main_window_set_tree_selection_callback(
|
||||||
MainWindow *main_window,
|
MainWindow *main_window,
|
||||||
|
|
@ -1995,6 +2016,17 @@ void main_window_set_selected_evidence(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void main_window_set_identity_ocr_runs(
|
||||||
|
MainWindow *main_window, GPtrArray *owned_records)
|
||||||
|
{
|
||||||
|
if (main_window == NULL) {
|
||||||
|
g_clear_pointer(&owned_records, g_ptr_array_unref);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workspace_set_identity_ocr_runs(
|
||||||
|
main_window->workspace, owned_records);
|
||||||
|
}
|
||||||
|
|
||||||
void main_window_set_evidence_observations(MainWindow *main_window,
|
void main_window_set_evidence_observations(MainWindow *main_window,
|
||||||
const GPtrArray *observations)
|
const GPtrArray *observations)
|
||||||
{
|
{
|
||||||
|
|
@ -2110,10 +2142,6 @@ void main_window_free(
|
||||||
* Les structures qui manipulent encore leurs widgets doivent être
|
* Les structures qui manipulent encore leurs widgets doivent être
|
||||||
* nettoyées avant gtk_window_destroy().
|
* nettoyées avant gtk_window_destroy().
|
||||||
*/
|
*/
|
||||||
sidebar_free(
|
|
||||||
main_window->sidebar
|
|
||||||
);
|
|
||||||
|
|
||||||
if (main_window->workspace != NULL)
|
if (main_window->workspace != NULL)
|
||||||
{
|
{
|
||||||
workspace_set_verify_evidence_callback(
|
workspace_set_verify_evidence_callback(
|
||||||
|
|
@ -2180,6 +2208,10 @@ void main_window_free(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sidebar_free(
|
||||||
|
main_window->sidebar
|
||||||
|
);
|
||||||
|
|
||||||
workspace_free(
|
workspace_free(
|
||||||
main_window->workspace
|
main_window->workspace
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,40 +3,53 @@
|
||||||
* @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 "widgets/evidence_preview_widget.h"
|
||||||
#include <gio/gio.h>
|
#include <gio/gio.h>
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
char *identifier;
|
char *identifier;
|
||||||
char *path;
|
char *path;
|
||||||
|
char *relative_path;
|
||||||
|
char *sha256;
|
||||||
|
char *mime_type;
|
||||||
GtkCheckButton *check;
|
GtkCheckButton *check;
|
||||||
} EvidenceChoice;
|
} EvidenceChoice;
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
GtkWindow *window;
|
GtkWindow *window;
|
||||||
GtkStack *preview;
|
EvidencePreviewWidget *preview;
|
||||||
GtkPicture *picture;
|
char *root_path;
|
||||||
GtkVideo *video;
|
|
||||||
GPtrArray *choices;
|
GPtrArray *choices;
|
||||||
ManageEntityEvidenceDialogCallback callback;
|
ManageEntityEvidenceDialogCallback callback;
|
||||||
|
ManageEntityEvidenceImportCallback import_callback;
|
||||||
gpointer callback_data;
|
gpointer callback_data;
|
||||||
gboolean completed;
|
gboolean completed;
|
||||||
} ManageEvidenceState;
|
} ManageEvidenceState;
|
||||||
|
/** @brief Ferme le gestionnaire puis lance un import pour cette personne. */
|
||||||
|
static void manage_evidence_on_import(GtkButton *button, gpointer data)
|
||||||
|
{
|
||||||
|
ManageEvidenceState *state = data;
|
||||||
|
(void) button;
|
||||||
|
if (state == NULL || state->completed) return;
|
||||||
|
state->completed = TRUE;
|
||||||
|
if (state->import_callback != NULL)
|
||||||
|
state->import_callback(state->callback_data);
|
||||||
|
gtk_window_close(state->window);
|
||||||
|
}
|
||||||
/** @brief Libère une option privée. */
|
/** @brief Libère une option privée. */
|
||||||
static void evidence_choice_free(gpointer data)
|
static void evidence_choice_free(gpointer data)
|
||||||
{
|
{
|
||||||
EvidenceChoice *choice = data;
|
EvidenceChoice *choice = data;
|
||||||
if (choice == NULL) return;
|
if (choice == NULL) return;
|
||||||
g_free(choice->identifier); g_free(choice->path); g_free(choice);
|
g_free(choice->identifier); g_free(choice->path);
|
||||||
|
g_free(choice->relative_path); g_free(choice->sha256);
|
||||||
|
g_free(choice->mime_type); g_free(choice);
|
||||||
}
|
}
|
||||||
/** @brief Arrête et efface l'aperçu courant. */
|
/** @brief Arrête et efface l'aperçu courant. */
|
||||||
static void manage_evidence_clear_preview(ManageEvidenceState *state)
|
static void manage_evidence_clear_preview(ManageEvidenceState *state)
|
||||||
{
|
{
|
||||||
GtkMediaStream *stream = NULL;
|
if (state == NULL || state->preview == NULL) return;
|
||||||
if (state == NULL || state->video == NULL) return;
|
evidence_preview_widget_clear(state->preview);
|
||||||
stream = gtk_video_get_media_stream(state->video);
|
|
||||||
if (stream != NULL) gtk_media_stream_pause(stream);
|
|
||||||
gtk_video_set_media_stream(state->video, NULL);
|
|
||||||
gtk_picture_set_paintable(state->picture, NULL);
|
|
||||||
}
|
}
|
||||||
/** @brief Libère l'état du dialogue. */
|
/** @brief Libère l'état du dialogue. */
|
||||||
static void manage_evidence_state_free(gpointer data)
|
static void manage_evidence_state_free(gpointer data)
|
||||||
|
|
@ -44,35 +57,21 @@ static void manage_evidence_state_free(gpointer data)
|
||||||
ManageEvidenceState *state = data;
|
ManageEvidenceState *state = data;
|
||||||
if (state == NULL) return;
|
if (state == NULL) return;
|
||||||
manage_evidence_clear_preview(state);
|
manage_evidence_clear_preview(state);
|
||||||
g_ptr_array_unref(state->choices); g_free(state);
|
g_clear_pointer(&state->preview, evidence_preview_widget_free);
|
||||||
|
g_ptr_array_unref(state->choices); g_free(state->root_path); g_free(state);
|
||||||
}
|
}
|
||||||
/** @brief Affiche l'aperçu de l'option manipulée. */
|
/** @brief Affiche l'aperçu de l'option manipulée. */
|
||||||
static void manage_evidence_on_toggled(GtkCheckButton *button, gpointer data)
|
static void manage_evidence_on_toggled(GtkCheckButton *button, gpointer data)
|
||||||
{
|
{
|
||||||
ManageEvidenceState *state = data;
|
ManageEvidenceState *state = data;
|
||||||
EvidenceChoice *choice = g_object_get_data(G_OBJECT(button), "choice");
|
EvidenceChoice *choice = g_object_get_data(G_OBJECT(button), "choice");
|
||||||
GFile *file = NULL; GFileInfo *info = NULL; char *mime = NULL;
|
|
||||||
const char *content_type = NULL;
|
|
||||||
if (state == NULL || choice == NULL || choice->path == NULL) return;
|
if (state == NULL || choice == NULL || choice->path == NULL) return;
|
||||||
manage_evidence_clear_preview(state);
|
EvidencePreviewRequest *request = evidence_preview_request_new(
|
||||||
file = g_file_new_for_path(choice->path);
|
state->root_path, choice->identifier, choice->relative_path,
|
||||||
info = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
|
choice->sha256, choice->mime_type, 1U);
|
||||||
G_FILE_QUERY_INFO_NONE, NULL, NULL);
|
evidence_preview_widget_show(state->preview, request,
|
||||||
if (info != NULL) content_type = g_file_info_get_content_type(info);
|
gtk_check_button_get_label(choice->check));
|
||||||
if (content_type != NULL) mime = g_content_type_get_mime_type(content_type);
|
evidence_preview_request_free(request);
|
||||||
if (mime != NULL && g_str_has_prefix(mime, "image/"))
|
|
||||||
{
|
|
||||||
gtk_picture_set_filename(state->picture, choice->path);
|
|
||||||
gtk_stack_set_visible_child_name(state->preview, "image");
|
|
||||||
}
|
|
||||||
else if (mime != NULL && g_str_has_prefix(mime, "video/"))
|
|
||||||
{
|
|
||||||
GtkMediaStream *stream = gtk_media_file_new_for_filename(choice->path);
|
|
||||||
gtk_video_set_media_stream(state->video, stream); g_object_unref(stream);
|
|
||||||
gtk_stack_set_visible_child_name(state->preview, "video");
|
|
||||||
}
|
|
||||||
else gtk_stack_set_visible_child_name(state->preview, "unsupported");
|
|
||||||
g_free(mime); g_clear_object(&info); g_clear_object(&file);
|
|
||||||
}
|
}
|
||||||
/** @brief Termine le dialogue en renvoyant la sélection. */
|
/** @brief Termine le dialogue en renvoyant la sélection. */
|
||||||
static void manage_evidence_on_save(GtkButton *button, gpointer data)
|
static void manage_evidence_on_save(GtkButton *button, gpointer data)
|
||||||
|
|
@ -98,27 +97,31 @@ static gboolean manage_evidence_on_close(GtkWindow *window, gpointer data)
|
||||||
}
|
}
|
||||||
gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
|
gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
|
||||||
const GPtrArray *records, const GPtrArray *selected_ids,
|
const GPtrArray *records, const GPtrArray *selected_ids,
|
||||||
const char *root_path, ManageEntityEvidenceDialogCallback callback,
|
const char *root_path, TaskManager *task_manager,
|
||||||
gpointer callback_data, GError **error)
|
ManageEntityEvidenceDialogCallback callback,
|
||||||
|
gpointer callback_data, ManageEntityEvidenceImportCallback import_callback,
|
||||||
|
GError **error)
|
||||||
{
|
{
|
||||||
ManageEvidenceState *state = NULL; GtkWidget *root = NULL;
|
ManageEvidenceState *state = NULL; GtkWidget *root = NULL;
|
||||||
GtkWidget *content = NULL; GtkWidget *list = NULL; GtkWidget *scroll = NULL;
|
GtkWidget *content = NULL; GtkWidget *list = NULL; GtkWidget *scroll = NULL;
|
||||||
GtkWidget *save = NULL;
|
GtkWidget *save = NULL; GtkWidget *import = NULL; GtkWidget *actions = NULL;
|
||||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
if (!GTK_IS_WINDOW(parent) || records == NULL || root_path == NULL ||
|
if (!GTK_IS_WINDOW(parent) || records == NULL || root_path == NULL ||
|
||||||
|
task_manager == NULL ||
|
||||||
callback == NULL) return FALSE;
|
callback == NULL) return FALSE;
|
||||||
state = g_new0(ManageEvidenceState, 1);
|
state = g_new0(ManageEvidenceState, 1);
|
||||||
state->choices = g_ptr_array_new_with_free_func(evidence_choice_free);
|
state->choices = g_ptr_array_new_with_free_func(evidence_choice_free);
|
||||||
state->callback = callback; state->callback_data = callback_data;
|
state->callback = callback; state->callback_data = callback_data;
|
||||||
|
state->root_path = g_strdup(root_path);
|
||||||
|
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);
|
gtk_window_set_transient_for(state->window, parent);
|
||||||
gtk_window_set_modal(state->window, TRUE);
|
gtk_window_set_modal(state->window, TRUE);
|
||||||
gtk_window_set_default_size(state->window, 900, 620);
|
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);
|
||||||
content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
|
|
||||||
list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||||
scroll = gtk_scrolled_window_new(); gtk_widget_set_size_request(scroll, 380, 480);
|
scroll = gtk_scrolled_window_new(); gtk_widget_set_size_request(scroll, 380, 480);
|
||||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
|
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
|
||||||
|
|
@ -134,6 +137,20 @@ gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
|
||||||
char *canonical_root = g_canonicalize_filename(root_path, NULL);
|
char *canonical_root = g_canonicalize_filename(root_path, NULL);
|
||||||
char *canonical_path = g_canonicalize_filename(candidate, NULL);
|
char *canonical_path = g_canonicalize_filename(candidate, NULL);
|
||||||
choice->identifier = g_strdup(evidence_record_get_identifier(record));
|
choice->identifier = g_strdup(evidence_record_get_identifier(record));
|
||||||
|
choice->relative_path = g_strdup(
|
||||||
|
evidence_record_get_relative_path(record));
|
||||||
|
choice->sha256 = g_strdup(evidence_record_get_sha256(record));
|
||||||
|
{
|
||||||
|
gboolean uncertain = FALSE;
|
||||||
|
char *content_type = g_content_type_guess(
|
||||||
|
choice->path != NULL ? choice->path : candidate,
|
||||||
|
NULL, 0U, &uncertain);
|
||||||
|
choice->mime_type = content_type != NULL
|
||||||
|
? g_content_type_get_mime_type(content_type)
|
||||||
|
: g_strdup("application/octet-stream");
|
||||||
|
g_free(content_type);
|
||||||
|
(void) uncertain;
|
||||||
|
}
|
||||||
if (canonical_root != NULL && canonical_path != NULL &&
|
if (canonical_root != NULL && canonical_path != NULL &&
|
||||||
g_str_has_prefix(canonical_path, canonical_root) &&
|
g_str_has_prefix(canonical_path, canonical_root) &&
|
||||||
(canonical_path[strlen(canonical_root)] == G_DIR_SEPARATOR ||
|
(canonical_path[strlen(canonical_root)] == G_DIR_SEPARATOR ||
|
||||||
|
|
@ -150,21 +167,32 @@ gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
|
||||||
gtk_box_append(GTK_BOX(list), GTK_WIDGET(choice->check));
|
gtk_box_append(GTK_BOX(list), GTK_WIDGET(choice->check));
|
||||||
g_ptr_array_add(state->choices, choice);
|
g_ptr_array_add(state->choices, choice);
|
||||||
}
|
}
|
||||||
state->preview = GTK_STACK(gtk_stack_new());
|
state->preview = evidence_preview_widget_new(task_manager, NULL, NULL);
|
||||||
state->picture = GTK_PICTURE(gtk_picture_new());
|
if (state->preview == NULL) {
|
||||||
gtk_picture_set_content_fit(state->picture, GTK_CONTENT_FIT_CONTAIN);
|
gtk_window_destroy(state->window);
|
||||||
state->video = GTK_VIDEO(gtk_video_new());
|
manage_evidence_state_free(state);
|
||||||
gtk_stack_add_named(state->preview, gtk_label_new("Sélectionnez une preuve."), "unsupported");
|
return FALSE;
|
||||||
gtk_stack_add_named(state->preview, GTK_WIDGET(state->picture), "image");
|
}
|
||||||
gtk_stack_add_named(state->preview, GTK_WIDGET(state->video), "video");
|
content = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||||
gtk_widget_set_hexpand(GTK_WIDGET(state->preview), TRUE);
|
gtk_widget_set_name(content, "manage-evidence-paned");
|
||||||
gtk_box_append(GTK_BOX(content), scroll); gtk_box_append(GTK_BOX(content), GTK_WIDGET(state->preview));
|
gtk_paned_set_start_child(GTK_PANED(content), scroll);
|
||||||
|
gtk_paned_set_end_child(GTK_PANED(content),
|
||||||
|
evidence_preview_widget_get_widget(state->preview));
|
||||||
|
gtk_paned_set_position(GTK_PANED(content), 380);
|
||||||
|
gtk_paned_set_resize_start_child(GTK_PANED(content), FALSE);
|
||||||
|
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");
|
||||||
|
import = gtk_button_new_with_label("Importer une nouvelle preuve");
|
||||||
|
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||||
gtk_widget_add_css_class(save, "suggested-action"); gtk_widget_set_halign(save, GTK_ALIGN_END);
|
gtk_widget_add_css_class(save, "suggested-action"); gtk_widget_set_halign(save, GTK_ALIGN_END);
|
||||||
gtk_box_append(GTK_BOX(root), content); gtk_box_append(GTK_BOX(root), save);
|
gtk_box_append(GTK_BOX(actions), import);
|
||||||
|
gtk_box_append(GTK_BOX(actions), save);
|
||||||
|
gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
||||||
|
gtk_box_append(GTK_BOX(root), content); gtk_box_append(GTK_BOX(root), actions);
|
||||||
gtk_window_set_child(state->window, root);
|
gtk_window_set_child(state->window, root);
|
||||||
g_object_set_data_full(G_OBJECT(state->window), "state", state, manage_evidence_state_free);
|
g_object_set_data_full(G_OBJECT(state->window), "state", state, manage_evidence_state_free);
|
||||||
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(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;
|
gtk_window_present(state->window); return TRUE;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,15 @@
|
||||||
|
|
||||||
#include "views/metadata_analysis_dialog.h"
|
#include "views/metadata_analysis_dialog.h"
|
||||||
|
|
||||||
|
static gboolean metadata_analysis_dialog_on_escape(
|
||||||
|
GtkWidget *widget, GVariant *arguments, gpointer user_data)
|
||||||
|
{
|
||||||
|
(void) widget;
|
||||||
|
(void) arguments;
|
||||||
|
gtk_window_destroy(GTK_WINDOW(user_data));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
void metadata_analysis_dialog_present(GtkWindow *parent, const char *summary,
|
void metadata_analysis_dialog_present(GtkWindow *parent, const char *summary,
|
||||||
const char *json_path, const char *version)
|
const char *json_path, const char *version)
|
||||||
{
|
{
|
||||||
|
|
@ -58,6 +67,18 @@ void metadata_analysis_dialog_present(GtkWindow *parent, const char *summary,
|
||||||
gtk_widget_set_halign(close_button, GTK_ALIGN_END);
|
gtk_widget_set_halign(close_button, GTK_ALIGN_END);
|
||||||
g_signal_connect_swapped(close_button, "clicked",
|
g_signal_connect_swapped(close_button, "clicked",
|
||||||
G_CALLBACK(gtk_window_destroy), window);
|
G_CALLBACK(gtk_window_destroy), window);
|
||||||
|
{
|
||||||
|
GtkShortcutController *shortcuts = GTK_SHORTCUT_CONTROLLER(
|
||||||
|
gtk_shortcut_controller_new());
|
||||||
|
gtk_shortcut_controller_set_scope(
|
||||||
|
shortcuts, GTK_SHORTCUT_SCOPE_LOCAL);
|
||||||
|
gtk_shortcut_controller_add_shortcut(shortcuts,
|
||||||
|
gtk_shortcut_new(
|
||||||
|
gtk_keyval_trigger_new(GDK_KEY_Escape, 0),
|
||||||
|
gtk_callback_action_new(
|
||||||
|
metadata_analysis_dialog_on_escape, window, NULL)));
|
||||||
|
gtk_widget_add_controller(window, GTK_EVENT_CONTROLLER(shortcuts));
|
||||||
|
}
|
||||||
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));
|
gtk_window_present(GTK_WINDOW(window));
|
||||||
|
|
|
||||||
118
src/widgets/ocr_provenance_overlay.c
Normal file
118
src/widgets/ocr_provenance_overlay.c
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
#include "widgets/ocr_provenance_overlay.h"
|
||||||
|
#include "core/ocr_region_geometry.h"
|
||||||
|
|
||||||
|
struct OcrProvenanceOverlay {
|
||||||
|
GtkWidget *root;
|
||||||
|
GtkPicture *picture;
|
||||||
|
GtkDrawingArea *drawing;
|
||||||
|
GtkLabel *details;
|
||||||
|
GdkTexture *texture;
|
||||||
|
IdentitySourceBox box;
|
||||||
|
gboolean has_box;
|
||||||
|
guint64 generation;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void draw_region(GtkDrawingArea *area, cairo_t *cr, int width,
|
||||||
|
int height, gpointer data)
|
||||||
|
{
|
||||||
|
OcrProvenanceOverlay *overlay = data;
|
||||||
|
OcrDisplayRegion region;
|
||||||
|
(void) area;
|
||||||
|
if (!overlay->has_box ||
|
||||||
|
!ocr_region_geometry_transform(overlay->box.image_width,
|
||||||
|
overlay->box.image_height, overlay->box.x, overlay->box.y,
|
||||||
|
overlay->box.width, overlay->box.height, width, height,
|
||||||
|
OCR_REGION_FIT_CONTAIN, ®ion))
|
||||||
|
return;
|
||||||
|
cairo_set_source_rgba(cr, 1.0, 0.15, 0.05, 0.20);
|
||||||
|
cairo_rectangle(cr, region.x, region.y, region.width, region.height);
|
||||||
|
cairo_fill_preserve(cr);
|
||||||
|
cairo_set_source_rgba(cr, 1.0, 0.1, 0.0, 0.95);
|
||||||
|
cairo_set_line_width(cr, 3.0);
|
||||||
|
cairo_stroke(cr);
|
||||||
|
}
|
||||||
|
|
||||||
|
OcrProvenanceOverlay *ocr_provenance_overlay_new(void)
|
||||||
|
{
|
||||||
|
OcrProvenanceOverlay *overlay = g_new0(OcrProvenanceOverlay, 1);
|
||||||
|
overlay->root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||||
|
GtkWidget *layer = gtk_overlay_new();
|
||||||
|
overlay->picture = GTK_PICTURE(gtk_picture_new());
|
||||||
|
gtk_picture_set_content_fit(overlay->picture, GTK_CONTENT_FIT_CONTAIN);
|
||||||
|
overlay->drawing = GTK_DRAWING_AREA(gtk_drawing_area_new());
|
||||||
|
gtk_drawing_area_set_draw_func(overlay->drawing, draw_region, overlay,
|
||||||
|
NULL);
|
||||||
|
gtk_overlay_set_child(GTK_OVERLAY(layer), GTK_WIDGET(overlay->picture));
|
||||||
|
gtk_overlay_add_overlay(GTK_OVERLAY(layer), GTK_WIDGET(overlay->drawing));
|
||||||
|
gtk_widget_set_size_request(layer, 360, 220);
|
||||||
|
overlay->details = GTK_LABEL(gtk_label_new("Zone source indisponible"));
|
||||||
|
gtk_label_set_wrap(overlay->details, TRUE);
|
||||||
|
gtk_box_append(GTK_BOX(overlay->root), layer);
|
||||||
|
gtk_box_append(GTK_BOX(overlay->root), GTK_WIDGET(overlay->details));
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ocr_provenance_overlay_free(OcrProvenanceOverlay *overlay)
|
||||||
|
{
|
||||||
|
if (overlay == NULL) return;
|
||||||
|
g_clear_object(&overlay->texture);
|
||||||
|
g_free(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkWidget *ocr_provenance_overlay_get_widget(OcrProvenanceOverlay *overlay)
|
||||||
|
{
|
||||||
|
return overlay != NULL ? overlay->root : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ocr_provenance_overlay_set_image(OcrProvenanceOverlay *overlay,
|
||||||
|
GBytes *bytes, guint64 generation)
|
||||||
|
{
|
||||||
|
if (overlay == NULL || generation < overlay->generation) return;
|
||||||
|
overlay->generation = generation;
|
||||||
|
g_clear_object(&overlay->texture);
|
||||||
|
overlay->texture = bytes != NULL ? gdk_texture_new_from_bytes(bytes, NULL)
|
||||||
|
: NULL;
|
||||||
|
gtk_picture_set_paintable(overlay->picture,
|
||||||
|
overlay->texture != NULL ? GDK_PAINTABLE(overlay->texture) : NULL);
|
||||||
|
gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ocr_provenance_overlay_set_field(OcrProvenanceOverlay *overlay,
|
||||||
|
const IdentityFieldObservation *field, guint64 generation)
|
||||||
|
{
|
||||||
|
if (overlay == NULL || generation < overlay->generation) return;
|
||||||
|
overlay->generation = generation;
|
||||||
|
const IdentitySourceBox *box = field != NULL
|
||||||
|
? identity_field_observation_get_box(field) : NULL;
|
||||||
|
overlay->has_box = box != NULL && box->available;
|
||||||
|
if (overlay->has_box) overlay->box = *box;
|
||||||
|
char *text = overlay->has_box
|
||||||
|
? g_strdup_printf("%s — confiance %.1f — page %d — "
|
||||||
|
"coordonnées %d,%d %dx%d — origine %s",
|
||||||
|
identity_field_observation_get_raw_value(field),
|
||||||
|
identity_field_observation_get_confidence(field), box->page,
|
||||||
|
box->x, box->y, box->width, box->height,
|
||||||
|
identity_field_observation_get_origin(field))
|
||||||
|
: g_strdup("Zone source indisponible");
|
||||||
|
gtk_label_set_text(overlay->details, text);
|
||||||
|
g_free(text);
|
||||||
|
gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ocr_provenance_overlay_clear(OcrProvenanceOverlay *overlay,
|
||||||
|
guint64 generation)
|
||||||
|
{
|
||||||
|
if (overlay == NULL || generation < overlay->generation) return;
|
||||||
|
overlay->generation = generation;
|
||||||
|
overlay->has_box = FALSE;
|
||||||
|
gtk_label_set_text(overlay->details, "Zone source indisponible");
|
||||||
|
gtk_picture_set_paintable(overlay->picture, NULL);
|
||||||
|
g_clear_object(&overlay->texture);
|
||||||
|
gtk_widget_queue_draw(GTK_WIDGET(overlay->drawing));
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean ocr_provenance_overlay_has_region(
|
||||||
|
const OcrProvenanceOverlay *overlay)
|
||||||
|
{
|
||||||
|
return overlay != NULL && overlay->has_box;
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
#include "widgets/entity_details_panel.h"
|
#include "widgets/entity_details_panel.h"
|
||||||
#include "widgets/investigation_graph_view.h"
|
#include "widgets/investigation_graph_view.h"
|
||||||
#include "widgets/evidence_preview_widget.h"
|
#include "widgets/evidence_preview_widget.h"
|
||||||
|
#include "widgets/ocr_provenance_overlay.h"
|
||||||
|
#include "views/evidence_identity_ocr_dialog.h"
|
||||||
|
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
#include <gio/gio.h>
|
#include <gio/gio.h>
|
||||||
|
|
@ -98,6 +100,20 @@ struct Workspace
|
||||||
GtkWidget *analyze_rib_button;
|
GtkWidget *analyze_rib_button;
|
||||||
GtkWidget *extract_metadata_button;
|
GtkWidget *extract_metadata_button;
|
||||||
GtkWidget *recover_pdf_password_button;
|
GtkWidget *recover_pdf_password_button;
|
||||||
|
GtkWidget *analyze_identity_button;
|
||||||
|
GtkWidget *identity_ocr_section;
|
||||||
|
GtkDropDown *identity_ocr_selector;
|
||||||
|
GtkStringList *identity_ocr_labels;
|
||||||
|
GtkLabel *identity_ocr_summary;
|
||||||
|
GtkTextBuffer *identity_ocr_raw;
|
||||||
|
GtkTextBuffer *identity_ocr_corrected;
|
||||||
|
GtkBox *identity_ocr_fields;
|
||||||
|
GtkWidget *revise_identity_button;
|
||||||
|
GtkWidget *rerun_identity_button;
|
||||||
|
OcrProvenanceOverlay *identity_ocr_overlay;
|
||||||
|
GPtrArray *identity_ocr_records;
|
||||||
|
WorkspaceIdentityOcrCallback identity_ocr_callback;
|
||||||
|
gpointer identity_ocr_user_data;
|
||||||
EvidencePreviewWidget *evidence_preview;
|
EvidencePreviewWidget *evidence_preview;
|
||||||
|
|
||||||
char *selected_evidence_identifier;
|
char *selected_evidence_identifier;
|
||||||
|
|
@ -1106,6 +1122,152 @@ static void workspace_on_recover_pdf_password_clicked(GtkButton *button,
|
||||||
workspace->recover_pdf_password_user_data);
|
workspace->recover_pdf_password_user_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorkspaceIdentityOcrRecord *workspace_identity_ocr_record_new(
|
||||||
|
IdentityOcrRun *owned_run, const char *person_identifier,
|
||||||
|
const char *executed_at, const char *text_relative_path,
|
||||||
|
const char *text_sha256, const char *tsv_relative_path,
|
||||||
|
const char *tsv_sha256)
|
||||||
|
{
|
||||||
|
if (owned_run == NULL) return NULL;
|
||||||
|
WorkspaceIdentityOcrRecord *record =
|
||||||
|
g_new0(WorkspaceIdentityOcrRecord, 1);
|
||||||
|
record->run = owned_run;
|
||||||
|
record->person_identifier = g_strdup(person_identifier);
|
||||||
|
record->executed_at = g_strdup(executed_at);
|
||||||
|
record->text_relative_path = g_strdup(text_relative_path);
|
||||||
|
record->text_sha256 = g_strdup(text_sha256);
|
||||||
|
record->tsv_relative_path = g_strdup(tsv_relative_path);
|
||||||
|
record->tsv_sha256 = g_strdup(tsv_sha256);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
void workspace_identity_ocr_record_free(WorkspaceIdentityOcrRecord *record)
|
||||||
|
{
|
||||||
|
if (record == NULL) return;
|
||||||
|
identity_ocr_run_free(record->run);
|
||||||
|
g_free(record->person_identifier);
|
||||||
|
g_free(record->executed_at);
|
||||||
|
g_free(record->text_relative_path);
|
||||||
|
g_free(record->text_sha256);
|
||||||
|
g_free(record->tsv_relative_path);
|
||||||
|
g_free(record->tsv_sha256);
|
||||||
|
g_free(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void workspace_clear_box(GtkBox *box)
|
||||||
|
{
|
||||||
|
GtkWidget *child = gtk_widget_get_first_child(GTK_WIDGET(box));
|
||||||
|
while (child != NULL) {
|
||||||
|
GtkWidget *next = gtk_widget_get_next_sibling(child);
|
||||||
|
gtk_box_remove(box, child);
|
||||||
|
child = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void workspace_render_identity_ocr(Workspace *workspace)
|
||||||
|
{
|
||||||
|
guint selected = gtk_drop_down_get_selected(
|
||||||
|
workspace->identity_ocr_selector);
|
||||||
|
workspace_clear_box(workspace->identity_ocr_fields);
|
||||||
|
gtk_text_buffer_set_text(workspace->identity_ocr_raw, "", -1);
|
||||||
|
gtk_text_buffer_set_text(workspace->identity_ocr_corrected, "", -1);
|
||||||
|
ocr_provenance_overlay_clear(workspace->identity_ocr_overlay,
|
||||||
|
selected + 1U);
|
||||||
|
if (workspace->identity_ocr_records == NULL ||
|
||||||
|
selected >= workspace->identity_ocr_records->len) {
|
||||||
|
gtk_label_set_text(workspace->identity_ocr_summary,
|
||||||
|
"Aucune analyse OCR sélectionnée.");
|
||||||
|
gtk_widget_set_sensitive(workspace->revise_identity_button, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WorkspaceIdentityOcrRecord *record = g_ptr_array_index(
|
||||||
|
workspace->identity_ocr_records, selected);
|
||||||
|
IdentityOcrRun *run = record->run;
|
||||||
|
char *summary = g_strdup_printf(
|
||||||
|
"Run actif : %s\nUTC : %s\nPersonne liée : %s\n"
|
||||||
|
"Document : %s — %s — page %u\nLangues : %s\n"
|
||||||
|
"Notes : %s\nArtefact texte : %s\nSHA-256 texte : %s\n"
|
||||||
|
"Artefact TSV : %s\nSHA-256 TSV : %s\nSHA-256 preuve : %s",
|
||||||
|
identity_ocr_run_get_identifier(run),
|
||||||
|
record->executed_at != NULL ? record->executed_at : "inconnue",
|
||||||
|
record->person_identifier != NULL
|
||||||
|
? record->person_identifier : "indisponible",
|
||||||
|
identity_ocr_run_get_document_type(run),
|
||||||
|
identity_ocr_run_get_document_side(run),
|
||||||
|
identity_ocr_run_get_page(run),
|
||||||
|
identity_ocr_run_get_languages(run),
|
||||||
|
identity_ocr_run_get_factual_notes(run) != NULL
|
||||||
|
? identity_ocr_run_get_factual_notes(run) : "Aucune note",
|
||||||
|
record->text_relative_path, record->text_sha256,
|
||||||
|
record->tsv_relative_path, record->tsv_sha256,
|
||||||
|
identity_ocr_run_get_expected_sha256(run));
|
||||||
|
gtk_label_set_text(workspace->identity_ocr_summary, summary);
|
||||||
|
g_free(summary);
|
||||||
|
gtk_text_buffer_set_text(workspace->identity_ocr_raw,
|
||||||
|
identity_ocr_run_get_raw_text(run), -1);
|
||||||
|
gtk_text_buffer_set_text(workspace->identity_ocr_corrected,
|
||||||
|
identity_ocr_run_get_corrected_transcription(run) != NULL
|
||||||
|
? identity_ocr_run_get_corrected_transcription(run)
|
||||||
|
: "Aucune transcription corrigée", -1);
|
||||||
|
const GPtrArray *fields = identity_ocr_run_get_fields(run);
|
||||||
|
for (guint index = 0; fields != NULL && index < fields->len; index++) {
|
||||||
|
IdentityFieldObservation *field = g_ptr_array_index(
|
||||||
|
(GPtrArray *) fields, index);
|
||||||
|
char *field_text = g_strdup_printf(
|
||||||
|
"%s — brut : %s — corrigé : %s — origine : %s — statut : %d",
|
||||||
|
identity_field_observation_get_code(field),
|
||||||
|
identity_field_observation_get_raw_value(field) != NULL
|
||||||
|
? identity_field_observation_get_raw_value(field) : "absent",
|
||||||
|
identity_field_observation_get_corrected_value(field) != NULL
|
||||||
|
? identity_field_observation_get_corrected_value(field)
|
||||||
|
: "absente",
|
||||||
|
identity_field_observation_get_origin(field),
|
||||||
|
identity_field_observation_get_status(field));
|
||||||
|
GtkWidget *label = gtk_label_new(field_text);
|
||||||
|
gtk_label_set_wrap(GTK_LABEL(label), TRUE);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
|
||||||
|
gtk_box_append(workspace->identity_ocr_fields, label);
|
||||||
|
g_free(field_text);
|
||||||
|
if (!ocr_provenance_overlay_has_region(
|
||||||
|
workspace->identity_ocr_overlay))
|
||||||
|
ocr_provenance_overlay_set_field(
|
||||||
|
workspace->identity_ocr_overlay, field, selected + 1U);
|
||||||
|
}
|
||||||
|
gtk_widget_set_sensitive(workspace->revise_identity_button, TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void workspace_on_identity_ocr_selected(
|
||||||
|
GObject *object, GParamSpec *pspec, gpointer data)
|
||||||
|
{
|
||||||
|
(void) object;
|
||||||
|
(void) pspec;
|
||||||
|
workspace_render_identity_ocr(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void workspace_on_identity_ocr_action(
|
||||||
|
GtkButton *button, gpointer data)
|
||||||
|
{
|
||||||
|
Workspace *workspace = data;
|
||||||
|
gboolean revise = button ==
|
||||||
|
GTK_BUTTON(workspace->revise_identity_button);
|
||||||
|
const char *run_identifier = NULL;
|
||||||
|
if (revise && workspace->identity_ocr_records != NULL) {
|
||||||
|
guint selected = gtk_drop_down_get_selected(
|
||||||
|
workspace->identity_ocr_selector);
|
||||||
|
if (selected < workspace->identity_ocr_records->len) {
|
||||||
|
WorkspaceIdentityOcrRecord *record = g_ptr_array_index(
|
||||||
|
workspace->identity_ocr_records, selected);
|
||||||
|
run_identifier =
|
||||||
|
identity_ocr_run_get_identifier(record->run);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (workspace->identity_ocr_callback != NULL &&
|
||||||
|
(!revise || run_identifier != NULL))
|
||||||
|
workspace->identity_ocr_callback(
|
||||||
|
workspace->selected_evidence_identifier,
|
||||||
|
revise, run_identifier, workspace->identity_ocr_user_data);
|
||||||
|
}
|
||||||
|
|
||||||
Workspace *workspace_new(TaskManager *task_manager)
|
Workspace *workspace_new(TaskManager *task_manager)
|
||||||
{
|
{
|
||||||
GtkWidget *evidence_content = NULL;
|
GtkWidget *evidence_content = NULL;
|
||||||
|
|
@ -1479,6 +1641,91 @@ Workspace *workspace_new(TaskManager *task_manager)
|
||||||
G_CALLBACK(workspace_on_recover_pdf_password_clicked), workspace);
|
G_CALLBACK(workspace_on_recover_pdf_password_clicked), workspace);
|
||||||
gtk_box_append(GTK_BOX(evidence_content),
|
gtk_box_append(GTK_BOX(evidence_content),
|
||||||
workspace->recover_pdf_password_button);
|
workspace->recover_pdf_password_button);
|
||||||
|
workspace->analyze_identity_button = gtk_button_new_with_label(
|
||||||
|
"Analyser comme document d’identité");
|
||||||
|
gtk_widget_set_name(workspace->analyze_identity_button,
|
||||||
|
"workspace-analyze-identity");
|
||||||
|
gtk_widget_set_halign(workspace->analyze_identity_button,
|
||||||
|
GTK_ALIGN_START);
|
||||||
|
gtk_widget_set_sensitive(workspace->analyze_identity_button, FALSE);
|
||||||
|
g_signal_connect(workspace->analyze_identity_button, "clicked",
|
||||||
|
G_CALLBACK(workspace_on_identity_ocr_action), workspace);
|
||||||
|
gtk_box_append(GTK_BOX(evidence_content),
|
||||||
|
workspace->analyze_identity_button);
|
||||||
|
|
||||||
|
workspace->identity_ocr_section =
|
||||||
|
gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||||
|
gtk_widget_set_name(workspace->identity_ocr_section,
|
||||||
|
"workspace-identity-ocr-section");
|
||||||
|
gtk_box_append(GTK_BOX(workspace->identity_ocr_section),
|
||||||
|
gtk_label_new("Analyse OCR d’identité"));
|
||||||
|
workspace->identity_ocr_labels = gtk_string_list_new(NULL);
|
||||||
|
workspace->identity_ocr_selector = GTK_DROP_DOWN(
|
||||||
|
gtk_drop_down_new(G_LIST_MODEL(g_object_ref(
|
||||||
|
workspace->identity_ocr_labels)), NULL));
|
||||||
|
gtk_widget_set_name(GTK_WIDGET(workspace->identity_ocr_selector),
|
||||||
|
"workspace-identity-ocr-selector");
|
||||||
|
gtk_box_append(GTK_BOX(workspace->identity_ocr_section),
|
||||||
|
GTK_WIDGET(workspace->identity_ocr_selector));
|
||||||
|
workspace->identity_ocr_summary = GTK_LABEL(gtk_label_new(NULL));
|
||||||
|
gtk_widget_set_name(GTK_WIDGET(workspace->identity_ocr_summary),
|
||||||
|
"workspace-identity-ocr-active-run");
|
||||||
|
gtk_label_set_wrap(workspace->identity_ocr_summary, TRUE);
|
||||||
|
gtk_label_set_xalign(workspace->identity_ocr_summary, 0.0f);
|
||||||
|
gtk_box_append(GTK_BOX(workspace->identity_ocr_section),
|
||||||
|
GTK_WIDGET(workspace->identity_ocr_summary));
|
||||||
|
GtkWidget *raw_view = gtk_text_view_new();
|
||||||
|
gtk_widget_set_name(raw_view, "workspace-identity-ocr-raw");
|
||||||
|
gtk_text_view_set_editable(GTK_TEXT_VIEW(raw_view), FALSE);
|
||||||
|
workspace->identity_ocr_raw =
|
||||||
|
gtk_text_view_get_buffer(GTK_TEXT_VIEW(raw_view));
|
||||||
|
gtk_box_append(GTK_BOX(workspace->identity_ocr_section),
|
||||||
|
gtk_label_new("Texte OCR brut"));
|
||||||
|
gtk_box_append(GTK_BOX(workspace->identity_ocr_section), raw_view);
|
||||||
|
GtkWidget *corrected_view = gtk_text_view_new();
|
||||||
|
gtk_widget_set_name(corrected_view,
|
||||||
|
"workspace-identity-ocr-corrected");
|
||||||
|
gtk_text_view_set_editable(GTK_TEXT_VIEW(corrected_view), FALSE);
|
||||||
|
workspace->identity_ocr_corrected =
|
||||||
|
gtk_text_view_get_buffer(GTK_TEXT_VIEW(corrected_view));
|
||||||
|
gtk_box_append(GTK_BOX(workspace->identity_ocr_section),
|
||||||
|
gtk_label_new("Transcription corrigée"));
|
||||||
|
gtk_box_append(GTK_BOX(workspace->identity_ocr_section),
|
||||||
|
corrected_view);
|
||||||
|
workspace->identity_ocr_fields = GTK_BOX(
|
||||||
|
gtk_box_new(GTK_ORIENTATION_VERTICAL, 6));
|
||||||
|
gtk_box_append(GTK_BOX(workspace->identity_ocr_section),
|
||||||
|
GTK_WIDGET(workspace->identity_ocr_fields));
|
||||||
|
workspace->identity_ocr_overlay = ocr_provenance_overlay_new();
|
||||||
|
gtk_box_append(GTK_BOX(workspace->identity_ocr_section),
|
||||||
|
ocr_provenance_overlay_get_widget(
|
||||||
|
workspace->identity_ocr_overlay));
|
||||||
|
GtkWidget *identity_actions =
|
||||||
|
gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
|
workspace->revise_identity_button =
|
||||||
|
gtk_button_new_with_label("Réviser l’analyse OCR");
|
||||||
|
workspace->rerun_identity_button =
|
||||||
|
gtk_button_new_with_label("Relancer une nouvelle analyse OCR");
|
||||||
|
gtk_widget_set_name(workspace->revise_identity_button,
|
||||||
|
"workspace-revise-identity-ocr");
|
||||||
|
gtk_widget_set_name(workspace->rerun_identity_button,
|
||||||
|
"workspace-rerun-identity-ocr");
|
||||||
|
g_signal_connect(workspace->revise_identity_button, "clicked",
|
||||||
|
G_CALLBACK(workspace_on_identity_ocr_action), workspace);
|
||||||
|
g_signal_connect(workspace->rerun_identity_button, "clicked",
|
||||||
|
G_CALLBACK(workspace_on_identity_ocr_action), workspace);
|
||||||
|
gtk_box_append(GTK_BOX(identity_actions),
|
||||||
|
workspace->revise_identity_button);
|
||||||
|
gtk_box_append(GTK_BOX(identity_actions),
|
||||||
|
workspace->rerun_identity_button);
|
||||||
|
gtk_box_append(GTK_BOX(workspace->identity_ocr_section),
|
||||||
|
identity_actions);
|
||||||
|
gtk_widget_set_visible(workspace->identity_ocr_section, FALSE);
|
||||||
|
gtk_box_append(GTK_BOX(evidence_content),
|
||||||
|
workspace->identity_ocr_section);
|
||||||
|
g_signal_connect(workspace->identity_ocr_selector,
|
||||||
|
"notify::selected",
|
||||||
|
G_CALLBACK(workspace_on_identity_ocr_selected), workspace);
|
||||||
|
|
||||||
evidence_separator =
|
evidence_separator =
|
||||||
gtk_separator_new(
|
gtk_separator_new(
|
||||||
|
|
@ -2686,6 +2933,7 @@ void workspace_set_selected_evidence(
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace_clear_evidence_preview(workspace);
|
workspace_clear_evidence_preview(workspace);
|
||||||
|
workspace_set_identity_ocr_runs(workspace, NULL);
|
||||||
|
|
||||||
g_clear_pointer(
|
g_clear_pointer(
|
||||||
&workspace->selected_evidence_identifier,
|
&workspace->selected_evidence_identifier,
|
||||||
|
|
@ -2747,6 +2995,10 @@ void workspace_set_selected_evidence(
|
||||||
g_str_has_suffix(lower, ".pdf")));
|
g_str_has_suffix(lower, ".pdf")));
|
||||||
gtk_widget_set_sensitive(workspace->recover_pdf_password_button,
|
gtk_widget_set_sensitive(workspace->recover_pdf_password_button,
|
||||||
lower != NULL && g_str_has_suffix(lower, ".pdf"));
|
lower != NULL && g_str_has_suffix(lower, ".pdf"));
|
||||||
|
gtk_widget_set_sensitive(workspace->analyze_identity_button,
|
||||||
|
evidence_identity_ocr_dialog_file_is_compatible(name) ||
|
||||||
|
evidence_identity_ocr_dialog_file_is_compatible(
|
||||||
|
evidence_record_get_internal_name(evidence_record)));
|
||||||
g_free(lower);
|
g_free(lower);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2889,6 +3141,56 @@ void workspace_set_eml_analysis_available(
|
||||||
workspace->analyze_eml_button, available);
|
workspace->analyze_eml_button, available);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void workspace_set_identity_ocr_runs(
|
||||||
|
Workspace *workspace, GPtrArray *owned_records)
|
||||||
|
{
|
||||||
|
if (workspace == NULL) {
|
||||||
|
g_clear_pointer(&owned_records, g_ptr_array_unref);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_clear_pointer(&workspace->identity_ocr_records, g_ptr_array_unref);
|
||||||
|
workspace->identity_ocr_records = owned_records;
|
||||||
|
guint old_count = g_list_model_get_n_items(
|
||||||
|
G_LIST_MODEL(workspace->identity_ocr_labels));
|
||||||
|
gtk_string_list_splice(workspace->identity_ocr_labels,
|
||||||
|
0, old_count, NULL);
|
||||||
|
for (guint index = 0;
|
||||||
|
owned_records != NULL && index < owned_records->len; index++) {
|
||||||
|
WorkspaceIdentityOcrRecord *record =
|
||||||
|
g_ptr_array_index(owned_records, index);
|
||||||
|
char *short_id = g_strndup(
|
||||||
|
identity_ocr_run_get_identifier(record->run), 8);
|
||||||
|
char *label = g_strdup_printf(
|
||||||
|
"%s — %s/%s — page %u — %s",
|
||||||
|
record->executed_at != NULL ? record->executed_at : "UTC inconnue",
|
||||||
|
identity_ocr_run_get_document_type(record->run),
|
||||||
|
identity_ocr_run_get_document_side(record->run),
|
||||||
|
identity_ocr_run_get_page(record->run), short_id);
|
||||||
|
gtk_string_list_append(workspace->identity_ocr_labels, label);
|
||||||
|
g_free(label);
|
||||||
|
g_free(short_id);
|
||||||
|
}
|
||||||
|
gboolean available =
|
||||||
|
owned_records != NULL && owned_records->len > 0;
|
||||||
|
gtk_widget_set_visible(workspace->identity_ocr_section, available);
|
||||||
|
gtk_widget_set_sensitive(workspace->revise_identity_button, available);
|
||||||
|
gtk_widget_set_sensitive(workspace->rerun_identity_button, available);
|
||||||
|
if (available)
|
||||||
|
gtk_drop_down_set_selected(workspace->identity_ocr_selector,
|
||||||
|
owned_records->len - 1);
|
||||||
|
else
|
||||||
|
workspace_render_identity_ocr(workspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
void workspace_set_identity_ocr_callback(
|
||||||
|
Workspace *workspace, WorkspaceIdentityOcrCallback callback,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
if (workspace == NULL) return;
|
||||||
|
workspace->identity_ocr_callback = callback;
|
||||||
|
workspace->identity_ocr_user_data = user_data;
|
||||||
|
}
|
||||||
|
|
||||||
void workspace_set_evidence_preview(Workspace *workspace,
|
void workspace_set_evidence_preview(Workspace *workspace,
|
||||||
const char *file_path, const char *display_name)
|
const char *file_path, const char *display_name)
|
||||||
{
|
{
|
||||||
|
|
@ -3758,6 +4060,10 @@ void workspace_free(Workspace *workspace)
|
||||||
&workspace->selected_evidence_identifier,
|
&workspace->selected_evidence_identifier,
|
||||||
g_free
|
g_free
|
||||||
);
|
);
|
||||||
|
g_clear_pointer(&workspace->identity_ocr_records, g_ptr_array_unref);
|
||||||
|
g_clear_object(&workspace->identity_ocr_labels);
|
||||||
|
g_clear_pointer(&workspace->identity_ocr_overlay,
|
||||||
|
ocr_provenance_overlay_free);
|
||||||
|
|
||||||
g_free(workspace);
|
g_free(workspace);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
tests/fake_document_tool
Executable file
BIN
tests/fake_document_tool
Executable file
Binary file not shown.
|
|
@ -54,6 +54,13 @@ static void write_repeated(FILE *stream, char value, long count)
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
|
if (has_argument(argc, argv, "--list-langs"))
|
||||||
|
{
|
||||||
|
puts("List of available languages in synthetic fixture (2):");
|
||||||
|
puts("eng");
|
||||||
|
puts("fra");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (has_argument(argc, argv, "--emit"))
|
if (has_argument(argc, argv, "--emit"))
|
||||||
{
|
{
|
||||||
long stdout_size = argument_long(
|
long stdout_size = argument_long(
|
||||||
|
|
@ -141,7 +148,27 @@ int main(int argc, char **argv)
|
||||||
write_repeated(stdout, 'T', 8192);
|
write_repeated(stdout, 'T', 8192);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (strstr(argv[1], "page-2") != NULL)
|
if (getenv("LABFY_FAKE_IDENTITY_OCR") != NULL &&
|
||||||
|
has_argument(argc, argv, "tsv"))
|
||||||
|
{
|
||||||
|
puts("level\tpage_num\tblock_num\tpar_num\tline_num\tword_num\t"
|
||||||
|
"left\ttop\twidth\theight\tconf\ttext");
|
||||||
|
puts("5\t1\t1\t1\t1\t1\t10\t10\t80\t20\t95\tNOM");
|
||||||
|
puts("5\t1\t1\t1\t1\t2\t100\t10\t120\t20\t94\tSPECIMEN");
|
||||||
|
puts("5\t1\t1\t1\t2\t1\t10\t40\t100\t20\t93\tPRENOM");
|
||||||
|
puts("5\t1\t1\t1\t2\t2\t120\t40\t100\t20\t92\tALICE");
|
||||||
|
puts("5\t1\t1\t1\t3\t1\t10\t70\t100\t20\t91\tNUMERO");
|
||||||
|
puts("5\t1\t1\t1\t3\t2\t120\t70\t100\t20\t90\tABC123");
|
||||||
|
puts("5\t1\t1\t1\t4\t1\t10\t100\t110\t20\t89\tNATIONALITE");
|
||||||
|
puts("5\t1\t1\t1\t4\t2\t130\t100\t120\t20\t88\tFRANCAIS");
|
||||||
|
}
|
||||||
|
else if (getenv("LABFY_FAKE_IDENTITY_OCR") != NULL &&
|
||||||
|
strstr(argv[1], "page-2") != NULL)
|
||||||
|
puts("NOM : PAGEDEUX\nNOM : SPECIMEN\nNOM : PAGE2");
|
||||||
|
else if (getenv("LABFY_FAKE_IDENTITY_OCR") != NULL)
|
||||||
|
puts("NOM : SPECIMEN\nNOM : ALICE\nNOM : ABC123\n"
|
||||||
|
"NATIONALITÉ : FRANCAIS");
|
||||||
|
else if (strstr(argv[1], "page-2") != NULL)
|
||||||
puts("Texte OCR synthétique page deux.");
|
puts("Texte OCR synthétique page deux.");
|
||||||
else
|
else
|
||||||
puts("Texte OCR synthétique page une.");
|
puts("Texte OCR synthétique page une.");
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,9 @@ static gboolean click_cancel(gpointer data)
|
||||||
g_signal_emit_by_name(button, "clicked");
|
g_signal_emit_by_name(button, "clicked");
|
||||||
button = find_button(GTK_WIDGET(dialog), "Suivant");
|
button = find_button(GTK_WIDGET(dialog), "Suivant");
|
||||||
g_signal_emit_by_name(button, "clicked");
|
g_signal_emit_by_name(button, "clicked");
|
||||||
|
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||||
|
"L’OCR produit des propositions à vérifier"));
|
||||||
|
g_signal_emit_by_name(button, "clicked");
|
||||||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||||
"Désignation : SPECIMEN"));
|
"Désignation : SPECIMEN"));
|
||||||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||||
|
|
@ -300,7 +303,7 @@ static void activate(GtkApplication *application, gpointer data)
|
||||||
gtk_application_window_new(application));
|
gtk_application_window_new(application));
|
||||||
gtk_window_present(context->main_window);
|
gtk_window_present(context->main_window);
|
||||||
g_assert_true(create_person_dialog_present(context->main_window,
|
g_assert_true(create_person_dialog_present(context->main_window,
|
||||||
records, "/tmp", context->task_manager, NULL, completed,
|
records, "/tmp", context->task_manager, NULL, NULL, completed,
|
||||||
context, NULL));
|
context, NULL));
|
||||||
g_ptr_array_unref(records);
|
g_ptr_array_unref(records);
|
||||||
g_idle_add(click_cancel, context);
|
g_idle_add(click_cancel, context);
|
||||||
|
|
|
||||||
651
tests/test_create_person_dialog_ocr_gtk.c
Normal file
651
tests/test_create_person_dialog_ocr_gtk.c
Normal file
|
|
@ -0,0 +1,651 @@
|
||||||
|
#include "views/create_person_dialog.h"
|
||||||
|
#include "core/file_hash.h"
|
||||||
|
#include "core/person_creation_coordinator.h"
|
||||||
|
#include "dao/evidence_dao.h"
|
||||||
|
#include "dao/identity_ocr_dao.h"
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "database/statement.h"
|
||||||
|
#include <cairo-pdf.h>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
GtkApplication *application;
|
||||||
|
GtkWindow *main_window;
|
||||||
|
GtkWindow *dialog;
|
||||||
|
TaskManager *task_manager;
|
||||||
|
ToolRegistry *registry;
|
||||||
|
char *root;
|
||||||
|
char *database_path;
|
||||||
|
guint phase;
|
||||||
|
guint guard;
|
||||||
|
guint completion_count;
|
||||||
|
guint layout_index;
|
||||||
|
gboolean layout_resize_pending;
|
||||||
|
gboolean passed;
|
||||||
|
gboolean persisted;
|
||||||
|
} OcrGtkContext;
|
||||||
|
static void remove_tree(const char *root)
|
||||||
|
{
|
||||||
|
GDir *directory=g_dir_open(root,0,NULL);const char *name;
|
||||||
|
if(directory==NULL)return;
|
||||||
|
while((name=g_dir_read_name(directory))!=NULL){
|
||||||
|
char *path=g_build_filename(root,name,NULL);
|
||||||
|
if(g_file_test(path,G_FILE_TEST_IS_DIR))remove_tree(path);
|
||||||
|
else g_unlink(path);
|
||||||
|
g_free(path);
|
||||||
|
}
|
||||||
|
g_dir_close(directory);g_rmdir(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWidget *find_button(GtkWidget *widget, const char *label)
|
||||||
|
{
|
||||||
|
if (GTK_IS_BUTTON(widget) &&
|
||||||
|
g_strcmp0(gtk_button_get_label(GTK_BUTTON(widget)), label) == 0)
|
||||||
|
return widget;
|
||||||
|
for (GtkWidget *child=gtk_widget_get_first_child(widget);child!=NULL;
|
||||||
|
child=gtk_widget_get_next_sibling(child)) {
|
||||||
|
GtkWidget *match=find_button(child,label);
|
||||||
|
if (match!=NULL)return match;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
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 *match=find_named(child,name);
|
||||||
|
if(match!=NULL)return match;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
static GtkWidget *find_ocr_field_row(GtkWidget *widget, const char *code,
|
||||||
|
guint order)
|
||||||
|
{
|
||||||
|
const char *widget_code = g_object_get_data(
|
||||||
|
G_OBJECT(widget), "identity-field-code");
|
||||||
|
guint widget_order = GPOINTER_TO_UINT(g_object_get_data(
|
||||||
|
G_OBJECT(widget), "identity-field-order"));
|
||||||
|
if (g_strcmp0(widget_code, code) == 0 && widget_order == order + 1)
|
||||||
|
return widget;
|
||||||
|
for(GtkWidget *child=gtk_widget_get_first_child(widget);child!=NULL;
|
||||||
|
child=gtk_widget_get_next_sibling(child)){
|
||||||
|
GtkWidget *match=find_ocr_field_row(child,code,order);
|
||||||
|
if(match!=NULL)return match;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
static guint count_ocr_field_rows(GtkWidget *widget)
|
||||||
|
{
|
||||||
|
guint count=g_object_get_data(
|
||||||
|
G_OBJECT(widget),"identity-field-code")!=NULL?1:0;
|
||||||
|
for(GtkWidget *child=gtk_widget_get_first_child(widget);child!=NULL;
|
||||||
|
child=gtk_widget_get_next_sibling(child))
|
||||||
|
count+=count_ocr_field_rows(child);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
static GtkWidget *find_entry(GtkWidget *widget,const char *placeholder)
|
||||||
|
{
|
||||||
|
if(GTK_IS_ENTRY(widget)&&g_strcmp0(
|
||||||
|
gtk_entry_get_placeholder_text(GTK_ENTRY(widget)),placeholder)==0)
|
||||||
|
return widget;
|
||||||
|
for(GtkWidget *child=gtk_widget_get_first_child(widget);child!=NULL;
|
||||||
|
child=gtk_widget_get_next_sibling(child)){
|
||||||
|
GtkWidget *match=find_entry(child,placeholder);
|
||||||
|
if(match!=NULL)return match;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
static GtkWidget *find_label(GtkWidget *widget,const char *text);
|
||||||
|
static void diagnose_ocr_field(GtkWidget *root, guint phase,
|
||||||
|
const char *code, guint order, GtkWidget *row)
|
||||||
|
{
|
||||||
|
GtkWidget *entry=row!=NULL?g_object_get_data(
|
||||||
|
G_OBJECT(row),"identity-value-entry"):NULL;
|
||||||
|
GtkWidget *modify=row!=NULL?g_object_get_data(
|
||||||
|
G_OBJECT(row),"identity-modify-button"):NULL;
|
||||||
|
GtkWidget *reedit=row!=NULL?g_object_get_data(
|
||||||
|
G_OBJECT(row),"identity-reedit-button"):NULL;
|
||||||
|
const char *task_state=find_label(root,"OCR terminé")!=NULL
|
||||||
|
?"terminée":"en cours ou en échec";
|
||||||
|
g_printerr("Diagnostic OCR: étape=%u propositions=%u "
|
||||||
|
"code=%s ordre=%u ligne=%s entrée=%s modifier=%s "
|
||||||
|
"rééditer=%s tâche=%s\n",phase,count_ocr_field_rows(root),code,order,
|
||||||
|
row!=NULL?"oui":"non",entry!=NULL?"oui":"non",
|
||||||
|
modify!=NULL?"oui":"non",reedit!=NULL?"oui":"non",task_state);
|
||||||
|
}
|
||||||
|
static GtkDropDown *find_evidence_dropdown(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 match=first!=NULL&&g_strcmp0(
|
||||||
|
gtk_string_object_get_string(first),"Aucune preuve associée")==0;
|
||||||
|
g_clear_object(&first);if(match)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_dropdown(child);
|
||||||
|
if(match!=NULL)return match;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
static GtkSpinButton *find_visible_spin(GtkWidget *widget)
|
||||||
|
{
|
||||||
|
if(GTK_IS_SPIN_BUTTON(widget)&>k_widget_get_mapped(widget))
|
||||||
|
return GTK_SPIN_BUTTON(widget);
|
||||||
|
for(GtkWidget *child=gtk_widget_get_first_child(widget);child!=NULL;
|
||||||
|
child=gtk_widget_get_next_sibling(child)){
|
||||||
|
GtkSpinButton *match=find_visible_spin(child);
|
||||||
|
if(match!=NULL)return match;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
static GtkWidget *find_label(GtkWidget *widget,const char *text)
|
||||||
|
{
|
||||||
|
if(GTK_IS_LABEL(widget)&&strstr(gtk_label_get_text(GTK_LABEL(widget)),
|
||||||
|
text)!=NULL)return widget;
|
||||||
|
for(GtkWidget *child=gtk_widget_get_first_child(widget);child!=NULL;
|
||||||
|
child=gtk_widget_get_next_sibling(child)){
|
||||||
|
GtkWidget *match=find_label(child,text);
|
||||||
|
if(match!=NULL)return match;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
static char *text_view_contents(GtkTextView *view)
|
||||||
|
{
|
||||||
|
GtkTextIter start,end;GtkTextBuffer *buffer=gtk_text_view_get_buffer(view);
|
||||||
|
gtk_text_buffer_get_bounds(buffer,&start,&end);
|
||||||
|
return gtk_text_buffer_get_text(buffer,&start,&end,FALSE);
|
||||||
|
}
|
||||||
|
static guint64 table_count(const char *database_path,const char *table)
|
||||||
|
{
|
||||||
|
Database *database=database_open(database_path);
|
||||||
|
char *sql=g_strdup_printf("SELECT COUNT(*) FROM %s;",table);
|
||||||
|
DatabaseStatement *statement=database_statement_prepare(database,sql);
|
||||||
|
int64_t value=0;g_assert_nonnull(statement);
|
||||||
|
g_assert_cmpint(database_statement_step(statement),==,
|
||||||
|
DATABASE_STATEMENT_STEP_ROW);
|
||||||
|
g_assert_true(database_statement_column_int64(statement,0,&value));
|
||||||
|
database_statement_finalize(statement);database_close(database);
|
||||||
|
g_free(sql);return (guint64)value;
|
||||||
|
}
|
||||||
|
static void verify_persisted_review(OcrGtkContext *context)
|
||||||
|
{
|
||||||
|
GError *error=NULL;
|
||||||
|
Database *database=database_open(context->database_path);
|
||||||
|
IdentityOcrDao *dao=identity_ocr_dao_new(database);
|
||||||
|
GPtrArray *runs=identity_ocr_dao_list_runs_by_evidence(dao,
|
||||||
|
"10000000-0000-4000-8000-000000000002",&error);
|
||||||
|
g_assert_no_error(error);g_assert_cmpuint(runs->len,==,1);
|
||||||
|
IdentityOcrRunRecord *record=g_ptr_array_index(runs,0);
|
||||||
|
IdentityOcrRun *run=identity_ocr_dao_load_run(dao,context->root,
|
||||||
|
record->id,NULL,&error);
|
||||||
|
g_assert_no_error(error);g_assert_nonnull(run);
|
||||||
|
g_assert_cmpstr(identity_ocr_run_get_corrected_transcription(run),==,
|
||||||
|
"TRANSCRIPTION PERSONNE SPECIMEN CORRIGÉE");
|
||||||
|
g_assert_nonnull(strstr(identity_ocr_run_get_factual_notes(run),
|
||||||
|
"note documentaire SPECIMEN"));
|
||||||
|
const GPtrArray *fields=identity_ocr_run_get_fields(run);
|
||||||
|
gboolean modified=FALSE,manual=FALSE;
|
||||||
|
for(guint index=0;index<fields->len;index++){
|
||||||
|
IdentityFieldObservation *field=g_ptr_array_index(
|
||||||
|
(GPtrArray *)fields,index);
|
||||||
|
modified|=g_strcmp0(
|
||||||
|
identity_field_observation_get_corrected_value(field),
|
||||||
|
"PAGE 2 VALEUR FINALE")==0&&
|
||||||
|
g_strcmp0(identity_field_observation_get_origin(field),
|
||||||
|
"manual_override")==0;
|
||||||
|
manual|=g_strcmp0(
|
||||||
|
identity_field_observation_get_corrected_value(field),
|
||||||
|
"MANUEL SPECIMEN MODIFIÉ")==0&&
|
||||||
|
g_strcmp0(identity_field_observation_get_origin(field),
|
||||||
|
"manual_entry")==0;
|
||||||
|
}
|
||||||
|
g_assert_true(modified);g_assert_true(manual);
|
||||||
|
identity_ocr_run_free(run);g_ptr_array_unref(runs);
|
||||||
|
identity_ocr_dao_free(dao);database_close(database);
|
||||||
|
}
|
||||||
|
static void completed(CreatePersonDialogResult *result,gpointer data)
|
||||||
|
{
|
||||||
|
OcrGtkContext *context=data;
|
||||||
|
if(result!=NULL){
|
||||||
|
GError *error=NULL;
|
||||||
|
Database *database=database_open(context->database_path);
|
||||||
|
PersonCreationCoordinatorResult *created=
|
||||||
|
person_creation_coordinator_execute(database,context->root,
|
||||||
|
create_person_dialog_result_get_input(result),
|
||||||
|
create_person_dialog_result_get_evidence_selection(result),
|
||||||
|
create_person_dialog_result_get_ocr_runs(result),
|
||||||
|
NULL,&error);
|
||||||
|
g_assert_no_error(error);g_assert_nonnull(created);
|
||||||
|
person_creation_coordinator_result_free(created);
|
||||||
|
database_close(database);
|
||||||
|
create_person_dialog_result_free(result);
|
||||||
|
context->persisted=TRUE;
|
||||||
|
}
|
||||||
|
context->completion_count++;
|
||||||
|
}
|
||||||
|
static gboolean drive(gpointer data)
|
||||||
|
{
|
||||||
|
OcrGtkContext *context=data;
|
||||||
|
GtkWidget *root=context->phase<4?GTK_WIDGET(context->dialog):NULL;
|
||||||
|
GtkWidget *button;char *text;
|
||||||
|
if(++context->guard>1000)g_error("Timeout du parcours OCR GTK");
|
||||||
|
switch(context->phase){
|
||||||
|
case 0:
|
||||||
|
{
|
||||||
|
static const int widths[]={1536,1366,1200,760};
|
||||||
|
static const int heights[]={864,768,800,560};
|
||||||
|
if(context->layout_index<G_N_ELEMENTS(widths)){
|
||||||
|
if(!context->layout_resize_pending){
|
||||||
|
gtk_window_set_default_size(context->dialog,
|
||||||
|
widths[context->layout_index],
|
||||||
|
heights[context->layout_index]);
|
||||||
|
context->layout_resize_pending=TRUE;
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
int width=0,height=0;
|
||||||
|
gtk_window_get_default_size(context->dialog,&width,&height);
|
||||||
|
g_assert_cmpint(width,==,widths[context->layout_index]);
|
||||||
|
g_assert_cmpint(height,==,heights[context->layout_index]);
|
||||||
|
GtkWidget *paned=find_named(root,
|
||||||
|
"create-person-ocr-paned");
|
||||||
|
GtkWidget *scroll=find_named(root,
|
||||||
|
"create-person-ocr-scroll");
|
||||||
|
GtkWidget *actions=find_named(root,
|
||||||
|
"create-person-actions");
|
||||||
|
g_assert_true(GTK_IS_PANED(paned));
|
||||||
|
g_assert_true(GTK_IS_SCROLLED_WINDOW(scroll));
|
||||||
|
GtkPolicyType horizontal_policy=GTK_POLICY_ALWAYS;
|
||||||
|
g_object_get(scroll,"hscrollbar-policy",
|
||||||
|
&horizontal_policy,NULL);
|
||||||
|
g_assert_cmpint(horizontal_policy,==,GTK_POLICY_NEVER);
|
||||||
|
g_assert_true(gtk_widget_get_mapped(actions));
|
||||||
|
if(context->layout_index==0){
|
||||||
|
int position=gtk_paned_get_position(GTK_PANED(paned));
|
||||||
|
g_assert_cmpint(position,>=,600);
|
||||||
|
g_assert_cmpint(position,<=,680);
|
||||||
|
}
|
||||||
|
context->layout_index++;
|
||||||
|
context->layout_resize_pending=FALSE;
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
GtkStack *transcription=GTK_STACK(find_named(root,
|
||||||
|
"create-person-ocr-transcription-stack"));
|
||||||
|
g_assert_true(GTK_IS_STACK(transcription));
|
||||||
|
gtk_stack_set_visible_child_name(transcription,"corrected");
|
||||||
|
g_assert_cmpstr(gtk_stack_get_visible_child_name(transcription),
|
||||||
|
==,"corrected");
|
||||||
|
gtk_stack_set_visible_child_name(transcription,"raw");
|
||||||
|
g_assert_cmpstr(gtk_stack_get_visible_child_name(transcription),
|
||||||
|
==,"raw");
|
||||||
|
}
|
||||||
|
if(find_button(root,"Analyser comme document d’identité")==NULL)
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
if(!gtk_widget_get_sensitive(find_button(root,
|
||||||
|
"Analyser comme document d’identité")))return G_SOURCE_CONTINUE;
|
||||||
|
g_signal_emit_by_name(find_button(root,
|
||||||
|
"Analyser comme document d’identité"),"clicked");
|
||||||
|
context->phase++;break;
|
||||||
|
case 1:
|
||||||
|
if(find_label(root,"OCR terminé")==NULL)return G_SOURCE_CONTINUE;
|
||||||
|
text=text_view_contents(GTK_TEXT_VIEW(find_named(
|
||||||
|
root,"create-person-ocr-raw-text")));
|
||||||
|
g_assert_nonnull(strstr(text,"NOM : SPECIMEN"));g_free(text);
|
||||||
|
g_assert_nonnull(find_button(root,"Voir la zone"));
|
||||||
|
g_assert_cmpuint(count_ocr_field_rows(root),>=,3);
|
||||||
|
GtkWidget *raw_row=find_ocr_field_row(root,"nationality",3);
|
||||||
|
diagnose_ocr_field(root,context->phase,"nationality",3,raw_row);
|
||||||
|
g_assert_nonnull(raw_row);
|
||||||
|
GtkWidget *raw_entry=g_object_get_data(
|
||||||
|
G_OBJECT(raw_row),"identity-value-entry");
|
||||||
|
GtkWidget *raw_modify=g_object_get_data(
|
||||||
|
G_OBJECT(raw_row),"identity-modify-button");
|
||||||
|
g_assert_nonnull(raw_entry);g_assert_nonnull(raw_modify);
|
||||||
|
g_assert_true(gtk_editable_get_editable(GTK_EDITABLE(raw_entry)));
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(raw_entry),"FRANÇAIS");
|
||||||
|
g_signal_emit_by_name(raw_modify,"clicked");
|
||||||
|
g_assert_nonnull(find_button(raw_row,"Modifiée"));
|
||||||
|
g_assert_false(gtk_editable_get_editable(GTK_EDITABLE(raw_entry)));
|
||||||
|
g_signal_emit_by_name(g_object_get_data(
|
||||||
|
G_OBJECT(raw_row),"identity-reedit-button"),"clicked");
|
||||||
|
g_assert_true(gtk_editable_get_editable(GTK_EDITABLE(raw_entry)));
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(raw_entry),"FRANÇAIS CORRIGÉ");
|
||||||
|
g_signal_emit_by_name(raw_modify,"clicked");
|
||||||
|
g_assert_nonnull(find_label(raw_row,
|
||||||
|
"Valeur OCR brute : FRANCAIS"));
|
||||||
|
g_signal_emit_by_name(find_button(raw_row,
|
||||||
|
"Revenir à la valeur OCR"),"clicked");
|
||||||
|
g_assert_cmpstr(gtk_editable_get_text(GTK_EDITABLE(raw_entry)),==,
|
||||||
|
"FRANCAIS");
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(raw_entry),"FRANÇAIS FINAL");
|
||||||
|
g_signal_emit_by_name(raw_modify,"clicked");
|
||||||
|
g_signal_emit_by_name(find_button(raw_row,"Rejeter"),"clicked");
|
||||||
|
g_signal_emit_by_name(find_button(raw_row,
|
||||||
|
"Modifier à nouveau"),"clicked");
|
||||||
|
g_signal_emit_by_name(raw_modify,"clicked");
|
||||||
|
GtkWidget *manual_entry=find_entry(root,
|
||||||
|
"Valeur réellement visible dans la preuve");
|
||||||
|
g_assert_nonnull(manual_entry);
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(manual_entry),
|
||||||
|
"DOCUMENT VISIBLE SPECIMEN");
|
||||||
|
g_signal_emit_by_name(find_button(root,
|
||||||
|
"Ajouter un champ manquant"),"clicked");
|
||||||
|
GtkWidget *manual_row=find_ocr_field_row(root,"document_type",4);
|
||||||
|
diagnose_ocr_field(root,context->phase,"document_type",4,manual_row);
|
||||||
|
g_assert_nonnull(manual_row);
|
||||||
|
g_assert_nonnull(find_label(manual_row,"Origine : manual_entry"));
|
||||||
|
g_signal_emit_by_name(g_object_get_data(
|
||||||
|
G_OBJECT(manual_row),"identity-modify-button"),"clicked");
|
||||||
|
GtkTextView *notes=GTK_TEXT_VIEW(find_named(
|
||||||
|
root,"create-person-ocr-factual-notes"));
|
||||||
|
g_assert_nonnull(notes);
|
||||||
|
gtk_text_buffer_set_text(gtk_text_view_get_buffer(notes),
|
||||||
|
"document tronqué", -1);
|
||||||
|
g_signal_emit_by_name(find_button(root,"Voir la zone"),"clicked");
|
||||||
|
g_assert_true(create_person_dialog_test_overlay_has_region(
|
||||||
|
context->dialog));
|
||||||
|
button=find_button(root,"Accepter");
|
||||||
|
g_signal_emit_by_name(button,"clicked");
|
||||||
|
GtkWidget *second_row=find_ocr_field_row(root,"surname",1);
|
||||||
|
diagnose_ocr_field(root,context->phase,"surname",1,second_row);
|
||||||
|
g_assert_nonnull(second_row);
|
||||||
|
button=g_object_get_data(
|
||||||
|
G_OBJECT(second_row),"identity-modify-button");
|
||||||
|
g_assert_nonnull(button);
|
||||||
|
GtkWidget *entry=g_object_get_data(
|
||||||
|
G_OBJECT(second_row),"identity-value-entry");
|
||||||
|
g_assert_nonnull(entry);
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(entry),"ALICE MODIFIÉE");
|
||||||
|
g_signal_emit_by_name(button,"clicked");
|
||||||
|
GtkWidget *third_row=find_ocr_field_row(root,"surname",2);
|
||||||
|
diagnose_ocr_field(root,context->phase,"surname",2,third_row);
|
||||||
|
g_assert_nonnull(third_row);
|
||||||
|
button=g_object_get_data(
|
||||||
|
G_OBJECT(third_row),"identity-reject-button");
|
||||||
|
g_assert_nonnull(button);g_signal_emit_by_name(button,"clicked");
|
||||||
|
g_assert_null(find_button(root,"Tout accepter"));
|
||||||
|
g_assert_nonnull(find_button(root,"Acceptée"));
|
||||||
|
g_assert_nonnull(find_button(root,"Modifiée"));
|
||||||
|
g_assert_nonnull(find_button(root,"Rejetée"));
|
||||||
|
g_signal_emit_by_name(find_button(root,"Précédent"),"clicked");
|
||||||
|
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
|
||||||
|
g_assert_nonnull(find_button(root,"Acceptée"));
|
||||||
|
guint64 generation=create_person_dialog_test_ocr_generation(
|
||||||
|
context->dialog);
|
||||||
|
button=find_button(root,"Analyser comme document d’identité");
|
||||||
|
g_signal_emit_by_name(button,"clicked");
|
||||||
|
g_signal_emit_by_name(button,"clicked");
|
||||||
|
g_assert_cmpuint(create_person_dialog_test_ocr_generation(
|
||||||
|
context->dialog),==,generation+2);
|
||||||
|
context->phase++;break;
|
||||||
|
case 2:
|
||||||
|
if(find_label(root,"OCR terminé")==NULL)return G_SOURCE_CONTINUE;
|
||||||
|
g_signal_emit_by_name(find_button(root,"Précédent"),"clicked");
|
||||||
|
g_signal_emit_by_name(find_button(root,
|
||||||
|
"Retirer"),"clicked");
|
||||||
|
g_assert_false(create_person_dialog_test_overlay_has_region(
|
||||||
|
context->dialog));
|
||||||
|
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
|
||||||
|
text=text_view_contents(GTK_TEXT_VIEW(find_named(
|
||||||
|
root,"create-person-ocr-raw-text")));
|
||||||
|
g_assert_cmpstr(text,==,"");g_free(text);
|
||||||
|
g_signal_emit_by_name(find_button(root,"Précédent"),"clicked");
|
||||||
|
GtkDropDown *evidence=find_evidence_dropdown(root);
|
||||||
|
g_assert_nonnull(evidence);gtk_drop_down_set_selected(evidence,2);
|
||||||
|
g_signal_emit_by_name(find_button(root,
|
||||||
|
"Ajouter à la sélection"),"clicked");
|
||||||
|
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
|
||||||
|
GtkSpinButton *page=find_visible_spin(root);g_assert_nonnull(page);
|
||||||
|
gtk_spin_button_set_value(page,2);
|
||||||
|
g_signal_emit_by_name(find_button(root,
|
||||||
|
"Analyser comme document d’identité"),"clicked");
|
||||||
|
context->phase++;break;
|
||||||
|
case 3:
|
||||||
|
if(find_label(root,"OCR terminé")==NULL)return G_SOURCE_CONTINUE;
|
||||||
|
{
|
||||||
|
GtkStack *transcription=GTK_STACK(find_named(root,
|
||||||
|
"create-person-ocr-transcription-stack"));
|
||||||
|
GtkTextView *corrected=GTK_TEXT_VIEW(find_named(root,
|
||||||
|
"identity-corrected-transcription"));
|
||||||
|
GtkTextView *raw=GTK_TEXT_VIEW(find_named(root,
|
||||||
|
"create-person-ocr-raw-text"));
|
||||||
|
g_assert_cmpstr(gtk_stack_get_visible_child_name(transcription),
|
||||||
|
==,"corrected");
|
||||||
|
g_assert_true(gtk_widget_get_mapped(GTK_WIDGET(corrected)));
|
||||||
|
g_assert_true(gtk_widget_get_sensitive(GTK_WIDGET(corrected)));
|
||||||
|
g_assert_true(gtk_text_view_get_editable(corrected));
|
||||||
|
g_assert_false(gtk_text_view_get_editable(raw));
|
||||||
|
gtk_text_buffer_set_text(gtk_text_view_get_buffer(corrected),
|
||||||
|
"TRANSCRIPTION PERSONNE SPECIMEN CORRIGÉE",-1);
|
||||||
|
gtk_stack_set_visible_child_name(transcription,"raw");
|
||||||
|
char *raw_text=text_view_contents(raw);
|
||||||
|
g_assert_nonnull(strstr(raw_text,"NOM : SPECIMEN"));
|
||||||
|
g_free(raw_text);
|
||||||
|
gtk_stack_set_visible_child_name(transcription,"corrected");
|
||||||
|
char *corrected_text=text_view_contents(corrected);
|
||||||
|
g_assert_cmpstr(corrected_text,==,
|
||||||
|
"TRANSCRIPTION PERSONNE SPECIMEN CORRIGÉE");
|
||||||
|
g_free(corrected_text);
|
||||||
|
}
|
||||||
|
button=find_button(root,"Accepter");
|
||||||
|
g_assert_nonnull(button);g_signal_emit_by_name(button,"clicked");
|
||||||
|
GtkWidget *pdf_second_row=find_ocr_field_row(root,"surname",1);
|
||||||
|
diagnose_ocr_field(root,context->phase,"surname",1,pdf_second_row);
|
||||||
|
g_assert_nonnull(pdf_second_row);
|
||||||
|
button=g_object_get_data(
|
||||||
|
G_OBJECT(pdf_second_row),"identity-modify-button");
|
||||||
|
g_assert_nonnull(button);
|
||||||
|
GtkWidget *pdf_entry=g_object_get_data(
|
||||||
|
G_OBJECT(pdf_second_row),"identity-value-entry");
|
||||||
|
g_assert_nonnull(pdf_entry);
|
||||||
|
g_assert_true(gtk_widget_get_sensitive(pdf_entry));
|
||||||
|
g_assert_true(gtk_editable_get_editable(GTK_EDITABLE(pdf_entry)));
|
||||||
|
char *pdf_raw_value=g_strdup(
|
||||||
|
gtk_editable_get_text(GTK_EDITABLE(pdf_entry)));
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(pdf_entry),"PAGE 2 PREMIÈRE");
|
||||||
|
g_signal_emit_by_name(button,"clicked");
|
||||||
|
g_signal_emit_by_name(g_object_get_data(
|
||||||
|
G_OBJECT(pdf_second_row),"identity-reedit-button"),"clicked");
|
||||||
|
g_assert_true(gtk_editable_get_editable(GTK_EDITABLE(pdf_entry)));
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(pdf_entry),"PAGE 2 DEUXIÈME");
|
||||||
|
g_signal_emit_by_name(button,"clicked");
|
||||||
|
g_signal_emit_by_name(g_object_get_data(
|
||||||
|
G_OBJECT(pdf_second_row),"identity-restore-button"),"clicked");
|
||||||
|
g_assert_cmpstr(gtk_editable_get_text(GTK_EDITABLE(pdf_entry)),
|
||||||
|
==,pdf_raw_value);
|
||||||
|
g_free(pdf_raw_value);
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(pdf_entry),
|
||||||
|
"PAGE 2 VALEUR FINALE");
|
||||||
|
g_signal_emit_by_name(button,"clicked");
|
||||||
|
GtkWidget *manual_input=find_entry(root,
|
||||||
|
"Valeur réellement visible dans la preuve");
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(manual_input),
|
||||||
|
"MANUEL SPECIMEN INITIAL");
|
||||||
|
g_signal_emit_by_name(find_button(root,
|
||||||
|
"Ajouter un champ manquant"),"clicked");
|
||||||
|
GtkWidget *pdf_manual_row=find_ocr_field_row(
|
||||||
|
root,"document_type",4);
|
||||||
|
g_assert_nonnull(pdf_manual_row);
|
||||||
|
GtkWidget *manual_value=g_object_get_data(
|
||||||
|
G_OBJECT(pdf_manual_row),"identity-value-entry");
|
||||||
|
g_assert_true(gtk_widget_get_sensitive(manual_value));
|
||||||
|
g_assert_true(gtk_editable_get_editable(GTK_EDITABLE(manual_value)));
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(manual_value),
|
||||||
|
"MANUEL SPECIMEN MODIFIÉ");
|
||||||
|
g_signal_emit_by_name(g_object_get_data(
|
||||||
|
G_OBJECT(pdf_manual_row),"identity-modify-button"),"clicked");
|
||||||
|
GtkTextView *factual_notes=GTK_TEXT_VIEW(find_named(root,
|
||||||
|
"create-person-ocr-factual-notes"));
|
||||||
|
g_assert_true(gtk_text_view_get_editable(factual_notes));
|
||||||
|
gtk_text_buffer_set_text(gtk_text_view_get_buffer(factual_notes),
|
||||||
|
"note documentaire SPECIMEN — document incomplet",-1);
|
||||||
|
gtk_window_set_default_size(context->dialog,760,560);
|
||||||
|
g_assert_true(gtk_widget_get_mapped(GTK_WIDGET(factual_notes)));
|
||||||
|
GtkWidget *pdf_third_row=find_ocr_field_row(root,"surname",2);
|
||||||
|
diagnose_ocr_field(root,context->phase,"surname",2,pdf_third_row);
|
||||||
|
g_assert_nonnull(pdf_third_row);
|
||||||
|
button=g_object_get_data(
|
||||||
|
G_OBJECT(pdf_third_row),"identity-reject-button");
|
||||||
|
g_assert_nonnull(button);g_signal_emit_by_name(button,"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 VALEUR FINALE"));
|
||||||
|
g_assert_nonnull(find_label(root,"authenticité n’est pas établie"));
|
||||||
|
g_assert_null(find_label(root,"brut : PAGE2"));
|
||||||
|
g_signal_emit_by_name(find_button(root,
|
||||||
|
"Créer la personne"),"clicked");
|
||||||
|
context->phase++;break;
|
||||||
|
case 4:
|
||||||
|
if(context->completion_count!=1)return G_SOURCE_CONTINUE;
|
||||||
|
g_assert_true(context->persisted);
|
||||||
|
g_assert_cmpuint(table_count(context->database_path,"entites"),==,1);
|
||||||
|
g_assert_cmpuint(table_count(context->database_path,
|
||||||
|
"identity_ocr_runs"),==,1);
|
||||||
|
verify_persisted_review(context);
|
||||||
|
g_assert_true(gtk_widget_get_visible(GTK_WIDGET(context->main_window)));
|
||||||
|
GPtrArray *empty=g_ptr_array_new();
|
||||||
|
g_assert_true(create_person_dialog_present(context->main_window,empty,
|
||||||
|
context->root,context->task_manager,
|
||||||
|
tool_registry_find(context->registry,"tesseract"),NULL,completed,
|
||||||
|
context,NULL));
|
||||||
|
g_ptr_array_unref(empty);
|
||||||
|
GList *windows=gtk_application_get_windows(context->application);
|
||||||
|
context->dialog=windows->data==context->main_window
|
||||||
|
?GTK_WINDOW(windows->next->data):GTK_WINDOW(windows->data);
|
||||||
|
g_signal_emit_by_name(find_button(GTK_WIDGET(context->dialog),
|
||||||
|
"Annuler"),"clicked");
|
||||||
|
context->phase++;break;
|
||||||
|
case 5:
|
||||||
|
if(context->completion_count!=2)return G_SOURCE_CONTINUE;
|
||||||
|
g_assert_true(gtk_widget_get_visible(GTK_WIDGET(context->main_window)));
|
||||||
|
context->passed=TRUE;
|
||||||
|
g_application_quit(G_APPLICATION(context->application));
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
default:break;
|
||||||
|
}
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
static void write_png(const char *path)
|
||||||
|
{
|
||||||
|
cairo_surface_t *surface=cairo_image_surface_create(
|
||||||
|
CAIRO_FORMAT_ARGB32,320,160);
|
||||||
|
cairo_t *cr=cairo_create(surface);cairo_set_source_rgb(cr,1,1,1);
|
||||||
|
cairo_paint(cr);cairo_set_source_rgb(cr,0,0,0);
|
||||||
|
cairo_move_to(cr,20,50);cairo_show_text(cr,"SPECIMEN OCR IDENTITE");
|
||||||
|
g_assert_cmpint(cairo_surface_write_to_png(surface,path),==,
|
||||||
|
CAIRO_STATUS_SUCCESS);cairo_destroy(cr);cairo_surface_destroy(surface);
|
||||||
|
}
|
||||||
|
static void write_pdf(const char *path)
|
||||||
|
{
|
||||||
|
cairo_surface_t *surface=cairo_pdf_surface_create(path,320,160);
|
||||||
|
cairo_t *cr=cairo_create(surface);
|
||||||
|
cairo_move_to(cr,20,50);cairo_show_text(cr,"SPECIMEN PAGE UNE");
|
||||||
|
cairo_show_page(cr);cairo_move_to(cr,20,50);
|
||||||
|
cairo_show_text(cr,"SPECIMEN PAGE DEUX");cairo_show_page(cr);
|
||||||
|
cairo_destroy(cr);cairo_surface_destroy(surface);
|
||||||
|
}
|
||||||
|
static void activate(GtkApplication *application,gpointer data)
|
||||||
|
{
|
||||||
|
OcrGtkContext *context=data;GError *error=NULL;
|
||||||
|
g_setenv("LABFY_FAKE_IDENTITY_OCR","1",TRUE);
|
||||||
|
char *database_directory=g_build_filename(
|
||||||
|
context->root,"00_BaseDeDonnees",NULL);
|
||||||
|
context->database_path=g_build_filename(database_directory,
|
||||||
|
"Enquete.sqlite",NULL);
|
||||||
|
g_assert_cmpint(g_mkdir_with_parents(database_directory,0700),==,0);
|
||||||
|
g_assert_true(database_initialize(context->database_path,
|
||||||
|
"Enquête SPECIMEN GTK OCR",context->root));
|
||||||
|
char *evidence_directory=g_build_filename(context->root,
|
||||||
|
"01_Preuves_Originales","identity",NULL);
|
||||||
|
g_assert_cmpint(g_mkdir_with_parents(evidence_directory,0700),==,0);
|
||||||
|
char *png=g_build_filename(evidence_directory,"SPECIMEN.png",NULL);
|
||||||
|
char *pdf=g_build_filename(evidence_directory,"SPECIMEN-page-2.pdf",NULL);
|
||||||
|
write_png(png);write_pdf(pdf);
|
||||||
|
char *png_sha=NULL,*pdf_sha=NULL;guint64 png_size=0,pdf_size=0;
|
||||||
|
g_assert_true(file_hash_compute_sha256(png,NULL,&png_sha,&png_size,&error));
|
||||||
|
g_assert_true(file_hash_compute_sha256(pdf,NULL,&pdf_sha,&pdf_size,&error));
|
||||||
|
GPtrArray *records=g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)evidence_record_free);
|
||||||
|
EvidenceRecord *record=evidence_record_new(
|
||||||
|
"10000000-0000-4000-8000-000000000001","SPECIMEN.png",
|
||||||
|
"SPECIMEN.png","01_Preuves_Originales/identity/SPECIMEN.png",
|
||||||
|
"document",png_size,png_sha,"2026-07-28T10:00:00Z",NULL,NULL,
|
||||||
|
"SPECIMEN synthétique",EVIDENCE_INTEGRITY_STATUS_VALID,&error);
|
||||||
|
g_assert_nonnull(record);evidence_record_set_display_metadata(
|
||||||
|
record,"Document d’identité","image/png");g_ptr_array_add(records,record);
|
||||||
|
record=evidence_record_new(
|
||||||
|
"10000000-0000-4000-8000-000000000002","SPECIMEN-page-2.pdf",
|
||||||
|
"SPECIMEN-page-2.pdf",
|
||||||
|
"01_Preuves_Originales/identity/SPECIMEN-page-2.pdf",
|
||||||
|
"document",pdf_size,pdf_sha,"2026-07-28T10:00:00Z",NULL,NULL,
|
||||||
|
"PDF SPECIMEN multipage",EVIDENCE_INTEGRITY_STATUS_VALID,&error);
|
||||||
|
g_assert_nonnull(record);evidence_record_set_display_metadata(
|
||||||
|
record,"Document d’identité","application/pdf");
|
||||||
|
g_ptr_array_add(records,record);g_assert_no_error(error);
|
||||||
|
Database *database=database_open(context->database_path);
|
||||||
|
EvidenceDao *evidence_dao=evidence_dao_new(database,&error);
|
||||||
|
g_assert_no_error(error);g_assert_nonnull(evidence_dao);
|
||||||
|
for(guint index=0;index<records->len;index++)
|
||||||
|
g_assert_true(evidence_dao_insert(evidence_dao,
|
||||||
|
g_ptr_array_index(records,index),&error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
evidence_dao_free(evidence_dao);database_close(database);
|
||||||
|
context->registry=tool_registry_new();
|
||||||
|
char *tool=g_canonicalize_filename("tests/fake_document_tool",NULL);
|
||||||
|
g_assert_true(tool_registry_register(context->registry,"tesseract",
|
||||||
|
"Tesseract synthétique",tool,TOOL_REQUIREMENT_OPTIONAL,&error));
|
||||||
|
g_assert_true(tool_registry_refresh(context->registry,&error));
|
||||||
|
g_assert_true(tool_registry_set_version(context->registry,"tesseract",
|
||||||
|
"5.0.0 SPECIMEN",&error));g_assert_no_error(error);
|
||||||
|
context->main_window=GTK_WINDOW(gtk_application_window_new(application));
|
||||||
|
gtk_window_present(context->main_window);
|
||||||
|
g_assert_true(create_person_dialog_present(context->main_window,records,
|
||||||
|
context->root,context->task_manager,
|
||||||
|
tool_registry_find(context->registry,"tesseract"),NULL,completed,
|
||||||
|
context,NULL));
|
||||||
|
GList *windows=gtk_application_get_windows(application);
|
||||||
|
context->dialog=windows->data==context->main_window
|
||||||
|
?GTK_WINDOW(windows->next->data):GTK_WINDOW(windows->data);
|
||||||
|
GtkWidget *designation=find_entry(GTK_WIDGET(context->dialog),
|
||||||
|
"Personne présumée liée aux comptes");
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(designation),"PERSONNE SPECIMEN");
|
||||||
|
g_signal_emit_by_name(find_button(GTK_WIDGET(context->dialog),
|
||||||
|
"Suivant"),"clicked");
|
||||||
|
GtkWidget *role=find_button(GTK_WIDGET(context->dialog),"Victime");
|
||||||
|
if(role!=NULL)g_signal_emit_by_name(role,"clicked");
|
||||||
|
g_signal_emit_by_name(find_button(GTK_WIDGET(context->dialog),
|
||||||
|
"Suivant"),"clicked");
|
||||||
|
GtkDropDown *evidence=find_evidence_dropdown(GTK_WIDGET(context->dialog));
|
||||||
|
gtk_drop_down_set_selected(evidence,1);
|
||||||
|
g_signal_emit_by_name(find_button(GTK_WIDGET(context->dialog),
|
||||||
|
"Ajouter à la sélection"),"clicked");
|
||||||
|
g_signal_emit_by_name(find_button(GTK_WIDGET(context->dialog),
|
||||||
|
"Suivant"),"clicked");
|
||||||
|
g_timeout_add(10,drive,context);
|
||||||
|
g_ptr_array_unref(records);g_free(database_directory);
|
||||||
|
g_free(evidence_directory);g_free(png);g_free(pdf);
|
||||||
|
g_free(png_sha);g_free(pdf_sha);g_free(tool);
|
||||||
|
}
|
||||||
|
int main(int argc,char **argv)
|
||||||
|
{
|
||||||
|
if(!gtk_init_check()){g_print("SKIP: aucun affichage GTK disponible.\n");
|
||||||
|
return 0;}
|
||||||
|
OcrGtkContext context={0};
|
||||||
|
context.root=g_dir_make_tmp("labfy-ocr-gtk-XXXXXX",NULL);
|
||||||
|
context.application=gtk_application_new(
|
||||||
|
"org.labfy.Investigation.OcrDialogTest",G_APPLICATION_NON_UNIQUE);
|
||||||
|
context.task_manager=task_manager_new();
|
||||||
|
g_signal_connect(context.application,"activate",G_CALLBACK(activate),
|
||||||
|
&context);
|
||||||
|
int status=g_application_run(G_APPLICATION(context.application),argc,argv);
|
||||||
|
g_assert_true(context.passed);
|
||||||
|
tool_registry_free(context.registry);task_manager_free(context.task_manager);
|
||||||
|
g_object_unref(context.application);
|
||||||
|
g_free(context.database_path);remove_tree(context.root);g_free(context.root);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void)
|
||||||
"FROM investigation;"
|
"FROM investigation;"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(strcmp(schema_version, "14") == 0);
|
assert(strcmp(schema_version, "17") == 0);
|
||||||
test_database_assert_table_exists(database, "person_role_assignments");
|
test_database_assert_table_exists(database, "person_role_assignments");
|
||||||
test_database_assert_table_exists(database, "bank_account_entities");
|
test_database_assert_table_exists(database, "bank_account_entities");
|
||||||
test_database_assert_table_exists(database, "relation_types");
|
test_database_assert_table_exists(database, "relation_types");
|
||||||
|
|
@ -993,7 +993,7 @@ static void test_database_migrate_v1_to_v2(void)
|
||||||
assert(
|
assert(
|
||||||
strcmp(
|
strcmp(
|
||||||
schema_version,
|
schema_version,
|
||||||
"14"
|
"17"
|
||||||
) == 0
|
) == 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1448,7 +1448,7 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
|
||||||
legacy_sources = test_database_read_single_text(sqlite_database,
|
legacy_sources = test_database_read_single_text(sqlite_database,
|
||||||
"SELECT COUNT(*) FROM preuve_entite_sources "
|
"SELECT COUNT(*) FROM preuve_entite_sources "
|
||||||
"WHERE source_kind='legacy_manual';");
|
"WHERE source_kind='legacy_manual';");
|
||||||
assert(strcmp(version, "14") == 0);
|
assert(strcmp(version, "17") == 0);
|
||||||
assert(strcmp(legacy_sources, "1") == 0);
|
assert(strcmp(legacy_sources, "1") == 0);
|
||||||
char *legacy_role = test_database_read_single_text(sqlite_database,
|
char *legacy_role = test_database_read_single_text(sqlite_database,
|
||||||
"SELECT role_code || ':' || provenance_kind "
|
"SELECT role_code || ':' || provenance_kind "
|
||||||
|
|
|
||||||
803
tests/test_evidence_identity_import_gtk.c
Normal file
803
tests/test_evidence_identity_import_gtk.c
Normal file
|
|
@ -0,0 +1,803 @@
|
||||||
|
#include "core/evidence_identity_import_task.h"
|
||||||
|
#include "core/file_hash.h"
|
||||||
|
#include "core/tool_registry.h"
|
||||||
|
#include "dao/entity_dao.h"
|
||||||
|
#include "dao/evidence_dao.h"
|
||||||
|
#include "dao/evidence_entity_dao.h"
|
||||||
|
#include "dao/identity_ocr_dao.h"
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "database/schema.h"
|
||||||
|
#include "models/evidence_type.h"
|
||||||
|
#include "views/evidence_identity_ocr_dialog.h"
|
||||||
|
#include "views/evidence_import_dialog.h"
|
||||||
|
#include "views/main_window.h"
|
||||||
|
#include "widgets/workspace.h"
|
||||||
|
#include <cairo.h>
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
GtkApplication *application;
|
||||||
|
GtkWindow *main_window;
|
||||||
|
MainWindow *production_main_window;
|
||||||
|
TaskManager *manager;
|
||||||
|
ToolRegistry *registry;
|
||||||
|
char *root;
|
||||||
|
char *database_path;
|
||||||
|
char *file_path;
|
||||||
|
GPtrArray *persons;
|
||||||
|
EvidenceImportDialogResult *metadata;
|
||||||
|
BackgroundTask *import_task;
|
||||||
|
GtkWindow *active_ocr_dialog;
|
||||||
|
guint phase;
|
||||||
|
guint guard;
|
||||||
|
gboolean metadata_ready;
|
||||||
|
gboolean import_done;
|
||||||
|
gboolean cancellation_done;
|
||||||
|
gboolean layout_resize_pending;
|
||||||
|
gboolean passed;
|
||||||
|
gboolean revision_done;
|
||||||
|
char *final_evidence_identifier;
|
||||||
|
char *run_identifier;
|
||||||
|
guint revision_requests;
|
||||||
|
guint rerun_requests;
|
||||||
|
guint import_start_count;
|
||||||
|
} TestContext;
|
||||||
|
|
||||||
|
static void remove_tree(const char *root)
|
||||||
|
{
|
||||||
|
GDir *directory = g_dir_open(root, 0, NULL);
|
||||||
|
const char *name;
|
||||||
|
if (directory == NULL) return;
|
||||||
|
while ((name = g_dir_read_name(directory)) != NULL) {
|
||||||
|
char *path = g_build_filename(root, name, NULL);
|
||||||
|
if (g_file_test(path, G_FILE_TEST_IS_DIR)) remove_tree(path);
|
||||||
|
else g_unlink(path);
|
||||||
|
g_free(path);
|
||||||
|
}
|
||||||
|
g_dir_close(directory);
|
||||||
|
g_rmdir(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 gboolean is_descendant_of(GtkWidget *widget, GtkWidget *ancestor)
|
||||||
|
{
|
||||||
|
for (GtkWidget *parent = widget; parent != NULL;
|
||||||
|
parent = gtk_widget_get_parent(parent))
|
||||||
|
if (parent == ancestor) return TRUE;
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWidget *find_button(GtkWidget *widget, const char *label)
|
||||||
|
{
|
||||||
|
if (GTK_IS_BUTTON(widget) &&
|
||||||
|
g_strcmp0(gtk_button_get_label(GTK_BUTTON(widget)), label) == 0)
|
||||||
|
return widget;
|
||||||
|
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||||
|
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||||
|
GtkWidget *found = find_button(child, label);
|
||||||
|
if (found != NULL) return found;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWidget *find_label(GtkWidget *widget, const char *text)
|
||||||
|
{
|
||||||
|
if (GTK_IS_LABEL(widget) &&
|
||||||
|
strstr(gtk_label_get_text(GTK_LABEL(widget)), text) != NULL)
|
||||||
|
return widget;
|
||||||
|
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||||
|
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||||
|
GtkWidget *found = find_label(child, text);
|
||||||
|
if (found != NULL) return found;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWindow *find_window(TestContext *context, const char *title)
|
||||||
|
{
|
||||||
|
for (GList *item = gtk_application_get_windows(context->application);
|
||||||
|
item != NULL; item = item->next)
|
||||||
|
if (g_strcmp0(gtk_window_get_title(item->data), title) == 0)
|
||||||
|
return item->data;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWidget *find_field_row(GtkWidget *widget, guint ordinal)
|
||||||
|
{
|
||||||
|
if (g_object_get_data(G_OBJECT(widget), "identity-field-code") != NULL &&
|
||||||
|
GPOINTER_TO_UINT(g_object_get_data(
|
||||||
|
G_OBJECT(widget), "identity-field-order")) == ordinal + 1)
|
||||||
|
return widget;
|
||||||
|
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||||
|
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||||
|
GtkWidget *found = find_field_row(child, ordinal);
|
||||||
|
if (found != NULL) return found;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static guint count_field_rows(GtkWidget *widget)
|
||||||
|
{
|
||||||
|
guint count = g_object_get_data(
|
||||||
|
G_OBJECT(widget), "identity-field-code") != NULL ? 1 : 0;
|
||||||
|
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||||
|
child != NULL; child = gtk_widget_get_next_sibling(child))
|
||||||
|
count += count_field_rows(child);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWidget *find_entry_placeholder(GtkWidget *widget,
|
||||||
|
const char *placeholder)
|
||||||
|
{
|
||||||
|
if (GTK_IS_ENTRY(widget) && g_strcmp0(
|
||||||
|
gtk_entry_get_placeholder_text(GTK_ENTRY(widget)),
|
||||||
|
placeholder) == 0)
|
||||||
|
return widget;
|
||||||
|
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||||
|
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||||
|
GtkWidget *found = find_entry_placeholder(child, placeholder);
|
||||||
|
if (found != NULL) return found;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *text_view_contents(GtkTextView *view)
|
||||||
|
{
|
||||||
|
GtkTextIter start;
|
||||||
|
GtkTextIter end;
|
||||||
|
GtkTextBuffer *buffer = gtk_text_view_get_buffer(view);
|
||||||
|
gtk_text_buffer_get_bounds(buffer, &start, &end);
|
||||||
|
return gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void import_completed(BackgroundTask *task, gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
context->import_done = TRUE;
|
||||||
|
g_assert_cmpint(background_task_get_state(task), ==,
|
||||||
|
BACKGROUND_TASK_STATE_COMPLETED);
|
||||||
|
evidence_identity_ocr_dialog_finish_import(
|
||||||
|
context->active_ocr_dialog, NULL);
|
||||||
|
g_clear_object(&context->active_ocr_dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ocr_completed(EvidenceIdentityOcrDialogResult *result,
|
||||||
|
gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
GError *error = NULL;
|
||||||
|
g_assert_nonnull(result);
|
||||||
|
g_assert_true(evidence_identity_ocr_dialog_result_has_ocr(result));
|
||||||
|
context->import_start_count++;
|
||||||
|
g_assert_cmpuint(context->import_start_count, ==, 1);
|
||||||
|
context->active_ocr_dialog = g_object_ref(
|
||||||
|
evidence_identity_ocr_dialog_result_get_dialog(result));
|
||||||
|
IdentityOcrRun *run =
|
||||||
|
evidence_identity_ocr_dialog_result_steal_run(result);
|
||||||
|
EvidenceIdentityImportTaskRequest *request =
|
||||||
|
evidence_identity_import_task_request_new(
|
||||||
|
context->database_path, context->root, context->file_path,
|
||||||
|
evidence_identity_ocr_dialog_result_get_person_identifier(result),
|
||||||
|
evidence_import_dialog_result_get_type_identifier(
|
||||||
|
context->metadata),
|
||||||
|
evidence_import_dialog_result_get_collected_at(context->metadata),
|
||||||
|
evidence_import_dialog_result_get_source(context->metadata),
|
||||||
|
evidence_import_dialog_result_get_description(context->metadata),
|
||||||
|
run);
|
||||||
|
g_assert_nonnull(request);
|
||||||
|
context->import_task = evidence_identity_import_task_start(
|
||||||
|
context->manager, request, import_completed, context, NULL, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_nonnull(context->import_task);
|
||||||
|
evidence_identity_import_task_request_free(request);
|
||||||
|
identity_ocr_run_free(run);
|
||||||
|
evidence_identity_ocr_dialog_result_free(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cancellation_completed(EvidenceIdentityOcrDialogResult *result,
|
||||||
|
gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
g_assert_null(result);
|
||||||
|
context->cancellation_done = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void metadata_completed(EvidenceImportDialogResult *result,
|
||||||
|
gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
GError *error = NULL;
|
||||||
|
g_assert_nonnull(result);
|
||||||
|
context->metadata = result;
|
||||||
|
context->metadata_ready = TRUE;
|
||||||
|
g_assert_true(evidence_identity_ocr_dialog_present(
|
||||||
|
context->main_window, context->file_path, context->persons,
|
||||||
|
NULL, tool_registry_find(context->registry, "tesseract"),
|
||||||
|
NULL, ocr_completed, context, NULL, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void verify_persistence(TestContext *context)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
Database *database = database_open(context->database_path);
|
||||||
|
g_assert_nonnull(database);
|
||||||
|
EvidenceDao *evidence_dao = evidence_dao_new(database, &error);
|
||||||
|
EvidenceEntityDao *link_dao =
|
||||||
|
evidence_entity_dao_new(database, &error);
|
||||||
|
IdentityOcrDao *ocr_dao = identity_ocr_dao_new(database);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
GPtrArray *evidences = evidence_dao_list_all(evidence_dao, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_cmpuint(evidences->len, ==, 1);
|
||||||
|
EvidenceRecord *evidence = g_ptr_array_index(evidences, 0);
|
||||||
|
const char *evidence_id = evidence_record_get_identifier(evidence);
|
||||||
|
gboolean link_exists = FALSE;
|
||||||
|
g_assert_nonnull(evidence_id);
|
||||||
|
if (context->final_evidence_identifier == NULL)
|
||||||
|
context->final_evidence_identifier = g_strdup(evidence_id);
|
||||||
|
g_assert_cmpstr(evidence_id, ==, context->final_evidence_identifier);
|
||||||
|
g_assert_true(evidence_entity_dao_exists(
|
||||||
|
link_dao, evidence_id,
|
||||||
|
"33333333-3333-4333-8333-333333333333", &link_exists, &error));
|
||||||
|
g_assert_true(link_exists);
|
||||||
|
GPtrArray *runs =
|
||||||
|
identity_ocr_dao_list_runs_by_evidence(ocr_dao, evidence_id, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_cmpuint(runs->len, ==, 1);
|
||||||
|
IdentityOcrRunRecord *run = g_ptr_array_index(runs, 0);
|
||||||
|
char *run_identifier = g_strdup(run->id);
|
||||||
|
if (context->run_identifier == NULL)
|
||||||
|
context->run_identifier = g_strdup(run->id);
|
||||||
|
g_assert_cmpstr(run->evidence_id, ==,
|
||||||
|
context->final_evidence_identifier);
|
||||||
|
g_assert_cmpstr(run->id, ==, context->run_identifier);
|
||||||
|
g_assert_nonnull(run->text_relative_path);
|
||||||
|
g_assert_nonnull(run->text_sha256);
|
||||||
|
g_assert_nonnull(run->tsv_relative_path);
|
||||||
|
g_assert_nonnull(run->tsv_sha256);
|
||||||
|
g_assert_true(run->transcription_is_human);
|
||||||
|
g_assert_cmpstr(run->transcription_origin, ==, "human");
|
||||||
|
g_assert_cmpstr(run->corrected_transcription, ==,
|
||||||
|
context->revision_done ? "TRANSCRIPTION SPECIMEN RÉVISÉE" :
|
||||||
|
"ÉTAT CIVIL\nNée à Besançon — Côte-d’Or");
|
||||||
|
GPtrArray *documents = identity_ocr_dao_list_documents_by_person(
|
||||||
|
ocr_dao, "33333333-3333-4333-8333-333333333333", &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_cmpuint(documents->len, ==, 1);
|
||||||
|
IdentityDocumentObservationRecord *document =
|
||||||
|
g_ptr_array_index(documents, 0);
|
||||||
|
g_assert_nonnull(strstr(document->factual_notes,
|
||||||
|
context->revision_done ? "révision" : "tronqué"));
|
||||||
|
GPtrArray *fields = identity_ocr_dao_list_fields_by_document(
|
||||||
|
ocr_dao, document->id, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
gboolean accepted = FALSE;
|
||||||
|
gboolean modified = FALSE;
|
||||||
|
gboolean manual = FALSE;
|
||||||
|
for (guint index = 0; index < fields->len; index++) {
|
||||||
|
IdentityFieldObservationRecord *field =
|
||||||
|
g_ptr_array_index(fields, index);
|
||||||
|
accepted |= g_strcmp0(field->review_status, "accepted") == 0;
|
||||||
|
modified |= g_strcmp0(field->review_status, "modified") == 0 &&
|
||||||
|
g_strcmp0(field->origin, "manual_override") == 0;
|
||||||
|
manual |= g_strcmp0(field->origin, "manual_entry") == 0;
|
||||||
|
}
|
||||||
|
g_assert_true(accepted);
|
||||||
|
g_assert_true(modified);
|
||||||
|
g_assert_true(manual);
|
||||||
|
char *person_identifier = NULL;
|
||||||
|
IdentityOcrRun *loaded = identity_ocr_dao_load_run(
|
||||||
|
ocr_dao, context->root, run_identifier, &person_identifier, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_nonnull(loaded);
|
||||||
|
g_assert_cmpstr(person_identifier, ==,
|
||||||
|
"33333333-3333-4333-8333-333333333333");
|
||||||
|
char *raw_before = g_strdup(identity_ocr_run_get_raw_text(loaded));
|
||||||
|
if (!context->revision_done) {
|
||||||
|
g_assert_true(identity_ocr_run_set_corrected_transcription(
|
||||||
|
loaded, "TRANSCRIPTION SPECIMEN RÉVISÉE",
|
||||||
|
"2026-07-29T12:00:00Z"));
|
||||||
|
identity_ocr_run_set_factual_notes(
|
||||||
|
loaded, "Document SPECIMEN incomplet — révision.");
|
||||||
|
g_assert_true(identity_ocr_dao_update_review(
|
||||||
|
ocr_dao, loaded, "2026-07-29T12:00:00Z", &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
context->revision_done = TRUE;
|
||||||
|
}
|
||||||
|
identity_ocr_run_free(loaded);
|
||||||
|
g_free(person_identifier);
|
||||||
|
g_ptr_array_unref(fields);
|
||||||
|
g_ptr_array_unref(documents);
|
||||||
|
g_ptr_array_unref(runs);
|
||||||
|
g_ptr_array_unref(evidences);
|
||||||
|
identity_ocr_dao_free(ocr_dao);
|
||||||
|
evidence_entity_dao_free(link_dao);
|
||||||
|
evidence_dao_free(evidence_dao);
|
||||||
|
database_close(database);
|
||||||
|
database = database_open(context->database_path);
|
||||||
|
g_assert_nonnull(database);
|
||||||
|
ocr_dao = identity_ocr_dao_new(database);
|
||||||
|
loaded = identity_ocr_dao_load_run(
|
||||||
|
ocr_dao, context->root, run_identifier, NULL, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_nonnull(loaded);
|
||||||
|
g_assert_cmpstr(identity_ocr_run_get_raw_text(loaded), ==, raw_before);
|
||||||
|
g_assert_cmpstr(identity_ocr_run_get_corrected_transcription(loaded),
|
||||||
|
==, "TRANSCRIPTION SPECIMEN RÉVISÉE");
|
||||||
|
g_assert_nonnull(strstr(identity_ocr_run_get_factual_notes(loaded),
|
||||||
|
"révision"));
|
||||||
|
identity_ocr_run_free(loaded);
|
||||||
|
identity_ocr_dao_free(ocr_dao);
|
||||||
|
database_close(database);
|
||||||
|
g_free(raw_before);
|
||||||
|
g_free(run_identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void workspace_identity_requested(const char *evidence_identifier,
|
||||||
|
gboolean revise_existing, const char *ocr_run_identifier, gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
g_assert_cmpstr(evidence_identifier, ==,
|
||||||
|
context->final_evidence_identifier);
|
||||||
|
if (revise_existing) {
|
||||||
|
g_assert_cmpstr(ocr_run_identifier, ==, context->run_identifier);
|
||||||
|
context->revision_requests++;
|
||||||
|
} else {
|
||||||
|
g_assert_null(ocr_run_identifier);
|
||||||
|
context->rerun_requests++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void load_workspace_from_reopened_database(
|
||||||
|
TestContext *context, gboolean trigger_actions)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
Database *database = database_open(context->database_path);
|
||||||
|
g_assert_nonnull(database);
|
||||||
|
EvidenceDao *evidence_dao = evidence_dao_new(database, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
GPtrArray *evidences = evidence_dao_list_all(evidence_dao, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_cmpuint(evidences->len, ==, 1);
|
||||||
|
EvidenceRecord *evidence = g_ptr_array_index(evidences, 0);
|
||||||
|
g_assert_cmpstr(evidence_record_get_identifier(evidence), ==,
|
||||||
|
context->final_evidence_identifier);
|
||||||
|
|
||||||
|
main_window_set_selected_evidence(
|
||||||
|
context->production_main_window, evidence);
|
||||||
|
IdentityOcrDao *ocr_dao = identity_ocr_dao_new(database);
|
||||||
|
GPtrArray *runs = identity_ocr_dao_list_runs_by_evidence(
|
||||||
|
ocr_dao, context->final_evidence_identifier, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_cmpuint(runs->len, ==, 1);
|
||||||
|
GPtrArray *workspace_records = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) workspace_identity_ocr_record_free);
|
||||||
|
for (guint index = 0; index < runs->len; index++) {
|
||||||
|
IdentityOcrRunRecord *record = g_ptr_array_index(runs, index);
|
||||||
|
char *person_identifier = NULL;
|
||||||
|
IdentityOcrRun *loaded = identity_ocr_dao_load_run(
|
||||||
|
ocr_dao, context->root, record->id,
|
||||||
|
&person_identifier, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_nonnull(loaded);
|
||||||
|
g_assert_cmpstr(identity_ocr_run_get_evidence_id(loaded),
|
||||||
|
==, context->final_evidence_identifier);
|
||||||
|
WorkspaceIdentityOcrRecord *workspace_record =
|
||||||
|
workspace_identity_ocr_record_new(
|
||||||
|
loaded, person_identifier, record->executed_at,
|
||||||
|
record->text_relative_path, record->text_sha256,
|
||||||
|
record->tsv_relative_path, record->tsv_sha256);
|
||||||
|
g_assert_nonnull(workspace_record);
|
||||||
|
g_ptr_array_add(workspace_records, workspace_record);
|
||||||
|
g_free(person_identifier);
|
||||||
|
}
|
||||||
|
main_window_set_identity_ocr_runs(
|
||||||
|
context->production_main_window, workspace_records);
|
||||||
|
g_ptr_array_unref(runs);
|
||||||
|
identity_ocr_dao_free(ocr_dao);
|
||||||
|
g_ptr_array_unref(evidences);
|
||||||
|
evidence_dao_free(evidence_dao);
|
||||||
|
database_close(database);
|
||||||
|
|
||||||
|
GtkWidget *root = GTK_WIDGET(context->main_window);
|
||||||
|
GtkWidget *section = find_named(
|
||||||
|
root, "workspace-identity-ocr-section");
|
||||||
|
GtkWidget *analyze = find_named(root, "workspace-analyze-identity");
|
||||||
|
GtkDropDown *selector = GTK_DROP_DOWN(find_named(
|
||||||
|
root, "workspace-identity-ocr-selector"));
|
||||||
|
g_assert_nonnull(section);
|
||||||
|
g_assert_true(gtk_widget_get_visible(section));
|
||||||
|
g_assert_nonnull(analyze);
|
||||||
|
g_assert_true(gtk_widget_get_sensitive(analyze));
|
||||||
|
g_assert_nonnull(selector);
|
||||||
|
g_assert_cmpuint(g_list_model_get_n_items(
|
||||||
|
gtk_drop_down_get_model(selector)), ==, 1);
|
||||||
|
char *raw = text_view_contents(GTK_TEXT_VIEW(find_named(
|
||||||
|
root, "workspace-identity-ocr-raw")));
|
||||||
|
char *corrected = text_view_contents(GTK_TEXT_VIEW(find_named(
|
||||||
|
root, "workspace-identity-ocr-corrected")));
|
||||||
|
g_assert_nonnull(strstr(raw, "NOM : SPECIMEN"));
|
||||||
|
g_assert_cmpstr(corrected, ==, "TRANSCRIPTION SPECIMEN RÉVISÉE");
|
||||||
|
g_free(raw);
|
||||||
|
g_free(corrected);
|
||||||
|
if (trigger_actions) {
|
||||||
|
g_signal_emit_by_name(find_named(
|
||||||
|
root, "workspace-revise-identity-ocr"), "clicked");
|
||||||
|
g_signal_emit_by_name(find_named(
|
||||||
|
root, "workspace-rerun-identity-ocr"), "clicked");
|
||||||
|
g_assert_cmpuint(context->revision_requests, ==, 1);
|
||||||
|
g_assert_cmpuint(context->rerun_requests, ==, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean drive(gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
if (++context->guard > 1500)
|
||||||
|
g_error("Timeout du test GTK d’import OCR complet (phase %u)",
|
||||||
|
context->phase);
|
||||||
|
if (context->phase == 0) {
|
||||||
|
GtkWindow *dialog = find_window(context, "Importer une preuve");
|
||||||
|
if (dialog == NULL) return G_SOURCE_CONTINUE;
|
||||||
|
GtkWidget *paned =
|
||||||
|
find_named(GTK_WIDGET(dialog), "evidence-import-paned");
|
||||||
|
g_assert_true(GTK_IS_PANED(paned));
|
||||||
|
g_assert_nonnull(gtk_paned_get_start_child(GTK_PANED(paned)));
|
||||||
|
g_assert_nonnull(gtk_paned_get_end_child(GTK_PANED(paned)));
|
||||||
|
g_signal_emit_by_name(
|
||||||
|
find_button(GTK_WIDGET(dialog), "Importer"), "clicked");
|
||||||
|
context->phase++;
|
||||||
|
} else if (context->phase == 1) {
|
||||||
|
if (!context->metadata_ready) return G_SOURCE_CONTINUE;
|
||||||
|
GtkWindow *dialog = find_window(
|
||||||
|
context, "Import — OCR d’identité facultatif");
|
||||||
|
if (dialog == NULL) return G_SOURCE_CONTINUE;
|
||||||
|
GtkWidget *root = GTK_WIDGET(dialog);
|
||||||
|
int default_width = 0;
|
||||||
|
int default_height = 0;
|
||||||
|
if (!context->layout_resize_pending)
|
||||||
|
gtk_window_set_default_size(dialog, 1200, 800);
|
||||||
|
gtk_window_get_default_size(
|
||||||
|
dialog, &default_width, &default_height);
|
||||||
|
g_assert_cmpint(default_width, ==,
|
||||||
|
context->layout_resize_pending ? 1000 : 1200);
|
||||||
|
g_assert_cmpint(default_height, ==,
|
||||||
|
context->layout_resize_pending ? 700 : 800);
|
||||||
|
GtkWidget *paned = find_named(
|
||||||
|
root, "evidence-identity-ocr-paned");
|
||||||
|
GtkWidget *left = find_named(root, "identity-left-panel");
|
||||||
|
GtkWidget *form_scroll =
|
||||||
|
find_named(root, "identity-form-scroll");
|
||||||
|
GtkWidget *actions =
|
||||||
|
find_named(root, "identity-final-actions");
|
||||||
|
g_assert_true(GTK_IS_PANED(paned));
|
||||||
|
g_assert_nonnull(left);
|
||||||
|
g_assert_true(GTK_IS_SCROLLED_WINDOW(form_scroll));
|
||||||
|
g_assert_nonnull(actions);
|
||||||
|
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_false(is_descendant_of(actions, form_scroll));
|
||||||
|
if (g_object_get_data(
|
||||||
|
G_OBJECT(paned), "identity-initial-position-set") == NULL)
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
if (!context->layout_resize_pending) {
|
||||||
|
int initial_position =
|
||||||
|
gtk_paned_get_position(GTK_PANED(paned));
|
||||||
|
g_assert_cmpint(initial_position, >=, 780);
|
||||||
|
g_assert_cmpint(initial_position, <=, 820);
|
||||||
|
gtk_paned_set_position(GTK_PANED(paned), 650);
|
||||||
|
g_assert_cmpint(
|
||||||
|
gtk_paned_get_position(GTK_PANED(paned)), ==, 650);
|
||||||
|
gtk_window_set_default_size(dialog, 1000, 700);
|
||||||
|
context->layout_resize_pending = TRUE;
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
g_assert_cmpint(
|
||||||
|
gtk_paned_get_position(GTK_PANED(paned)), ==, 650);
|
||||||
|
GtkDropDown *person = GTK_DROP_DOWN(find_named(root,
|
||||||
|
"identity-person"));
|
||||||
|
g_assert_nonnull(person);
|
||||||
|
g_assert_cmpuint(gtk_drop_down_get_selected(person), ==, 0);
|
||||||
|
gtk_drop_down_set_selected(person, 1);
|
||||||
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(find_named(
|
||||||
|
root, "identity-document-type")), 0);
|
||||||
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(find_named(
|
||||||
|
root, "identity-document-side")), 0);
|
||||||
|
GtkWidget *start = find_button(
|
||||||
|
root, "Analyser comme document d’identité");
|
||||||
|
g_assert_nonnull(find_named(root, "identity-notes"));
|
||||||
|
g_assert_nonnull(find_entry_placeholder(
|
||||||
|
root, "Valeur visible mais omise par l’OCR"));
|
||||||
|
g_assert_true(gtk_widget_get_mapped(
|
||||||
|
find_button(root, "Annuler")));
|
||||||
|
g_assert_true(gtk_widget_get_mapped(
|
||||||
|
find_button(root, "Importer sans OCR")));
|
||||||
|
gtk_window_set_default_size(dialog, 760, 560);
|
||||||
|
gtk_window_get_default_size(
|
||||||
|
dialog, &default_width, &default_height);
|
||||||
|
g_assert_cmpint(default_width, ==, 760);
|
||||||
|
g_assert_cmpint(default_height, ==, 560);
|
||||||
|
g_assert_true(gtk_widget_get_sensitive(start));
|
||||||
|
g_assert_cmpuint(count_field_rows(root), ==, 0);
|
||||||
|
g_signal_emit_by_name(start, "clicked");
|
||||||
|
g_signal_emit_by_name(start, "clicked");
|
||||||
|
context->phase++;
|
||||||
|
} else if (context->phase == 2) {
|
||||||
|
GtkWindow *dialog = find_window(
|
||||||
|
context, "Import — OCR d’identité facultatif");
|
||||||
|
if (dialog == NULL) return G_SOURCE_CONTINUE;
|
||||||
|
GtkWidget *root = GTK_WIDGET(dialog);
|
||||||
|
int default_width = 0;
|
||||||
|
int default_height = 0;
|
||||||
|
gtk_window_get_default_size(
|
||||||
|
dialog, &default_width, &default_height);
|
||||||
|
g_assert_cmpint(default_width, ==, 760);
|
||||||
|
g_assert_cmpint(default_height, ==, 560);
|
||||||
|
GtkWidget *continue_button =
|
||||||
|
find_button(root, "Continuer l’import avec OCR");
|
||||||
|
if (!gtk_widget_get_sensitive(continue_button))
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
GtkTextView *raw = GTK_TEXT_VIEW(find_named(
|
||||||
|
root, "identity-raw-text"));
|
||||||
|
g_assert_false(gtk_text_view_get_editable(raw));
|
||||||
|
char *raw_text = text_view_contents(raw);
|
||||||
|
g_assert_nonnull(strstr(raw_text, "NOM : SPECIMEN"));
|
||||||
|
GtkTextView *corrected = GTK_TEXT_VIEW(find_named(
|
||||||
|
root, "identity-corrected-transcription"));
|
||||||
|
g_assert_true(gtk_text_view_get_editable(corrected));
|
||||||
|
char *initial_corrected = text_view_contents(corrected);
|
||||||
|
g_assert_cmpstr(initial_corrected, ==, raw_text);
|
||||||
|
g_free(initial_corrected);
|
||||||
|
gtk_text_buffer_set_text(gtk_text_view_get_buffer(corrected),
|
||||||
|
"ÉTAT CIVIL\nNée à Besançon — Côte-d'Or", -1);
|
||||||
|
g_signal_emit_by_name(find_button(
|
||||||
|
root, "Enregistrer la correction"), "clicked");
|
||||||
|
g_assert_false(gtk_text_view_get_editable(corrected));
|
||||||
|
g_signal_emit_by_name(find_named(
|
||||||
|
root, "identity-corrected-transcription-reedit"), "clicked");
|
||||||
|
g_assert_true(gtk_text_view_get_editable(corrected));
|
||||||
|
gtk_text_buffer_set_text(gtk_text_view_get_buffer(corrected),
|
||||||
|
"ÉTAT CIVIL\nNée à Besançon — Côte-d’Or", -1);
|
||||||
|
g_signal_emit_by_name(find_button(
|
||||||
|
root, "Enregistrer la correction"), "clicked");
|
||||||
|
g_free(raw_text);
|
||||||
|
g_assert_cmpuint(count_field_rows(root), >=, 3);
|
||||||
|
GtkWidget *form_scroll =
|
||||||
|
find_named(root, "identity-form-scroll");
|
||||||
|
GtkAdjustment *adjustment = gtk_scrolled_window_get_vadjustment(
|
||||||
|
GTK_SCROLLED_WINDOW(form_scroll));
|
||||||
|
double bottom = gtk_adjustment_get_upper(adjustment) -
|
||||||
|
gtk_adjustment_get_page_size(adjustment);
|
||||||
|
g_assert_cmpfloat(bottom, >, 0.0);
|
||||||
|
gtk_adjustment_set_value(adjustment, bottom);
|
||||||
|
g_assert_true(gtk_widget_get_mapped(
|
||||||
|
find_button(root, "Annuler")));
|
||||||
|
g_assert_true(gtk_widget_get_mapped(
|
||||||
|
find_button(root, "Importer sans OCR")));
|
||||||
|
g_assert_true(gtk_widget_get_mapped(continue_button));
|
||||||
|
GtkWidget *left = find_named(root, "identity-left-panel");
|
||||||
|
GtkWidget *actions =
|
||||||
|
find_named(root, "identity-final-actions");
|
||||||
|
graphene_rect_t action_bounds;
|
||||||
|
g_assert_true(gtk_widget_compute_bounds(
|
||||||
|
actions, left, &action_bounds));
|
||||||
|
g_assert_cmpfloat(
|
||||||
|
action_bounds.origin.y + action_bounds.size.height, <=,
|
||||||
|
(float) gtk_widget_get_height(left));
|
||||||
|
GtkWidget *modified = find_field_row(root, 1);
|
||||||
|
g_assert_nonnull(modified);
|
||||||
|
GtkWidget *entry = g_object_get_data(
|
||||||
|
G_OBJECT(modified), "identity-value-entry");
|
||||||
|
GtkWidget *modify = g_object_get_data(
|
||||||
|
G_OBJECT(modified), "identity-modify-button");
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(entry), "SPECIMEN CORRIGÉ");
|
||||||
|
g_signal_emit_by_name(modify, "clicked");
|
||||||
|
modified = find_field_row(root, 1);
|
||||||
|
g_signal_emit_by_name(g_object_get_data(
|
||||||
|
G_OBJECT(modified), "identity-reedit-button"), "clicked");
|
||||||
|
modified = find_field_row(root, 1);
|
||||||
|
entry = g_object_get_data(
|
||||||
|
G_OBJECT(modified), "identity-value-entry");
|
||||||
|
modify = g_object_get_data(
|
||||||
|
G_OBJECT(modified), "identity-modify-button");
|
||||||
|
g_assert_true(gtk_editable_get_editable(GTK_EDITABLE(entry)));
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(entry), "SPECIMEN FINAL");
|
||||||
|
g_signal_emit_by_name(modify, "clicked");
|
||||||
|
GtkWidget *accepted = find_field_row(root, 0);
|
||||||
|
g_assert_nonnull(accepted);
|
||||||
|
g_signal_emit_by_name(
|
||||||
|
find_button(accepted, "Accepter"), "clicked");
|
||||||
|
GtkWidget *manual = find_entry_placeholder(
|
||||||
|
root, "Valeur visible mais omise par l’OCR");
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(manual), "VISIBLE SPECIMEN");
|
||||||
|
g_signal_emit_by_name(
|
||||||
|
find_button(root, "Ajouter un champ manquant"), "clicked");
|
||||||
|
GtkWidget *manual_row =
|
||||||
|
find_field_row(root, count_field_rows(root) - 1);
|
||||||
|
g_assert_nonnull(manual_row);
|
||||||
|
g_signal_emit_by_name(g_object_get_data(
|
||||||
|
G_OBJECT(manual_row), "identity-modify-button"), "clicked");
|
||||||
|
GtkTextView *notes =
|
||||||
|
GTK_TEXT_VIEW(find_named(root, "identity-notes"));
|
||||||
|
gtk_text_buffer_set_text(gtk_text_view_get_buffer(notes),
|
||||||
|
"Document SPECIMEN tronqué sur le bord droit.", -1);
|
||||||
|
gtk_adjustment_set_value(adjustment, 0.0);
|
||||||
|
GtkDropDown *person = GTK_DROP_DOWN(
|
||||||
|
find_named(root, "identity-person"));
|
||||||
|
g_assert_cmpuint(gtk_drop_down_get_selected(person), ==, 1);
|
||||||
|
g_signal_emit_by_name(
|
||||||
|
find_button(root, "Voir la zone"), "clicked");
|
||||||
|
gtk_drop_down_set_selected(person, 0);
|
||||||
|
g_signal_emit_by_name(continue_button, "clicked");
|
||||||
|
g_assert_nonnull(find_label(
|
||||||
|
root, "Sélectionnez une personne avant de poursuivre."));
|
||||||
|
g_assert_true(gtk_widget_get_sensitive(continue_button));
|
||||||
|
g_assert_cmpuint(context->import_start_count, ==, 0);
|
||||||
|
gtk_drop_down_set_selected(person, 1);
|
||||||
|
g_signal_emit_by_name(continue_button, "clicked");
|
||||||
|
g_assert_false(gtk_widget_get_sensitive(continue_button));
|
||||||
|
g_assert_nonnull(find_label(root, "Import OCR en cours"));
|
||||||
|
g_signal_emit_by_name(continue_button, "clicked");
|
||||||
|
g_assert_cmpuint(context->import_start_count, ==, 1);
|
||||||
|
context->phase++;
|
||||||
|
} else if (context->phase == 3) {
|
||||||
|
if (!context->import_done) return G_SOURCE_CONTINUE;
|
||||||
|
g_assert_null(find_window(
|
||||||
|
context, "Import — OCR d’identité facultatif"));
|
||||||
|
verify_persistence(context);
|
||||||
|
load_workspace_from_reopened_database(context, TRUE);
|
||||||
|
GError *error = NULL;
|
||||||
|
g_assert_true(evidence_identity_ocr_dialog_present(
|
||||||
|
context->main_window, context->file_path, context->persons,
|
||||||
|
"33333333-3333-4333-8333-333333333333",
|
||||||
|
tool_registry_find(context->registry, "tesseract"), NULL,
|
||||||
|
cancellation_completed, context, NULL, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
context->phase++;
|
||||||
|
} else if (context->phase == 4) {
|
||||||
|
GtkWindow *dialog = find_window(
|
||||||
|
context, "Import — OCR d’identité facultatif");
|
||||||
|
if (dialog == NULL) return G_SOURCE_CONTINUE;
|
||||||
|
GtkWidget *start = find_button(
|
||||||
|
GTK_WIDGET(dialog), "Analyser comme document d’identité");
|
||||||
|
if (!gtk_widget_get_sensitive(start)) return G_SOURCE_CONTINUE;
|
||||||
|
g_signal_emit_by_name(start, "clicked");
|
||||||
|
g_signal_emit_by_name(
|
||||||
|
find_button(GTK_WIDGET(dialog), "Annuler"), "clicked");
|
||||||
|
context->phase++;
|
||||||
|
} else if (context->phase == 5) {
|
||||||
|
if (!context->cancellation_done) return G_SOURCE_CONTINUE;
|
||||||
|
verify_persistence(context);
|
||||||
|
load_workspace_from_reopened_database(context, FALSE);
|
||||||
|
g_assert_true(gtk_widget_get_visible(
|
||||||
|
GTK_WIDGET(context->main_window)));
|
||||||
|
context->passed = TRUE;
|
||||||
|
g_application_quit(G_APPLICATION(context->application));
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_png(const char *path)
|
||||||
|
{
|
||||||
|
cairo_surface_t *surface = cairo_image_surface_create(
|
||||||
|
CAIRO_FORMAT_ARGB32, 480, 280);
|
||||||
|
cairo_t *cr = cairo_create(surface);
|
||||||
|
cairo_set_source_rgb(cr, 1, 1, 1);
|
||||||
|
cairo_paint(cr);
|
||||||
|
cairo_set_source_rgb(cr, 0, 0, 0);
|
||||||
|
cairo_move_to(cr, 24, 60);
|
||||||
|
cairo_show_text(cr, "SPECIMEN IDENTITE");
|
||||||
|
g_assert_cmpint(cairo_surface_write_to_png(surface, path), ==,
|
||||||
|
CAIRO_STATUS_SUCCESS);
|
||||||
|
cairo_destroy(cr);
|
||||||
|
cairo_surface_destroy(surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void activate(GtkApplication *application, gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
GError *error = NULL;
|
||||||
|
char *database_directory =
|
||||||
|
g_build_filename(context->root, "00_BaseDeDonnees", NULL);
|
||||||
|
g_assert_cmpint(g_mkdir_with_parents(database_directory, 0700), ==, 0);
|
||||||
|
context->database_path =
|
||||||
|
g_build_filename(database_directory, "Enquete.sqlite", NULL);
|
||||||
|
g_assert_true(database_initialize(context->database_path,
|
||||||
|
"Enquête SPECIMEN import OCR", context->root));
|
||||||
|
write_png(context->file_path);
|
||||||
|
Database *database = database_open(context->database_path);
|
||||||
|
EntityDao *entity_dao = entity_dao_new(database, &error);
|
||||||
|
EntityRecord *person = entity_record_new(
|
||||||
|
"33333333-3333-4333-8333-333333333333", "person",
|
||||||
|
"PERSONNE SPECIMEN", "PERSONNE SPECIMEN", NULL, 0,
|
||||||
|
"2026-07-29T10:00:00Z", "2026-07-29T10:00:00Z",
|
||||||
|
ENTITY_STATUS_ACTIVE, &error);
|
||||||
|
g_assert_true(entity_dao_insert(entity_dao, person, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_ptr_array_add(context->persons, person);
|
||||||
|
entity_dao_free(entity_dao);
|
||||||
|
database_close(database);
|
||||||
|
context->registry = tool_registry_new();
|
||||||
|
char *tool = g_canonicalize_filename("tests/fake_document_tool", NULL);
|
||||||
|
g_assert_true(tool_registry_register(context->registry, "tesseract",
|
||||||
|
"Tesseract SPECIMEN", tool, TOOL_REQUIREMENT_OPTIONAL, &error));
|
||||||
|
g_assert_true(tool_registry_refresh(context->registry, &error));
|
||||||
|
g_assert_true(tool_registry_set_version(context->registry, "tesseract",
|
||||||
|
"5.0.0 SPECIMEN", &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
context->production_main_window =
|
||||||
|
main_window_new(application, context->manager);
|
||||||
|
g_assert_nonnull(context->production_main_window);
|
||||||
|
main_window_set_identity_ocr_callback(
|
||||||
|
context->production_main_window,
|
||||||
|
workspace_identity_requested, context);
|
||||||
|
context->main_window =
|
||||||
|
main_window_get_window(context->production_main_window);
|
||||||
|
main_window_present(context->production_main_window);
|
||||||
|
GPtrArray *types = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) evidence_type_free);
|
||||||
|
g_ptr_array_add(types, evidence_type_new(
|
||||||
|
1, "document", "Document SPECIMEN", NULL, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_true(evidence_import_dialog_present(
|
||||||
|
context->main_window, context->file_path, types,
|
||||||
|
metadata_completed, context, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_ptr_array_unref(types);
|
||||||
|
g_free(tool);
|
||||||
|
g_free(database_directory);
|
||||||
|
g_timeout_add(10, drive, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (!gtk_init_check()) {
|
||||||
|
g_print("SKIP: aucun affichage GTK disponible.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
g_setenv("LABFY_FAKE_IDENTITY_OCR", "1", TRUE);
|
||||||
|
TestContext context = {0};
|
||||||
|
context.root =
|
||||||
|
g_dir_make_tmp("labfy-identity-import-gtk-XXXXXX", NULL);
|
||||||
|
context.file_path =
|
||||||
|
g_build_filename(context.root, "SPECIMEN-identite.png", NULL);
|
||||||
|
context.persons = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) entity_record_free);
|
||||||
|
context.manager = task_manager_new();
|
||||||
|
context.application = gtk_application_new(
|
||||||
|
"org.labfy.Investigation.IdentityImportTest",
|
||||||
|
G_APPLICATION_NON_UNIQUE);
|
||||||
|
g_signal_connect(context.application, "activate",
|
||||||
|
G_CALLBACK(activate), &context);
|
||||||
|
int status = g_application_run(
|
||||||
|
G_APPLICATION(context.application), argc, argv);
|
||||||
|
g_assert_true(context.passed);
|
||||||
|
background_task_unref(context.import_task);
|
||||||
|
evidence_import_dialog_result_free(context.metadata);
|
||||||
|
tool_registry_free(context.registry);
|
||||||
|
main_window_free(context.production_main_window);
|
||||||
|
task_manager_free(context.manager);
|
||||||
|
g_ptr_array_unref(context.persons);
|
||||||
|
g_object_unref(context.application);
|
||||||
|
g_free(context.database_path);
|
||||||
|
g_free(context.file_path);
|
||||||
|
g_free(context.final_evidence_identifier);
|
||||||
|
g_free(context.run_identifier);
|
||||||
|
remove_tree(context.root);
|
||||||
|
g_free(context.root);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
513
tests/test_evidence_metadata_dialog_gtk.c
Normal file
513
tests/test_evidence_metadata_dialog_gtk.c
Normal file
|
|
@ -0,0 +1,513 @@
|
||||||
|
#include "views/evidence_metadata_dialog.h"
|
||||||
|
#include "dao/entity_dao.h"
|
||||||
|
#include "dao/evidence_dao.h"
|
||||||
|
#include "dao/identity_ocr_dao.h"
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "models/entity_record.h"
|
||||||
|
#include "models/evidence_type.h"
|
||||||
|
#include "widgets/ocr_provenance_overlay.h"
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
#define EVIDENCE_ID "11111111-1111-4111-8111-111111111111"
|
||||||
|
#define PERSON_A "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
|
||||||
|
#define PERSON_B "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
|
||||||
|
#define SHA_EVIDENCE "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
|
||||||
|
#define SHA_A "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
#define SHA_B "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
#define SHA_C "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
GtkApplication *application;
|
||||||
|
GtkWindow *main_window;
|
||||||
|
EvidenceRecord *record;
|
||||||
|
GPtrArray *types;
|
||||||
|
char *root;
|
||||||
|
char *database_path;
|
||||||
|
Database *database;
|
||||||
|
char *run_a;
|
||||||
|
char *run_b;
|
||||||
|
char *run_c;
|
||||||
|
guint phase;
|
||||||
|
guint close_cycles;
|
||||||
|
guint callbacks;
|
||||||
|
gboolean revision_requested;
|
||||||
|
gboolean relaunch_requested;
|
||||||
|
gboolean passed;
|
||||||
|
} TestContext;
|
||||||
|
|
||||||
|
static gboolean drive(gpointer data);
|
||||||
|
|
||||||
|
static void remove_tree(const char *path)
|
||||||
|
{
|
||||||
|
GDir *directory = g_dir_open(path, 0, NULL);
|
||||||
|
if (directory != NULL) {
|
||||||
|
const char *name = NULL;
|
||||||
|
while ((name = g_dir_read_name(directory)) != NULL) {
|
||||||
|
char *child = g_build_filename(path, name, NULL);
|
||||||
|
if (g_file_test(child, G_FILE_TEST_IS_DIR)) remove_tree(child);
|
||||||
|
else g_remove(child);
|
||||||
|
g_free(child);
|
||||||
|
}
|
||||||
|
g_dir_close(directory);
|
||||||
|
}
|
||||||
|
g_rmdir(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWindow *find_metadata_window(TestContext *context)
|
||||||
|
{
|
||||||
|
for (GList *item = gtk_application_get_windows(context->application);
|
||||||
|
item != NULL; item = item->next) {
|
||||||
|
GtkWindow *window = item->data;
|
||||||
|
if (g_strcmp0(gtk_window_get_title(window),
|
||||||
|
"Modifier les métadonnées") == 0) return window;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 GtkWidget *find_button(GtkWidget *widget, const char *label)
|
||||||
|
{
|
||||||
|
if (GTK_IS_BUTTON(widget) &&
|
||||||
|
g_strcmp0(gtk_button_get_label(GTK_BUTTON(widget)), label) == 0)
|
||||||
|
return widget;
|
||||||
|
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||||
|
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||||
|
GtkWidget *found = find_button(child, label);
|
||||||
|
if (found != NULL) return found;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *text_view_text(GtkWidget *widget)
|
||||||
|
{
|
||||||
|
GtkTextBuffer *buffer =
|
||||||
|
gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
|
||||||
|
GtkTextIter start;
|
||||||
|
GtkTextIter end;
|
||||||
|
gtk_text_buffer_get_bounds(buffer, &start, &end);
|
||||||
|
return gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *widget_text(GtkWidget *widget)
|
||||||
|
{
|
||||||
|
GString *text = g_string_new(NULL);
|
||||||
|
if (GTK_IS_LABEL(widget))
|
||||||
|
g_string_append(text, gtk_label_get_text(GTK_LABEL(widget)));
|
||||||
|
else if (GTK_IS_TEXT_VIEW(widget)) {
|
||||||
|
char *value = text_view_text(widget);
|
||||||
|
g_string_append(text, value);
|
||||||
|
g_free(value);
|
||||||
|
}
|
||||||
|
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||||
|
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||||
|
char *value = widget_text(child);
|
||||||
|
g_string_append_c(text, '\n');
|
||||||
|
g_string_append(text, value);
|
||||||
|
g_free(value);
|
||||||
|
}
|
||||||
|
return g_string_free(text, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IdentityOcrRun *new_run(
|
||||||
|
const char *type, const char *side, guint page,
|
||||||
|
const char *raw, const char *corrected, const char *note,
|
||||||
|
gboolean second)
|
||||||
|
{
|
||||||
|
IdentityOcrRun *run = identity_ocr_run_new(
|
||||||
|
EVIDENCE_ID, SHA_EVIDENCE, type, side, page,
|
||||||
|
second ? "eng" : "fra", "none");
|
||||||
|
identity_ocr_run_set_outputs(run,
|
||||||
|
second ? "tesseract 5 B" : "tesseract 5 A",
|
||||||
|
second ? "eng" : "fra", "SPECIMEN", raw,
|
||||||
|
second ? "TSV RUN B" : "TSV RUN A");
|
||||||
|
g_assert_true(identity_ocr_run_set_corrected_transcription(
|
||||||
|
run, corrected, "2026-07-29T10:10:00Z"));
|
||||||
|
identity_ocr_run_set_factual_notes(run, note);
|
||||||
|
IdentitySourceBox box = {
|
||||||
|
.page = (gint) page,
|
||||||
|
.x = second ? 51 : 11, .y = second ? 52 : 12,
|
||||||
|
.width = 80, .height = 20,
|
||||||
|
.image_width = 640, .image_height = 400, .available = TRUE
|
||||||
|
};
|
||||||
|
IdentityFieldObservation *field = identity_field_observation_new(
|
||||||
|
second ? "surname" : "document_number",
|
||||||
|
second ? "RUN B RAW FIELD" : "RUN A RAW FIELD",
|
||||||
|
second ? 82.0 : 91.0, &box, 0);
|
||||||
|
if (second)
|
||||||
|
g_assert_true(identity_field_observation_modify(
|
||||||
|
field, "RUN B MODIFIÉ", "SPECIMEN"));
|
||||||
|
else
|
||||||
|
g_assert_true(identity_field_observation_accept(field));
|
||||||
|
identity_ocr_run_add_field(run, field);
|
||||||
|
IdentityFieldObservation *human = second
|
||||||
|
? identity_field_observation_new_manual(
|
||||||
|
"given_names", "RUN B MANUEL", 1)
|
||||||
|
: identity_field_observation_new(
|
||||||
|
"surname", "RUN A OCR NOM", 70.0, &box, 1);
|
||||||
|
if (!second)
|
||||||
|
g_assert_true(identity_field_observation_modify(
|
||||||
|
human, "RUN A SURCHARGE", "SPECIMEN"));
|
||||||
|
identity_ocr_run_add_field(run, human);
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void insert_run(TestContext *context, gboolean second)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
IdentityOcrRun *run = new_run(
|
||||||
|
second ? "passport" : "identity_card",
|
||||||
|
second ? "identity_page" : "front", second ? 2 : 1,
|
||||||
|
second ? "RUN B OCR BRUT" : "RUN A OCR BRUT",
|
||||||
|
second ? "RUN B CORRIGÉ" : "RUN A CORRIGÉ",
|
||||||
|
second ? "NOTE RUN B" : "NOTE RUN A", second);
|
||||||
|
const char *directory = second
|
||||||
|
? "02_Preuves_Traitees/OCR/run-b"
|
||||||
|
: "02_Preuves_Traitees/OCR/run-a";
|
||||||
|
char *absolute_directory =
|
||||||
|
g_build_filename(context->root, directory, NULL);
|
||||||
|
g_assert_cmpint(g_mkdir_with_parents(
|
||||||
|
absolute_directory, 0700), ==, 0);
|
||||||
|
char *text_relative = g_build_filename(directory, "ocr.txt", NULL);
|
||||||
|
char *tsv_relative = g_build_filename(directory, "ocr.tsv", NULL);
|
||||||
|
char *text_path = g_build_filename(context->root,
|
||||||
|
text_relative, NULL);
|
||||||
|
char *tsv_path = g_build_filename(context->root,
|
||||||
|
tsv_relative, NULL);
|
||||||
|
g_assert_true(g_file_set_contents(text_path,
|
||||||
|
second ? "RUN B OCR BRUT" : "RUN A OCR BRUT", -1, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_true(g_file_set_contents(tsv_path,
|
||||||
|
second ? "TSV RUN B" : "TSV RUN A", -1, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
IdentityOcrDao *dao = identity_ocr_dao_new(context->database);
|
||||||
|
g_assert_true(identity_ocr_dao_insert(dao,
|
||||||
|
second ? PERSON_B : PERSON_A, EVIDENCE_ID, run,
|
||||||
|
text_relative, second ? SHA_B : SHA_A,
|
||||||
|
tsv_relative, second ? SHA_B : SHA_A,
|
||||||
|
second ? "2026-07-29T11:00:00Z"
|
||||||
|
: "2026-07-29T10:00:00Z", &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
if (second) context->run_b =
|
||||||
|
g_strdup(identity_ocr_run_get_identifier(run));
|
||||||
|
else context->run_a =
|
||||||
|
g_strdup(identity_ocr_run_get_identifier(run));
|
||||||
|
identity_ocr_dao_free(dao);
|
||||||
|
identity_ocr_run_free(run);
|
||||||
|
g_free(absolute_directory);
|
||||||
|
g_free(text_relative);
|
||||||
|
g_free(tsv_relative);
|
||||||
|
g_free(text_path);
|
||||||
|
g_free(tsv_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void insert_run_c(TestContext *context)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
IdentityOcrRun *run = new_run(
|
||||||
|
"passport", "identity_page", 3, "RUN C OCR BRUT",
|
||||||
|
"RUN C CORRIGÉ", "NOTE RUN C", TRUE);
|
||||||
|
char *directory = g_build_filename(context->root,
|
||||||
|
"02_Preuves_Traitees/OCR/run-c", NULL);
|
||||||
|
g_mkdir_with_parents(directory, 0700);
|
||||||
|
char *text_path = g_build_filename(directory, "ocr.txt", NULL);
|
||||||
|
char *tsv_path = g_build_filename(directory, "ocr.tsv", NULL);
|
||||||
|
g_file_set_contents(text_path, "RUN C OCR BRUT", -1, NULL);
|
||||||
|
g_file_set_contents(tsv_path, "TSV RUN C", -1, NULL);
|
||||||
|
IdentityOcrDao *dao = identity_ocr_dao_new(context->database);
|
||||||
|
g_assert_true(identity_ocr_dao_insert(dao, PERSON_B, EVIDENCE_ID,
|
||||||
|
run, "02_Preuves_Traitees/OCR/run-c/ocr.txt", SHA_C,
|
||||||
|
"02_Preuves_Traitees/OCR/run-c/ocr.tsv", SHA_C,
|
||||||
|
"2026-07-29T12:00:00Z", &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
context->run_c = g_strdup(identity_ocr_run_get_identifier(run));
|
||||||
|
identity_ocr_dao_free(dao);
|
||||||
|
identity_ocr_run_free(run);
|
||||||
|
g_free(directory);
|
||||||
|
g_free(text_path);
|
||||||
|
g_free(tsv_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void completed(EvidenceMetadataDialogResult *result, gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
g_assert_null(result);
|
||||||
|
context->callbacks++;
|
||||||
|
g_test_message("fiche fermée phase=%u callbacks=%u",
|
||||||
|
context->phase, context->callbacks);
|
||||||
|
g_idle_add(drive, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void analyze_requested(
|
||||||
|
GtkWindow *dialog, const char *evidence_identifier,
|
||||||
|
gboolean revise_existing, const char *ocr_run_identifier,
|
||||||
|
gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
g_assert_true(GTK_IS_WINDOW(dialog));
|
||||||
|
g_assert_cmpstr(evidence_identifier, ==, EVIDENCE_ID);
|
||||||
|
if (revise_existing) {
|
||||||
|
g_assert_cmpstr(ocr_run_identifier, ==, context->run_a);
|
||||||
|
context->revision_requested = TRUE;
|
||||||
|
IdentityOcrDao *dao = identity_ocr_dao_new(context->database);
|
||||||
|
GError *error = NULL;
|
||||||
|
char *person = NULL;
|
||||||
|
IdentityOcrRun *run = identity_ocr_dao_load_run(
|
||||||
|
dao, context->root, context->run_a, &person, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_cmpstr(person, ==, PERSON_A);
|
||||||
|
g_assert_true(identity_ocr_run_set_corrected_transcription(
|
||||||
|
run, "RUN A RÉVISÉ", "2026-07-29T11:30:00Z"));
|
||||||
|
g_assert_true(identity_ocr_dao_update_review(
|
||||||
|
dao, run, "2026-07-29T11:30:00Z", &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
identity_ocr_run_free(run);
|
||||||
|
identity_ocr_dao_free(dao);
|
||||||
|
g_free(person);
|
||||||
|
} else {
|
||||||
|
g_assert_null(ocr_run_identifier);
|
||||||
|
context->relaunch_requested = TRUE;
|
||||||
|
insert_run_c(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void assert_run_content(
|
||||||
|
GtkWindow *dialog, guint selected, const char *present,
|
||||||
|
const char *absent, const char *run_identifier,
|
||||||
|
const char *person, const char *note, const char *origin,
|
||||||
|
const char *artifact_sha)
|
||||||
|
{
|
||||||
|
GtkDropDown *selector = GTK_DROP_DOWN(find_named(
|
||||||
|
GTK_WIDGET(dialog), "evidence-ocr-run-selector"));
|
||||||
|
g_assert_nonnull(selector);
|
||||||
|
g_assert_cmpuint(g_list_model_get_n_items(
|
||||||
|
gtk_drop_down_get_model(selector)), >, selected);
|
||||||
|
gtk_drop_down_set_selected(selector, selected);
|
||||||
|
while (g_main_context_iteration(NULL, FALSE)) {}
|
||||||
|
GtkWidget *section = find_named(
|
||||||
|
GTK_WIDGET(dialog), "evidence-ocr-readonly");
|
||||||
|
char *all = widget_text(section);
|
||||||
|
g_assert_nonnull(g_strstr_len(all, -1, present));
|
||||||
|
g_assert_nonnull(g_strstr_len(all, -1, run_identifier));
|
||||||
|
g_assert_nonnull(g_strstr_len(all, -1, person));
|
||||||
|
g_assert_nonnull(g_strstr_len(all, -1, note));
|
||||||
|
g_assert_nonnull(g_strstr_len(all, -1, origin));
|
||||||
|
g_assert_nonnull(g_strstr_len(all, -1, artifact_sha));
|
||||||
|
g_assert_null(g_strstr_len(all, -1, absent));
|
||||||
|
g_free(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean present_dialog(TestContext *context)
|
||||||
|
{
|
||||||
|
return evidence_metadata_dialog_present_with_ocr(
|
||||||
|
context->main_window, context->record, context->types,
|
||||||
|
context->database, context->root,
|
||||||
|
analyze_requested, completed, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean drive(gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
GtkWindow *dialog = find_metadata_window(context);
|
||||||
|
g_test_message("drive phase=%u fenêtre=%s cycles=%u",
|
||||||
|
context->phase, dialog != NULL ? "oui" : "non",
|
||||||
|
context->close_cycles);
|
||||||
|
if (dialog == NULL) {
|
||||||
|
if (context->phase == 1) {
|
||||||
|
insert_run(context, FALSE);
|
||||||
|
context->phase = 2;
|
||||||
|
} else if (context->phase == 3) {
|
||||||
|
insert_run(context, TRUE);
|
||||||
|
context->phase = 4;
|
||||||
|
} else if (context->phase == 6) {
|
||||||
|
database_close(context->database);
|
||||||
|
context->database = database_open(context->database_path);
|
||||||
|
g_assert_nonnull(context->database);
|
||||||
|
}
|
||||||
|
g_assert_true(present_dialog(context));
|
||||||
|
g_idle_add(drive, context);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
if (!gtk_widget_get_visible(GTK_WIDGET(dialog)))
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
GtkDropDown *selector = GTK_DROP_DOWN(find_named(
|
||||||
|
GTK_WIDGET(dialog), "evidence-ocr-run-selector"));
|
||||||
|
if (context->phase == 0) {
|
||||||
|
g_assert_null(selector);
|
||||||
|
g_assert_null(find_named(GTK_WIDGET(dialog),
|
||||||
|
"revise-existing-identity-ocr"));
|
||||||
|
g_assert_nonnull(find_named(GTK_WIDGET(dialog),
|
||||||
|
"analyze-existing-identity"));
|
||||||
|
context->phase = 1;
|
||||||
|
gtk_window_close(dialog);
|
||||||
|
} else if (context->phase == 2) {
|
||||||
|
g_assert_nonnull(selector);
|
||||||
|
g_assert_cmpuint(g_list_model_get_n_items(
|
||||||
|
gtk_drop_down_get_model(selector)), ==, 1);
|
||||||
|
assert_run_content(dialog, 0, "RUN A OCR BRUT",
|
||||||
|
"RUN B OCR BRUT", context->run_a, PERSON_A,
|
||||||
|
"NOTE RUN A", "manual_override", SHA_A);
|
||||||
|
context->phase = 3;
|
||||||
|
gtk_window_close(dialog);
|
||||||
|
} else if (context->phase == 4) {
|
||||||
|
g_assert_nonnull(selector);
|
||||||
|
g_assert_cmpuint(g_list_model_get_n_items(
|
||||||
|
gtk_drop_down_get_model(selector)), ==, 2);
|
||||||
|
assert_run_content(dialog, 0, "RUN A OCR BRUT",
|
||||||
|
"RUN B OCR BRUT", context->run_a, PERSON_A,
|
||||||
|
"NOTE RUN A", "manual_override", SHA_A);
|
||||||
|
GtkWidget *show = find_button(
|
||||||
|
GTK_WIDGET(dialog), "Voir la zone");
|
||||||
|
g_assert_nonnull(show);
|
||||||
|
g_signal_emit_by_name(show, "clicked");
|
||||||
|
GtkWidget *section = find_named(
|
||||||
|
GTK_WIDGET(dialog), "evidence-ocr-readonly");
|
||||||
|
OcrProvenanceOverlay *overlay = g_object_get_data(
|
||||||
|
G_OBJECT(section), "ocr-readonly-overlay");
|
||||||
|
g_assert_true(ocr_provenance_overlay_has_region(overlay));
|
||||||
|
for (guint index = 0; index < 20; index++)
|
||||||
|
gtk_drop_down_set_selected(selector, index % 2);
|
||||||
|
assert_run_content(dialog, 1, "RUN B OCR BRUT",
|
||||||
|
"RUN A OCR BRUT", context->run_b, PERSON_B,
|
||||||
|
"NOTE RUN B", "manual_entry", SHA_B);
|
||||||
|
assert_run_content(dialog, 0, "RUN A CORRIGÉ",
|
||||||
|
"RUN B CORRIGÉ", context->run_a, PERSON_A,
|
||||||
|
"NOTE RUN A", "manual_override", SHA_A);
|
||||||
|
context->phase = 5;
|
||||||
|
g_signal_emit_by_name(find_named(GTK_WIDGET(dialog),
|
||||||
|
"revise-existing-identity-ocr"), "clicked");
|
||||||
|
} else if (context->phase == 5) {
|
||||||
|
g_assert_nonnull(selector);
|
||||||
|
g_assert_true(context->revision_requested);
|
||||||
|
assert_run_content(dialog, 0, "RUN A RÉVISÉ",
|
||||||
|
"RUN B CORRIGÉ", context->run_a, PERSON_A,
|
||||||
|
"NOTE RUN A", "manual_override", SHA_A);
|
||||||
|
assert_run_content(dialog, 1, "RUN B CORRIGÉ",
|
||||||
|
"RUN A RÉVISÉ", context->run_b, PERSON_B,
|
||||||
|
"NOTE RUN B", "manual_entry", SHA_B);
|
||||||
|
context->phase = 6;
|
||||||
|
g_signal_emit_by_name(find_named(GTK_WIDGET(dialog),
|
||||||
|
"analyze-existing-identity"), "clicked");
|
||||||
|
} else if (context->phase == 6) {
|
||||||
|
g_assert_nonnull(selector);
|
||||||
|
g_assert_true(context->relaunch_requested);
|
||||||
|
g_assert_cmpuint(g_list_model_get_n_items(
|
||||||
|
gtk_drop_down_get_model(selector)), ==, 3);
|
||||||
|
g_assert_cmpuint(gtk_drop_down_get_selected(selector), ==, 2);
|
||||||
|
assert_run_content(dialog, 2, "RUN C OCR BRUT",
|
||||||
|
"RUN A OCR BRUT", context->run_c, PERSON_B,
|
||||||
|
"NOTE RUN C", "manual_entry", SHA_C);
|
||||||
|
assert_run_content(dialog, 0, "RUN A RÉVISÉ",
|
||||||
|
"RUN C OCR BRUT", context->run_a, PERSON_A,
|
||||||
|
"NOTE RUN A", "manual_override", SHA_A);
|
||||||
|
context->phase = 7;
|
||||||
|
gtk_window_close(dialog);
|
||||||
|
} else {
|
||||||
|
context->close_cycles++;
|
||||||
|
if (context->close_cycles < 20) {
|
||||||
|
gtk_window_close(dialog);
|
||||||
|
} else {
|
||||||
|
context->passed = TRUE;
|
||||||
|
gtk_window_close(dialog);
|
||||||
|
g_application_quit(G_APPLICATION(context->application));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void activate(GtkApplication *application, gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
GError *error = NULL;
|
||||||
|
char *database_directory =
|
||||||
|
g_build_filename(context->root, "00_BaseDeDonnees", NULL);
|
||||||
|
g_mkdir_with_parents(database_directory, 0700);
|
||||||
|
context->database_path =
|
||||||
|
g_build_filename(database_directory, "Enquete.sqlite", NULL);
|
||||||
|
g_assert_true(database_initialize(context->database_path,
|
||||||
|
"Enquête fiche OCR SPECIMEN", context->root));
|
||||||
|
context->database = database_open(context->database_path);
|
||||||
|
g_assert_nonnull(context->database);
|
||||||
|
EntityDao *entity_dao = entity_dao_new(context->database, &error);
|
||||||
|
EntityRecord *person_a = entity_record_new(
|
||||||
|
PERSON_A, "person", "PERSONNE A SPECIMEN", "PERSONNE A SPECIMEN",
|
||||||
|
NULL, 0, "2026-07-29T09:00:00Z", "2026-07-29T09:00:00Z",
|
||||||
|
ENTITY_STATUS_ACTIVE, &error);
|
||||||
|
EntityRecord *person_b = entity_record_new(
|
||||||
|
PERSON_B, "person", "PERSONNE B SPECIMEN", "PERSONNE B SPECIMEN",
|
||||||
|
NULL, 0, "2026-07-29T09:00:00Z", "2026-07-29T09:00:00Z",
|
||||||
|
ENTITY_STATUS_ACTIVE, &error);
|
||||||
|
g_assert_true(entity_dao_insert(entity_dao, person_a, &error));
|
||||||
|
g_assert_true(entity_dao_insert(entity_dao, person_b, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
entity_record_free(person_a);
|
||||||
|
entity_record_free(person_b);
|
||||||
|
entity_dao_free(entity_dao);
|
||||||
|
context->record = evidence_record_new(
|
||||||
|
EVIDENCE_ID, "SPECIMEN.png", "SPECIMEN.png",
|
||||||
|
"01_Preuves_Originales/SPECIMEN.png", "document", 8,
|
||||||
|
SHA_EVIDENCE, "2026-07-29T09:00:00Z",
|
||||||
|
"2026-07-29T09:00:00Z", "SPECIMEN",
|
||||||
|
"Preuve multi-run SPECIMEN",
|
||||||
|
EVIDENCE_INTEGRITY_STATUS_VALID, &error);
|
||||||
|
EvidenceDao *evidence_dao = evidence_dao_new(context->database, &error);
|
||||||
|
gboolean evidence_inserted = evidence_dao_insert(
|
||||||
|
evidence_dao, context->record, &error);
|
||||||
|
if (!evidence_inserted)
|
||||||
|
g_test_message("Insertion preuve SPECIMEN: %s",
|
||||||
|
error != NULL ? error->message : "erreur inconnue");
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_true(evidence_inserted);
|
||||||
|
evidence_dao_free(evidence_dao);
|
||||||
|
context->main_window =
|
||||||
|
GTK_WINDOW(gtk_application_window_new(application));
|
||||||
|
gtk_window_present(context->main_window);
|
||||||
|
g_assert_true(present_dialog(context));
|
||||||
|
g_idle_add(drive, context);
|
||||||
|
g_free(database_directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (!gtk_init_check()) {
|
||||||
|
g_print("SKIP: aucun affichage GTK disponible.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
TestContext context = {0};
|
||||||
|
context.root = g_dir_make_tmp("labfy-metadata-ocr-gtk-XXXXXX", NULL);
|
||||||
|
context.types = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) evidence_type_free);
|
||||||
|
GError *error = NULL;
|
||||||
|
g_ptr_array_add(context.types,
|
||||||
|
evidence_type_new(1, "document", "Document SPECIMEN", NULL, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
context.application = gtk_application_new(
|
||||||
|
"com.labfytools.test.metadata.ocr",
|
||||||
|
G_APPLICATION_NON_UNIQUE);
|
||||||
|
g_signal_connect(context.application, "activate",
|
||||||
|
G_CALLBACK(activate), &context);
|
||||||
|
int status = g_application_run(
|
||||||
|
G_APPLICATION(context.application), argc, argv);
|
||||||
|
g_assert_true(context.passed);
|
||||||
|
database_close(context.database);
|
||||||
|
g_ptr_array_unref(context.types);
|
||||||
|
evidence_record_free(context.record);
|
||||||
|
g_object_unref(context.application);
|
||||||
|
g_free(context.database_path);
|
||||||
|
g_free(context.run_a);
|
||||||
|
g_free(context.run_b);
|
||||||
|
g_free(context.run_c);
|
||||||
|
remove_tree(context.root);
|
||||||
|
g_free(context.root);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
#include "widgets/evidence_preview_widget.h"
|
#include "widgets/evidence_preview_widget.h"
|
||||||
|
#include "core/file_hash.h"
|
||||||
|
#include <cairo-pdf.h>
|
||||||
#include <glib/gstdio.h>
|
#include <glib/gstdio.h>
|
||||||
|
|
||||||
static void drain_until(EvidencePreviewWidget *widget, const char *state)
|
static void drain_until(EvidencePreviewWidget *widget, const char *state)
|
||||||
|
|
@ -71,6 +73,132 @@ static void test_widget_lifecycle_and_eml(void)
|
||||||
g_free(directory);
|
g_free(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void write_image(const char *path, const char *format)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
GdkPixbuf *pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8,
|
||||||
|
640, 360);
|
||||||
|
g_assert_nonnull(pixbuf);
|
||||||
|
gdk_pixbuf_fill(pixbuf, 0x2f6fa8ff);
|
||||||
|
g_assert_true(gdk_pixbuf_save(pixbuf, path, format, &error, NULL));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_object_unref(pixbuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_pdf(const char *path)
|
||||||
|
{
|
||||||
|
cairo_surface_t *surface = cairo_pdf_surface_create(path, 640, 360);
|
||||||
|
cairo_t *cr = cairo_create(surface);
|
||||||
|
cairo_set_source_rgb(cr, 1, 1, 1);
|
||||||
|
cairo_paint(cr);
|
||||||
|
cairo_set_source_rgb(cr, 0, 0, 0);
|
||||||
|
cairo_move_to(cr, 30, 80);
|
||||||
|
cairo_show_text(cr, "PAGE PDF SPECIMEN");
|
||||||
|
cairo_show_page(cr);
|
||||||
|
cairo_destroy(cr);
|
||||||
|
cairo_surface_destroy(surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
static EvidencePreviewRequest *request_for(const char *directory,
|
||||||
|
const char *name, const char *identifier, const char *mime,
|
||||||
|
guint64 generation)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
char *path = g_build_filename(directory, name, NULL);
|
||||||
|
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(
|
||||||
|
directory, identifier, name, sha256, mime, generation);
|
||||||
|
g_free(sha256);
|
||||||
|
g_free(path);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_paned_png_jpeg_pdf(void)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
char *directory =
|
||||||
|
g_dir_make_tmp("labfy-preview-formats-XXXXXX", &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
char *png = g_build_filename(directory, "SPECIMEN.png", NULL);
|
||||||
|
char *jpeg = g_build_filename(directory, "SPECIMEN.jpg", NULL);
|
||||||
|
char *pdf = g_build_filename(directory, "SPECIMEN.pdf", NULL);
|
||||||
|
write_image(png, "png");
|
||||||
|
write_image(jpeg, "jpeg");
|
||||||
|
write_pdf(pdf);
|
||||||
|
TaskManager *manager = task_manager_new();
|
||||||
|
EvidencePreviewWidget *widget =
|
||||||
|
evidence_preview_widget_new(manager, NULL, NULL);
|
||||||
|
GtkWidget *paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||||
|
GtkWidget *list = gtk_list_box_new();
|
||||||
|
gtk_widget_set_size_request(list, 320, -1);
|
||||||
|
gtk_list_box_append(GTK_LIST_BOX(list), gtk_label_new("SPECIMEN.png"));
|
||||||
|
gtk_list_box_append(GTK_LIST_BOX(list), gtk_label_new("SPECIMEN.jpg"));
|
||||||
|
gtk_list_box_append(GTK_LIST_BOX(list), gtk_label_new("SPECIMEN.pdf"));
|
||||||
|
gtk_paned_set_start_child(GTK_PANED(paned), list);
|
||||||
|
gtk_paned_set_end_child(GTK_PANED(paned),
|
||||||
|
evidence_preview_widget_get_widget(widget));
|
||||||
|
gtk_paned_set_position(GTK_PANED(paned), 380);
|
||||||
|
gtk_paned_set_resize_start_child(GTK_PANED(paned), FALSE);
|
||||||
|
gtk_paned_set_resize_end_child(GTK_PANED(paned), TRUE);
|
||||||
|
GtkWindow *window = GTK_WINDOW(gtk_window_new());
|
||||||
|
gtk_window_set_default_size(window, 1200, 800);
|
||||||
|
gtk_window_set_child(window, paned);
|
||||||
|
gtk_window_present(window);
|
||||||
|
while (g_main_context_iteration(NULL, FALSE));
|
||||||
|
g_assert_true(gtk_paned_get_start_child(GTK_PANED(paned)) == list);
|
||||||
|
g_assert_true(gtk_paned_get_end_child(GTK_PANED(paned)) ==
|
||||||
|
evidence_preview_widget_get_widget(widget));
|
||||||
|
g_assert_cmpint(gtk_widget_get_width(
|
||||||
|
evidence_preview_widget_get_widget(widget)), >,
|
||||||
|
gtk_widget_get_width(list));
|
||||||
|
EvidencePreviewRequest *png_request = request_for(
|
||||||
|
directory, "SPECIMEN.png",
|
||||||
|
"10000000-0000-4000-8000-000000000001", "image/png", 1);
|
||||||
|
evidence_preview_widget_show(widget, png_request, "PNG SPECIMEN");
|
||||||
|
drain_until(widget, "image");
|
||||||
|
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "image");
|
||||||
|
EvidencePreviewRequest *jpeg_request = request_for(
|
||||||
|
directory, "SPECIMEN.jpg",
|
||||||
|
"10000000-0000-4000-8000-000000000002", "image/jpeg", 2);
|
||||||
|
evidence_preview_widget_show(widget, jpeg_request, "JPEG SPECIMEN");
|
||||||
|
drain_until(widget, "image");
|
||||||
|
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "image");
|
||||||
|
EvidencePreviewRequest *pdf_request = request_for(
|
||||||
|
directory, "SPECIMEN.pdf",
|
||||||
|
"10000000-0000-4000-8000-000000000003", "application/pdf", 3);
|
||||||
|
evidence_preview_widget_show(widget, pdf_request, "PDF SPECIMEN");
|
||||||
|
drain_until(widget, "pdf");
|
||||||
|
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "pdf");
|
||||||
|
gtk_window_set_default_size(window, 760, 560);
|
||||||
|
gtk_paned_set_position(GTK_PANED(paned), 300);
|
||||||
|
while (g_main_context_iteration(NULL, FALSE));
|
||||||
|
g_assert_cmpint(gtk_paned_get_position(GTK_PANED(paned)), ==, 300);
|
||||||
|
evidence_preview_widget_show(widget, png_request, "résultat ancien");
|
||||||
|
evidence_preview_widget_show(widget, jpeg_request, "résultat courant");
|
||||||
|
drain_until(widget, "image");
|
||||||
|
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "image");
|
||||||
|
evidence_preview_widget_show(widget, pdf_request, "fermeture");
|
||||||
|
gtk_window_destroy(window);
|
||||||
|
evidence_preview_widget_cancel(widget);
|
||||||
|
evidence_preview_widget_free(widget);
|
||||||
|
task_manager_free(manager);
|
||||||
|
evidence_preview_request_free(png_request);
|
||||||
|
evidence_preview_request_free(jpeg_request);
|
||||||
|
evidence_preview_request_free(pdf_request);
|
||||||
|
g_unlink(png);
|
||||||
|
g_unlink(jpeg);
|
||||||
|
g_unlink(pdf);
|
||||||
|
g_rmdir(directory);
|
||||||
|
g_free(png);
|
||||||
|
g_free(jpeg);
|
||||||
|
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()) {
|
||||||
|
|
@ -80,5 +208,7 @@ int main(int argc, char **argv)
|
||||||
g_test_init(&argc, &argv, NULL);
|
g_test_init(&argc, &argv, NULL);
|
||||||
g_test_add_func("/evidence-preview-widget/eml-lifecycle",
|
g_test_add_func("/evidence-preview-widget/eml-lifecycle",
|
||||||
test_widget_lifecycle_and_eml);
|
test_widget_lifecycle_and_eml);
|
||||||
|
g_test_add_func("/evidence-preview-widget/paned-png-jpeg-pdf",
|
||||||
|
test_paned_png_jpeg_pdf);
|
||||||
return g_test_run();
|
return g_test_run();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
119
tests/test_identity_ocr.c
Normal file
119
tests/test_identity_ocr.c
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
#include "core/identity_field_extractor.h"
|
||||||
|
#include "core/mrz_parser.h"
|
||||||
|
#include "core/ocr_analysis.h"
|
||||||
|
#include "core/ocr_region_geometry.h"
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
|
static void test_models_review(void)
|
||||||
|
{
|
||||||
|
IdentitySourceBox box={.page=1,.x=10,.y=20,.width=80,.height=16,
|
||||||
|
.image_width=800,.image_height=500,.available=TRUE};
|
||||||
|
IdentityFieldObservation*f=identity_field_observation_new("surname",
|
||||||
|
"SPÉCIMEN-D'ESSAI",87.5,&box,0);g_assert_nonnull(f);
|
||||||
|
g_assert_cmpint(identity_field_observation_get_status(f),==,IDENTITY_REVIEW_PROPOSED);
|
||||||
|
g_assert_true(identity_field_observation_accept(f));
|
||||||
|
g_assert_true(identity_field_observation_modify(f,"SPÉCIMEN-D'ESSAI","revue"));
|
||||||
|
g_assert_cmpstr(identity_field_observation_get_raw_value(f),==,"SPÉCIMEN-D'ESSAI");
|
||||||
|
g_assert_cmpint(identity_field_observation_get_status(f),==,IDENTITY_REVIEW_MODIFIED);
|
||||||
|
g_assert_true(identity_field_observation_modify(f,"SPÉCIMEN-D’ESSAI","seconde revue"));
|
||||||
|
g_assert_cmpstr(identity_field_observation_get_raw_value(f),==,"SPÉCIMEN-D'ESSAI");
|
||||||
|
g_assert_true(identity_field_observation_restore_raw(f));
|
||||||
|
g_assert_null(identity_field_observation_get_corrected_value(f));
|
||||||
|
g_assert_cmpint(identity_field_observation_get_status(f),==,IDENTITY_REVIEW_PROPOSED);
|
||||||
|
g_assert_true(identity_field_observation_modify(f,"SPÉCIMEN-D’ESSAI","revue finale"));
|
||||||
|
IdentityFieldObservation*c=identity_field_observation_copy(f);
|
||||||
|
identity_field_observation_reject(c);
|
||||||
|
g_assert_cmpint(identity_field_observation_get_status(c),==,IDENTITY_REVIEW_REJECTED);
|
||||||
|
identity_field_observation_free(c);identity_field_observation_free(f);
|
||||||
|
IdentityFieldObservation *manual =
|
||||||
|
identity_field_observation_new_manual("nationality","FRANÇAISE",1);
|
||||||
|
g_assert_nonnull(manual);
|
||||||
|
g_assert_null(identity_field_observation_get_raw_value(manual));
|
||||||
|
g_assert_cmpfloat(identity_field_observation_get_confidence(manual),==,-1.0);
|
||||||
|
g_assert_cmpstr(identity_field_observation_get_origin(manual),==,"manual_entry");
|
||||||
|
g_assert_cmpint(identity_field_observation_get_status(manual),==,
|
||||||
|
IDENTITY_REVIEW_PROPOSED);
|
||||||
|
g_assert_true(identity_field_observation_modify(
|
||||||
|
manual,"FRANÇAISE","saisie manuelle contrôlée"));
|
||||||
|
g_assert_cmpstr(identity_field_observation_get_origin(manual),==,"manual_entry");
|
||||||
|
identity_field_observation_free(manual);
|
||||||
|
IdentityOcrRun*r=identity_ocr_run_new("11111111-1111-4111-8111-111111111111",
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"identity_card","front",1,"fra+eng","0");
|
||||||
|
g_assert_nonnull(r);identity_ocr_run_set_outputs(r,"5.5","fra\neng","params",
|
||||||
|
"NOM : SPECIMEN","tsv");
|
||||||
|
identity_ocr_run_set_factual_notes(r,
|
||||||
|
"Document tronqué. Fragment visible : « publique française ».");
|
||||||
|
g_assert_true(identity_ocr_run_set_corrected_transcription(r,
|
||||||
|
"NOM : SPÉCIMEN CORRIGÉ","2026-07-29T10:00:00Z"));
|
||||||
|
g_assert_true(identity_ocr_run_has_human_transcription(r));
|
||||||
|
IdentityOcrRun*copy=identity_ocr_run_copy(r);
|
||||||
|
g_assert_cmpstr(identity_ocr_run_get_raw_text(copy),==,"NOM : SPECIMEN");
|
||||||
|
g_assert_cmpstr(identity_ocr_run_get_corrected_transcription(copy),==,
|
||||||
|
"NOM : SPÉCIMEN CORRIGÉ");
|
||||||
|
g_assert_cmpstr(identity_ocr_run_get_transcription_corrected_at(copy),==,
|
||||||
|
"2026-07-29T10:00:00Z");
|
||||||
|
g_assert_cmpstr(identity_ocr_run_get_factual_notes(copy),==,
|
||||||
|
"Document tronqué. Fragment visible : « publique française ».");
|
||||||
|
identity_ocr_run_reset_corrected_transcription(copy);
|
||||||
|
g_assert_false(identity_ocr_run_has_human_transcription(copy));
|
||||||
|
g_assert_null(identity_ocr_run_get_corrected_transcription(copy));
|
||||||
|
identity_ocr_run_free(copy);identity_ocr_run_free(r);
|
||||||
|
}
|
||||||
|
static void test_extractor(void)
|
||||||
|
{
|
||||||
|
const char*text="NOM : SPÉCIMEN\nPRÉNOMS : ALICE TEST\n"
|
||||||
|
"DATE DE NAISSANCE : 01/01/1990\nNUMÉRO DU DOCUMENT : TEST000000\n";
|
||||||
|
const char*tsv="level\tpage_num\tblock_num\tpar_num\tline_num\tword_num\tleft\ttop\twidth\theight\tconf\ttext\n"
|
||||||
|
"5\t1\t1\t1\t1\t1\t120\t20\t100\t20\t91\tSPÉCIMEN\n";
|
||||||
|
GPtrArray*a=identity_field_extractor_extract(text,tsv,800,500,NULL);
|
||||||
|
g_assert_cmpuint(a->len,==,4);IdentityFieldObservation*f=g_ptr_array_index(a,0);
|
||||||
|
g_assert_true(identity_field_observation_get_box(f)->available);
|
||||||
|
g_assert_cmpint(identity_field_observation_get_status(f),==,IDENTITY_REVIEW_PROPOSED);
|
||||||
|
g_ptr_array_unref(a);
|
||||||
|
}
|
||||||
|
static void test_mrz(void)
|
||||||
|
{
|
||||||
|
g_assert_cmpint(mrz_parser_check_digit("L898902C3"),==,6);
|
||||||
|
MrzParseResult*r=mrz_parser_parse(
|
||||||
|
"P<UTOERIKSSON<<SPECIMEN<<<<<<<<<<<<<<<<<<<<<\n"
|
||||||
|
"L898902C36UTO7408122F1204159ZE184226B<<<<<10\n");
|
||||||
|
g_assert_true(r->structure_valid);g_assert_true(r->check_digits_valid);
|
||||||
|
g_assert_cmpstr(r->document_number,==,"L898902C3");
|
||||||
|
mrz_parse_result_free(r);
|
||||||
|
}
|
||||||
|
static void test_geometry(void)
|
||||||
|
{
|
||||||
|
OcrDisplayRegion r;
|
||||||
|
g_assert_true(ocr_region_geometry_transform(1000,500,100,50,200,100,
|
||||||
|
500,500,OCR_REGION_FIT_CONTAIN,&r));
|
||||||
|
g_assert_cmpfloat_with_epsilon(r.x,50,0.01);
|
||||||
|
g_assert_cmpfloat_with_epsilon(r.y,150,0.01);
|
||||||
|
g_assert_cmpfloat_with_epsilon(r.width,100,0.01);
|
||||||
|
g_assert_true(ocr_region_geometry_transform(500,1000,-10,900,100,200,
|
||||||
|
1000,500,OCR_REGION_FIT_CONTAIN,&r));
|
||||||
|
g_assert_false(ocr_region_geometry_transform(0,100,0,0,1,1,10,10,
|
||||||
|
OCR_REGION_FIT_CONTAIN,&r));
|
||||||
|
g_assert_false(ocr_region_geometry_transform(100,100,10,10,0,1,10,10,
|
||||||
|
OCR_REGION_FIT_CONTAIN,&r));
|
||||||
|
}
|
||||||
|
static void test_languages(void)
|
||||||
|
{
|
||||||
|
GPtrArray*parsed=ocr_analysis_parse_languages(
|
||||||
|
"List of available languages in /tmp (4):\neng\nfra\neng\nosd\n");
|
||||||
|
g_assert_cmpuint(parsed->len,==,3);
|
||||||
|
GPtrArray*choices=ocr_analysis_build_language_choices(parsed);
|
||||||
|
g_assert_cmpuint(choices->len,==,4);
|
||||||
|
g_assert_cmpstr(g_ptr_array_index(choices,3),==,"fra+eng");
|
||||||
|
g_ptr_array_unref(choices);g_ptr_array_unref(parsed);
|
||||||
|
parsed=ocr_analysis_parse_languages(NULL);
|
||||||
|
choices=ocr_analysis_build_language_choices(parsed);
|
||||||
|
g_assert_cmpuint(choices->len,==,0);
|
||||||
|
g_ptr_array_unref(choices);g_ptr_array_unref(parsed);
|
||||||
|
}
|
||||||
|
int main(int argc,char**argv){g_test_init(&argc,&argv,NULL);
|
||||||
|
g_test_add_func("/identity-ocr/models-review",test_models_review);
|
||||||
|
g_test_add_func("/identity-ocr/extractor",test_extractor);
|
||||||
|
g_test_add_func("/identity-ocr/mrz",test_mrz);
|
||||||
|
g_test_add_func("/identity-ocr/geometry",test_geometry);
|
||||||
|
g_test_add_func("/identity-ocr/languages",test_languages);
|
||||||
|
return g_test_run();}
|
||||||
275
tests/test_identity_ocr_preprocessor.c
Normal file
275
tests/test_identity_ocr_preprocessor.c
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
#include "core/identity_ocr_preprocessor.h"
|
||||||
|
#include "core/file_hash.h"
|
||||||
|
#include "core/ocr_analysis.h"
|
||||||
|
#include "core/tool_registry.h"
|
||||||
|
#include <cairo.h>
|
||||||
|
#include <cairo-pdf.h>
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
#include <jpeglib.h>
|
||||||
|
#include <libheif/heif.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static void remove_fixture(char *root, char *path, char *sha)
|
||||||
|
{
|
||||||
|
g_unlink(path); g_rmdir(root); g_free(path); g_free(root); g_free(sha);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_png_profiles_integrity_cleanup(void)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
char *root = g_dir_make_tmp("labfy-ocr-preprocess-XXXXXX", &error);
|
||||||
|
char *path = g_build_filename(root, "SPECIMEN.png", NULL);
|
||||||
|
cairo_surface_t *surface = cairo_image_surface_create(
|
||||||
|
CAIRO_FORMAT_RGB24, 80, 40);
|
||||||
|
cairo_t *cr = cairo_create(surface);
|
||||||
|
cairo_set_source_rgb(cr, 1, 1, 1); cairo_paint(cr);
|
||||||
|
cairo_set_source_rgb(cr, 0, 0, 0); cairo_move_to(cr, 4, 24);
|
||||||
|
cairo_show_text(cr, "SPECIMEN");
|
||||||
|
g_assert_cmpint(cairo_surface_write_to_png(surface, path), ==,
|
||||||
|
CAIRO_STATUS_SUCCESS);
|
||||||
|
cairo_destroy(cr); cairo_surface_destroy(surface);
|
||||||
|
char *sha = NULL; guint64 size = 0;
|
||||||
|
g_assert_true(file_hash_compute_sha256(path, NULL, &sha, &size, &error));
|
||||||
|
for (guint profile = IDENTITY_OCR_PREPROCESS_NONE;
|
||||||
|
profile <= IDENTITY_OCR_PREPROCESS_UPSCALE; profile++) {
|
||||||
|
EvidencePreviewRequest *request = evidence_preview_request_new(root,
|
||||||
|
"10000000-0000-4000-8000-000000000109", "SPECIMEN.png", sha,
|
||||||
|
"image/png", profile);
|
||||||
|
IdentityOcrWorkImage *image = identity_ocr_preprocessor_prepare(
|
||||||
|
request, 1, profile, NULL, &error);
|
||||||
|
g_assert_no_error(error); g_assert_nonnull(image);
|
||||||
|
g_assert_cmpuint(strlen(image->sha256), ==, 64);
|
||||||
|
char *work_path = g_strdup(image->path);
|
||||||
|
identity_ocr_work_image_free(image);
|
||||||
|
g_assert_false(g_file_test(work_path, G_FILE_TEST_EXISTS));
|
||||||
|
g_free(work_path); evidence_preview_request_free(request);
|
||||||
|
}
|
||||||
|
char *after = NULL; guint64 after_size = 0;
|
||||||
|
g_assert_true(file_hash_compute_sha256(path, NULL, &after, &after_size,
|
||||||
|
&error));
|
||||||
|
g_assert_cmpstr(after, ==, sha); g_free(after);
|
||||||
|
EvidencePreviewRequest *bad = evidence_preview_request_new(root,
|
||||||
|
"10000000-0000-4000-8000-000000000109", "SPECIMEN.png",
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"image/png", 1);
|
||||||
|
g_assert_null(identity_ocr_preprocessor_prepare(bad, 1,
|
||||||
|
IDENTITY_OCR_PREPROCESS_NONE, NULL, &error));
|
||||||
|
g_assert_nonnull(error); g_clear_error(&error);
|
||||||
|
evidence_preview_request_free(bad);
|
||||||
|
GCancellable *cancel = g_cancellable_new(); g_cancellable_cancel(cancel);
|
||||||
|
bad = evidence_preview_request_new(root,
|
||||||
|
"10000000-0000-4000-8000-000000000109", "SPECIMEN.png", sha,
|
||||||
|
"image/png", 1);
|
||||||
|
g_assert_null(identity_ocr_preprocessor_prepare(bad, 1,
|
||||||
|
IDENTITY_OCR_PREPROCESS_NONE, cancel, &error));
|
||||||
|
g_assert_nonnull(error);
|
||||||
|
g_clear_error(&error); g_object_unref(cancel);
|
||||||
|
evidence_preview_request_free(bad);
|
||||||
|
remove_fixture(root, path, sha);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_missing(void)
|
||||||
|
{
|
||||||
|
EvidencePreviewRequest *request = evidence_preview_request_new("/tmp",
|
||||||
|
"10000000-0000-4000-8000-000000000109", "ABSENT-SPECIMEN.png",
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"image/png", 1);
|
||||||
|
GError *error = NULL;
|
||||||
|
g_assert_null(identity_ocr_preprocessor_prepare(request, 1,
|
||||||
|
IDENTITY_OCR_PREPROCESS_NONE, NULL, &error));
|
||||||
|
g_assert_nonnull(error); g_clear_error(&error);
|
||||||
|
evidence_preview_request_free(request);
|
||||||
|
}
|
||||||
|
static IdentityOcrWorkImage *prepare_path(const char*root,const char*name,
|
||||||
|
const char*mime,guint page,IdentityOcrPreprocessProfile profile,char**sha)
|
||||||
|
{
|
||||||
|
GError*error=NULL;guint64 size=0;char*path=g_build_filename(root,name,NULL);
|
||||||
|
g_assert_true(file_hash_compute_sha256(path,NULL,sha,&size,&error));
|
||||||
|
EvidencePreviewRequest*r=evidence_preview_request_new(root,
|
||||||
|
"10000000-0000-4000-8000-000000000119",name,*sha,mime,1);
|
||||||
|
IdentityOcrWorkImage*out=identity_ocr_preprocessor_prepare(r,page,profile,
|
||||||
|
NULL,&error);g_assert_no_error(error);g_assert_nonnull(out);
|
||||||
|
evidence_preview_request_free(r);g_free(path);return out;
|
||||||
|
}
|
||||||
|
static void write_jpeg(const char*path,guint orientation)
|
||||||
|
{
|
||||||
|
FILE*out=fopen(path,"wb");struct jpeg_compress_struct c;
|
||||||
|
struct jpeg_error_mgr e;c.err=jpeg_std_error(&e);jpeg_create_compress(&c);
|
||||||
|
jpeg_stdio_dest(&c,out);c.image_width=96;c.image_height=48;
|
||||||
|
c.input_components=3;c.in_color_space=JCS_RGB;jpeg_set_defaults(&c);
|
||||||
|
jpeg_start_compress(&c,TRUE);
|
||||||
|
if(orientation>0){guint8 exif[]={ 'E','x','i','f',0,0,'I','I',42,0,8,0,0,0,
|
||||||
|
1,0,0x12,1,3,0,1,0,0,0,0,0,0,0,0,0,0,0};
|
||||||
|
exif[24]=(guint8)orientation;jpeg_write_marker(&c,JPEG_APP0+1,exif,sizeof(exif));}
|
||||||
|
JSAMPLE row[96*3];for(guint x=0;x<96;x++){row[x*3]=x<48?255:0;
|
||||||
|
row[x*3+1]=x<48?0:255;row[x*3+2]=32;}
|
||||||
|
while(c.next_scanline<c.image_height){JSAMPROW rows[]={row};
|
||||||
|
jpeg_write_scanlines(&c,rows,1);}jpeg_finish_compress(&c);
|
||||||
|
jpeg_destroy_compress(&c);fclose(out);
|
||||||
|
}
|
||||||
|
static void test_jpeg_and_exif(void)
|
||||||
|
{
|
||||||
|
GError*error=NULL;char*root=g_dir_make_tmp("labfy-ocr-jpeg-XXXXXX",&error);
|
||||||
|
const guint orientations[]={0,1,3,6,8,9};
|
||||||
|
for(guint i=0;i<G_N_ELEMENTS(orientations);i++){char*name=g_strdup_printf(
|
||||||
|
"SPECIMEN-%u.bin",orientations[i]);char*path=g_build_filename(root,name,NULL);
|
||||||
|
write_jpeg(path,orientations[i]);char*sha=NULL;IdentityOcrWorkImage*image=
|
||||||
|
prepare_path(root,name,NULL,1,IDENTITY_OCR_PREPROCESS_ORIENTATION,&sha);
|
||||||
|
if(orientations[i]==6||orientations[i]==8){g_assert_cmpint(image->width,==,48);
|
||||||
|
g_assert_cmpint(image->height,==,96);}else{g_assert_cmpint(image->width,==,96);
|
||||||
|
g_assert_cmpint(image->height,==,48);}
|
||||||
|
identity_ocr_work_image_free(image);g_unlink(path);g_free(sha);g_free(path);g_free(name);}
|
||||||
|
g_rmdir(root);g_free(root);
|
||||||
|
}
|
||||||
|
static gboolean write_heif(const char*path)
|
||||||
|
{
|
||||||
|
struct heif_context*c=heif_context_alloc();struct heif_encoder*encoder=NULL;
|
||||||
|
struct heif_image*image=NULL;struct heif_image_handle*handle=NULL;
|
||||||
|
struct heif_error e=heif_context_get_encoder_for_format(c,
|
||||||
|
heif_compression_HEVC,&encoder);if(e.code!=heif_error_Ok){heif_context_free(c);return FALSE;}
|
||||||
|
e=heif_image_create(64,40,heif_colorspace_RGB,heif_chroma_interleaved_RGB,&image);
|
||||||
|
if(e.code==heif_error_Ok)e=heif_image_add_plane(image,heif_channel_interleaved,64,40,8);
|
||||||
|
if(e.code==heif_error_Ok){gint stride=0;guint8*p=heif_image_get_plane(image,
|
||||||
|
heif_channel_interleaved,&stride);memset(p,127,(gsize)stride*40);
|
||||||
|
e=heif_context_encode_image(c,image,encoder,NULL,&handle);}
|
||||||
|
if(e.code==heif_error_Ok)e=heif_context_write_to_file(c,path);
|
||||||
|
if(handle)heif_image_handle_release(handle);
|
||||||
|
if(image)heif_image_release(image);
|
||||||
|
heif_encoder_release(encoder);heif_context_free(c);return e.code==heif_error_Ok;
|
||||||
|
}
|
||||||
|
static void test_heic_heif(void)
|
||||||
|
{
|
||||||
|
GError*error=NULL;char*root=g_dir_make_tmp("labfy-ocr-heif-XXXXXX",&error);
|
||||||
|
char*heic=g_build_filename(root,"SPECIMEN",NULL);
|
||||||
|
if(!write_heif(heic)){g_test_skip("Encodeur HEVC indisponible.");g_rmdir(root);
|
||||||
|
g_free(heic);g_free(root);return;}
|
||||||
|
char*contents=NULL;gsize length=0;
|
||||||
|
g_assert_true(g_file_get_contents(heic,&contents,&length,&error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
char*sha=NULL;IdentityOcrWorkImage*image=prepare_path(root,"SPECIMEN",NULL,1,
|
||||||
|
IDENTITY_OCR_PREPROCESS_NONE,&sha);g_assert_cmpint(image->width,==,64);
|
||||||
|
identity_ocr_work_image_free(image);g_free(sha);sha=NULL;
|
||||||
|
for(gsize i=0;i+4<=length;i++)if(memcmp(contents+i,"heic",4)==0)memcpy(contents+i,"mif1",4);
|
||||||
|
char*heif=g_build_filename(root,"SPECIMEN.heif",NULL);
|
||||||
|
g_assert_true(g_file_set_contents(heif,contents,(gssize)length,&error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
image=prepare_path(root,"SPECIMEN.heif",NULL,1,
|
||||||
|
IDENTITY_OCR_PREPROCESS_GRAYSCALE,&sha);g_assert_cmpint(image->height,==,40);
|
||||||
|
identity_ocr_work_image_free(image);g_free(sha);g_free(contents);
|
||||||
|
g_unlink(heic);g_unlink(heif);g_free(heic);g_free(heif);g_rmdir(root);g_free(root);
|
||||||
|
}
|
||||||
|
static void test_pdf_pages(void)
|
||||||
|
{
|
||||||
|
GError*error=NULL;char*root=g_dir_make_tmp("labfy-ocr-pdf-XXXXXX",&error);
|
||||||
|
char*path=g_build_filename(root,"SPECIMEN.pdf",NULL);
|
||||||
|
cairo_surface_t*s=cairo_pdf_surface_create(path,320,200);cairo_t*cr=cairo_create(s);
|
||||||
|
cairo_set_source_rgb(cr,1,0,0);cairo_paint(cr);cairo_show_page(cr);
|
||||||
|
cairo_set_source_rgb(cr,0,0,1);cairo_paint(cr);cairo_show_page(cr);
|
||||||
|
cairo_destroy(cr);cairo_surface_destroy(s);
|
||||||
|
char*sha1=NULL,*sha2=NULL;IdentityOcrWorkImage*p1=prepare_path(root,
|
||||||
|
"SPECIMEN.pdf","application/pdf",1,IDENTITY_OCR_PREPROCESS_NONE,&sha1);
|
||||||
|
IdentityOcrWorkImage*p2=prepare_path(root,"SPECIMEN.pdf","application/pdf",2,
|
||||||
|
IDENTITY_OCR_PREPROCESS_UPSCALE,&sha2);
|
||||||
|
g_assert_cmpstr(p1->sha256,!=,p2->sha256);
|
||||||
|
identity_ocr_work_image_free(p1);identity_ocr_work_image_free(p2);
|
||||||
|
guint64 size=0;char*source_after=NULL;file_hash_compute_sha256(path,NULL,
|
||||||
|
&source_after,&size,&error);g_assert_cmpstr(source_after,==,sha1);
|
||||||
|
g_free(source_after);g_free(sha1);g_free(sha2);g_unlink(path);g_free(path);
|
||||||
|
g_rmdir(root);g_free(root);
|
||||||
|
}
|
||||||
|
static void test_direct_limits(void)
|
||||||
|
{
|
||||||
|
g_assert_false(identity_ocr_preprocessor_dimensions_are_safe(12001,1,
|
||||||
|
IDENTITY_OCR_PREPROCESS_NONE));
|
||||||
|
g_assert_false(identity_ocr_preprocessor_dimensions_are_safe(1,12001,
|
||||||
|
IDENTITY_OCR_PREPROCESS_NONE));
|
||||||
|
g_assert_false(identity_ocr_preprocessor_dimensions_are_safe(8001,5000,
|
||||||
|
IDENTITY_OCR_PREPROCESS_NONE));
|
||||||
|
g_assert_false(identity_ocr_preprocessor_dimensions_are_safe(0,1,
|
||||||
|
IDENTITY_OCR_PREPROCESS_NONE));
|
||||||
|
g_assert_false(identity_ocr_preprocessor_dimensions_are_safe(-1,1,
|
||||||
|
IDENTITY_OCR_PREPROCESS_NONE));
|
||||||
|
g_assert_false(identity_ocr_preprocessor_dimensions_are_safe(6001,1,
|
||||||
|
IDENTITY_OCR_PREPROCESS_UPSCALE));
|
||||||
|
g_assert_true(identity_ocr_preprocessor_dimensions_are_safe(2000,1000,
|
||||||
|
IDENTITY_OCR_PREPROCESS_UPSCALE));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_real_tesseract_optional(void)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
ToolRegistry *registry = tool_registry_new();
|
||||||
|
g_assert_true(tool_registry_register(registry, "ocr.tesseract",
|
||||||
|
"Tesseract OCR", "tesseract", TOOL_REQUIREMENT_OPTIONAL, &error));
|
||||||
|
g_assert_true(tool_registry_refresh(registry, &error));
|
||||||
|
const ToolInfo *tool = tool_registry_find(registry, "ocr.tesseract");
|
||||||
|
if (tool == NULL || tool_info_get_availability(tool) !=
|
||||||
|
TOOL_AVAILABILITY_AVAILABLE) {
|
||||||
|
g_test_skip("Tesseract absent.");
|
||||||
|
tool_registry_free(registry); return;
|
||||||
|
}
|
||||||
|
char *raw = ocr_analysis_list_languages(
|
||||||
|
tool_info_get_resolved_path(tool), NULL, &error);
|
||||||
|
if (raw == NULL) {
|
||||||
|
g_test_skip("Tesseract ne peut pas lister ses langues.");
|
||||||
|
g_clear_error(&error); tool_registry_free(registry); return;
|
||||||
|
}
|
||||||
|
GPtrArray *parsed = ocr_analysis_parse_languages(raw);
|
||||||
|
const char *language = NULL;
|
||||||
|
for (guint i = 0; i < parsed->len; i++) {
|
||||||
|
const char *candidate = g_ptr_array_index(parsed, i);
|
||||||
|
if (g_str_equal(candidate, "eng") || g_str_equal(candidate, "fra")) {
|
||||||
|
language = candidate; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (language == NULL) {
|
||||||
|
g_test_skip("Aucune langue eng/fra installée.");
|
||||||
|
g_ptr_array_unref(parsed); g_free(raw);
|
||||||
|
tool_registry_free(registry); return;
|
||||||
|
}
|
||||||
|
char *root = g_dir_make_tmp("labfy-real-tesseract-XXXXXX", &error);
|
||||||
|
char *path = g_build_filename(root, "SPECIMEN.png", NULL);
|
||||||
|
cairo_surface_t *surface = cairo_image_surface_create(
|
||||||
|
CAIRO_FORMAT_RGB24, 1000, 260);
|
||||||
|
cairo_t *cr = cairo_create(surface);
|
||||||
|
cairo_set_source_rgb(cr, 1, 1, 1); cairo_paint(cr);
|
||||||
|
cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL,
|
||||||
|
CAIRO_FONT_WEIGHT_BOLD);
|
||||||
|
cairo_set_font_size(cr, 100); cairo_set_source_rgb(cr, 0, 0, 0);
|
||||||
|
cairo_move_to(cr, 40, 160); cairo_show_text(cr, "SPECIMEN 109");
|
||||||
|
cairo_surface_write_to_png(surface, path);
|
||||||
|
cairo_destroy(cr); cairo_surface_destroy(surface);
|
||||||
|
char *sha = NULL; guint64 size = 0;
|
||||||
|
g_assert_true(file_hash_compute_sha256(path, NULL, &sha, &size, &error));
|
||||||
|
OcrAnalysisResult *result = ocr_analysis_run(
|
||||||
|
tool_info_get_resolved_path(tool), path, language, NULL, &error);
|
||||||
|
if (result == NULL) {
|
||||||
|
g_test_skip("Tesseract ne peut pas initialiser la fixture.");
|
||||||
|
g_clear_error(&error);
|
||||||
|
} else {
|
||||||
|
g_assert_nonnull(result->text); g_assert_cmpuint(strlen(result->text),>,0);
|
||||||
|
g_assert_nonnull(result->tsv); g_assert_nonnull(strstr(result->tsv,"left"));
|
||||||
|
g_assert_nonnull(result->execution->version);
|
||||||
|
g_assert_cmpstr(result->requested_languages, ==, language);
|
||||||
|
g_assert_cmpuint(strlen(sha), ==, 64);
|
||||||
|
ocr_analysis_result_free(result);
|
||||||
|
}
|
||||||
|
g_unlink(path); g_rmdir(root); g_free(path); g_free(root); g_free(sha);
|
||||||
|
g_ptr_array_unref(parsed); g_free(raw); tool_registry_free(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
g_test_init(&argc, &argv, NULL);
|
||||||
|
g_test_add_func("/identity-preprocessor/png-profiles-integrity-cleanup",
|
||||||
|
test_png_profiles_integrity_cleanup);
|
||||||
|
g_test_add_func("/identity-preprocessor/missing", test_missing);
|
||||||
|
g_test_add_func("/identity-preprocessor/jpeg-exif",test_jpeg_and_exif);
|
||||||
|
g_test_add_func("/identity-preprocessor/heic-heif",test_heic_heif);
|
||||||
|
g_test_add_func("/identity-preprocessor/pdf-pages",test_pdf_pages);
|
||||||
|
g_test_add_func("/identity-preprocessor/limits",test_direct_limits);
|
||||||
|
g_test_add_func("/identity-preprocessor/real-tesseract",
|
||||||
|
test_real_tesseract_optional);
|
||||||
|
return g_test_run();
|
||||||
|
}
|
||||||
31
tests/test_ocr_provenance_overlay_gtk.c
Normal file
31
tests/test_ocr_provenance_overlay_gtk.c
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#include "widgets/ocr_provenance_overlay.h"
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
(void) argc; (void) argv;
|
||||||
|
if (!gtk_init_check()) {
|
||||||
|
g_print("SKIP: aucun affichage GTK disponible.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
OcrProvenanceOverlay *overlay = ocr_provenance_overlay_new();
|
||||||
|
GtkWidget *widget = ocr_provenance_overlay_get_widget(overlay);
|
||||||
|
g_object_ref_sink(widget);
|
||||||
|
IdentitySourceBox box = {.page=2,.x=10,.y=20,.width=80,.height=25,
|
||||||
|
.image_width=800,.image_height=500,.available=TRUE};
|
||||||
|
IdentityFieldObservation *field = identity_field_observation_new(
|
||||||
|
"surname", "SPECIMEN", 88, &box, 0);
|
||||||
|
ocr_provenance_overlay_set_field(overlay, field, 2);
|
||||||
|
g_assert_true(ocr_provenance_overlay_has_region(overlay));
|
||||||
|
IdentityFieldObservation *without = identity_field_observation_new(
|
||||||
|
"given_names", "ALICE", 70, NULL, 1);
|
||||||
|
ocr_provenance_overlay_set_field(overlay, without, 3);
|
||||||
|
g_assert_false(ocr_provenance_overlay_has_region(overlay));
|
||||||
|
ocr_provenance_overlay_set_field(overlay, field, 2);
|
||||||
|
g_assert_false(ocr_provenance_overlay_has_region(overlay));
|
||||||
|
ocr_provenance_overlay_clear(overlay, 4);
|
||||||
|
identity_field_observation_free(field);
|
||||||
|
identity_field_observation_free(without);
|
||||||
|
ocr_provenance_overlay_free(overlay);
|
||||||
|
g_object_unref(widget);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
#include "core/person_creation_coordinator.h"
|
#include "core/person_creation_coordinator.h"
|
||||||
#include "database/database.h"
|
#include "database/database.h"
|
||||||
#include "database/statement.h"
|
#include "database/statement.h"
|
||||||
|
#include "database/transaction.h"
|
||||||
|
#include "dao/identity_ocr_dao.h"
|
||||||
|
#include "dao/entity_dao.h"
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
#include <glib/gstdio.h>
|
#include <glib/gstdio.h>
|
||||||
|
|
||||||
|
|
@ -18,6 +21,14 @@ static guint64 count(Database *database, const char *table)
|
||||||
g_free(sql);
|
g_free(sql);
|
||||||
return (guint64) value;
|
return (guint64) value;
|
||||||
}
|
}
|
||||||
|
static void execute_sql(Database *database, const char *sql)
|
||||||
|
{
|
||||||
|
DatabaseStatement *statement = database_statement_prepare(database, sql);
|
||||||
|
g_assert_nonnull(statement);
|
||||||
|
g_assert_cmpint(database_statement_step(statement), ==,
|
||||||
|
DATABASE_STATEMENT_STEP_DONE);
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
}
|
||||||
static void remove_tree(const char *root)
|
static void remove_tree(const char *root)
|
||||||
{
|
{
|
||||||
GDir *directory = g_dir_open(root, 0, NULL);
|
GDir *directory = g_dir_open(root, 0, NULL);
|
||||||
|
|
@ -32,6 +43,290 @@ static void remove_tree(const char *root)
|
||||||
g_dir_close(directory);
|
g_dir_close(directory);
|
||||||
g_rmdir(root);
|
g_rmdir(root);
|
||||||
}
|
}
|
||||||
|
typedef struct {
|
||||||
|
guint calls;
|
||||||
|
} SessionCounter;
|
||||||
|
static gboolean session_changes_before_commit(gpointer data)
|
||||||
|
{
|
||||||
|
SessionCounter *counter = data;
|
||||||
|
return counter->calls++ == 0;
|
||||||
|
}
|
||||||
|
static void assert_empty_after_reopen(const char *database_path)
|
||||||
|
{
|
||||||
|
static const char *const tables[] = {
|
||||||
|
"entites", "preuves", "preuve_entites", "preuve_entite_sources",
|
||||||
|
"person_role_assignments", "identity_ocr_runs",
|
||||||
|
"identity_document_observations", "identity_field_observations"
|
||||||
|
};
|
||||||
|
Database *database = database_open(database_path);
|
||||||
|
g_assert_nonnull(database);
|
||||||
|
for (guint i = 0; i < G_N_ELEMENTS(tables); i++)
|
||||||
|
g_assert_cmpuint(count(database, tables[i]), ==, 0);
|
||||||
|
database_close(database);
|
||||||
|
}
|
||||||
|
static void test_failure_matrix(void)
|
||||||
|
{
|
||||||
|
static const struct {
|
||||||
|
PersonCreationFailurePoint point;
|
||||||
|
guint occurrence;
|
||||||
|
} cases[] = {
|
||||||
|
{PERSON_CREATION_FAILURE_VALIDATE,0},
|
||||||
|
{PERSON_CREATION_FAILURE_SESSION_BEFORE_START,0},
|
||||||
|
{PERSON_CREATION_FAILURE_SOURCE_HASH,0},
|
||||||
|
{PERSON_CREATION_FAILURE_CREATE_OCR_DIRECTORY,0},
|
||||||
|
{PERSON_CREATION_FAILURE_COPY_OCR_TEXT,0},
|
||||||
|
{PERSON_CREATION_FAILURE_HASH_OCR_TEXT,0},
|
||||||
|
{PERSON_CREATION_FAILURE_COPY_OCR_TSV,0},
|
||||||
|
{PERSON_CREATION_FAILURE_HASH_OCR_TSV,0},
|
||||||
|
{PERSON_CREATION_FAILURE_CREATE_PERSON,0},
|
||||||
|
{PERSON_CREATION_FAILURE_CREATE_ROLE,0},
|
||||||
|
{PERSON_CREATION_FAILURE_CREATE_ROLE,1},
|
||||||
|
{PERSON_CREATION_FAILURE_CREATE_ROLE,2},
|
||||||
|
{PERSON_CREATION_FAILURE_IMPORT_EVIDENCE,0},
|
||||||
|
{PERSON_CREATION_FAILURE_IMPORT_EVIDENCE,1},
|
||||||
|
{PERSON_CREATION_FAILURE_IMPORT_EVIDENCE,2},
|
||||||
|
{PERSON_CREATION_FAILURE_LINK_EVIDENCE,0},
|
||||||
|
{PERSON_CREATION_FAILURE_LINK_EVIDENCE,1},
|
||||||
|
{PERSON_CREATION_FAILURE_LINK_EVIDENCE,2},
|
||||||
|
{PERSON_CREATION_FAILURE_INSERT_OCR_RUN,0},
|
||||||
|
{PERSON_CREATION_FAILURE_INSERT_OCR_RUN,1},
|
||||||
|
{PERSON_CREATION_FAILURE_INSERT_OCR_RUN,2},
|
||||||
|
{PERSON_CREATION_FAILURE_INSERT_DOCUMENT_OBSERVATION,0},
|
||||||
|
{PERSON_CREATION_FAILURE_INSERT_DOCUMENT_OBSERVATION,1},
|
||||||
|
{PERSON_CREATION_FAILURE_INSERT_DOCUMENT_OBSERVATION,2},
|
||||||
|
{PERSON_CREATION_FAILURE_INSERT_FIELD,0},
|
||||||
|
{PERSON_CREATION_FAILURE_INSERT_FIELD,1},
|
||||||
|
{PERSON_CREATION_FAILURE_INSERT_FIELD,2},
|
||||||
|
{PERSON_CREATION_FAILURE_CREATE_SOURCE,0},
|
||||||
|
{PERSON_CREATION_FAILURE_CREATE_SOURCE,1},
|
||||||
|
{PERSON_CREATION_FAILURE_CREATE_SOURCE,2},
|
||||||
|
{PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,0},
|
||||||
|
{PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,0},
|
||||||
|
{PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,0},
|
||||||
|
{PERSON_CREATION_FAILURE_COMMIT,0},
|
||||||
|
{PERSON_CREATION_FAILURE_COMMIT,0}
|
||||||
|
};
|
||||||
|
for (guint scenario = 0; scenario < G_N_ELEMENTS(cases); scenario++) {
|
||||||
|
char *root = g_dir_make_tmp("labfy-coordinator-matrix-XXXXXX", NULL);
|
||||||
|
char *database_directory = g_build_filename(
|
||||||
|
root, "00_BaseDeDonnees", NULL);
|
||||||
|
char *database_path = g_build_filename(
|
||||||
|
database_directory, "Enquete.sqlite", NULL);
|
||||||
|
g_assert_cmpint(g_mkdir_with_parents(database_directory,0700),==,0);
|
||||||
|
g_assert_true(database_initialize(database_path,
|
||||||
|
"Enquête SPECIMEN matrice", root));
|
||||||
|
EvidenceStaging *staging = evidence_staging_new(NULL);
|
||||||
|
PersonEvidenceSelection *selection = person_evidence_selection_new();
|
||||||
|
GPtrArray *prepared = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) evidence_staging_result_free);
|
||||||
|
GPtrArray *runs = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) identity_ocr_run_free);
|
||||||
|
GPtrArray *roles = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) person_role_assignment_input_free);
|
||||||
|
for (guint i = 0; i < 3; i++) {
|
||||||
|
char *name = g_strdup_printf("SPECIMEN-%u.txt", i);
|
||||||
|
char *source = g_build_filename(root, name, NULL);
|
||||||
|
char *contents = g_strdup_printf("SPECIMEN SOURCE %u", i);
|
||||||
|
g_assert_true(g_file_set_contents(source, contents, -1, NULL));
|
||||||
|
EvidenceStagingResult *item = evidence_staging_prepare(
|
||||||
|
staging, source, NULL, NULL);
|
||||||
|
g_assert_nonnull(item);
|
||||||
|
g_ptr_array_add(prepared, item);
|
||||||
|
g_assert_true(person_evidence_selection_add_staged(selection,
|
||||||
|
item->source_path, item->staging_path, item->original_name,
|
||||||
|
item->mime_type, "text", item->size_bytes, item->sha256,
|
||||||
|
NULL, item->prepared_at, NULL));
|
||||||
|
const PersonEvidenceSelectionItem *selected =
|
||||||
|
person_evidence_selection_get(selection, i);
|
||||||
|
IdentityOcrRun *run = identity_ocr_run_new(
|
||||||
|
person_evidence_selection_item_get_identifier(selected),
|
||||||
|
item->sha256, "identity_card", "front", i + 1,
|
||||||
|
"eng", "none");
|
||||||
|
identity_ocr_run_set_outputs(run, "SPECIMEN", "eng", "params",
|
||||||
|
"NOM: SPECIMEN\nPRÉNOM: TEST\nNUMÉRO: ABC123",
|
||||||
|
"level\tpage_num\tblock_num\tpar_num\tline_num\tword_num\t"
|
||||||
|
"left\ttop\twidth\theight\tconf\ttext\n");
|
||||||
|
IdentityFieldObservation *field =
|
||||||
|
identity_field_observation_new("surname", "SPECIMEN",
|
||||||
|
90, NULL, i);
|
||||||
|
identity_field_observation_accept(field);
|
||||||
|
identity_ocr_run_add_field(run, field);
|
||||||
|
g_ptr_array_add(runs, run);
|
||||||
|
PersonRoleAssignmentInput role = {
|
||||||
|
.role_code = i == 0 ? "victim" :
|
||||||
|
i == 1 ? "witness" : "mentioned_person",
|
||||||
|
.provenance_kind = "manual"
|
||||||
|
};
|
||||||
|
g_ptr_array_add(roles,
|
||||||
|
person_role_assignment_input_copy(&role));
|
||||||
|
g_free(name); g_free(source); g_free(contents);
|
||||||
|
}
|
||||||
|
PersonEntityInput person = {
|
||||||
|
.designation = "Personne SPECIMEN matrice",
|
||||||
|
.identification_status = "suspected",
|
||||||
|
.confidence = 10,
|
||||||
|
.role_assignments = roles
|
||||||
|
};
|
||||||
|
PersonCreationCoordinatorOptions options = {
|
||||||
|
.failure_point = cases[scenario].point,
|
||||||
|
.failure_occurrence = cases[scenario].occurrence,
|
||||||
|
.inject_compensation_failure =
|
||||||
|
scenario == G_N_ELEMENTS(cases) - 1
|
||||||
|
};
|
||||||
|
char *ocr_parent=g_build_filename(root,"02_Preuves_Traitees",
|
||||||
|
"OCR",NULL);
|
||||||
|
gboolean parent_preexisted=scenario%2==0;
|
||||||
|
char *parent_marker=g_build_filename(ocr_parent,"SPECIMEN.marker",NULL);
|
||||||
|
if(parent_preexisted){
|
||||||
|
g_assert_cmpint(g_mkdir_with_parents(ocr_parent,0700),==,0);
|
||||||
|
g_assert_true(g_file_set_contents(parent_marker,"SPECIMEN",-1,NULL));
|
||||||
|
}
|
||||||
|
Database *database = database_open(database_path);
|
||||||
|
GError *error = NULL;
|
||||||
|
PersonCreationCoordinatorResult *result =
|
||||||
|
person_creation_coordinator_execute_with_options(database, root,
|
||||||
|
&person, selection, runs, &options, NULL, &error);
|
||||||
|
g_assert_null(result);
|
||||||
|
g_assert_nonnull(error);
|
||||||
|
if (options.inject_compensation_failure)
|
||||||
|
g_assert_nonnull(strstr(error->message,
|
||||||
|
"Compensation incomplète"));
|
||||||
|
g_clear_error(&error);
|
||||||
|
database_close(database);
|
||||||
|
assert_empty_after_reopen(database_path);
|
||||||
|
guint residual_runs=0;
|
||||||
|
for(guint i=0;i<runs->len;i++){
|
||||||
|
IdentityOcrRun *run=g_ptr_array_index(runs,i);
|
||||||
|
char *run_directory=g_build_filename(ocr_parent,
|
||||||
|
identity_ocr_run_get_identifier(run),NULL);
|
||||||
|
if(g_file_test(run_directory,G_FILE_TEST_EXISTS))
|
||||||
|
residual_runs++;
|
||||||
|
if(!options.inject_compensation_failure&&
|
||||||
|
g_file_test(run_directory,G_FILE_TEST_EXISTS))
|
||||||
|
g_error("Résidu au scénario %u, point %u occurrence %u, "
|
||||||
|
"run %u",scenario,(guint)cases[scenario].point,
|
||||||
|
cases[scenario].occurrence,i);
|
||||||
|
g_free(run_directory);
|
||||||
|
}
|
||||||
|
g_assert_cmpuint(residual_runs,==,
|
||||||
|
options.inject_compensation_failure?1:0);
|
||||||
|
if(parent_preexisted){
|
||||||
|
g_assert_true(g_file_test(parent_marker,G_FILE_TEST_IS_REGULAR));
|
||||||
|
}else if(!options.inject_compensation_failure)
|
||||||
|
g_assert_false(g_file_test(ocr_parent,G_FILE_TEST_EXISTS));
|
||||||
|
for (guint i = 0; i < prepared->len; i++) {
|
||||||
|
EvidenceStagingResult *item = g_ptr_array_index(prepared, i);
|
||||||
|
char *contents = NULL;
|
||||||
|
g_assert_true(g_file_get_contents(item->source_path,
|
||||||
|
&contents, NULL, NULL));
|
||||||
|
g_assert_true(g_str_has_prefix(contents, "SPECIMEN SOURCE"));
|
||||||
|
g_free(contents);
|
||||||
|
}
|
||||||
|
g_ptr_array_unref(roles);
|
||||||
|
g_ptr_array_unref(runs);
|
||||||
|
person_evidence_selection_free(selection);
|
||||||
|
g_ptr_array_unref(prepared);
|
||||||
|
evidence_staging_free(staging);
|
||||||
|
g_free(ocr_parent);g_free(parent_marker);
|
||||||
|
g_free(database_directory); g_free(database_path);
|
||||||
|
remove_tree(root); g_free(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void test_real_session_change_before_commit(void)
|
||||||
|
{
|
||||||
|
char *root = g_dir_make_tmp("labfy-session-before-commit-XXXXXX", NULL);
|
||||||
|
char *database_directory = g_build_filename(root,"00_BaseDeDonnees",NULL);
|
||||||
|
char *database_path = g_build_filename(database_directory,
|
||||||
|
"Enquete.sqlite", NULL);
|
||||||
|
char *source = g_build_filename(root, "SPECIMEN.txt", NULL);
|
||||||
|
g_assert_cmpint(g_mkdir_with_parents(database_directory,0700),==,0);
|
||||||
|
g_assert_true(database_initialize(database_path,"SPECIMEN",root));
|
||||||
|
g_assert_true(g_file_set_contents(source,"SPECIMEN",-1,NULL));
|
||||||
|
EvidenceStaging *staging = evidence_staging_new(NULL);
|
||||||
|
EvidenceStagingResult *prepared =
|
||||||
|
evidence_staging_prepare(staging,source,NULL,NULL);
|
||||||
|
PersonEvidenceSelection *selection = person_evidence_selection_new();
|
||||||
|
g_assert_true(person_evidence_selection_add_staged(selection,
|
||||||
|
prepared->source_path,prepared->staging_path,prepared->original_name,
|
||||||
|
prepared->mime_type,"text",prepared->size_bytes,prepared->sha256,
|
||||||
|
NULL,prepared->prepared_at,NULL));
|
||||||
|
PersonEntityInput person={.designation="SPECIMEN",
|
||||||
|
.identification_status="unknown"};
|
||||||
|
SessionCounter counter={0};
|
||||||
|
PersonCreationCoordinatorOptions options={
|
||||||
|
.session_check=session_changes_before_commit,
|
||||||
|
.session_check_data=&counter};
|
||||||
|
Database *database=database_open(database_path);
|
||||||
|
GError *error=NULL;
|
||||||
|
g_assert_null(person_creation_coordinator_execute_with_options(database,
|
||||||
|
root,&person,selection,NULL,&options,NULL,&error));
|
||||||
|
g_assert_nonnull(error);g_clear_error(&error);database_close(database);
|
||||||
|
assert_empty_after_reopen(database_path);
|
||||||
|
person_evidence_selection_free(selection);
|
||||||
|
evidence_staging_result_free(prepared);evidence_staging_free(staging);
|
||||||
|
g_free(database_directory);g_free(database_path);g_free(source);
|
||||||
|
remove_tree(root);g_free(root);
|
||||||
|
}
|
||||||
|
static void test_existing_person_session_change_compensates(void)
|
||||||
|
{
|
||||||
|
char *root=g_dir_make_tmp("labfy-existing-session-XXXXXX",NULL);
|
||||||
|
char *database_directory=g_build_filename(root,"00_BaseDeDonnees",NULL);
|
||||||
|
char *database_path=g_build_filename(database_directory,"Enquete.sqlite",NULL);
|
||||||
|
char *source=g_build_filename(root,"SPECIMEN-IDENTITE.txt",NULL);
|
||||||
|
g_assert_cmpint(g_mkdir_with_parents(database_directory,0700),==,0);
|
||||||
|
g_assert_true(database_initialize(database_path,"SPECIMEN",root));
|
||||||
|
g_assert_true(g_file_set_contents(source,"SPECIMEN IDENTITE",-1,NULL));
|
||||||
|
Database *database=database_open(database_path);GError *error=NULL;
|
||||||
|
EntityDao *entity_dao=entity_dao_new(database,&error);
|
||||||
|
EntityRecord *person=entity_record_new(
|
||||||
|
"22222222-2222-4222-8222-222222222222","person",
|
||||||
|
"PERSONNE SPECIMEN","PERSONNE SPECIMEN",NULL,0,
|
||||||
|
"2026-01-01T00:00:00Z","2026-01-01T00:00:00Z",
|
||||||
|
ENTITY_STATUS_ACTIVE,&error);
|
||||||
|
g_assert_true(entity_dao_insert(entity_dao,person,&error));
|
||||||
|
entity_record_free(person);entity_dao_free(entity_dao);
|
||||||
|
EvidenceStaging *staging=evidence_staging_new(&error);
|
||||||
|
EvidenceStagingResult *prepared=evidence_staging_prepare(
|
||||||
|
staging,source,NULL,&error);
|
||||||
|
PersonEvidenceSelection *selection=person_evidence_selection_new();
|
||||||
|
g_assert_true(person_evidence_selection_add_staged(selection,
|
||||||
|
prepared->source_path,prepared->staging_path,prepared->original_name,
|
||||||
|
prepared->mime_type,"document",prepared->size_bytes,prepared->sha256,
|
||||||
|
"SPECIMEN",prepared->prepared_at,&error));
|
||||||
|
const PersonEvidenceSelectionItem *item=
|
||||||
|
person_evidence_selection_get(selection,0);
|
||||||
|
IdentityOcrRun *run=identity_ocr_run_new(
|
||||||
|
person_evidence_selection_item_get_identifier(item),prepared->sha256,
|
||||||
|
"identity_card","front",1,"fra","none");
|
||||||
|
identity_ocr_run_set_outputs(run,"SPECIMEN","fra","SPECIMEN",
|
||||||
|
"NOM SPECIMEN","TSV SPECIMEN");
|
||||||
|
IdentityFieldObservation *field=identity_field_observation_new(
|
||||||
|
"surname","SPECIMEN",99,NULL,0);
|
||||||
|
identity_field_observation_accept(field);
|
||||||
|
identity_ocr_run_add_field(run,field);
|
||||||
|
GPtrArray *runs=g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)identity_ocr_run_free);
|
||||||
|
g_ptr_array_add(runs,run);
|
||||||
|
SessionCounter counter={0};
|
||||||
|
PersonCreationCoordinatorOptions options={
|
||||||
|
.session_check=session_changes_before_commit,
|
||||||
|
.session_check_data=&counter};
|
||||||
|
g_assert_null(person_creation_coordinator_attach_to_existing_person(
|
||||||
|
database,root,"22222222-2222-4222-8222-222222222222",
|
||||||
|
selection,runs,NULL,&options,NULL,&error));
|
||||||
|
g_assert_nonnull(error);g_clear_error(&error);
|
||||||
|
g_assert_cmpuint(count(database,"entites"),==,1);
|
||||||
|
g_assert_cmpuint(count(database,"preuves"),==,0);
|
||||||
|
g_assert_cmpuint(count(database,"preuve_entites"),==,0);
|
||||||
|
g_assert_cmpuint(count(database,"identity_ocr_runs"),==,0);
|
||||||
|
char *ocr_root=g_build_filename(root,"02_Preuves_Traitees","OCR",NULL);
|
||||||
|
g_assert_false(g_file_test(ocr_root,G_FILE_TEST_EXISTS));
|
||||||
|
g_free(ocr_root);g_ptr_array_unref(runs);
|
||||||
|
person_evidence_selection_free(selection);
|
||||||
|
evidence_staging_result_free(prepared);evidence_staging_free(staging);
|
||||||
|
database_close(database);g_free(source);g_free(database_directory);
|
||||||
|
g_free(database_path);remove_tree(root);g_free(root);
|
||||||
|
}
|
||||||
static void test_success_and_rollback(void)
|
static void test_success_and_rollback(void)
|
||||||
{
|
{
|
||||||
char *root = g_dir_make_tmp("labfy-coordinator-XXXXXX", NULL);
|
char *root = g_dir_make_tmp("labfy-coordinator-XXXXXX", NULL);
|
||||||
|
|
@ -71,9 +366,35 @@ static void test_success_and_rollback(void)
|
||||||
prepared_one->original_name, prepared_one->mime_type, "text",
|
prepared_one->original_name, prepared_one->mime_type, "text",
|
||||||
prepared_one->size_bytes, prepared_one->sha256, NULL,
|
prepared_one->size_bytes, prepared_one->sha256, NULL,
|
||||||
prepared_one->prepared_at, &error));
|
prepared_one->prepared_at, &error));
|
||||||
|
GPtrArray *coordinator_runs = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) identity_ocr_run_free);
|
||||||
|
IdentityOcrRun *coordinator_run = identity_ocr_run_new(
|
||||||
|
person_evidence_selection_item_get_identifier(
|
||||||
|
person_evidence_selection_get(selection, 0)),
|
||||||
|
prepared_one->sha256, "identity_card", "front", 1, "eng", "none");
|
||||||
|
identity_ocr_run_set_outputs(coordinator_run, "tesseract SPECIMEN", "eng",
|
||||||
|
"paramètres SPECIMEN", "NOM SPECIMEN", "TSV SPECIMEN");
|
||||||
|
IdentityFieldObservation *coordinator_field =
|
||||||
|
identity_field_observation_new("surname", "SPECIMEN", 90, NULL, 0);
|
||||||
|
identity_field_observation_accept(coordinator_field);
|
||||||
|
identity_ocr_run_add_field(coordinator_run, coordinator_field);
|
||||||
|
IdentityFieldObservation *manual_field =
|
||||||
|
identity_field_observation_new_manual(
|
||||||
|
"nationality", "FRANÇAISE", 1);
|
||||||
|
g_assert_nonnull(manual_field);
|
||||||
|
g_assert_true(identity_field_observation_modify(manual_field,
|
||||||
|
"FRANÇAISE", "saisie manuelle contrôlée"));
|
||||||
|
identity_ocr_run_add_field(coordinator_run, manual_field);
|
||||||
|
identity_ocr_run_set_factual_notes(coordinator_run,
|
||||||
|
"Document tronqué ; bord gauche coupé. "
|
||||||
|
"Fragment visible : « publique française ».");
|
||||||
|
g_ptr_array_add(coordinator_runs, coordinator_run);
|
||||||
|
PersonEvidenceSelection *task_selection =
|
||||||
|
person_evidence_selection_copy(selection);
|
||||||
database = database_open(database_path);
|
database = database_open(database_path);
|
||||||
result = person_creation_coordinator_execute(database, root, &person,
|
result = person_creation_coordinator_execute(database, root, &person,
|
||||||
selection, NULL, &error);
|
task_selection, coordinator_runs, NULL, &error);
|
||||||
|
person_evidence_selection_free(task_selection);
|
||||||
g_assert_no_error(error);
|
g_assert_no_error(error);
|
||||||
g_assert_nonnull(result);
|
g_assert_nonnull(result);
|
||||||
g_assert_cmpuint(result->evidence_identifiers->len, ==, 1);
|
g_assert_cmpuint(result->evidence_identifiers->len, ==, 1);
|
||||||
|
|
@ -81,28 +402,181 @@ static void test_success_and_rollback(void)
|
||||||
g_assert_cmpuint(count(database, "preuves"), ==, 1);
|
g_assert_cmpuint(count(database, "preuves"), ==, 1);
|
||||||
g_assert_cmpuint(count(database, "preuve_entites"), ==, 1);
|
g_assert_cmpuint(count(database, "preuve_entites"), ==, 1);
|
||||||
g_assert_cmpuint(count(database, "preuve_entite_sources"), ==, 1);
|
g_assert_cmpuint(count(database, "preuve_entite_sources"), ==, 1);
|
||||||
|
g_assert_cmpuint(count(database, "identity_ocr_runs"), ==, 1);
|
||||||
|
g_assert_cmpuint(count(database, "identity_document_observations"), ==, 1);
|
||||||
|
g_assert_cmpuint(count(database, "identity_field_observations"), ==, 2);
|
||||||
|
char *ocr_root = g_build_filename(root, "02_Preuves_Traitees", "OCR",
|
||||||
|
identity_ocr_run_get_identifier(coordinator_run), NULL);
|
||||||
|
char *ocr_text = g_build_filename(ocr_root, "ocr.txt", NULL);
|
||||||
|
char *ocr_tsv = g_build_filename(ocr_root, "ocr.tsv", NULL);
|
||||||
|
g_assert_true(g_file_test(ocr_text, G_FILE_TEST_IS_REGULAR));
|
||||||
|
g_assert_true(g_file_test(ocr_tsv, G_FILE_TEST_IS_REGULAR));
|
||||||
|
IdentityOcrRun *ocr = identity_ocr_run_new(
|
||||||
|
person_evidence_selection_item_get_identifier(
|
||||||
|
person_evidence_selection_get(selection, 0)),
|
||||||
|
prepared_one->sha256, "identity_card", "front", 1, "eng",
|
||||||
|
"none");
|
||||||
|
identity_ocr_run_set_outputs(ocr, "tesseract 5 SPECIMEN", "eng",
|
||||||
|
"paramètres SPECIMEN", "SURNAME SPECIMEN", "TSV SPECIMEN");
|
||||||
|
IdentitySourceBox box = {.page=1,.x=1,.y=2,.width=30,.height=10,
|
||||||
|
.image_width=80,.image_height=40,.available=TRUE};
|
||||||
|
IdentityFieldObservation *accepted =
|
||||||
|
identity_field_observation_new("surname", "SPECIMEN", 91, &box, 0);
|
||||||
|
IdentityFieldObservation *modified =
|
||||||
|
identity_field_observation_new("given_names", "ALICE", 80, &box, 1);
|
||||||
|
IdentityFieldObservation *rejected =
|
||||||
|
identity_field_observation_new("birth_place", "TEST", 70, NULL, 2);
|
||||||
|
IdentityFieldObservation *mrz =
|
||||||
|
identity_field_observation_new("document_number", "MRZSPECIMEN", 99,
|
||||||
|
&box, 3);
|
||||||
|
IdentityFieldObservation *conflict =
|
||||||
|
identity_field_observation_new("birth_date", "01/01/1990", 50,
|
||||||
|
NULL, 4);
|
||||||
|
identity_field_observation_accept(accepted);
|
||||||
|
identity_field_observation_modify(modified, "ALICE SPECIMEN", "revue");
|
||||||
|
identity_field_observation_reject(rejected);
|
||||||
|
identity_field_observation_accept(mrz);
|
||||||
|
g_assert_true(identity_field_observation_set_origin(mrz, "mrz"));
|
||||||
|
identity_field_observation_mark_conflict(conflict);
|
||||||
|
identity_ocr_run_add_field(ocr, accepted);
|
||||||
|
identity_ocr_run_add_field(ocr, modified);
|
||||||
|
identity_ocr_run_add_field(ocr, rejected);
|
||||||
|
identity_ocr_run_add_field(ocr, mrz);
|
||||||
|
identity_ocr_run_add_field(ocr, conflict);
|
||||||
|
IdentityOcrDao *ocr_dao = identity_ocr_dao_new(database);
|
||||||
|
const char *artifact_sha =
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
|
||||||
|
g_assert_true(database_transaction_begin(database));
|
||||||
|
g_assert_true(identity_ocr_dao_insert(ocr_dao,
|
||||||
|
result->person_identifier,
|
||||||
|
g_ptr_array_index(result->evidence_identifiers, 0), ocr,
|
||||||
|
"02_Preuves_Traitees/OCR/run/ocr.txt", artifact_sha,
|
||||||
|
"02_Preuves_Traitees/OCR/run/ocr.tsv", artifact_sha,
|
||||||
|
"2026-07-28T12:00:00Z", &error));
|
||||||
|
g_assert_true(database_transaction_rollback(database));
|
||||||
|
g_assert_cmpuint(count(database, "identity_ocr_runs"), ==, 1);
|
||||||
|
g_assert_true(database_transaction_begin(database));
|
||||||
|
g_assert_true(identity_ocr_dao_insert(ocr_dao,
|
||||||
|
result->person_identifier,
|
||||||
|
g_ptr_array_index(result->evidence_identifiers, 0), ocr,
|
||||||
|
"02_Preuves_Traitees/OCR/run/ocr.txt", artifact_sha,
|
||||||
|
"02_Preuves_Traitees/OCR/run/ocr.tsv", artifact_sha,
|
||||||
|
"2026-07-28T12:00:00Z", &error));
|
||||||
|
g_assert_true(database_transaction_commit(database));
|
||||||
|
g_assert_cmpuint(count(database, "identity_ocr_runs"), ==, 2);
|
||||||
|
g_assert_cmpuint(count(database, "identity_document_observations"), ==, 2);
|
||||||
|
g_assert_cmpuint(count(database, "identity_field_observations"), ==, 7);
|
||||||
|
GPtrArray *read_runs = identity_ocr_dao_list_runs_by_evidence(ocr_dao,
|
||||||
|
g_ptr_array_index(result->evidence_identifiers, 0), &error);
|
||||||
|
g_assert_no_error(error); g_assert_cmpuint(read_runs->len, ==, 2);
|
||||||
|
IdentityOcrRunRecord *read_run = g_ptr_array_index(read_runs, 1);
|
||||||
|
g_assert_cmpstr(read_run->engine, ==, "tesseract");
|
||||||
|
g_assert_cmpstr(read_run->engine_version, ==, "tesseract 5 SPECIMEN");
|
||||||
|
g_assert_cmpstr(read_run->requested_languages, ==, "eng");
|
||||||
|
g_assert_cmpstr(read_run->preprocessing_profile, ==, "none");
|
||||||
|
g_assert_null(read_run->error_message);
|
||||||
|
IdentityOcrRunRecord *found_run = identity_ocr_dao_find_run(ocr_dao,
|
||||||
|
read_run->id, &error);
|
||||||
|
g_assert_no_error(error); g_assert_cmpstr(found_run->id, ==, read_run->id);
|
||||||
|
identity_ocr_run_record_free(found_run);
|
||||||
|
GPtrArray *documents = identity_ocr_dao_list_documents_by_person(ocr_dao,
|
||||||
|
result->person_identifier, &error);
|
||||||
|
g_assert_no_error(error); g_assert_cmpuint(documents->len, ==, 2);
|
||||||
|
IdentityDocumentObservationRecord *document =
|
||||||
|
g_ptr_array_index(documents, 1);
|
||||||
|
g_assert_cmpstr(document->person_id, ==, result->person_identifier);
|
||||||
|
g_assert_cmpstr(document->document_side, ==, "front");
|
||||||
|
g_assert_null(document->issuing_country_declared);
|
||||||
|
GPtrArray *read_fields = identity_ocr_dao_list_fields_by_document(ocr_dao,
|
||||||
|
document->id, &error);
|
||||||
|
g_assert_no_error(error); g_assert_cmpuint(read_fields->len, ==, 5);
|
||||||
|
IdentityFieldObservationRecord *field0=g_ptr_array_index(read_fields,0);
|
||||||
|
IdentityFieldObservationRecord *field1=g_ptr_array_index(read_fields,1);
|
||||||
|
IdentityFieldObservationRecord *field2=g_ptr_array_index(read_fields,2);
|
||||||
|
IdentityFieldObservationRecord *field3=g_ptr_array_index(read_fields,3);
|
||||||
|
IdentityFieldObservationRecord *field4=g_ptr_array_index(read_fields,4);
|
||||||
|
g_assert_cmpstr(field0->origin, ==, "ocr");
|
||||||
|
g_assert_cmpstr(field1->origin, ==, "manual_override");
|
||||||
|
g_assert_cmpstr(field1->raw_value, ==, "ALICE");
|
||||||
|
g_assert_cmpstr(field1->corrected_value, ==, "ALICE SPECIMEN");
|
||||||
|
g_assert_cmpstr(field2->review_status, ==, "rejected");
|
||||||
|
g_assert_cmpstr(field3->origin, ==, "mrz");
|
||||||
|
g_assert_cmpstr(field4->review_status, ==, "conflict");
|
||||||
|
g_assert_true(field0->has_source_box);
|
||||||
|
g_assert_null(identity_ocr_dao_find_field(ocr_dao,
|
||||||
|
"00000000-0000-4000-8000-000000000000", &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_ptr_array_unref(read_fields);g_ptr_array_unref(documents);
|
||||||
|
g_ptr_array_unref(read_runs);
|
||||||
|
identity_ocr_dao_free(ocr_dao);
|
||||||
|
identity_ocr_run_free(ocr);
|
||||||
|
char *persisted_person = g_strdup(result->person_identifier);
|
||||||
|
char *persisted_evidence = g_strdup(
|
||||||
|
g_ptr_array_index(result->evidence_identifiers, 0));
|
||||||
person_creation_coordinator_result_free(result);
|
person_creation_coordinator_result_free(result);
|
||||||
person_evidence_selection_free(selection);
|
person_evidence_selection_free(selection);
|
||||||
selection = person_evidence_selection_new();
|
selection = person_evidence_selection_new();
|
||||||
|
g_assert_true(person_evidence_selection_add_staged(selection,
|
||||||
|
prepared_two->source_path, prepared_two->staging_path,
|
||||||
|
prepared_two->original_name, prepared_two->mime_type, "text",
|
||||||
|
prepared_two->size_bytes, prepared_two->sha256, NULL,
|
||||||
|
prepared_two->prepared_at, &error));
|
||||||
|
GPtrArray *failed_runs = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) identity_ocr_run_free);
|
||||||
|
IdentityOcrRun *failed_run = identity_ocr_run_new(
|
||||||
|
person_evidence_selection_item_get_identifier(
|
||||||
|
person_evidence_selection_get(selection, 0)),
|
||||||
|
prepared_two->sha256, "identity_card", "back", 1, "eng", "none");
|
||||||
|
identity_ocr_run_set_outputs(failed_run, "SPECIMEN", "eng", "params",
|
||||||
|
"SPECIMEN", "TSV");
|
||||||
|
g_ptr_array_add(failed_runs, failed_run);
|
||||||
|
char *failed_directory = g_build_filename(root, "02_Preuves_Traitees",
|
||||||
|
"OCR", identity_ocr_run_get_identifier(failed_run), NULL);
|
||||||
|
execute_sql(database, "CREATE TEMP TRIGGER fail_identity_ocr "
|
||||||
|
"BEFORE INSERT ON identity_ocr_runs BEGIN SELECT RAISE(ABORT,"
|
||||||
|
"'SPECIMEN injected failure'); END;");
|
||||||
|
result = person_creation_coordinator_execute(database, root, &person,
|
||||||
|
selection, failed_runs, NULL, &error);
|
||||||
|
g_assert_null(result); g_assert_nonnull(error); g_clear_error(&error);
|
||||||
|
g_assert_false(g_file_test(failed_directory, G_FILE_TEST_EXISTS));
|
||||||
|
g_assert_cmpuint(count(database, "entites"), ==, 1);
|
||||||
|
execute_sql(database, "DROP TRIGGER fail_identity_ocr;");
|
||||||
|
g_free(failed_directory); g_ptr_array_unref(failed_runs);
|
||||||
|
person_evidence_selection_free(selection);
|
||||||
|
selection = person_evidence_selection_new();
|
||||||
g_assert_true(person_evidence_selection_add_staged(selection,
|
g_assert_true(person_evidence_selection_add_staged(selection,
|
||||||
prepared_two->source_path, prepared_two->staging_path,
|
prepared_two->source_path, prepared_two->staging_path,
|
||||||
prepared_two->original_name, prepared_two->mime_type, "missing_type",
|
prepared_two->original_name, prepared_two->mime_type, "missing_type",
|
||||||
prepared_two->size_bytes, prepared_two->sha256, NULL,
|
prepared_two->size_bytes, prepared_two->sha256, NULL,
|
||||||
prepared_two->prepared_at, &error));
|
prepared_two->prepared_at, &error));
|
||||||
result = person_creation_coordinator_execute(database, root, &person,
|
result = person_creation_coordinator_execute(database, root, &person,
|
||||||
selection, NULL, &error);
|
selection, NULL, NULL, &error);
|
||||||
g_assert_null(result);
|
g_assert_null(result);
|
||||||
g_assert_nonnull(error);
|
g_assert_nonnull(error);
|
||||||
g_clear_error(&error);
|
g_clear_error(&error);
|
||||||
g_assert_cmpuint(count(database, "entites"), ==, 1);
|
g_assert_cmpuint(count(database, "entites"), ==, 1);
|
||||||
g_assert_cmpuint(count(database, "preuves"), ==, 1);
|
g_assert_cmpuint(count(database, "preuves"), ==, 1);
|
||||||
database_close(database);
|
database_close(database);
|
||||||
|
database = database_open(database_path);
|
||||||
|
g_assert_nonnull(database);
|
||||||
|
ocr_dao = identity_ocr_dao_new(database);
|
||||||
|
read_runs = identity_ocr_dao_list_runs_by_evidence(ocr_dao,
|
||||||
|
persisted_evidence, &error);
|
||||||
|
g_assert_no_error(error); g_assert_cmpuint(read_runs->len, ==, 2);
|
||||||
|
documents = identity_ocr_dao_list_documents_by_person(ocr_dao,
|
||||||
|
persisted_person, &error);
|
||||||
|
g_assert_no_error(error); g_assert_cmpuint(documents->len, ==, 2);
|
||||||
|
g_ptr_array_unref(read_runs); g_ptr_array_unref(documents);
|
||||||
|
identity_ocr_dao_free(ocr_dao); database_close(database);
|
||||||
|
g_free(persisted_person); g_free(persisted_evidence);
|
||||||
|
g_ptr_array_unref(coordinator_runs);
|
||||||
person_evidence_selection_free(selection);
|
person_evidence_selection_free(selection);
|
||||||
evidence_staging_result_free(prepared_one);
|
evidence_staging_result_free(prepared_one);
|
||||||
evidence_staging_result_free(prepared_two);
|
evidence_staging_result_free(prepared_two);
|
||||||
evidence_staging_free(staging);
|
evidence_staging_free(staging);
|
||||||
g_free(database_directory); g_free(database_path);
|
g_free(database_directory); g_free(database_path);
|
||||||
g_free(source_one); g_free(source_two);
|
g_free(source_one); g_free(source_two);
|
||||||
|
g_free(ocr_root); g_free(ocr_text); g_free(ocr_tsv);
|
||||||
remove_tree(root); g_free(root);
|
remove_tree(root); g_free(root);
|
||||||
}
|
}
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
|
|
@ -110,5 +584,11 @@ int main(int argc, char **argv)
|
||||||
g_test_init(&argc, &argv, NULL);
|
g_test_init(&argc, &argv, NULL);
|
||||||
g_test_add_func("/person-coordinator/success-rollback",
|
g_test_add_func("/person-coordinator/success-rollback",
|
||||||
test_success_and_rollback);
|
test_success_and_rollback);
|
||||||
|
g_test_add_func("/person-coordinator/failure-matrix",
|
||||||
|
test_failure_matrix);
|
||||||
|
g_test_add_func("/person-coordinator/session-before-commit",
|
||||||
|
test_real_session_change_before_commit);
|
||||||
|
g_test_add_func("/person-coordinator/existing-session-compensation",
|
||||||
|
test_existing_person_session_change_compensates);
|
||||||
return g_test_run();
|
return g_test_run();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,38 @@ static void test_deep_copy_survives_sources(void)
|
||||||
person_evidence_selection_item_get_identifier(item)));
|
person_evidence_selection_item_get_identifier(item)));
|
||||||
person_evidence_selection_free(copy);
|
person_evidence_selection_free(copy);
|
||||||
}
|
}
|
||||||
|
static void test_staged_copy_keeps_selection_identity(void)
|
||||||
|
{
|
||||||
|
PersonEvidenceSelection *source = person_evidence_selection_new();
|
||||||
|
g_assert_true(person_evidence_selection_add_staged(source,
|
||||||
|
"/tmp/SPECIMEN-source.png", "/tmp/SPECIMEN-stage.png",
|
||||||
|
"SPECIMEN.png", "image/png", "identity", 24,
|
||||||
|
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||||
|
"SPECIMEN synthétique", "2026-07-28T10:00:00Z", NULL));
|
||||||
|
const PersonEvidenceSelectionItem *source_item =
|
||||||
|
person_evidence_selection_get(source, 0);
|
||||||
|
char *selection_identifier = g_strdup(
|
||||||
|
person_evidence_selection_item_get_identifier(source_item));
|
||||||
|
g_assert_true(person_evidence_selection_set_active(
|
||||||
|
source, selection_identifier));
|
||||||
|
|
||||||
|
PersonEvidenceSelection *copy = person_evidence_selection_copy(source);
|
||||||
|
const PersonEvidenceSelectionItem *copied_item =
|
||||||
|
person_evidence_selection_get(copy, 0);
|
||||||
|
g_assert_cmpstr(person_evidence_selection_item_get_identifier(copied_item),
|
||||||
|
==, selection_identifier);
|
||||||
|
g_assert_cmpstr(person_evidence_selection_item_get_identifier(
|
||||||
|
person_evidence_selection_get_active(copy)), ==,
|
||||||
|
selection_identifier);
|
||||||
|
g_assert_cmpint(person_evidence_selection_item_get_origin(copied_item),
|
||||||
|
==, PERSON_EVIDENCE_ORIGIN_STAGED);
|
||||||
|
g_assert_cmpint(person_evidence_selection_item_get_state(copied_item),
|
||||||
|
==, PERSON_EVIDENCE_STATE_READY);
|
||||||
|
|
||||||
|
g_free(selection_identifier);
|
||||||
|
person_evidence_selection_free(copy);
|
||||||
|
person_evidence_selection_free(source);
|
||||||
|
}
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
g_test_init(&argc, &argv, NULL);
|
g_test_init(&argc, &argv, NULL);
|
||||||
|
|
@ -113,5 +145,7 @@ int main(int argc, char **argv)
|
||||||
g_test_add_func("/person-evidence/duplicates", test_duplicates);
|
g_test_add_func("/person-evidence/duplicates", test_duplicates);
|
||||||
g_test_add_func("/person-evidence/deep-copy",
|
g_test_add_func("/person-evidence/deep-copy",
|
||||||
test_deep_copy_survives_sources);
|
test_deep_copy_survives_sources);
|
||||||
|
g_test_add_func("/person-evidence/staged-copy-selection-identity",
|
||||||
|
test_staged_copy_keeps_selection_identity);
|
||||||
return g_test_run();
|
return g_test_run();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
288
tests/test_workspace_identity_ocr_gtk.c
Normal file
288
tests/test_workspace_identity_ocr_gtk.c
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
#include "views/main_window.h"
|
||||||
|
#include "views/evidence_identity_ocr_dialog.h"
|
||||||
|
#include "widgets/workspace.h"
|
||||||
|
#include "models/evidence_record.h"
|
||||||
|
#include "models/entity_record.h"
|
||||||
|
#include "core/task_manager.h"
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define EVIDENCE_ID "11111111-1111-4111-8111-111111111111"
|
||||||
|
#define SHA "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
GtkApplication *application;
|
||||||
|
MainWindow *main_window;
|
||||||
|
TaskManager *manager;
|
||||||
|
EvidenceRecord *evidence;
|
||||||
|
char *run_a;
|
||||||
|
char *run_b;
|
||||||
|
char *fixture_path;
|
||||||
|
IdentityOcrRun *review_run;
|
||||||
|
GtkWindow *review_dialog;
|
||||||
|
guint revisions;
|
||||||
|
guint reruns;
|
||||||
|
gboolean passed;
|
||||||
|
} TestContext;
|
||||||
|
|
||||||
|
static gboolean session_current(gpointer data)
|
||||||
|
{
|
||||||
|
return data != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void review_saved(
|
||||||
|
EvidenceIdentityOcrDialogResult *result, gpointer data);
|
||||||
|
|
||||||
|
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 char *text_view_text(GtkWidget *widget)
|
||||||
|
{
|
||||||
|
GtkTextBuffer *buffer =
|
||||||
|
gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
|
||||||
|
GtkTextIter start;
|
||||||
|
GtkTextIter end;
|
||||||
|
gtk_text_buffer_get_bounds(buffer, &start, &end);
|
||||||
|
return gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IdentityOcrRun *make_run(
|
||||||
|
const char *raw, const char *corrected, guint page)
|
||||||
|
{
|
||||||
|
IdentityOcrRun *run = identity_ocr_run_new(
|
||||||
|
EVIDENCE_ID, SHA, page == 1 ? "identity_card" : "passport",
|
||||||
|
page == 1 ? "front" : "identity_page", page, "fra", "none");
|
||||||
|
identity_ocr_run_set_outputs(run, "tesseract SPECIMEN", "fra",
|
||||||
|
"SPECIMEN", raw, "TSV SPECIMEN");
|
||||||
|
identity_ocr_run_set_corrected_transcription(
|
||||||
|
run, corrected, "2026-07-29T10:00:00Z");
|
||||||
|
identity_ocr_run_set_factual_notes(run,
|
||||||
|
page == 1 ? "NOTE WORKSPACE A" : "NOTE WORKSPACE B");
|
||||||
|
IdentitySourceBox box = {
|
||||||
|
.page = (gint) page, .x = 10, .y = 20,
|
||||||
|
.width = 100, .height = 30,
|
||||||
|
.image_width = 640, .image_height = 400, .available = TRUE
|
||||||
|
};
|
||||||
|
IdentityFieldObservation *field = identity_field_observation_new(
|
||||||
|
"surname", page == 1 ? "BRUT A" : "BRUT B", 90, &box, 0);
|
||||||
|
identity_field_observation_accept(field);
|
||||||
|
identity_ocr_run_add_field(run, field);
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void identity_requested(
|
||||||
|
const char *evidence_identifier, gboolean revise,
|
||||||
|
const char *run_identifier, gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
g_assert_cmpstr(evidence_identifier, ==, EVIDENCE_ID);
|
||||||
|
if (revise) {
|
||||||
|
g_assert_cmpstr(run_identifier, ==, context->run_a);
|
||||||
|
context->revisions++;
|
||||||
|
GPtrArray *persons = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) entity_record_free);
|
||||||
|
GError *error = NULL;
|
||||||
|
EntityRecord *person = entity_record_new(
|
||||||
|
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "person",
|
||||||
|
"PERSONNE SPECIMEN", "PERSONNE SPECIMEN", NULL, 0,
|
||||||
|
"2026-07-29T10:00:00Z", "2026-07-29T10:00:00Z",
|
||||||
|
ENTITY_STATUS_ACTIVE, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_ptr_array_add(persons, person);
|
||||||
|
g_assert_true(evidence_identity_ocr_dialog_present_review(
|
||||||
|
main_window_get_window(context->main_window),
|
||||||
|
context->fixture_path, persons,
|
||||||
|
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||||
|
context->review_run, session_current,
|
||||||
|
review_saved,
|
||||||
|
context, NULL, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_ptr_array_unref(persons);
|
||||||
|
} else {
|
||||||
|
g_assert_null(run_identifier);
|
||||||
|
context->reruns++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWindow *find_review_dialog(void)
|
||||||
|
{
|
||||||
|
GListModel *windows = gtk_window_get_toplevels();
|
||||||
|
for (guint index = 0; index < g_list_model_get_n_items(windows);
|
||||||
|
index++) {
|
||||||
|
GtkWindow *window = g_list_model_get_item(windows, index);
|
||||||
|
gboolean matches = g_strcmp0(gtk_window_get_title(window),
|
||||||
|
"Réviser l’analyse OCR") == 0;
|
||||||
|
if (matches) return window;
|
||||||
|
g_object_unref(window);
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean finish_review(gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
evidence_identity_ocr_dialog_finish_import(
|
||||||
|
context->review_dialog, NULL);
|
||||||
|
g_clear_object(&context->review_dialog);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void review_saved(
|
||||||
|
EvidenceIdentityOcrDialogResult *result, gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
GtkWindow *dialog =
|
||||||
|
evidence_identity_ocr_dialog_result_get_dialog(result);
|
||||||
|
IdentityOcrRun *run =
|
||||||
|
evidence_identity_ocr_dialog_result_steal_run(result);
|
||||||
|
GtkWidget *save = find_named(GTK_WIDGET(dialog),
|
||||||
|
"evidence-identity-continue-with-ocr");
|
||||||
|
GtkWidget *status = find_named(GTK_WIDGET(dialog),
|
||||||
|
"evidence-identity-ocr-status");
|
||||||
|
g_assert_false(gtk_widget_get_sensitive(save));
|
||||||
|
g_assert_nonnull(g_strstr_len(
|
||||||
|
gtk_label_get_text(GTK_LABEL(status)), -1,
|
||||||
|
"Enregistrement de la révision"));
|
||||||
|
g_assert_cmpstr(identity_ocr_run_get_identifier(run),
|
||||||
|
==, context->run_a);
|
||||||
|
context->review_dialog = g_object_ref(dialog);
|
||||||
|
identity_ocr_run_free(run);
|
||||||
|
evidence_identity_ocr_dialog_result_free(result);
|
||||||
|
g_idle_add(finish_review, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean drive(gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
GtkWidget *root = GTK_WIDGET(
|
||||||
|
main_window_get_window(context->main_window));
|
||||||
|
GtkWidget *analyze = find_named(root,
|
||||||
|
"workspace-analyze-identity");
|
||||||
|
GtkWidget *section = find_named(root,
|
||||||
|
"workspace-identity-ocr-section");
|
||||||
|
GtkDropDown *selector = GTK_DROP_DOWN(find_named(root,
|
||||||
|
"workspace-identity-ocr-selector"));
|
||||||
|
g_assert_nonnull(analyze);
|
||||||
|
g_assert_true(gtk_widget_get_sensitive(analyze));
|
||||||
|
g_assert_true(gtk_widget_get_visible(section));
|
||||||
|
g_assert_cmpuint(g_list_model_get_n_items(
|
||||||
|
gtk_drop_down_get_model(selector)), ==, 2);
|
||||||
|
if (context->revisions == 0) {
|
||||||
|
g_assert_cmpuint(gtk_drop_down_get_selected(selector), ==, 1);
|
||||||
|
char *raw = text_view_text(find_named(root,
|
||||||
|
"workspace-identity-ocr-raw"));
|
||||||
|
g_assert_cmpstr(raw, ==, "RUN B OCR BRUT");
|
||||||
|
g_free(raw);
|
||||||
|
gtk_drop_down_set_selected(selector, 0);
|
||||||
|
while (g_main_context_iteration(NULL, FALSE)) {}
|
||||||
|
raw = text_view_text(find_named(root,
|
||||||
|
"workspace-identity-ocr-raw"));
|
||||||
|
g_assert_cmpstr(raw, ==, "RUN A OCR BRUT");
|
||||||
|
g_free(raw);
|
||||||
|
g_signal_emit_by_name(find_named(root,
|
||||||
|
"workspace-revise-identity-ocr"), "clicked");
|
||||||
|
GtkWindow *dialog = find_review_dialog();
|
||||||
|
g_assert_nonnull(dialog);
|
||||||
|
GtkWidget *save = find_named(GTK_WIDGET(dialog),
|
||||||
|
"evidence-identity-continue-with-ocr");
|
||||||
|
GtkWidget *corrected = find_named(GTK_WIDGET(dialog),
|
||||||
|
"identity-corrected-transcription");
|
||||||
|
gtk_text_buffer_set_text(gtk_text_view_get_buffer(
|
||||||
|
GTK_TEXT_VIEW(corrected)), "RUN A RÉVISION SPECIMEN", -1);
|
||||||
|
g_signal_emit_by_name(save, "clicked");
|
||||||
|
g_signal_emit_by_name(save, "clicked");
|
||||||
|
g_object_unref(dialog);
|
||||||
|
g_timeout_add(20, drive, context);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
GtkWindow *pending_dialog = find_review_dialog();
|
||||||
|
if (pending_dialog != NULL) {
|
||||||
|
g_object_unref(pending_dialog);
|
||||||
|
g_timeout_add(20, drive, context);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
g_signal_emit_by_name(find_named(root,
|
||||||
|
"workspace-rerun-identity-ocr"), "clicked");
|
||||||
|
g_assert_cmpuint(context->revisions, ==, 1);
|
||||||
|
g_assert_cmpuint(context->reruns, ==, 1);
|
||||||
|
context->passed = TRUE;
|
||||||
|
g_application_quit(G_APPLICATION(context->application));
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void activate(GtkApplication *application, gpointer data)
|
||||||
|
{
|
||||||
|
TestContext *context = data;
|
||||||
|
GError *error = NULL;
|
||||||
|
context->main_window = main_window_new(application, context->manager);
|
||||||
|
main_window_set_identity_ocr_callback(
|
||||||
|
context->main_window, identity_requested, context);
|
||||||
|
context->evidence = evidence_record_new(
|
||||||
|
EVIDENCE_ID, "IDENTITE-SPECIMEN.jpeg",
|
||||||
|
"IDENTITE-SPECIMEN.jpeg", "01_Preuves_Originales/specimen.jpeg",
|
||||||
|
"document", 32, SHA, "2026-07-29T10:00:00Z", NULL,
|
||||||
|
NULL, NULL, EVIDENCE_INTEGRITY_STATUS_VALID, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
main_window_set_selected_evidence(
|
||||||
|
context->main_window, context->evidence);
|
||||||
|
GPtrArray *records = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) workspace_identity_ocr_record_free);
|
||||||
|
IdentityOcrRun *run_a = make_run(
|
||||||
|
"RUN A OCR BRUT", "RUN A CORRIGÉ", 1);
|
||||||
|
IdentityOcrRun *run_b = make_run(
|
||||||
|
"RUN B OCR BRUT", "RUN B CORRIGÉ", 2);
|
||||||
|
context->run_a = g_strdup(identity_ocr_run_get_identifier(run_a));
|
||||||
|
context->review_run = identity_ocr_run_copy(run_a);
|
||||||
|
context->run_b = g_strdup(identity_ocr_run_get_identifier(run_b));
|
||||||
|
g_ptr_array_add(records, workspace_identity_ocr_record_new(
|
||||||
|
run_a, "PERSONNE A SPECIMEN", "2026-07-29T10:00:00Z",
|
||||||
|
"OCR/A.txt", SHA, "OCR/A.tsv", SHA));
|
||||||
|
g_ptr_array_add(records, workspace_identity_ocr_record_new(
|
||||||
|
run_b, "PERSONNE B SPECIMEN", "2026-07-29T11:00:00Z",
|
||||||
|
"OCR/B.txt", SHA, "OCR/B.tsv", SHA));
|
||||||
|
main_window_set_identity_ocr_runs(context->main_window, records);
|
||||||
|
main_window_present(context->main_window);
|
||||||
|
g_idle_add(drive, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (!gtk_init_check()) {
|
||||||
|
g_print("SKIP: aucun affichage GTK disponible.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
TestContext context = {0};
|
||||||
|
context.fixture_path = g_strdup_printf(
|
||||||
|
"%s/labfy-workspace-identity-%u.jpeg",
|
||||||
|
g_get_tmp_dir(), g_random_int());
|
||||||
|
g_assert_true(g_file_set_contents(context.fixture_path,
|
||||||
|
"SPECIMEN JPEG", -1, NULL));
|
||||||
|
context.manager = task_manager_new();
|
||||||
|
context.application = gtk_application_new(
|
||||||
|
"com.labfytools.test.workspace.identity",
|
||||||
|
G_APPLICATION_NON_UNIQUE);
|
||||||
|
g_signal_connect(context.application, "activate",
|
||||||
|
G_CALLBACK(activate), &context);
|
||||||
|
int status = g_application_run(
|
||||||
|
G_APPLICATION(context.application), argc, argv);
|
||||||
|
g_assert_true(context.passed);
|
||||||
|
evidence_record_free(context.evidence);
|
||||||
|
main_window_free(context.main_window);
|
||||||
|
task_manager_free(context.manager);
|
||||||
|
g_object_unref(context.application);
|
||||||
|
g_free(context.run_a);
|
||||||
|
g_free(context.run_b);
|
||||||
|
identity_ocr_run_free(context.review_run);
|
||||||
|
g_remove(context.fixture_path);
|
||||||
|
g_free(context.fixture_path);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue