import io import os import math import re from collections import Counter from datetime import datetime import pandas as pd 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 • Full-Page & Custom Layout Generator") st.markdown( "Upload images, filter by orientation, reorder visually, and generate PDFs with various layouts." ) # --- 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"**Page size:** {page_width}×{page_height} pt") # --- Main: Upload, Filter & Reorder ------------------- st.header("2️⃣ Upload, Filter & Reorder Images") uploaded = st.file_uploader( "📂 Select PNG/JPG images", type=["png","jpg","jpeg"], accept_multiple_files=True ) ordered_files = [] if uploaded: records = [] for idx, f in enumerate(uploaded): 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") records.append({ "filename": f.name, "width": w, "height": h, "aspect_ratio": ar, "orientation": orient, "order": idx }) df = pd.DataFrame(records) 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("#### Image Metadata") st.dataframe(df.style.format({"aspect_ratio": "{:.2f}"}), use_container_width=True) st.markdown("#### Reorder Panels") 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) name2file = {f.name: f for f in uploaded} ordered_files = [name2file[n] for n in ordered_df["filename"] if n in name2file] # --- Utility: Word extraction -------------------------- def top_n_words(names, n=5): words = [] for fn in names: stem = os.path.splitext(fn)[0] words += re.findall(r"\w+", stem.lower()) return [w for w,_ in Counter(words).most_common(n)] def first_n_words_ordered(names, n=5): words = [] for fn in names: stem = os.path.splitext(fn)[0] tokens = re.findall(r"\w+", stem) for t in tokens: if len(words) < n: words.append(t) else: break if len(words) >= n: break return words # --- PDF Creation: Full Page ---------------------------- def make_fullpage_pdf(images, w_pt, h_pt): buf = io.BytesIO() c = canvas.Canvas(buf, pagesize=(w_pt, h_pt)) for f in images: im = Image.open(f) iw, ih = im.size scale = min(w_pt/iw, h_pt/ih) new_w, new_h = iw*scale, ih*scale x = (w_pt - new_w)/2 y = (h_pt - new_h)/2 im = im.resize((int(new_w), int(new_h)), Image.LANCZOS) c.drawImage(ImageReader(im), x, y, new_w, new_h, preserveAspectRatio=False, mask='auto') c.showPage() c.save(); buf.seek(0) return buf.getvalue() # --- PDF Creation: Image-sized + Captions -------------- def make_image_sized_pdf(images): buf = io.BytesIO() c = canvas.Canvas(buf) for f in images: im = Image.open(f) iw, ih = im.size # set page to image dims + caption space cap_h = 20 page_w, page_h = iw, ih + cap_h c.setPageSize((page_w, page_h)) # draw image c.drawImage(ImageReader(im), 0, cap_h, iw, ih, preserveAspectRatio=True, mask='auto') # caption caption = os.path.splitext(f.name)[0] c.setFont("Helvetica", 12) c.drawCentredString(page_w/2, cap_h/2, caption) c.showPage() c.save(); buf.seek(0) return buf.getvalue() # --- Generate & Download ------------------------------- st.header("3️⃣ Generate & Download PDF") # Full-page button def build_timestamp(prefix_time=False): fmt = "%Y-%m%d-%I%M%p" if prefix_time else "%Y-%m%d" return datetime.now().strftime(fmt) if st.button("🎉 Generate Full-Page PDF"): if not ordered_files: st.warning("Upload and reorder at least one image.") else: date_s = build_timestamp() words = top_n_words([f.name for f in ordered_files]) slug = "-".join(words) fname = f"{date_s}-{slug}.pdf" pdf = make_fullpage_pdf(ordered_files, page_width, page_height) st.success(f"✅ PDF ready: **{fname}**") st.download_button("⬇️ Download PDF", data=pdf, file_name=fname, mime="application/pdf") st.markdown("#### Preview") try: import fitz doc = fitz.open(stream=pdf, filetype="pdf") pix = doc[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.") # Custom-sized + captions button if st.button("🖋️ Generate Image-Sized PDF with Captions"): if not ordered_files: st.warning("Upload and reorder at least one image.") else: date_s = build_timestamp(prefix_time=True) words = first_n_words_ordered([f.name for f in ordered_files]) slug = "-".join(words) fname = f"{date_s}-{slug}.pdf" pdf = make_image_sized_pdf(ordered_files) st.success(f"✅ PDF ready: **{fname}**") st.download_button("⬇️ Download PDF", data=pdf, file_name=fname, mime="application/pdf") st.markdown("#### Preview") try: import fitz doc = fitz.open(stream=pdf, filetype="pdf") pix = doc[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")