# cc @2024 COPAINT # troubleshooting: groueix@copaint.com """ # usage python copaint.py --input data/input_design.png --back data/back_design.png --outputfolder output # install dependencies pip install torch torchvision reportlab PyPDF2 Pillow argparse # if you are using a mac, you might need to install cairosvg and cairo to load SVG files pip install cairosvg ; brew install cairo libffi export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:/opt/homebrew/lib/pkgconfig:$PKG_CONFIG_PATH" export DYLD_LIBRARY_PATH="/usr/local/lib:/opt/homebrew/lib:$DYLD_LIBRARY_PATH" """ import argparse import os import numpy as np import torchvision import torch import time # Add this import for timing from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import letter, A4 from reportlab.lib.units import inch import PyPDF2 import logging from functools import lru_cache from matplotlib import font_manager from PIL import Image, ImageDraw, ImageFont # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger(__name__) # Configure debug logging based on environment variable logger.setLevel(logging.DEBUG) Image.MAX_IMAGE_PIXELS = None # Removes the limit entirely fromPIltoTensor = torchvision.transforms.ToTensor() fromTensortoPIL = torchvision.transforms.ToPILImage() pre_loaded_images = {} @lru_cache(maxsize=1) def get_font(debug=False) -> str: """ Get the path to the Bradley Hand font, cached after first call. """ start_time = time.time() good_font_options = ["Avenir Next", "HelveticaNeue", "AdobeClean-Regular", "Arial"] # "Bradley Hand" font_paths = ["/System/Library/Fonts/Avenir Next.ttc"] for font_path in font_paths: if os.path.exists(font_path): logger.info(f"Found '{font_path}' font") return font_path available_fonts = font_manager.findSystemFonts(fontpaths=None, fontext='ttf') font_path = None for good_font in good_font_options: font_path = next((font for font in available_fonts if good_font in font), None) if font_path: logger.info(f"Found '{good_font}' font: {font_path}") break if font_path is None: font_path = available_fonts[0] logger.warning(f"No good fonts found. Using default: {font_path}") logger.warning("Please install one of the recommended fonts.") if debug: logger.debug(f"Font loading took {time.time() - start_time:.4f} seconds") return font_path font_path = get_font() def load_image(image_path, debug=False): """ Load an image from a file path and return a tensor. """ start_time = time.time() # check if the path exists assert os.path.exists(image_path), f"File not found: {image_path}" # check if the file is an SVG if image_path.endswith(".svg"): import cairosvg import io # Convert SVG to PNG with open(image_path, "rb") as svg_file: png_data = cairosvg.svg2png(file_obj=svg_file) # Load the PNG data into a Pillow Image image = Image.open(io.BytesIO(png_data)) else: image = Image.open(image_path) # convert to RGBA image = image.convert("RGBA") # Apply a white background background = Image.new("RGB", image.size, (255, 255, 255)) background.paste(image, mask=image.split()[3]) image = background image_open_time = time.time() if debug: logger.debug(f"Image opening took {image_open_time - start_time:.4f} seconds") image = fromPIltoTensor(image).unsqueeze(0) logger.info(f"Loaded image of shape {image.shape}, from {image_path}") if debug: logger.debug(f"Image to tensor conversion took {time.time() - image_open_time:.4f} seconds") logger.debug(f"Total image loading took {time.time() - start_time:.4f} seconds") return image def save_image(tensor, image_path, debug=False): """ Save a tensor to an image file. """ start_time = time.time() logger.info(f"Saving image of shape {tensor.shape} to {image_path}") image = fromTensortoPIL(tensor.squeeze(0)) conversion_time = time.time() if debug: logger.debug(f"Tensor to PIL conversion took {conversion_time - start_time:.4f} seconds") image.save(image_path) if debug: logger.debug(f"Image saving took {time.time() - conversion_time:.4f} seconds") logger.debug(f"Total save_image took {time.time() - start_time:.4f} seconds") def save_tensor_to_pdf(tensor, pdf_path, is_front=True, margin=0.25, img_small_side_in_cm=None, a4=False, high_res=False, scale=None, debug=False): """ Save a tensor to a PDF, the tensor is assumed to be a single image, and is centered on the page. """ start_time = time.time() image = fromTensortoPIL(tensor.squeeze(0)) img_width, img_height = image.size # 1 Inch = 72 Points : ad-hoc metric used in typography and the printing industry. # The US Letter format is US Letter size: 8.5 by 11 inches W, H = 8.5, 11 # the unit is inch if a4: logger.info("Using A4 format") W, H = 8.27, 11.69 # the unit is inch page_width_in_pt = (W - 2*margin) * inch page_height_in_pt = (H - 2*margin) * inch img_small_side_in_pt = None img_large_side_in_pt = None if img_small_side_in_cm is not None: img_small_side_in_pt = img_small_side_in_cm * inch * 0.393701 # 1 cm = 0.393701 inches img_large_side_in_pt = img_small_side_in_pt * max(img_width, img_height) / min(img_width, img_height) assert(img_small_side_in_pt < page_width_in_pt and img_large_side_in_pt < page_height_in_pt), f"Cell size in cm is too large for the page, max in pt unit is {page_width_in_pt}x{page_height_in_pt}, got {img_small_side_in_pt}x{img_large_side_in_pt}. It looks you manually set the size of the cell in cm, but the image is too large for the page, try a smaller cell size." logger.info(f"Saving tensor of shape {tensor.shape} to {pdf_path}") # Convert tensor to image t1 = time.time() if debug: logger.debug(f"Tensor to PIL conversion took {time.time() - t1:.4f} seconds") t2 = time.time() # Check if image should be rotated scale_1, rotated = None, True if scale is not None: scale_1, rotated = scale if image.width > image.height and rotated: logger.info(f"Rotating image. Size in pixels: {image.width}, {image.height}") image = image.rotate(90, expand=True) logger.info(f"Rotated image. Size in pixels: {image.width}, {image.height}") img_width, img_height = image.width, image.height rotated = True else: rotated = False # check if it's better to maxout width or height if scale_1 is None: if img_small_side_in_pt is not None: scale_1 = img_small_side_in_pt / min(img_width, img_height) # this might go over the page else: # Calculate the scaling factor to fit the image within the page scale_width = page_width_in_pt / img_width scale_height = page_height_in_pt / img_height scale_1 = min(scale_width, scale_height) # Choose the smaller scale to preserve aspect ratio # Calculate the resized image dimensions new_width = img_width * scale_1 new_height = img_height * scale_1 # Calculate offsets to center the image on the page x_offset = (page_width_in_pt - new_width) // 2 y_offset = (page_height_in_pt - new_height) // 2 if debug: logger.debug(f"Image calculations took {time.time() - t2:.4f} seconds") # Save image to PDF t3 = time.time() # Use PNG for high-res mode instead of JPG image_path = "temp.png" if high_res else "temp.jpg" ram_folder_linux = "/dev/shm/" if os.path.exists(ram_folder_linux): image_path = os.path.join(ram_folder_linux, image_path) image.save(image_path) if debug: logger.debug(f"Temporary image saving took {time.time() - t3:.4f} seconds") # Create a PDF t4 = time.time() if a4: c = canvas.Canvas(pdf_path, pagesize = A4) else: c = canvas.Canvas(pdf_path, pagesize = letter) c.drawImage(image_path, x_offset+margin*inch, y_offset+margin*inch, width=new_width, height=new_height, preserveAspectRatio=True) c.save() if debug: logger.debug(f"PDF creation took {time.time() - t4:.4f} seconds") os.remove(image_path) if debug: logger.debug(f"Total PDF saving took {time.time() - start_time:.4f} seconds") return pdf_path, (scale_1, rotated) def merge_pdf_list(pdfs, output_path, debug=False): """ Merge a list of PDFs into a single PDF. """ start_time = time.time() merger = PyPDF2.PdfMerger() for pdf in pdfs: merger.append(pdf) merger.write(output_path) merger.close() if debug: logger.debug(f"PDF merging took {time.time() - start_time:.4f} seconds") return output_path def create_image_with_text(text: str = "1", size: int = 400, underline: bool = True, debug=False) -> torch.Tensor: """ Create an image with text using PIL. Returns a torch tensor. """ start_time = time.time() # Create a blank image (200x200 pixels, white background) if isinstance(size, int): size = (size, size) image = Image.new("RGB", size, "white") # Create a drawing object draw = ImageDraw.Draw(image) # Set the font (optional) try: font = ImageFont.truetype(font_path, size=int(size[1]/1.3)) # Ensure the font is available except IOError: font = ImageFont.load_default() # turn size to 100 # Use textbbox to measure the text dimensions visual_bbox = draw.textbbox((0, 0), text, font=font) # (-4, 101, 340, 260) text_width = visual_bbox[2] - visual_bbox[0] # Width of the text text_height = visual_bbox[3] - visual_bbox[1] # Height of the text center_point = (size[0] // 2, size[1] // 2) top_left_of_BB = (center_point[0] - text_width // 2, center_point[1] - text_height // 2) baseline = (top_left_of_BB[0] - visual_bbox[0], top_left_of_BB[1] - visual_bbox[1]) visual_bbox = draw.textbbox(baseline, text, font=font) # draw.rectangle(visual_bbox, outline="red", width=2) # print(f" text {text} Text width: {text_width}, Text height: {text_height}", f"Image width: {image.width}, Image height: {image.height}", f"Text position: {baseline}") # # Draw the text draw.text(baseline, text, fill="black", font=font) if underline: # # Add a line under the text x = baseline[0] y = visual_bbox[3] + 20 draw.line((x, y, x+text_width, y), fill="black", width=5) tensor = fromPIltoTensor(image).unsqueeze(0) if debug and len(text) <= 2: # Only log for short texts (cell numbers) when debugging logger.debug(f"Creating image with text '{text}' took {time.time() - start_time:.4f} seconds") return tensor def create_back_image(h, w, h_cells, w_cells, logo_image, logo_insta_image, unique_identifier, list_of_cell_idx=None, debug=False): """ Create back image tensor, of size hxw - Black pixels at the separation of cells to draw the lines The logo is in each cell, with the cell number underlined logo_image : tensor of size 1x3xhxw """ logger.info(f"Creating back image of size {h}x{w} for {h_cells}x{w_cells} cells") start_time = time.time() num_channels = 3 # do not consider the alpha channel back_image = torch.ones(1, num_channels, h, w) # cell size in pixels cell_h = h // h_cells cell_w = w // w_cells # hyperparameters controlling the thickness of the lines and the logo size line_thickness = min(cell_h, cell_w) // 100 logo_size = min(cell_h, cell_w) // 4 logo_offset = min(cell_h, cell_w) // 50 number_size = min(cell_h, cell_w) // 2 if debug: logger.debug(f"thickness of the lines: {line_thickness}") logger.debug(f"Initialization took {time.time() - start_time:.4f} seconds") # Create the grid lines grid_start_time = time.time() line_half_thickness = line_thickness // 2 for i in range(h_cells): for j in range(w_cells): h0 = i * cell_h # height start h1 = (i + 1) * cell_h # height end w0 = j * cell_w # width start w1 = (j + 1) * cell_w # width end if h0+line_half_thickness < h: back_image[:, :num_channels, h0:(h0+line_half_thickness), :] = 0 if w0+line_half_thickness < w: back_image[:, :num_channels, :, w0:(w0+line_half_thickness)] = 0 if h1 - line_half_thickness > 0: back_image[:, :num_channels, (h1-line_half_thickness):h1, :] = 0 if w1 - line_half_thickness > 0: back_image[:, :num_channels, :, (w1-line_half_thickness):w1] = 0 if debug: logger.debug(f"Creating grid lines took {time.time() - grid_start_time:.4f} seconds") # Resize logo for all cells logo_resize_time = time.time() _, _, h, w = logo_image.size() scale_logo = min(logo_size / h, logo_size / w) new_h, new_w = int(h * scale_logo), int(w * scale_logo) logo_image_resized = torch.nn.functional.interpolate(logo_image, size=(new_h, new_w), mode='bilinear') t_insta = time.time() _, _, h_insta, w_insta = logo_insta_image.size() scale_insta = min(logo_size / h_insta, logo_size / w_insta) / 5 new_h_insta, new_w_insta = int(h_insta * scale_insta), int(w_insta * scale_insta) logo_insta_image_resized = torch.nn.functional.interpolate(logo_insta_image, size=(new_h_insta, new_w_insta), mode='bilinear') if debug: logger.debug(f"Logo resizing took {time.time() - logo_resize_time:.4f} seconds ({time.time() - t_insta:.4f} for insta and {t_insta - logo_resize_time:.4f} for copaint logo)") # save logo_insta_image_resized save_image(logo_insta_image_resized, "logo_insta_image_resized.png", debug=debug) # Add content to cells cell_content_time = time.time() letscopaint = create_image_with_text("copaint.art", underline=False, size=(int(0.8*number_size), number_size//8), debug=debug) # add unique identifier unique_identifier_size_w = number_size unique_identifier_size_h = number_size // 4 image_with_unique_identifier = create_image_with_text(unique_identifier, underline=False, size=(unique_identifier_size_w, unique_identifier_size_h), debug=debug) for i in range(h_cells): for j in range(w_cells): h0 = i * cell_h # height start h1 = (i + 1) * cell_h # height end w0 = j * cell_w # width start w1 = (j + 1) * cell_w # width end # add logo at the bottom right of the cell logo_size_h, logo_size_w = logo_image_resized.shape[2:] back_image[:, :, h1-logo_size_h-logo_offset:h1-logo_offset, w1-logo_size_w-logo_offset:w1-logo_offset] = logo_image_resized[:, :num_channels, :, :] # add cell number at the center of the cell # invert cell number to match the order of the canvas. 1 is at the top right, and w_cells is at the top left if list_of_cell_idx is not None: logger.info(f"list_of_cell_idx: {list_of_cell_idx}") if list_of_cell_idx is not None: cell_number = list_of_cell_idx[i*w_cells+j] else: cell_number = i*w_cells+(w_cells-j) image_with_number = create_image_with_text(f"{cell_number}", size=number_size, debug=debug) start_h_big = h0 + (h1 - h0) // 2 - number_size // 2 start_w_big = w0 + (w1 - w0) // 2 - number_size // 2 back_image[:, :, start_h_big:start_h_big+number_size, start_w_big:start_w_big+number_size] = image_with_number[:, :num_channels, :, :] start_h = h0 + unique_identifier_size_h // 2 # Fix start_w = w0 + unique_identifier_size_h // 2 # Fix back_image[:, :, start_h:start_h+unique_identifier_size_h, start_w:start_w+unique_identifier_size_w] = image_with_unique_identifier[:, :num_channels, :, :] start_letscopaint_h = h1-logo_offset # Fix start_letscopaint_w = w0 + unique_identifier_size_h // 16 # Fix back_image[:, :, start_letscopaint_h-(number_size//8):start_letscopaint_h, start_letscopaint_w:start_letscopaint_w+(int(0.8*number_size))] = letscopaint[:, :num_channels, :, :] # add instagram logo at the bottom left of the cell _, _, h_insta, w_insta = logo_insta_image_resized.shape start_insta_h = h1-logo_offset # Fix start_insta_w = w0 + unique_identifier_size_h // 6 # Fix back_image[:, :, start_insta_h-(number_size//8):start_insta_h-(number_size//8)+h_insta, start_insta_w:start_insta_w+w_insta] = logo_insta_image_resized[:, :num_channels, :, :] if debug: logger.debug(f"Adding content to cells took {time.time() - cell_content_time:.4f} seconds") logger.debug(f"Created back image of shape {back_image.shape}") logger.debug(f"Total back image creation took {time.time() - start_time:.4f} seconds") return back_image def image_to_pdf_core(input_image, file_name, logo_image, outputfolder, h_cells, w_cells, unique_identifier="Mauricette", cell_size_in_cm=None, a4=False, high_res=False, list_of_cell_idx=None, scale=None, debug=False): overall_start_time = time.time() os.makedirs(outputfolder, exist_ok=True) scale_1, scale_2, scale_3, scale_4 = None, None, None, None if scale is not None: scale_1, scale_2, scale_3, scale_4 = scale # Load image t1 = time.time() if not isinstance(input_image, torch.Tensor): if input_image in pre_loaded_images: image = pre_loaded_images[input_image] logger.info(f"Loaded image from cache: {input_image}") else: image = load_image(input_image, debug=debug) pre_loaded_images[input_image] = image else: image = input_image if debug: logger.debug(f"Image loading took {time.time() - t1:.4f} seconds") _, c, h, w = image.shape logger.info(f"Image shape: {image.shape}") t1_2 = time.time() if logo_image in pre_loaded_images: logo_image = pre_loaded_images[logo_image] logger.info(f"Loaded logo copaint image from cache: {logo_image}") else: logo_image = load_image(logo_image, debug=debug) pre_loaded_images[logo_image] = logo_image if debug: logger.debug(f"Logo copaint Image loading took {time.time() - t1_2:.4f} seconds") t1_3 = time.time() logo_insta_path = "./copaint/static/logo_instagram.png" if logo_insta_path in pre_loaded_images: logo_insta_image = pre_loaded_images[logo_insta_path] logger.info(f"Loaded logo instagram image from cache: {logo_insta_path}") else: logo_insta_image = load_image(logo_insta_path, debug=debug) pre_loaded_images[logo_insta_path] = logo_insta_image if debug: logger.debug(f"Logo instagram Image loading took {time.time() - t1_3:.4f} seconds") # # Quick check that the greatest dimension corresponds to the greatest number of cells # if h > w and h_cells < w_cells: # print("Swapping h_cells and w_cells") # h_cells, w_cells = w_cells, h_cells # elif w > h and w_cells < h_cells: # print("Swapping h_cells and w_cells") # h_cells, w_cells = w_cells, h_cells # Create back image t2 = time.time() multiplier_w = max(1, 10000 // w) multiplier_h = max(1, 10000 // h) if scale_3 is None: scale_3 = max(multiplier_w, multiplier_h) logger.info(f"Creating back image with {h*scale_3} x {w*scale_3} pixels for {h_cells} x {w_cells} cells") back_image = create_back_image(h*scale_3, w*scale_3, h_cells, w_cells, logo_image, logo_insta_image, unique_identifier=unique_identifier, list_of_cell_idx=list_of_cell_idx, debug=debug) if debug: save_image(back_image, os.path.join(outputfolder, "back_image.png"), debug=debug) logger.debug(f"Back image creation and saving took {time.time() - t2:.4f} seconds") # Save to PDF t3 = time.time() os.makedirs(outputfolder, exist_ok=True) output_path_front = os.path.join(outputfolder, "output_front.pdf") output_path_back = os.path.join(outputfolder, "output_back.pdf") img_small_side_in_cm = None if cell_size_in_cm is not None: # Why Min? Cells are not neccearily square, depending on the aspect ratio of the image, and the number of H and W cells, so we assume cell_size_in_cm is the smallest side of the cell. logger.info(f"cell_size_in_cm: {cell_size_in_cm}") min_cells = min(h_cells, w_cells) img_small_side_in_cm = cell_size_in_cm * min_cells # smallest side in cm. # print image and back image shapes if debug: logger.debug(f"Image shape: {image.shape}") logger.debug(f"Back image shape: {back_image.shape}") # Only resize back image if not high-res if not high_res: back_image_h, back_image_w = back_image.shape[2:] scale_h = 4096 / back_image_h if scale_4 is None: scale_4 = scale_h back_image = torch.nn.functional.interpolate(back_image, scale_factor=scale_4, mode='bilinear') _, scale_1 = save_tensor_to_pdf(image, output_path_front, is_front=True, img_small_side_in_cm=img_small_side_in_cm, a4=a4, high_res=high_res, scale=scale_1, debug=debug) _, scale_2 = save_tensor_to_pdf(back_image, output_path_back, is_front=False, img_small_side_in_cm=img_small_side_in_cm, a4=a4, high_res=high_res, scale=scale_2, debug=debug) scale = (scale_1 , scale_2, scale_3, scale_4) if debug: logger.debug(f"PDF creation took {time.time() - t3:.4f} seconds") # concatenate pdfs t4 = time.time() logger.info("Concatenating PDFs") output_path = os.path.join(outputfolder, f"{file_name}_{h_cells}x{w_cells}_copaint.pdf") merge_pdf_list([output_path_front, output_path_back], output_path, debug=debug) # clean unnecessary files os.remove(output_path_front) os.remove(output_path_back) if debug: logger.debug(f"PDF concatenation and cleanup took {time.time() - t4:.4f} seconds") logger.info(f"Total processing time: {time.time() - overall_start_time:.4f} seconds") logger.info(f"Done! Output saved to {output_path}") return output_path, scale def image_to_pdf(input_image, logo_image, outputfolder, h_cells, w_cells, unique_identifier="Mauricette", cell_size_in_cm=None, a4=False, high_res=False, min_cell_size_in_cm=2, list_of_cell_idx=None, debug=False): """ Create a copaint PDF from an image and a logo. """ logger.info(f"h_cells: {h_cells}, w_cells: {w_cells}, a4: {a4}") image = load_image(input_image, debug=debug) _, c, h, w = image.shape file_name = os.path.basename(input_image) # Check if the image needs to be split to fit in the page. if cell_size_in_cm is not None: min_cell_size_in_cm = cell_size_in_cm # The US Letter format is US Letter size: 8.5 by 11 inches W, H = 8.5, 11 # the unit is inch if a4: logger.info("Using A4 format") W, H = 8.27, 11.69 # the unit is inch margin = 0.25 # hardcoded margin page_width_in_pt = (W - 2 * margin) * inch page_height_in_pt = (H - 2 * margin) * inch max_cell_per_page_h = h_cells max_cell_per_page_w = w_cells established_cell_size = False while not established_cell_size: img_small_side_in_pt = min(max_cell_per_page_h, max_cell_per_page_w) * min_cell_size_in_cm * inch * 0.393701 # 1 cm = 0.393701 inches minimum_is_width = min(w, h) == w img_large_side_in_pt = img_small_side_in_pt * max(w, h) / min(w, h) logger.info(f"img_small_side_in_pt: {img_small_side_in_pt}, img_large_side_in_pt: {img_large_side_in_pt}") logger.info(f"page_width_in_pt: {page_width_in_pt}, page_height_in_pt: {page_height_in_pt}") if img_large_side_in_pt < page_height_in_pt and img_small_side_in_pt < page_width_in_pt: established_cell_size = True else: max_cell_per_page_h = max_cell_per_page_h // 2 max_cell_per_page_w = max_cell_per_page_w // 2 logger.info(f"Decreasing max_cell_per_page to {max_cell_per_page_h}x{max_cell_per_page_w}") divide_factor_h = int(np.ceil(h_cells / max_cell_per_page_h)) divide_factor_w = int(np.ceil(w_cells / max_cell_per_page_w)) logger.info(f"divide_factor_h: {divide_factor_h}, divide_factor_w: {divide_factor_w}") copaint_pdfs = [] scale = None for i in range(divide_factor_h): for j in range(divide_factor_w): cell_h_start = i * max_cell_per_page_h cell_h_end = min((i + 1) * max_cell_per_page_h, h_cells) cell_w_start = j * max_cell_per_page_w cell_w_end = min((j + 1) * max_cell_per_page_w, w_cells) list_of_cell_idx = [cell_h_idx * w_cells + (w_cells-cell_w_idx) for cell_h_idx in range(cell_h_start, cell_h_end) for cell_w_idx in range(cell_w_start, cell_w_end)] logger.info(f"cell_h_start: {cell_h_start}, cell_h_end: {cell_h_end}, cell_w_start: {cell_w_start}, cell_w_end: {cell_w_end}") h_cells_new = cell_h_end - cell_h_start w_cells_new = cell_w_end - cell_w_start file_name_new = f"{file_name}_{i}x{j}" px_h_start = int(cell_h_start * h / h_cells) px_h_end = int(cell_h_end * h / h_cells) px_w_start = int(cell_w_start * w / w_cells) px_w_end = int(cell_w_end * w / w_cells) image_new = image[:, :, px_h_start:px_h_end, px_w_start:px_w_end] pdf_path, new_scale = image_to_pdf_core(image_new, file_name_new, logo_image, outputfolder, h_cells_new, w_cells_new, unique_identifier, cell_size_in_cm, a4, high_res, list_of_cell_idx=list_of_cell_idx, scale=scale, debug=debug) if scale is None: scale = new_scale copaint_pdfs.append(pdf_path) # Merge the copaint PDFs output_path = os.path.join(outputfolder, "copaint-design.pdf") merge_pdf_list(copaint_pdfs, output_path, debug=debug) # clean unnecessary files for pdf in copaint_pdfs: os.remove(pdf) logger.info(f"Done! Final output saved to {output_path}") return output_path if __name__ == "__main__": parser = argparse.ArgumentParser(description='CoPaint') parser.add_argument('--input_image', type=str, default='./data/bear.png', help='input image') parser.add_argument('--copaint_logo', type=str, default='./data/logo_copaint.png', help='copaint logo') parser.add_argument('--outputfolder', type=str, default='output/', help='output image') parser.add_argument('--h_cells', type=int, help='number of cells in height', default=9) parser.add_argument('--w_cells', type=int, help='number of cells in width', default=6) parser.add_argument('--debug', action='store_true', help='show timing information') # done adding arguments args = parser.parse_args() image_to_pdf(args.input_image, args.copaint_logo, args.outputfolder, args.h_cells, args.w_cells, cell_size_in_cm=None, debug=args.debug)