labfy-investigation/src/core/pdf_analysis.c
2026-07-28 07:28:04 +02:00

327 lines
11 KiB
C

/******************************************************************************
* @file pdf_analysis.c
* @brief Extraction PDF native puis OCR de secours.
******************************************************************************/
#include "core/pdf_analysis.h"
#include "core/document_tool_runner.h"
#include <glib/gstdio.h>
static void pdf_page_analysis_free(gpointer data)
{
PdfPageAnalysis *page = data;
if (page == NULL)
return;
g_free(page->text);
document_tool_execution_free(page->render_execution);
document_tool_execution_free(page->execution);
g_ptr_array_unref(page->warnings);
g_free(page);
}
void pdf_analysis_result_free(PdfAnalysisResult *result)
{
if (result == NULL)
return;
g_free(result->source_path);
g_free(result->native_text);
document_tool_execution_free(result->pdfinfo_execution);
document_tool_execution_free(result->native_execution);
g_ptr_array_unref(result->pages);
g_ptr_array_unref(result->warnings);
g_free(result);
}
gboolean pdf_analysis_text_is_usable(const char *text)
{
if (text == NULL)
return FALSE;
gsize total = 0;
gsize non_space = 0;
gsize printable = 0;
for (const char *cursor = text; *cursor != '\0';
cursor = g_utf8_next_char(cursor))
{
gunichar character = g_utf8_get_char(cursor);
total++;
if (!g_unichar_isspace(character))
non_space++;
if (g_unichar_isprint(character) || g_unichar_isspace(character))
printable++;
}
return non_space >= 32 && total > 0 &&
((double) printable / (double) total) >= 0.70;
}
static guint pdf_analysis_parse_pages(const char *text)
{
GRegex *regex = g_regex_new("(?im)^Pages:\\s*([0-9]+)", 0, 0, NULL);
GMatchInfo *match = NULL;
guint pages = 0;
g_regex_match(regex, text != NULL ? text : "", 0, &match);
if (g_match_info_matches(match))
{
char *value = g_match_info_fetch(match, 1);
pages = (guint) g_ascii_strtoull(value, NULL, 10);
g_free(value);
}
g_match_info_free(match);
g_regex_unref(regex);
return pages;
}
static gboolean pdf_analysis_parse_encrypted(const char *text)
{
GRegex *regex = g_regex_new(
"(?im)^Encrypted:\\s*(yes|oui|true)", 0, 0, NULL);
gboolean encrypted = g_regex_match(
regex, text != NULL ? text : "", 0, NULL);
g_regex_unref(regex);
return encrypted;
}
static PdfPageAnalysis *pdf_page_new(
guint page_number,
PdfPageMethod method,
const char *text,
DocumentAnalysisState state
)
{
PdfPageAnalysis *page = g_new0(PdfPageAnalysis, 1);
page->page_number = page_number;
page->method = method;
page->text = g_strdup(text);
page->state = state;
page->warnings = g_ptr_array_new_with_free_func(g_free);
return page;
}
static void pdf_analysis_add_native_pages(PdfAnalysisResult *result)
{
char **pages = g_strsplit(result->native_text, "\f", -1);
guint added = 0;
for (guint index = 0; pages[index] != NULL; index++)
{
if (pages[index][0] == '\0' && pages[index + 1] == NULL)
break;
g_ptr_array_add(result->pages, pdf_page_new(
index + 1, PDF_PAGE_METHOD_NATIVE, pages[index],
DOCUMENT_ANALYSIS_STATE_SUCCESS));
added++;
}
if (result->page_count == 0)
result->page_count = added;
g_strfreev(pages);
}
static gboolean pdf_analysis_render_and_ocr(
PdfAnalysisResult *result,
const PdfAnalysisTools *tools,
const char *languages,
GCancellable *cancellable,
GError **error
)
{
GError *temporary_error = NULL;
char *temporary_directory = g_dir_make_tmp(
"labfy-pdf-analysis-XXXXXX", &temporary_error);
if (temporary_directory == NULL)
{
g_propagate_error(error, temporary_error);
return FALSE;
}
guint pages = MIN(result->page_count, DOCUMENT_ANALYSIS_MAX_PDF_PAGES);
if (result->page_count > DOCUMENT_ANALYSIS_MAX_PDF_PAGES)
{
result->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
g_ptr_array_add(result->warnings,
g_strdup("Le nombre de pages PDF dépasse la limite."));
}
gboolean success = TRUE;
char *source_basename = g_path_get_basename(result->source_path);
for (guint page_number = 1; page_number <= pages; page_number++)
{
if (cancellable != NULL &&
g_cancellable_set_error_if_cancelled(cancellable, error))
{
result->state = result->pages->len > 0
? DOCUMENT_ANALYSIS_STATE_PARTIAL
: DOCUMENT_ANALYSIS_STATE_CANCELLED;
success = FALSE;
break;
}
char *prefix = g_strdup_printf("%s/%s-page-%u",
temporary_directory, source_basename, page_number);
char *page_text = g_strdup_printf("%u", page_number);
const char *render_arguments[] = {
"-f", page_text, "-singlefile", "-png",
result->source_path, prefix, NULL
};
DocumentToolExecution *render_execution = NULL;
if (!document_tool_runner_run("pdftoppm", tools->pdftoppm,
render_arguments, result->source_path, cancellable,
&render_execution, error))
{
result->state = result->pages->len > 0
? DOCUMENT_ANALYSIS_STATE_PARTIAL
: DOCUMENT_ANALYSIS_STATE_CANCELLED;
document_tool_execution_free(render_execution);
g_free(page_text);
g_free(prefix);
success = FALSE;
break;
}
char *image_path = g_strconcat(prefix, ".png", NULL);
if (render_execution->state != DOCUMENT_ANALYSIS_STATE_SUCCESS)
{
PdfPageAnalysis *page = pdf_page_new(page_number,
PDF_PAGE_METHOD_OCR, NULL,
DOCUMENT_ANALYSIS_STATE_FAILED);
page->execution = render_execution;
g_ptr_array_add(result->pages, page);
result->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
}
else
{
OcrAnalysisResult *ocr = ocr_analysis_run(
tools->tesseract, image_path, languages,
cancellable, error);
PdfPageAnalysis *page = pdf_page_new(page_number,
PDF_PAGE_METHOD_OCR,
ocr != NULL ? ocr->text : NULL,
ocr != NULL ? ocr->execution->state :
DOCUMENT_ANALYSIS_STATE_FAILED);
page->render_execution = render_execution;
if (ocr != NULL)
{
page->execution = ocr->execution;
ocr->execution = NULL;
ocr_analysis_result_free(ocr);
}
g_ptr_array_add(result->pages, page);
if (page->state != DOCUMENT_ANALYSIS_STATE_SUCCESS)
result->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
if (ocr == NULL && error != NULL && *error != NULL &&
g_error_matches(*error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
result->state = result->pages->len > 1
? DOCUMENT_ANALYSIS_STATE_PARTIAL
: DOCUMENT_ANALYSIS_STATE_CANCELLED;
success = FALSE;
}
}
g_remove(image_path);
g_free(image_path);
g_free(page_text);
g_free(prefix);
if (!success)
break;
}
g_free(source_basename);
g_rmdir(temporary_directory);
g_free(temporary_directory);
return success;
}
PdfAnalysisResult *pdf_analysis_run(
const PdfAnalysisTools *tools,
const char *pdf_path,
const char *ocr_languages,
GCancellable *cancellable,
GError **error
)
{
if (tools == NULL || pdf_path == NULL || ocr_languages == NULL)
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Les paramètres d'analyse PDF sont invalides.");
return NULL;
}
PdfAnalysisResult *result = g_new0(PdfAnalysisResult, 1);
result->source_path = g_strdup(pdf_path);
result->pages = g_ptr_array_new_with_free_func(pdf_page_analysis_free);
result->warnings = g_ptr_array_new_with_free_func(g_free);
result->state = DOCUMENT_ANALYSIS_STATE_SUCCESS;
const char *info_arguments[] = { pdf_path, NULL };
if (!document_tool_runner_run("pdfinfo", tools->pdfinfo,
info_arguments, pdf_path, cancellable,
&result->pdfinfo_execution, error))
{
if (error != NULL && *error != NULL &&
g_error_matches(*error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
result->state = DOCUMENT_ANALYSIS_STATE_CANCELLED;
g_clear_error(error);
return result;
}
goto failure;
}
if (result->pdfinfo_execution->state ==
DOCUMENT_ANALYSIS_STATE_UNAVAILABLE)
{
result->state = DOCUMENT_ANALYSIS_STATE_UNAVAILABLE;
return result;
}
const char *version_arguments[] = { "-v", NULL };
result->pdfinfo_execution->version =
document_tool_runner_read_version(
tools->pdfinfo, version_arguments, cancellable);
result->encrypted = pdf_analysis_parse_encrypted(
result->pdfinfo_execution->raw_stdout);
result->page_count = pdf_analysis_parse_pages(
result->pdfinfo_execution->raw_stdout);
if (result->encrypted)
{
result->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
g_ptr_array_add(result->warnings,
g_strdup("Le PDF est chiffré ; aucun contournement n'est tenté."));
return result;
}
const char *text_arguments[] = {
"-enc", "UTF-8", "-layout", pdf_path, "-", NULL
};
if (!document_tool_runner_run("pdftotext", tools->pdftotext,
text_arguments, pdf_path, cancellable,
&result->native_execution, error))
{
if (error != NULL && *error != NULL &&
g_error_matches(*error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
result->state = DOCUMENT_ANALYSIS_STATE_CANCELLED;
g_clear_error(error);
return result;
}
goto failure;
}
result->native_execution->version =
document_tool_runner_read_version(
tools->pdftotext, version_arguments, cancellable);
if (result->native_execution->state ==
DOCUMENT_ANALYSIS_STATE_SUCCESS)
result->native_text = g_strdup(
result->native_execution->raw_stdout);
result->native_text_usable = pdf_analysis_text_is_usable(
result->native_text);
if (result->native_text_usable)
pdf_analysis_add_native_pages(result);
else if (!pdf_analysis_render_and_ocr(result, tools, ocr_languages,
cancellable, error))
{
if (error != NULL && *error != NULL &&
g_error_matches(*error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
g_clear_error(error);
return result;
}
result->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
g_clear_error(error);
}
return result;
failure:
pdf_analysis_result_free(result);
return NULL;
}