Spaces:
Running
Running
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() | |