from fasthtml.common import * from PyPDF2 import PdfReader, PdfWriter, PdfMerger import os, io, uuid, platform, subprocess # 1. Setup 'static' folder (FastHTML auto-serves this directory) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) UPLOAD_DIR = os.path.join(BASE_DIR, "pdf_outputs") if not os.path.exists(UPLOAD_DIR): os.makedirs(UPLOAD_DIR) # 2. FastHTML App Setup app, rt = fast_app( hdrs=( Link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"), Style(""" :root { --pico-font-size: 90%; } /* Shrinks everything by 10% */ h4 { margin-bottom: 0.5rem; font-size: 1.1rem; } /* Slims down card titles */ .card { padding: 1rem; } /* Reduces internal card spacing */ button { padding: 0.5rem 1rem; font-size: 0.9rem; } /* Slimmer buttons */ """) ), live=False ) @rt("/") def get(): return Titled("Modern PDF Toolbox", Container( P("Upload your PDFs and choose an operation."), Grid( Card(H4("Split PDF"), Form(Input(type="file", name="file", accept=".pdf", required=True), Button("Split into Pages", cls="secondary"), hx_post="/split", hx_target="#result", enctype="multipart/form-data")), Card(H4("Merge PDFs"), Form(Input(type="file", name="files", accept=".pdf", multiple=True, required=True), Button("Merge Files"), hx_post="/merge", hx_target="#result", enctype="multipart/form-data")), ), Grid( Card(H4("Rotate PDF"), Form(Input(type="file", name="file", accept=".pdf", required=True), Group( Button("90° CW", name="angle", value="90", cls="outline"), Button("180°", name="angle", value="180", cls="outline"), Button("270° CW", name="angle", value="270", cls="outline"), ), hx_post="/rotate", hx_target="#result", enctype="multipart/form-data")), Card(H4("Extract Range"), Form(Input(type="file", name="file", accept=".pdf", required=True), Group(Input(type="number", name="start", placeholder="Start"), Input(type="number", name="end", placeholder="End")), Button("Extract", cls="secondary"), hx_post="/extract", hx_target="#result", enctype="multipart/form-data")), ), Hr(), Section(id="result"), Hr(), # --- Utility Section with the new button --- Div( Group( Button("List Cached Files", hx_post="/open_folder", hx_target="#result", cls="outline"), Button("Clear Cache", hx_post="/clear_cache", hx_target="#result", cls="contrast outline"), ), style="margin-top: 20px;" ) ) ) # --- UTILITY ROUTES --- @rt("/open_folder") def post(): files = os.listdir(UPLOAD_DIR) if not files: return P("The cache is currently empty.", style="color: gray;") # Create a list of links for every file in the folder item_list = [] for f in files: item_list.append( Li( A(f, href=f"/get-file/{f}", target="_blank"), style="font-size: 0.85rem;" ) ) return Div( H5("Files in Cache:"), Ul(*item_list), style="border: 1px solid #ccc; padding: 1rem; border-radius: 8px; margin-top: 1rem;" ) @rt("/clear_cache") def post(): files_deleted = 0 for f in os.listdir(UPLOAD_DIR): file_path = os.path.join(UPLOAD_DIR, f) if os.path.isfile(file_path): os.remove(file_path) files_deleted += 1 return Div(f"🧹 Cache cleared! {files_deleted} files removed.", cls="pico-color-green-500", style="font-weight: bold;") # --- PDF OPERATION ROUTES --- @rt("/split") async def post(file: UploadFile): reader = PdfReader(io.BytesIO(await file.read())) sid = uuid.uuid4().hex[:6] links = [] for i, page in enumerate(reader.pages): writer = PdfWriter() writer.add_page(page) fname = f"{sid}_p{i+1}.pdf" with open(os.path.join(UPLOAD_DIR, fname), "wb") as f: writer.write(f) links.append(Li(A(f"Download Page {i+1}", href=f"/pdf_outputs/{fname}", target="_blank"))) return Div(H3(f"Split Complete ({len(reader.pages)} pages)"), Ul(*links)) @rt("/merge") async def post(files: list[UploadFile]): merger = PdfMerger() for f in files: merger.append(io.BytesIO(await f.read())) fname = f"merged_{uuid.uuid4().hex[:6]}.pdf" with open(os.path.join(UPLOAD_DIR, fname), "wb") as f: merger.write(f) return Div(H3("Merge Complete!"), A("Download Combined PDF", href=f"/pdf_outputs/{fname}", role="button", target="_blank")) @rt("/rotate") async def post(file: UploadFile, angle: int): reader = PdfReader(io.BytesIO(await file.read())) writer = PdfWriter() for page in reader.pages: page.rotate(angle) writer.add_page(page) fname = f"rotated_{uuid.uuid4().hex[:6]}.pdf" with open(os.path.join(UPLOAD_DIR, fname), "wb") as f: writer.write(f) return Div(H3("Rotation Complete!"), A(f"View Rotated PDF", href=f"/pdf_outputs/{fname}", role="button", target="_blank")) @rt("/extract") async def post(file: UploadFile, start: int, end: int): reader = PdfReader(io.BytesIO(await file.read())) writer = PdfWriter() for i in range(max(0, start-1), min(end, len(reader.pages))): writer.add_page(reader.pages[i]) fname = f"extract_{uuid.uuid4().hex[:6]}.pdf" with open(os.path.join(UPLOAD_DIR, fname), "wb") as f: writer.write(f) return Div(H3("Extraction Complete!"), A("Download Segment", href=f"/pdf_outputs/{fname}", role="button", target="_blank")) if __name__ == "__main__": import uvicorn # The '0.0.0.0' tells the server to listen to the outside world uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", 7860)))