diff --git a/app/api/schemas/forms.py b/app/api/schemas/forms.py index cdfe8cd..8ba3ed4 100644 --- a/app/api/schemas/forms.py +++ b/app/api/schemas/forms.py @@ -7,6 +7,7 @@ class FormFill(BaseModel): template_id: int input_id: UUID model: str | None = None + sign: bool = False # Ajouté pour l'issue #231 class FormFillResponse(BaseModel): @@ -32,6 +33,7 @@ class AsyncFormFill(BaseModel): template_ids: list[int] input_id: UUID model: str | None = None + sign: bool = False # Ajouté pour l'issue #231 @field_validator("template_ids") def validate_template_ids(cls, value): diff --git a/app/services/file_manipulator.py b/app/services/file_manipulator.py index 183d809..a8101b8 100644 --- a/app/services/file_manipulator.py +++ b/app/services/file_manipulator.py @@ -10,15 +10,9 @@ class FileManipulator: def prepare_fillable(self, pdf_path: str): - """ - Run commonforms on a flat PDF to detect form regions and produce a - fillable PDF. Returns the new path (alongside the original). - """ - # Disable CUDA to force CPU usage, preventing errors on Mac Silicon / Docker import os os.environ["CUDA_VISIBLE_DEVICES"] = "" - # Monkey patch rfdetr to force CPU usage on Mac Silicon / Docker try: import rfdetr.detr original_ensure = rfdetr.detr._ensure_model_on_device @@ -35,7 +29,7 @@ def patched_ensure(model_ctx): prepare_form(pdf_path, template_path) return template_path - def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: str = None): + def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: str = None, sign: bool = False): """ It receives the raw data, runs the PDF filling logic, and returns the path to the newly created file. @@ -59,10 +53,73 @@ def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: st ) filler.fill(pdf_form_path, user_input, output_name, model) - logger.info("Process complete. Output saved to: %s", output_name) + # Metadata scrubbing (#315) + try: + from pdfrw import PdfReader, PdfWriter, PdfDict + reader = PdfReader(output_name) + reader.Info = PdfDict( + Title='FireForm Automated Report', + Author='FireForm', + Producer='FireForm', + Creator='FireForm' + ) + PdfWriter().write(output_name, reader) + except Exception as meta_err: + logger.warning("Could not strip PDF metadata: %s", meta_err) + + # ISSUE #231: Digital Signature Integration + if sign: + output_name = self._sign_pdf(output_name) + logger.info("Process complete. Output saved to: %s", output_name) return output_name except Exception as e: logger.error("An error occurred during PDF generation: %s", e) raise e + + def _sign_pdf(self, pdf_path: str) -> str: + """ + Sign the PDF using pyhanko if a certificate is configured. + Falls back gracefully if no cert is found (preventing test breaks). + """ + try: + from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter + from pyhanko.sign import fields, signers + + cert_path = os.getenv("SIGNING_CERT_PATH", "cert.p12") + cert_pass = os.getenv("SIGNING_CERT_PASSWORD", "").encode("utf-8") + + if not os.path.exists(cert_path): + logger.warning("Signing requested, but certificate not found at %s. Skipping digital signature.", cert_path) + return pdf_path + + with open(pdf_path, 'rb') as inf: + w = IncrementalPdfFileWriter(inf) + fields.append_signature_field( + w, + sig_field_spec=fields.SigFieldSpec( + sig_field_name='Sig1', + box=(50, 50, 200, 100) + ) + ) + + signer = signers.load_pkcs12( + path=cert_path, + passphrase=cert_pass + ) + + output_signed_path = pdf_path.replace("_filled.pdf", "_signed.pdf") + with open(output_signed_path, 'wb') as outf: + signers.sign_pdf( + w, + signers.PdfSignatureMetadata(field_name='Sig1'), + signer=signer, + output=outf + ) + + os.replace(output_signed_path, pdf_path) + logger.info("Successfully applied digital signature to PDF.") + except Exception as e: + logger.error("Failed to apply digital signature: %s", e) + return pdf_path \ No newline at end of file diff --git a/benchmark/accuracy.py b/benchmark/accuracy.py index 7cfb46c..d9393b6 100644 --- a/benchmark/accuracy.py +++ b/benchmark/accuracy.py @@ -83,7 +83,7 @@ def calculate_accuracy( if _is_unchecked_checkbox(_path, ground_truth) and _is_empty_value(extracted): return 1.0 - if type(extracted) != type(ground_truth): + if type(extracted) is not type(ground_truth): # Allow string representations of booleans/numbers if isinstance(ground_truth, bool) and isinstance(extracted, str): extracted = extracted.lower() in ("true", "1", "yes") diff --git a/benchmark/compare.py b/benchmark/compare.py index e4a5daf..fd9d543 100644 --- a/benchmark/compare.py +++ b/benchmark/compare.py @@ -8,14 +8,14 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) -from accuracy import ( +from accuracy import ( # noqa: E402 _is_blank, _is_empty_value, _is_ignored_field, _is_unchecked_checkbox, calculate_accuracy, calculate_accuracy_with_judge, -) +) _MISSING = object() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7c3839b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "FireForm", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/requirements.txt b/requirements.txt index 588068e..3f6f03d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,3 +21,4 @@ retry-requests pandas geopy alembic +pyhanko \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py index e656f65..1d8aa3f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -11,7 +11,6 @@ from app.api.schemas.enums import InputStatus, InputType from app.models import Template, FormSubmission, Input from app.core.config import API_PREFIX -from app.models import FormSubmission, Template # ═══════════════════════════════════════════════════════════════════════════ # DB model sanity