""" PDF related utilities. """ import asyncio import os from io import BytesIO from tempfile import NamedTemporaryFile import anyio import fitz # PDF manipulation library from .. import logger from ..config import TEMP_DIR from ..constants import FileExtensions, FileTypes logger = logger.new_logger(__name__) async def convert_pptx_to_pdf(buffer: BytesIO) -> BytesIO | None: """Convert a PPTX file to PDF using LibreOffice Headless mode. Args: buffer: BytesIO object containing the PPTX file data. """ # Place the temporary files in the TEMP_DIR directory, which should be a directory # allowed by AWS for temporary files (generally /tmp). with NamedTemporaryFile( suffix=FileExtensions.PPTX, dir=TEMP_DIR, delete=False ) as temp_source: temp_source.write(buffer.getvalue()) temp_source_path = temp_source.name output_pdf_path: str = ( temp_source_path.removesuffix(FileExtensions.PPTX) + FileExtensions.PDF ) cmd = [ "soffice", "--headless", "--convert-to", "pdf:writer_pdf_Export", "--outdir", os.path.dirname(output_pdf_path), temp_source_path, ] try: process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) _, stderr = await process.communicate() _check_pdf_conversion_success(process.returncode, stderr, output_pdf_path) logger.debug("Successfully converted PPTX to PDF: {}", output_pdf_path) buffered_pdf = BytesIO() async with await anyio.open_file(output_pdf_path, "rb") as f: buffered_pdf.write(await f.read()) buffered_pdf.seek(0) finally: # Clean up temp files os.remove(temp_source_path) if os.path.exists(output_pdf_path): os.remove(output_pdf_path) return buffered_pdf def adjust_pdf_file(pdf_bytes: BytesIO, new_exif_title: str = None) -> BytesIO: """This function performs final adjustments to a PDF file, such as updating the title in the EXIF metadata and compressing the file to reduce its final size. The input buffer is consumed and truncated to free up memory, and a new buffer containing the updated PDF file is returned. Args: pdf_bytes: BytesIO object containing the PDF file data. new_exif_title: New title to set in the PDF metadata. The EXIF title is what is displayed in the tab of the browser when the PDF is opened in a web browser, and it is also what is displayed in the title bar of the window when the PDF is opened in a PDF reader. """ doc = fitz.open(stream=pdf_bytes, filetype=FileTypes.PDF) if new_exif_title is not None: # Update the EXIF title in the metadata. This is important because the EXIF # title is what is displayed in the tab of the browser when the PDF is opened # in a web browser, and it is also what is displayed in the title bar of the # window when the PDF is opened in a PDF reader. doc.metadata["title"] = new_exif_title doc.set_metadata(doc.metadata) output_buffer = BytesIO() # Save the changes to a new file, applying compression and cleaning up the file # to remove any garbage. This way we get a smaller file size. doc.save(output_buffer, garbage=4, deflate=True, clean=True) # Truncate the input buffer to free up memory. This assumes that the input buffer # is not needed anymore after this function returns. pdf_bytes.truncate(0) doc.close() output_buffer.seek(0) return output_buffer def _check_pdf_conversion_success( process_returncode: int, stderr, output_pdf_path: str ) -> None: """Check if the PDF conversion was successful and raise an error if not, based on the issue. Args: process_returncode: Return code of the process. stderr: Standard error output of the process. output_pdf_path: Path to the output PDF file. """ if process_returncode != 0: raise ValueError( f"Failed to convert PPTX to PDF: {stderr.decode().strip()}. " f"Are the input bytes a valid PPTX file?" ) if not os.path.exists(output_pdf_path): raise RuntimeError( f"Failed to convert PPTX to PDF: {output_pdf_path} does not exist." ) if os.path.getsize(output_pdf_path) == 0: raise RuntimeError( f"Failed to convert PPTX to PDF: {output_pdf_path} is empty." )