import gradio as gr import numpy as np import cv2 from PIL import Image import os import shutil # Para manejar directorios y archivos # Importa tu módulo resize_utils # Asegúrate de que 'app' sea el paquete correcto o ajusta la importación from resize_utils import resize_image_for_print # --- Tema y CSS personalizados --- theme = gr.themes.Default( primary_hue=gr.themes.Color( c50="#e0f2f7", c100="#b3e1ef", c200="#80cee7", c300="#4dbbde", c400="#26ace0", c500="#034077", # Tu azul principal c600="#023768", c700="#022e59", c800="#01254a", c900="#011b3b", c950="#000f22" ), secondary_hue=gr.themes.Color( c50="#f9fafb", c100="#f4f6f8", c200="#e9ecef", c300="#dee2e6", c400="#ced4da", c500="#adb5bd", c600="#889096", c700="#6c757d", c800="#495057", c900="#343a40", c950="#212529" ), neutral_hue=gr.themes.Color( c50="#f8f9fa", c100="#f1f3f5", c200="#e9ecef", c300="#dee2e6", c400="#ced4da", c500="#adb5bd", c600="#889096", c700="#6c757d", c800="#495057", c900="#343a40", c950="#212529" ) ).set( button_primary_background_fill="#034077", button_primary_background_fill_hover="#023768", button_primary_text_color="white", checkbox_background_color_selected="#034077", checkbox_border_color_selected="#034077", slider_color="#034077", ) css = """ body { background-color: #f0f2f5; } .gradio-container { max-width: 1200px; margin: 30px auto; border-radius: 15px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); background-color: white; overflow: hidden; } h1 { color: #034077; text-align: center; padding-top: 20px; margin-bottom: 20px; font-size: 2.5em; font-weight: 700; } .gr-text-input, .gr-slider { border-radius: 8px; } .gr-button.primary { border-radius: 10px; font-weight: bold; padding: 10px 20px; font-size: 1.1em; transition: all 0.3s ease; } .gr-button.primary:hover { transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); } .gr-checkbox-group label { font-weight: 500; } .gr-accordion { border-radius: 10px; border: 1px solid #e0e0e0; margin-bottom: 15px; background-color: #ffffff; } .gr-accordion.open > .gr-accordion-header { border-bottom: 1px solid #e0e0e0; } .gr-accordion-header { background-color: #f8f8f8; border-top-left-radius: 10px; border-top-right-radius: 10px; padding: 15px; font-weight: bold; color: #034077; font-size: 1.1em; } .gr-accordion-content .gr-checkbox label, .gr-accordion-content .gr-slider label, .gr-accordion-content .gr-textbox label { color: #333333; font-weight: 500; margin-bottom: 5px; } .gr-image { border: 1px solid #e0e0e0; border-radius: 10px; background-color: #fdfdfd; } .output_image_label { text-align: center; font-weight: bold; color: #034077; margin-top: 10px; font-size: 1.2em; } """ # Directorio temporal para guardar los resultados OUTPUT_DIR = "resized_images_output" os.makedirs(OUTPUT_DIR, exist_ok=True) def process_images_for_print( image_files: list, # Ahora espera una lista de archivos input_folder: str, # Ruta a una carpeta target_dpi: int, longest_side_cm: float ) -> tuple: # Devolverá un listado de archivos para descarga y un mensaje """ Procesa una o varias imágenes (o una carpeta) para redimensionar y guarda como TIFF. """ processed_file_paths = [] output_messages = [] # Limpiar el directorio de salida antes de cada procesamiento # Comentar esta línea si deseas que los archivos se acumulen entre ejecuciones. # Pero para una interfaz de usuario limpia, es mejor borrarlos. if os.path.exists(OUTPUT_DIR): shutil.rmtree(OUTPUT_DIR) os.makedirs(OUTPUT_DIR, exist_ok=True) images_to_process = [] # Manejar entrada de carpeta if input_folder and os.path.isdir(input_folder): output_messages.append(f"Procesando imágenes de la carpeta: {input_folder}") for filename in os.listdir(input_folder): file_path = os.path.join(input_folder, filename) if os.path.isfile(file_path) and filename.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.tif')): images_to_process.append(file_path) # Manejar entrada de múltiples archivos (si no se seleccionó carpeta o si también hay archivos individuales) # Si se seleccionaron archivos individuales, se añaden a la lista. # Gradio `gr.File(file_count="multiple")` devuelve una lista de objetos `tempfile.NamedTemporaryFile` if image_files: for img_obj in image_files: if img_obj is not None: images_to_process.append(img_obj.name) # Usar el atributo .name para obtener la ruta del archivo temporal if not images_to_process: raise gr.Error("Por favor, sube al menos una imagen o selecciona una carpeta con imágenes.") for i, img_path in enumerate(images_to_process): try: # Obtener el nombre original del archivo desde la ruta original_filename_ext = os.path.basename(img_path) original_filename_no_ext = os.path.splitext(original_filename_ext)[0] # Abrir la imagen con PIL original_pil_img = Image.open(img_path) # Convertir a BGR numpy array para resize_image_for_print img_np_rgb = np.array(original_pil_img) img_np_bgr = cv2.cvtColor(img_np_rgb, cv2.COLOR_RGB2BGR) # Aplicar redimensionamiento resized_np_bgr = resize_image_for_print( img_np_bgr, target_dpi=target_dpi, target_long_side_cm=longest_side_cm ) # Convertir de nuevo a PIL Image para guardar como TIFF con DPI resized_pil_rgb = Image.fromarray(cv2.cvtColor(resized_np_bgr, cv2.COLOR_BGR2RGB)) # Guardar la imagen en formato TIFF con la información de DPI output_filename = f"{original_filename_no_ext}_resized.tiff" output_filepath = os.path.join(OUTPUT_DIR, output_filename) # PIL guarda DPI en puntos por pulgada (dots per inch) # Asegúrate de que los metadatos de DPI se establezcan correctamente al guardar resized_pil_rgb.save(output_filepath, dpi=(target_dpi, target_dpi)) processed_file_paths.append(output_filepath) output_messages.append(f"✅ Procesado: {original_filename_ext} -> {output_filename}") except Exception as e: output_messages.append(f"❌ Error al procesar {os.path.basename(img_path)}: {e}") print(f"Error processing {img_path}: {e}") if not processed_file_paths: raise gr.Error("No se pudo procesar ninguna imagen. Revisa los archivos de entrada y los parámetros.") # Gradio requiere que se devuelva una lista de rutas de archivo para gr.File(file_count="multiple") # Y un mensaje para el Textbox return processed_file_paths, "\n".join(output_messages) with gr.Blocks(theme=theme, css=css, title="Redimensionador de Imágenes para Impresión") as demo: gr.Markdown( """ # 🖼️ **Redimensionador de Imágenes para Impresión** Sube tus imágenes o una carpeta, define los parámetros de impresión y obtén tus imágenes redimensionadas en formato TIFF. """ ) with gr.Row(): with gr.Column(scale=1): with gr.Group(elem_classes="input-card"): gr.Markdown("## ⬆️ **Cargar Imágenes**") # Opción para subir múltiples archivos image_files_input = gr.File( label="Seleccionar una o varias imágenes", file_count="multiple", # Permite múltiples archivos type="filepath", # *** CORREGIDO A 'filepath' *** file_types=[".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".tif"], height=200 ) gr.Markdown("---") # Opción para seleccionar una carpeta input_folder_path = gr.Textbox( label="O ingresar la ruta a una carpeta con imágenes", placeholder="Ej: /ruta/a/mis/fotos (dejar en blanco si subes archivos)", interactive=True ) gr.Markdown("*(Si seleccionas archivos y una carpeta, se procesarán AMBOS.)*") with gr.Group(elem_classes="options-card"): gr.Markdown("## ⚙️ **Parámetros de Impresión**") longest_side_cm = gr.Slider( minimum=1, maximum=200, step=1, value=50, label="Lado más largo (cm)", info="Define el tamaño físico del lado más largo de la imagen resultante en centímetros.", interactive=True ) target_dpi = gr.Slider( minimum=72, maximum=1200, step=10, value=300, label="DPI (Puntos por pulgada)", info="Resolución deseada para impresión (300 DPI es estándar para alta calidad).", interactive=True ) process_button = gr.Button("¡Redimensionar y Guardar como TIFF! 🚀", variant="primary", scale=0) with gr.Column(scale=1): with gr.Group(elem_classes="output-card"): gr.Markdown("## ✅ **Resultados Procesados**") # Para mostrar el estado del procesamiento status_output = gr.Textbox( label="Estado del Proceso", lines=10, interactive=False, max_lines=20, show_copy_button=True ) # Para descargar los archivos procesados output_files = gr.File( label="Imágenes TIFF Redimensionadas", file_count="multiple", # Permite descargar múltiples archivos type="filepath", # *** CORREGIDO A 'filepath' *** interactive=False # No permite al usuario subir archivos aquí ) gr.Markdown("---") gr.Markdown("✨*Tus imágenes redimensionadas aparecerán aquí. Haz clic para descargar.*") # Acción del botón process_button.click( fn=process_images_for_print, inputs=[ image_files_input, input_folder_path, target_dpi, longest_side_cm ], outputs=[output_files, status_output] ) # FastAPI + Uvicorn if __name__ == '__main__': demo.launch(debug=True, show_api=False, server_name="0.0.0.0", server_port=7860)