import streamlit as st import cloudinary import cloudinary.uploader import cloudinary.api import requests import io import zipfile import logging # Configuración inicial de la página y logging st.set_page_config( page_title="✂️ Cloudinary Smart Crop", page_icon="✂️", layout="wide" ) logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def init_cloudinary(): """ Inicializa Cloudinary usando las credenciales definidas en st.secrets. Realiza una limpieza inicial de recursos y registra el estado de inicialización. """ if 'cloudinary_initialized' not in st.session_state: try: cloudinary.config(url=st.secrets['CLOUDINARY_URL']) st.session_state.cloudinary_initialized = True cleanup_cloudinary() # Limpieza inicial de recursos logging.info("Cloudinary inicializado correctamente.") except Exception as e: st.error("Error: No se encontraron las credenciales de Cloudinary en secrets.toml") st.session_state.cloudinary_initialized = False logging.error(f"Error al inicializar Cloudinary: {e}") def check_file_size(file, max_size_mb=10): """ Verifica que el tamaño del archivo no exceda el límite especificado (en MB). """ file.seek(0, 2) # Mover al final del archivo file_size = file.tell() / (1024 * 1024) file.seek(0) # Regresar al inicio return file_size <= max_size_mb def cleanup_cloudinary(): """ Limpia todos los recursos almacenados en Cloudinary. """ if not st.session_state.get('cloudinary_initialized', False): return try: result = cloudinary.api.resources() if 'resources' in result and result['resources']: public_ids = [resource['public_id'] for resource in result['resources']] if public_ids: cloudinary.api.delete_resources(public_ids) logging.info("Recursos de Cloudinary limpiados.") except Exception as e: st.error(f"Error al limpiar recursos: {e}") logging.error(f"Error en cleanup_cloudinary: {e}") def process_image(image, width, height, gravity_option, dpr): """ Procesa la imagen usando Cloudinary y retorna la imagen procesada. Reinicia el puntero del stream y utiliza el atributo 'name' para determinar si se debe preservar la transparencia en PNG. """ if not st.session_state.get('cloudinary_initialized', False): st.error("Cloudinary no está inicializado correctamente") return None try: image.seek(0) image_name = getattr(image, 'name', '') if not check_file_size(image, 10): st.error(f"{image_name} excede el límite de 10MB") return None image_content = image.read() # Configurar la transformación para Cloudinary transformation = { "width": width, "height": height, "crop": "fill", "gravity": gravity_option, "quality": 100, "dpr": dpr, } if image_name.lower().endswith('.png'): transformation["flags"] = "preserve_transparency" else: transformation["flags"] = None response = cloudinary.uploader.upload( image_content, transformation=[transformation] ) processed_url = response.get('secure_url') processed_image = requests.get(processed_url).content # Limpia el recurso procesado en Cloudinary cloudinary.api.delete_resources([response.get('public_id')]) logging.info(f"Imagen {image_name} procesada correctamente.") return processed_image except Exception as e: st.error(f"Error procesando imagen: {e}") logging.error(f"Error en process_image para {image_name}: {e}") return None def main(): """ Función principal que ejecuta la aplicación Streamlit. """ init_cloudinary() st.title("✂️ Cloudinary Smart Crop") with st.expander("📌 Instrucciones de uso", expanded=True): st.markdown(""" **Recorta y redimensiona imágenes inteligentemente con Cloudinary** **Formatos soportados:** ✅ PNG, JPG, JPEG, WEBP **Características principales:** - 🔍 Detección automática de rostros (opción 'face'/'faces') - 🖼️ Mantenimiento de transparencia en PNG - 📐 Redimensionado preciso con diferentes modos de gravedad - 🚀 Procesamiento por lotes y descarga en ZIP **Pasos para usar:** 1. ⚙️ Configura dimensiones deseadas y parámetros de recorte 2. 🎯 Selecciona el tipo de gravedad y el DPR (entre 1 y 3) 3. 📤 Sube tus imágenes (máx. 10MB c/u) 4. 🚀 Procesa y descarga los resultados """) # Parámetros de configuración col1, col2, col3, col4 = st.columns(4) with col1: width = st.number_input("Ancho (px)", value=1000, min_value=100, max_value=3000) with col2: height = st.number_input("Alto (px)", value=460, min_value=100, max_value=3000) with col3: gravity_option = st.selectbox( "Gravedad", ["auto", "center", "face", "faces", "north", "south", "east", "west"], help="Configura cómo se enfocará el recorte en la imagen" ) with col4: dpr = st.number_input("DPR", value=3, min_value=1, max_value=3, step=1) # Carga de imágenes uploaded_files = st.file_uploader( "Sube tus imágenes (máx. 10MB por archivo)", type=['png', 'jpg', 'jpeg', 'webp'], accept_multiple_files=True ) if uploaded_files: st.header("Vista Previa Original") cols = st.columns(3) original_images = [] for idx, file in enumerate(uploaded_files): file_bytes = file.getvalue() original_images.append((file.name, file_bytes)) with cols[idx % 3]: st.image(file_bytes, caption=file.name, use_column_width=True) if st.button("✨ Procesar Imágenes"): processed_images = [] progress_bar = st.progress(0) total_images = len(original_images) for idx, (name, img_bytes) in enumerate(original_images): st.write(f"Procesando: {name}") img_io = io.BytesIO(img_bytes) with st.spinner(f"Procesando {name}..."): processed = process_image(img_io, width, height, gravity_option, dpr) if processed: processed_images.append((name, processed)) progress_bar.progress((idx + 1) / total_images) if processed_images: st.header("Resultados Finales") cols = st.columns(3) for idx, (name, img_bytes) in enumerate(processed_images): with cols[idx % 3]: st.image(img_bytes, caption=name, use_column_width=True) # Crear archivo ZIP con las imágenes procesadas zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, 'w') as zip_file: for name, img_bytes in processed_images: zip_file.writestr(f"procesada_{name}", img_bytes) st.download_button( label="📥 Descargar Todas", data=zip_buffer.getvalue(), file_name="imagenes_procesadas.zip", mime="application/zip" ) if __name__ == "__main__": main()