Spaces:
Sleeping
Sleeping
| import io | |
| import os | |
| import re | |
| from datetime import datetime | |
| from collections import Counter | |
| import pandas as pd | |
| import pytz | |
| import streamlit as st | |
| from PIL import Image | |
| from reportlab.pdfgen import canvas | |
| from reportlab.lib.utils import ImageReader | |
| # --- App Configuration ---------------------------------- | |
| st.set_page_config( | |
| page_title="Image → PDF Comic Layout", | |
| layout="wide", | |
| initial_sidebar_state="expanded", | |
| ) | |
| st.title("🖼️ Image → PDF • Scan, Reorder & Caption Generator") | |
| st.markdown( | |
| "Scan docs or upload images, filter by orientation, reorder, then generate a captioned PDF matching each image’s dimensions." | |
| ) | |
| # --- Session State for Snapshots ----------------------- | |
| st.session_state.setdefault('snapshots', []) | |
| # --- Sidebar: Page Settings ----------------------------- | |
| st.sidebar.header("1️⃣ Page Aspect Ratio & Size") | |
| ratio_map = { | |
| "4:3 (Landscape)": (4, 3), | |
| "16:9 (Landscape)": (16, 9), | |
| "1:1 (Square)": (1, 1), | |
| "2:3 (Portrait)": (2, 3), | |
| "9:16 (Portrait)": (9, 16), | |
| } | |
| ratio_choice = st.sidebar.selectbox( | |
| "Preset Ratio", list(ratio_map.keys()) + ["Custom…"] | |
| ) | |
| if ratio_choice != "Custom…": | |
| rw, rh = ratio_map[ratio_choice] | |
| else: | |
| rw = st.sidebar.number_input("Custom Width Ratio", min_value=1, value=4) | |
| rh = st.sidebar.number_input("Custom Height Ratio", min_value=1, value=3) | |
| BASE_WIDTH_PT = st.sidebar.slider( | |
| "Base Page Width (pt)", min_value=400, max_value=1200, value=800, step=100 | |
| ) | |
| page_width = BASE_WIDTH_PT | |
| page_height = int(BASE_WIDTH_PT * (rh / rw)) | |
| st.sidebar.markdown(f"**Preview page size:** {page_width}×{page_height} pt") | |
| # --- Main: Document Scan & Image Upload ---------------- | |
| st.header("2️⃣ Document Scan & Image Upload") | |
| # Camera scan | |
| cam = st.camera_input("📸 Scan Document") | |
| if cam: | |
| central = pytz.timezone("US/Central") | |
| now = datetime.now(central) | |
| prefix = now.strftime("%Y-%m%d-%I%M%p") + "-" + now.strftime("%a").upper() | |
| scan_name = f"{prefix}-scan.png" | |
| with open(scan_name, "wb") as f: | |
| f.write(cam.getvalue()) | |
| st.image(Image.open(scan_name), caption=scan_name, use_container_width=True) | |
| if scan_name not in st.session_state['snapshots']: | |
| st.session_state['snapshots'].append(scan_name) | |
| # File uploader | |
| uploads = st.file_uploader( | |
| "📂 Upload PNG/JPG images", type=["png","jpg","jpeg"], accept_multiple_files=True | |
| ) | |
| # --- Build combined list -------------------------------- | |
| all_records = [] | |
| # From snapshots | |
| for idx, path in enumerate(st.session_state['snapshots']): | |
| im = Image.open(path) | |
| w, h = im.size | |
| ar = round(w / h, 2) | |
| orient = "Square" if 0.9 <= ar <= 1.1 else ("Landscape" if ar > 1.1 else "Portrait") | |
| all_records.append({ | |
| "filename": os.path.basename(path), | |
| "source": path, | |
| "width": w, | |
| "height": h, | |
| "aspect_ratio": ar, | |
| "orientation": orient, | |
| "order": idx, | |
| }) | |
| # From uploads | |
| if uploads: | |
| for jdx, f in enumerate(uploads, start=len(all_records)): | |
| im = Image.open(f) | |
| w, h = im.size | |
| ar = round(w / h, 2) | |
| orient = "Square" if 0.9 <= ar <= 1.1 else ("Landscape" if ar > 1.1 else "Portrait") | |
| all_records.append({ | |
| "filename": f.name, | |
| "source": f, | |
| "width": w, | |
| "height": h, | |
| "aspect_ratio": ar, | |
| "orientation": orient, | |
| "order": jdx, | |
| }) | |
| # DataFrame | |
| df = pd.DataFrame(all_records) | |
| # Filter | |
| dims = st.sidebar.multiselect( | |
| "Include orientations:", options=["Landscape","Portrait","Square"], | |
| default=["Landscape","Portrait","Square"] | |
| ) | |
| df = df[df['orientation'].isin(dims)].reset_index(drop=True) | |
| st.markdown("#### Images & Scan Metadata (drag/order)") | |
| st.dataframe(df.style.format({"aspect_ratio":"{:.2f}"}), use_container_width=True) | |
| st.markdown("*Drag rows or edit `order` to set PDF page sequence.*") | |
| # Reordering | |
| try: | |
| edited = st.experimental_data_editor(df, num_rows="fixed", use_container_width=True) | |
| ordered_df = edited | |
| except Exception: | |
| edited = st.data_editor( | |
| df, | |
| column_config={"order": st.column_config.NumberColumn("Order", min_value=0, max_value=len(df)-1)}, | |
| hide_index=True, | |
| use_container_width=True, | |
| ) | |
| ordered_df = edited.sort_values('order').reset_index(drop=True) | |
| # Resolve ordered_sources list | |
| ordered_sources = [row['source'] for _, row in ordered_df.iterrows()] | |
| # --- Utility: Clean stems ------------------------------- | |
| def clean_stem(fn: str) -> str: | |
| return os.path.splitext(fn)[0].replace('-', ' ').replace('_', ' ') | |
| # --- PDF Creation: Image Sized + Captions -------------- | |
| def make_image_sized_pdf(sources): | |
| buf = io.BytesIO() | |
| c = canvas.Canvas(buf) | |
| for idx, src in enumerate(sources, start=1): | |
| im = Image.open(src) | |
| iw, ih = im.size | |
| cap_h = 20 | |
| pw, ph = iw, ih + cap_h | |
| c.setPageSize((pw, ph)) | |
| c.drawImage(ImageReader(im), 0, cap_h, iw, ih, preserveAspectRatio=True, mask='auto') | |
| caption = clean_stem(os.path.basename(src)) | |
| c.setFont('Helvetica', 12) | |
| c.drawCentredString(pw/2, cap_h/2, caption) | |
| c.setFont('Helvetica', 8) | |
| c.drawRightString(pw-10, 10, str(idx)) | |
| c.showPage() | |
| c.save(); buf.seek(0) | |
| return buf.getvalue() | |
| # --- Generate & Download ------------------------------- | |
| st.header("3️⃣ Generate & Download PDF") | |
| if st.button("🖋️ Generate Captioned PDF"): | |
| if not ordered_sources: | |
| st.warning("No images or scans to include.") | |
| else: | |
| central = pytz.timezone("US/Central") | |
| now = datetime.now(central) | |
| prefix = now.strftime("%Y-%m%d-%I%M%p") + "-" + now.strftime("%a").upper() | |
| stems = [clean_stem(os.path.basename(s)) for s in ordered_sources] | |
| # limit to 4 stems if too long | |
| stems = stems[:4] | |
| basename = " - ".join(stems) | |
| fname = f"{prefix}-{basename}.pdf" | |
| pdf_bytes = make_image_sized_pdf(ordered_sources) | |
| st.success(f"✅ PDF ready: **{fname}**") | |
| st.download_button("⬇️ Download PDF", data=pdf_bytes, file_name=fname, mime="application/pdf") | |
| st.markdown("#### Preview Page 1") | |
| try: | |
| import fitz | |
| doc = fitz.open(stream=pdf_bytes, filetype='pdf') | |
| pix = doc.load_page(0).get_pixmap(matrix=fitz.Matrix(1.5,1.5)) | |
| st.image(pix.tobytes(), use_container_width=True) | |
| except: | |
| st.info("Install `pymupdf` for preview.") | |
| # --- Footer ------------------------------------------------ | |
| st.sidebar.markdown("---") | |
| st.sidebar.markdown("Built by Aaron C. Wacker • Senior AI Engineer") | |