From 9d6bfde2882fe74356bef8b64fb79bfa99870f56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:27:34 +0000 Subject: [PATCH] feat(tasks): extract and map embedded PDF metadata in upload_to_email Implement extraction of embedded PDF metadata using pypdf, mapping /Title, /Author, /Subject, /Keywords to filename, absender, document_type, and tags respectively. JSON metadata takes precedence; PDF metadata fills missing fields only. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/tasks/upload_to_email.py | 24 +++++++++--- tests/test_upload_email.py | 71 +++++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/app/tasks/upload_to_email.py b/app/tasks/upload_to_email.py index c68e4bf7..7366440a 100644 --- a/app/tasks/upload_to_email.py +++ b/app/tasks/upload_to_email.py @@ -24,6 +24,15 @@ logger = logging.getLogger(__name__) # Constants _LOGO_FILENAME = "logo.png" +# Mapping from PDF metadata keys (with leading slash stripped) to application-specific names. +# This mirrors the inverse of the mapping used in embed_metadata_into_pdf.py. +_PDF_METADATA_KEY_MAP = { + "Title": "filename", + "Author": "absender", + "Subject": "document_type", + "Keywords": "tags", +} + def get_email_template(template_name="default.html"): """ @@ -64,9 +73,12 @@ def extract_metadata_from_file(file_path): """ Try to extract metadata from a file using several methods: 1. Check for a .json metadata file with the same name - 2. Extract metadata from PDF if it's embedded + 2. Extract embedded metadata from PDF using pypdf - Returns a dictionary of metadata or None if not found + JSON metadata takes precedence; embedded PDF metadata fills in any missing + fields using the application's standard key mapping (e.g., /Title → filename). + + Returns a dictionary of metadata or empty dict if not found. """ metadata = {} @@ -77,7 +89,6 @@ def extract_metadata_from_file(file_path): with open(metadata_path, "r", encoding="utf-8") as f: metadata = json.load(f) logger.info(f"Loaded metadata from external JSON file: {metadata_path}") - return metadata except Exception as e: logger.warning(f"Failed to load metadata from JSON file: {str(e)}") @@ -88,11 +99,14 @@ def extract_metadata_from_file(file_path): pdf_reader = pypdf.PdfReader(f) pdf_metadata = pdf_reader.metadata if pdf_metadata: - # Convert metadata to a standard dictionary for key, value in pdf_metadata.items(): # Remove the leading slash from PDF metadata keys (e.g., '/Title' -> 'Title') clean_key = key[1:] if key.startswith("/") else key - metadata[clean_key] = str(value) + # Map to application-specific key names where possible + mapped_key = _PDF_METADATA_KEY_MAP.get(clean_key, clean_key) + # Only set if not already present (JSON metadata takes precedence) + if mapped_key not in metadata: + metadata[mapped_key] = str(value) logger.info(f"Extracted embedded metadata from PDF: {file_path}") except Exception as e: diff --git a/tests/test_upload_email.py b/tests/test_upload_email.py index d84dc59f..f1e57ef2 100644 --- a/tests/test_upload_email.py +++ b/tests/test_upload_email.py @@ -143,11 +143,72 @@ class TestExtractMetadataFromFile: result = extract_metadata_from_file(str(file_path)) - # Check that the leading slash is stripped and keys/values match - assert result.get("Title") == "Test Title" - assert result.get("Author") == "Test Author" - assert result.get("Subject") == "Test Document" - assert result.get("Keywords") == "test, metadata, pypdf" + # Keys are mapped to application-specific names + assert result.get("filename") == "Test Title" + assert result.get("absender") == "Test Author" + assert result.get("document_type") == "Test Document" + assert result.get("tags") == "test, metadata, pypdf" + + def test_extracts_embedded_metadata_from_pdf(self, tmp_path): + """Test that embedded PDF metadata is mapped to application-specific keys.""" + import pypdf + + file_path = tmp_path / "mapped.pdf" + + writer = pypdf.PdfWriter() + writer.add_blank_page(width=100, height=100) + writer.add_metadata( + { + "/Title": "Invoice 2024", + "/Author": "Acme Corp", + "/Subject": "invoice", + "/Keywords": "finance, billing", + } + ) + with open(file_path, "wb") as f: + writer.write(f) + + result = extract_metadata_from_file(str(file_path)) + + # Verify the PDF-to-app key mapping + assert result["filename"] == "Invoice 2024" + assert result["absender"] == "Acme Corp" + assert result["document_type"] == "invoice" + assert result["tags"] == "finance, billing" + + def test_pdf_metadata_does_not_overwrite_json(self, tmp_path): + """Test that JSON metadata takes precedence over embedded PDF metadata.""" + import pypdf + + file_path = tmp_path / "dual.pdf" + + # Create a PDF with embedded metadata + writer = pypdf.PdfWriter() + writer.add_blank_page(width=100, height=100) + writer.add_metadata( + { + "/Title": "PDF Title", + "/Author": "PDF Author", + "/Subject": "PDF Subject", + "/Keywords": "pdf, keywords", + } + ) + with open(file_path, "wb") as f: + writer.write(f) + + # Create a companion JSON file that sets some overlapping fields + json_metadata = {"filename": "JSON Filename", "absender": "JSON Author"} + json_path = tmp_path / "dual.json" + json_path.write_text(json.dumps(json_metadata)) + + result = extract_metadata_from_file(str(file_path)) + + # JSON values must not be overwritten by PDF metadata + assert result["filename"] == "JSON Filename" + assert result["absender"] == "JSON Author" + # Fields missing from JSON are filled from PDF metadata + assert result["document_type"] == "PDF Subject" + assert result["tags"] == "pdf, keywords" @pytest.mark.unit