feat: add PDF rotation functionality and integrate with Azure Document Intelligence processing

This commit is contained in:
Christian Krakau-Louis
2025-04-04 01:30:14 +02:00
parent 6e0a0b43bb
commit 98df430f8f
5 changed files with 151 additions and 9 deletions
+1 -1
View File
@@ -1 +1 @@
0.4.3-dev 0.4.5-dev
+1
View File
@@ -11,6 +11,7 @@ from app import tasks # <— This imports app/tasks.py so Celery can register t
# **Ensure all tasks are imported before Celery starts** # **Ensure all tasks are imported before Celery starts**
from app.tasks.process_document import process_document from app.tasks.process_document import process_document
from app.tasks.process_with_azure_document_intelligence import process_with_azure_document_intelligence from app.tasks.process_with_azure_document_intelligence import process_with_azure_document_intelligence
from app.tasks.rotate_pdf_pages import rotate_pdf_pages
from app.tasks.refine_text_with_gpt import refine_text_with_gpt from app.tasks.refine_text_with_gpt import refine_text_with_gpt
from app.tasks.extract_metadata_with_gpt import extract_metadata_with_gpt from app.tasks.extract_metadata_with_gpt import extract_metadata_with_gpt
from app.tasks.embed_metadata_into_pdf import embed_metadata_into_pdf from app.tasks.embed_metadata_into_pdf import embed_metadata_into_pdf
@@ -7,7 +7,7 @@ from azure.ai.documentintelligence.models import AnalyzeOutputOption, AnalyzeRes
from app.config import settings from app.config import settings
from app.tasks.retry_config import BaseTaskWithRetry from app.tasks.retry_config import BaseTaskWithRetry
from app.tasks.extract_metadata_with_gpt import extract_metadata_with_gpt from app.tasks.rotate_pdf_pages import rotate_pdf_pages
from app.celery_app import celery from app.celery_app import celery
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -41,22 +41,30 @@ def check_page_rotation(result, filename):
Args: Args:
result: The AnalyzeResult from Azure Document Intelligence API result: The AnalyzeResult from Azure Document Intelligence API
filename: The name of the file being processed filename: The name of the file being processed
Returns:
dict: Dictionary mapping page indices (integers) to rotation angles
""" """
logger.error(f"Checking rotation for document: {filename}") logger.error(f"Checking rotation for document: {filename}")
rotation_data = {}
if not hasattr(result, 'pages') or not result.pages: if not hasattr(result, 'pages') or not result.pages:
logger.error(f"No page information available for rotation check: {filename}") logger.error(f"No page information available for rotation check: {filename}")
return return rotation_data
for i, page in enumerate(result.pages): for i, page in enumerate(result.pages):
if hasattr(page, 'angle'): if hasattr(page, 'angle'):
rotation_angle = page.angle rotation_angle = page.angle
if rotation_angle != 0: if rotation_angle != 0:
logger.error(f"Page {i+1} is rotated by {rotation_angle} degrees") logger.error(f"Page {i+1} is rotated by {rotation_angle} degrees")
# Store page index as integer, not string
rotation_data[i] = rotation_angle
else: else:
logger.error(f"Page {i+1} has no rotation (0 degrees)") logger.error(f"Page {i+1} has no rotation (0 degrees)")
else: else:
logger.error(f"Page {i+1} rotation information not available") logger.error(f"Page {i+1} rotation information not available")
return rotation_data
@celery.task(base=BaseTaskWithRetry) @celery.task(base=BaseTaskWithRetry)
def process_with_azure_document_intelligence(filename: str): def process_with_azure_document_intelligence(filename: str):
@@ -69,8 +77,8 @@ def process_with_azure_document_intelligence(filename: str):
1. Uploads the document for OCR using Azure Document Intelligence. 1. Uploads the document for OCR using Azure Document Intelligence.
2. Retrieves the processed PDF with embedded text. 2. Retrieves the processed PDF with embedded text.
3. Saves the OCR-processed PDF locally in the same location as before. 3. Saves the OCR-processed PDF locally in the same location as before.
4. Extracts the text content for metadata processing. 4. Checks for page rotation and triggers page rotation if needed.
5. Triggers downstream metadata extraction by calling extract_metadata_with_gpt. 5. Triggers downstream metadata extraction.
""" """
try: try:
tmp_file_path = os.path.join(settings.workdir, "tmp", filename) tmp_file_path = os.path.join(settings.workdir, "tmp", filename)
@@ -106,7 +114,7 @@ def process_with_azure_document_intelligence(filename: str):
operation_id = poller.details["operation_id"] operation_id = poller.details["operation_id"]
# Check and log page rotation information # Check and log page rotation information
check_page_rotation(result, filename) rotation_data = check_page_rotation(result, filename)
# Retrieve the processed searchable PDF # Retrieve the processed searchable PDF
response = document_intelligence_client.get_analyze_result_pdf( response = document_intelligence_client.get_analyze_result_pdf(
@@ -121,8 +129,8 @@ def process_with_azure_document_intelligence(filename: str):
extracted_text = result.content if result.content else "" extracted_text = result.content if result.content else ""
logger.info(f"Extracted text for {filename}: {len(extracted_text)} characters") logger.info(f"Extracted text for {filename}: {len(extracted_text)} characters")
# Trigger downstream metadata extraction # Trigger page rotation task if rotation is detected, otherwise proceed to metadata extraction
extract_metadata_with_gpt.delay(filename, extracted_text) rotate_pdf_pages.delay(filename, extracted_text, rotation_data)
return {"file": filename, "searchable_pdf": searchable_pdf_path, "cleaned_text": extracted_text} return {"file": filename, "searchable_pdf": searchable_pdf_path, "cleaned_text": extracted_text}
except Exception as e: except Exception as e:
+133
View File
@@ -0,0 +1,133 @@
import os
import logging
import PyPDF2
import math
import json
from app.config import settings
from app.tasks.retry_config import BaseTaskWithRetry
from app.tasks.extract_metadata_with_gpt import extract_metadata_with_gpt
from app.celery_app import celery
logger = logging.getLogger(__name__)
def determine_rotation_angle(detected_angle):
"""
Determine the optimal rotation angle based on detected angle.
Args:
detected_angle: The angle detected by Azure Document Intelligence
Returns:
int: The angle to rotate the page in PyPDF2 (must be multiple of 90 degrees)
"""
# Normalize angle to be between 0 and 360
normalized_angle = detected_angle % 360
if normalized_angle < 0:
normalized_angle += 360
# If angle is very small (< 1 degree), don't rotate
if abs(normalized_angle) < 1 or abs(normalized_angle - 360) < 1:
return 0
# For angles close to 90, 180, or 270 degrees (±5°), round to nearest 90° increment
for target in [90, 180, 270]:
if abs(normalized_angle - target) < 5:
# PyPDF2 uses clockwise rotation, so we need to use the complementary angle
rotation_value = (360 - target) % 360
logger.info(f"Detected angle {detected_angle}° is close to {target}°, will rotate by {rotation_value}°")
return rotation_value
# For other significant angles, round to nearest 90° increment
# (PyPDF2 only supports rotations in 90-degree increments)
closest_90_multiple = round(normalized_angle / 90) * 90
# Convert to PyPDF2 rotation value (clockwise)
rotation_value = (360 - closest_90_multiple) % 360
logger.info(f"Detected angle {detected_angle}° rounded to {closest_90_multiple}°, will rotate by {rotation_value}°")
return rotation_value
@celery.task(base=BaseTaskWithRetry)
def rotate_pdf_pages(filename: str, extracted_text: str, rotation_data=None):
"""
Rotates pages in a PDF document based on detected rotation angles.
Args:
filename: The name of the file to rotate
extracted_text: The extracted text from the document
rotation_data: Optional rotation data dictionary {page_index: angle}
"""
try:
pdf_path = os.path.join(settings.workdir, "tmp", filename)
if not os.path.exists(pdf_path):
raise FileNotFoundError(f"PDF file not found: {pdf_path}")
# Skip rotation if no rotation data provided
if not rotation_data:
logger.info(f"No rotation data provided for {filename}, proceeding with metadata extraction")
extract_metadata_with_gpt.delay(filename, extracted_text)
return {"file": filename, "status": "no_rotation_needed"}
# Standardize rotation_data keys to integers
normalized_rotation_data = {}
for key, value in rotation_data.items():
try:
normalized_rotation_data[int(key)] = float(value)
except (ValueError, TypeError):
logger.warning(f"Invalid rotation data key-value: {key}:{value}")
if not any(abs(angle) > 0 for angle in normalized_rotation_data.values()):
logger.info(f"No significant rotations detected in {filename}, proceeding with metadata extraction")
extract_metadata_with_gpt.delay(filename, extracted_text)
return {"file": filename, "status": "no_rotation_needed"}
logger.info(f"Rotating {len(normalized_rotation_data)} pages in {filename}")
applied_rotations = {}
# Load the PDF
with open(pdf_path, 'rb') as file:
pdf_reader = PyPDF2.PdfReader(file)
pdf_writer = PyPDF2.PdfWriter()
# Process each page
for page_idx in range(len(pdf_reader.pages)):
page = pdf_reader.pages[page_idx]
# Apply rotation if this page has rotation data
if page_idx in normalized_rotation_data and abs(normalized_rotation_data[page_idx]) > 0:
detected_angle = normalized_rotation_data[page_idx]
rotation_angle = determine_rotation_angle(detected_angle)
if rotation_angle > 0:
# PyPDF2 uses clockwise rotation in 90-degree increments
page.rotate(rotation_angle)
logger.info(f"Page {page_idx+1} rotated by {rotation_angle}° (from detected {detected_angle}°)")
applied_rotations[str(page_idx)] = rotation_angle
else:
logger.info(f"Page {page_idx+1} had detected angle {detected_angle}° but determined it doesn't need rotation")
pdf_writer.add_page(page)
# Save the rotated PDF
with open(pdf_path, 'wb') as output_file:
pdf_writer.write(output_file)
if applied_rotations:
logger.info(f"Successfully rotated PDF: {filename} with rotations: {json.dumps(applied_rotations)}")
else:
logger.info(f"Detected rotations in {filename} but no rotations were actually applied (angles too small or not multiples of 90°)")
# Continue with metadata extraction
extract_metadata_with_gpt.delay(filename, extracted_text)
return {
"file": filename,
"status": "rotated" if applied_rotations else "no_rotation_needed",
"detected_rotations": rotation_data,
"applied_rotations": applied_rotations
}
except Exception as e:
logger.error(f"Error rotating PDF {filename}: {e}")
# Continue with metadata extraction despite rotation failure
extract_metadata_with_gpt.delay(filename, extracted_text)
return {"file": filename, "status": "rotation_failed", "error": str(e)}
+1 -1
View File
@@ -6,7 +6,7 @@ sqlalchemy # Database ORM
pydantic # Data validation pydantic # Data validation
openai # GPT integration for metadata extraction openai # GPT integration for metadata extraction
pymupdf # PDF processing, text extraction, and detection (imported as 'fitz') pymupdf # PDF processing, text extraction, and detection (imported as 'fitz')
PyPDF2 # PDF processing for page counting PyPDF2 # PDF processing for page counting and now also for rotation
requests # HTTP client requests # HTTP client
dropbox>=11.36.0 # Dropbox integration dropbox>=11.36.0 # Dropbox integration
azure-ai-documentintelligence # Azure OCR service azure-ai-documentintelligence # Azure OCR service