"""Gradio based ui for Copaint pdf generator. """ import gradio as gr import shutil import tempfile from gradio_pdf import PDF from importlib.resources import files from pathlib import Path from copaint.copaint import image_to_pdf import torchvision import torch fromPIltoTensor = torchvision.transforms.ToTensor() fromTensortoPIL = torchvision.transforms.ToPILImage() def add_grid_to_image(image, h_cells, w_cells): # image is a torch tensor of shape (3, h, w) image = image.convert("RGB") image = fromPIltoTensor(image) grid_color = torch.tensor([16,15,46]).unsqueeze(1).unsqueeze(1) / 255.0 h,w = image.shape[1:] thickness = max(min(1, int(min(h,w)/100)), 1) print("thickness, h, w", thickness, h, w) for i in range(h_cells+1): idx_i = int(i*h/h_cells) image[:, idx_i-thickness:idx_i+thickness, :] = grid_color for j in range(w_cells+1): idx_j = int(j*w/w_cells) image[:, :, idx_j-thickness:idx_j+thickness] = grid_color image = fromTensortoPIL(image) return image def canvas_ratio(image, h_cells, w_cells): w,h = image.size aspect_ratio = w/h if aspect_ratio > 1: aspect_ratio = 1/aspect_ratio # find nearest aspect ratio in the list of predefined ones predefined_aspect_ratios = [2/3, 1/2, 1/1, 5/6, 4/5, 5/7] predefined_aspect_ratios_str = ["2:3", "1:2", "1:1", "5:6", "4:5", "5:7"] min_diff = float('inf') closest_ratio_idx = None for idx, ratio in enumerate(predefined_aspect_ratios): diff = abs(aspect_ratio - ratio) if diff < min_diff: min_diff = diff closest_ratio_idx = idx closest_ratio_str = predefined_aspect_ratios_str[closest_ratio_idx] if min_diff > 0.1: return None else: example_str = "" if closest_ratio_str == "1:1": if h_cells == 2 and h_cells == 2: example_str = " (for example, a 6” by 6” canvas)" if h_cells == 3 and h_cells == 3: example_str = " (for example, an 8” by 8” canvas)" elif closest_ratio_str == "5:6": example_str = " (for example, a 10” by 12” canvas)" elif closest_ratio_str == "3:4": if (h_cells == 3 and w_cells == 4) or (h_cells == 4 and w_cells == 3): example_str = " (for example, a 6” by 8” canvas)" if (h_cells == 4 and w_cells == 6) or (h_cells == 6 and w_cells == 4): example_str = " (for example, an 18” by 24”, or a 12” by 16” canvas)" elif closest_ratio_str == "2:3" and ((h_cells == 6 and w_cells == 9) or (h_cells == 9 and w_cells == 6)): example_str = " (for example, a 24” by 36” canvas)" return f"Preparing your canvas: *choose a canvas with a {closest_ratio_str} ratio to respect your design’s size{example_str}.*" def add_grid_and_display_ratio(image, h_cells, w_cells): if image is None: return None, gr.update(visible=False) return add_grid_to_image(image, h_cells, w_cells), gr.update(visible=True, value=canvas_ratio(image, h_cells, w_cells)) def process_copaint( input_image, h_cells=None, w_cells=None, a4=False, high_res=False, cell_size_in_cm=None, min_cell_size_in_cm=2, copaint_name="", copaint_logo=None, ): """Process the input and generate CoPaint PDF""" # Create temporary directories for processing temp_input_dir = tempfile.mkdtemp() temp_output_dir = tempfile.mkdtemp() try: # Save uploaded images to temp directory input_path = Path(temp_input_dir) / "input_image.png" input_image.save(input_path) logo_path = None if copaint_logo is not None: logo_path = Path(temp_input_dir) / "logo.png" copaint_logo.save(logo_path) else: # Use default logo path from the package logo_path = files("copaint.static") / "logo_copaint.png" if copaint_name == "" or copaint_name is None: from copaint.cli import default_identifier copaint_name = default_identifier() if a4 == "A4": a4 = True else: a4 = False # Generate the PDF pdf_path = image_to_pdf( input_image=str(input_path), logo_image=str(logo_path), outputfolder=temp_output_dir, h_cells=h_cells, w_cells=w_cells, unique_identifier=copaint_name, cell_size_in_cm=cell_size_in_cm, a4=a4, high_res=high_res, min_cell_size_in_cm=min_cell_size_in_cm ) return pdf_path, None # Return path and no error except Exception as e: # Return error message return None, f"Error generating PDF: {str(e)}" finally: # Clean up temporary input directory shutil.rmtree(temp_input_dir) def build_gradio_ui(): # Create Gradio Interface with gr.Blocks(title="CoPaint Generator", theme='NoCrypt/miku') as demo: gr.Markdown("# 🤖 CoPaint Generator") gr.Markdown("Upload an image with your painting design and set grid parameters to generate a CoPaint PDF template 🖨️📄✂️ for your next collaborative painting activities. 🎨🖌️") # --- inputs --- with gr.Row(equal_height=True): # Upload Design Template with gr.Column(scale=2): input_image = gr.Image(type="pil", label="Upload Your Design") with gr.Column(scale=1): # Grid with gr.Tab("Grid Layout"): gr.Markdown("
Squares' Grid
") w_cells = gr.Number(label="↔ (width)", value=4, precision=0) h_cells = gr.Number(label=" by ↕ (heigth)", value=6, precision=0) gr.Examples( examples=[ [6, 9], [4, 6], [3, 3], [3, 4], [2, 2] ], example_labels=[ "Copaint Wedding 6x9 Grid (54 squares)", "Copaint Classic 4x6 Grid (24 squares)", "Copaint Mini 3x3 Grid (9 squares)", "Copaint Mini 3x4 Grid (12 squares)", "Copaint Mini 2x2 Grid (4 squares)"], inputs=[w_cells, h_cells], ) # Grid + Design preview gr.Markdown("
Preview
") output_image = gr.Image(label="Squares' Grid Preview", interactive=False) # canvas ratio message canvas_msg = gr.Markdown(label="Canvas Ratio", visible=False) # PDF options with gr.Tab("PDF Printing Options"): use_a4 = gr.Dropdown(choices=["US letter", "A4"], label="Paper Format", value="US letter") with gr.Accordion("Advanced settings (optional)", open=False): with gr.Row(): with gr.Column(scale=1): high_res = gr.Checkbox(label="High Resolution Mode (>20sec long processing)") cell_size = gr.Number(label="Square Size, in cm (optional)", value="", info="If none is provided, the design size automatically adjusts to fit on a single page.") copaint_name = gr.Textbox(label="Add a Custom Design Name (optional)", value="", max_length=10, info="You can add a custom design name: it will appear on the back of each square, in the top left corner.") copaint_logo = gr.Image(type="pil", label="Add a Custom Logo (optional)") gr.Markdown( "
" "You can add a custom logo: it will appear on the back of each square, in the bottom right corner." "
") # --- outputs --- with gr.Row(): # PDF with gr.Column(scale=1): submit_btn = gr.Button("Generate Copaint PDF", variant="primary") with gr.Row(): with gr.Column(scale=1): output_file = gr.File(label="Download PDF", visible=False, interactive=False) with gr.Column(scale=1): output_error_msg = gr.Textbox(label="Error Message", visible=False) with gr.Row(): with gr.Column(scale=1): output_pdf = PDF(label="PDF Preview") # Update output_image: trigger update when any input changes for component in [input_image, h_cells, w_cells]: component.change( fn=add_grid_and_display_ratio, inputs=[input_image, h_cells, w_cells], outputs=[output_image, canvas_msg] ) # Submit function: generate pdf def on_submit(input_image, h_cells, w_cells, use_a4, high_res, cell_size, copaint_name, copaint_logo): if input_image is None: return None, None, gr.update(visible=True, value="Please upload an image first 👀") if cell_size is None or cell_size == "" or cell_size == 0: cell_size = None pdf_path, error = process_copaint( input_image=input_image, h_cells=int(h_cells), w_cells=int(w_cells), a4=use_a4, high_res=high_res, cell_size_in_cm=cell_size if cell_size else None, min_cell_size_in_cm=float(2), copaint_name=copaint_name, copaint_logo=copaint_logo ) if error: # Show error message return None, None, gr.update(visible=True, value=error) else: # Show successful PDF return pdf_path, gr.update(visible=True, value=pdf_path, interactive=False), gr.update(visible=False) submit_btn.click( on_submit, inputs=[input_image, h_cells, w_cells, use_a4, high_res, cell_size, copaint_name, copaint_logo], outputs=[output_pdf, output_file, output_error_msg] ) return demo