Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/api/schemas/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
73 changes: 65 additions & 8 deletions app/services/file_manipulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
2 changes: 1 addition & 1 deletion benchmark/accuracy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions benchmark/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ retry-requests
pandas
geopy
alembic
pyhanko
1 change: 0 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down