defcf746ea
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
197 lines
7.7 KiB
Python
197 lines
7.7 KiB
Python
import json
|
|
import logging
|
|
import os
|
|
|
|
import pypdf # Upgraded from PyPDF2 to fix CVE-2023-36464
|
|
|
|
from app.celery_app import celery
|
|
from app.config import settings
|
|
from app.tasks.extract_metadata_with_gpt import extract_metadata_with_gpt
|
|
from app.tasks.retry_config import BaseTaskWithRetry
|
|
from app.utils import log_task_progress
|
|
|
|
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 pypdf (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:
|
|
# pypdf 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
|
|
# (pypdf only supports rotations in 90-degree increments)
|
|
closest_90_multiple = round(normalized_angle / 90) * 90
|
|
# Convert to pypdf 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, bind=True)
|
|
def rotate_pdf_pages(self, filename: str, extracted_text: str, rotation_data=None, file_id: int = 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}
|
|
file_id: Optional file ID to pass through to subsequent tasks
|
|
"""
|
|
try:
|
|
task_id = self.request.id
|
|
log_task_progress(
|
|
task_id,
|
|
"rotate_pdf_pages",
|
|
"in_progress",
|
|
f"Checking page rotation for {filename}",
|
|
file_id=file_id,
|
|
)
|
|
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"[{task_id}] No rotation data provided for {filename}, proceeding with metadata extraction")
|
|
log_task_progress(
|
|
task_id,
|
|
"rotate_pdf_pages",
|
|
"success",
|
|
"No rotation needed, proceeding to metadata extraction",
|
|
file_id=file_id,
|
|
)
|
|
extract_metadata_with_gpt.delay(filename, extracted_text, file_id)
|
|
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"[{task_id}] Invalid rotation data key-value: {key}:{value}")
|
|
|
|
if not any(abs(angle) > 0 for angle in normalized_rotation_data.values()):
|
|
logger.info(
|
|
f"[{task_id}] No significant rotations detected in {filename}, proceeding with metadata extraction"
|
|
)
|
|
log_task_progress(
|
|
task_id,
|
|
"rotate_pdf_pages",
|
|
"success",
|
|
"No rotation needed, proceeding to metadata extraction",
|
|
file_id=file_id,
|
|
)
|
|
extract_metadata_with_gpt.delay(filename, extracted_text, file_id)
|
|
return {"file": filename, "status": "no_rotation_needed"}
|
|
|
|
logger.info(f"[{task_id}] Rotating {len(normalized_rotation_data)} pages in {filename}")
|
|
log_task_progress(
|
|
task_id,
|
|
"apply_rotation",
|
|
"in_progress",
|
|
f"Rotating {len(normalized_rotation_data)} pages",
|
|
file_id=file_id,
|
|
)
|
|
applied_rotations = {}
|
|
|
|
# Load the PDF
|
|
with open(pdf_path, "rb") as file:
|
|
pdf_reader = pypdf.PdfReader(file)
|
|
pdf_writer = pypdf.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:
|
|
# pypdf uses clockwise rotation in 90-degree increments
|
|
page.rotate(rotation_angle)
|
|
logger.info(
|
|
f"[{task_id}] Page {page_idx + 1} rotated by {rotation_angle}° "
|
|
f"(from detected {detected_angle}°)"
|
|
)
|
|
applied_rotations[str(page_idx)] = rotation_angle
|
|
else:
|
|
logger.info(
|
|
f"[{task_id}] 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"[{task_id}] Successfully rotated PDF: {filename} with rotations: {json.dumps(applied_rotations)}"
|
|
)
|
|
else:
|
|
logger.info(
|
|
f"[{task_id}] 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, file_id)
|
|
|
|
log_task_progress(
|
|
task_id,
|
|
"rotate_pdf_pages",
|
|
"success",
|
|
f"Rotation complete for {filename}",
|
|
file_id=file_id,
|
|
detail=json.dumps({"applied_rotations": applied_rotations}),
|
|
)
|
|
|
|
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"[{task_id}] Error rotating PDF {filename}: {e}")
|
|
log_task_progress(
|
|
task_id,
|
|
"rotate_pdf_pages",
|
|
"failure",
|
|
f"Rotation failed: {str(e)}",
|
|
file_id=file_id,
|
|
detail=json.dumps({"error": str(e), "filename": filename}),
|
|
)
|
|
# Continue with metadata extraction despite rotation failure
|
|
extract_metadata_with_gpt.delay(filename, extracted_text, file_id)
|
|
return {"file": filename, "status": "rotation_failed", "error": str(e)}
|