Spaces:
Running
Running
Wkatir
commited on
Commit
·
065cb1a
1
Parent(s):
59ac13d
feat: adding converter
Browse files- pages/1_Cloudinary_AI.py +74 -44
- pages/2_Cloudinary_Crop.py +63 -27
- pages/3_Image_Compression_Tool.py +59 -37
- pages/4_Image_Converter.py +119 -0
pages/1_Cloudinary_AI.py
CHANGED
@@ -5,60 +5,85 @@ import cloudinary.api
|
|
5 |
import requests
|
6 |
import io
|
7 |
import zipfile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
|
10 |
def init_cloudinary():
|
11 |
-
"""
|
|
|
|
|
|
|
12 |
if 'cloudinary_initialized' not in st.session_state:
|
13 |
try:
|
14 |
cloudinary.config(url=st.secrets['CLOUDINARY_URL'])
|
15 |
st.session_state.cloudinary_initialized = True
|
16 |
-
cleanup_cloudinary()
|
|
|
17 |
except Exception as e:
|
18 |
st.error("Error: No se encontraron las credenciales de Cloudinary en secrets.toml")
|
19 |
st.session_state.cloudinary_initialized = False
|
|
|
20 |
|
21 |
|
22 |
def check_file_size(file, max_size_mb=10):
|
23 |
-
|
|
|
|
|
|
|
24 |
file_size = file.tell() / (1024 * 1024)
|
25 |
file.seek(0)
|
26 |
return file_size <= max_size_mb
|
27 |
|
28 |
|
29 |
def cleanup_cloudinary():
|
30 |
-
"""
|
|
|
|
|
31 |
if not st.session_state.get('cloudinary_initialized', False):
|
32 |
return
|
33 |
-
|
34 |
try:
|
35 |
result = cloudinary.api.resources()
|
36 |
if 'resources' in result and result['resources']:
|
37 |
public_ids = [resource['public_id'] for resource in result['resources']]
|
38 |
if public_ids:
|
39 |
cloudinary.api.delete_resources(public_ids)
|
|
|
40 |
except Exception as e:
|
41 |
st.error(f"Error al limpiar recursos: {e}")
|
|
|
42 |
|
43 |
|
44 |
def process_image(image, width, height, dpr):
|
45 |
"""
|
46 |
-
Procesa la imagen
|
47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
"""
|
49 |
if not st.session_state.get('cloudinary_initialized', False):
|
50 |
st.error("Cloudinary no está inicializado correctamente")
|
51 |
return None, None
|
52 |
|
53 |
try:
|
54 |
-
# Reiniciar el puntero para garantizar la lectura completa
|
55 |
image.seek(0)
|
56 |
if not check_file_size(image, 10):
|
57 |
st.error("La imagen excede el límite de 10MB")
|
58 |
return None, None
|
59 |
|
60 |
image_content = image.read()
|
61 |
-
|
62 |
response = cloudinary.uploader.upload(
|
63 |
image_content,
|
64 |
transformation=[{
|
@@ -71,52 +96,57 @@ def process_image(image, width, height, dpr):
|
|
71 |
"flags": "preserve_transparency"
|
72 |
}]
|
73 |
)
|
74 |
-
|
75 |
processed_url = response['secure_url']
|
76 |
processed_image = requests.get(processed_url).content
|
77 |
-
file_format = response.get('format', 'jpg')
|
|
|
78 |
return processed_image, file_format
|
79 |
except Exception as e:
|
80 |
st.error(f"Error procesando imagen: {e}")
|
|
|
81 |
return None, None
|
82 |
|
83 |
|
84 |
def main():
|
|
|
|
|
|
|
85 |
init_cloudinary()
|
86 |
|
87 |
st.title("🤖 Cloudinary AI Background Generator")
|
88 |
|
89 |
with st.expander("📌 ¿Cómo usar esta herramienta?", expanded=True):
|
90 |
-
st.markdown(
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
|
|
|
120 |
col1, col2, col3 = st.columns(3)
|
121 |
with col1:
|
122 |
width = st.number_input("Ancho (px)", value=1000, min_value=100, max_value=3000)
|
@@ -125,6 +155,7 @@ def main():
|
|
125 |
with col3:
|
126 |
dpr = st.number_input("DPR", value=3, min_value=1, max_value=3, step=1)
|
127 |
|
|
|
128 |
uploaded_files = st.file_uploader(
|
129 |
"Sube tus imágenes (máx. 10MB por archivo)",
|
130 |
type=['png', 'jpg', 'jpeg', 'webp'],
|
@@ -134,13 +165,12 @@ def main():
|
|
134 |
if uploaded_files:
|
135 |
st.header("Imágenes Originales")
|
136 |
cols = st.columns(3)
|
137 |
-
# Guardar el contenido original para evitar que se consuma el stream
|
138 |
original_images = []
|
139 |
for idx, file in enumerate(uploaded_files):
|
140 |
file_bytes = file.getvalue()
|
141 |
original_images.append((file.name, file_bytes))
|
142 |
with cols[idx % 3]:
|
143 |
-
st.image(file_bytes)
|
144 |
|
145 |
if st.button("Procesar Imágenes"):
|
146 |
if not st.session_state.get('cloudinary_initialized', False):
|
@@ -151,7 +181,6 @@ def main():
|
|
151 |
progress_bar = st.progress(0)
|
152 |
|
153 |
for idx, (name, img_bytes) in enumerate(original_images):
|
154 |
-
# Crear un nuevo objeto BytesIO para cada imagen
|
155 |
img_io = io.BytesIO(img_bytes)
|
156 |
with st.spinner(f'Procesando imagen {idx + 1}/{len(original_images)}...'):
|
157 |
processed, file_format = process_image(img_io, width, height, dpr)
|
@@ -164,8 +193,9 @@ def main():
|
|
164 |
cols = st.columns(3)
|
165 |
for idx, (img_bytes, file_format) in enumerate(processed_images):
|
166 |
with cols[idx % 3]:
|
167 |
-
st.image(img_bytes)
|
168 |
|
|
|
169 |
zip_buffer = io.BytesIO()
|
170 |
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
|
171 |
for idx, (img_bytes, file_format) in enumerate(processed_images):
|
|
|
5 |
import requests
|
6 |
import io
|
7 |
import zipfile
|
8 |
+
import logging
|
9 |
+
|
10 |
+
# Configuración de la página y del logging
|
11 |
+
st.set_page_config(
|
12 |
+
page_title="Cloudinary AI Background Generator",
|
13 |
+
page_icon="🤖",
|
14 |
+
layout="wide"
|
15 |
+
)
|
16 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
17 |
|
18 |
|
19 |
def init_cloudinary():
|
20 |
+
"""
|
21 |
+
Inicializa Cloudinary utilizando las credenciales definidas en st.secrets.
|
22 |
+
Registra en session_state si se inicializó correctamente y realiza una limpieza inicial.
|
23 |
+
"""
|
24 |
if 'cloudinary_initialized' not in st.session_state:
|
25 |
try:
|
26 |
cloudinary.config(url=st.secrets['CLOUDINARY_URL'])
|
27 |
st.session_state.cloudinary_initialized = True
|
28 |
+
cleanup_cloudinary() # Limpieza de recursos al iniciar
|
29 |
+
logging.info("Cloudinary inicializado correctamente.")
|
30 |
except Exception as e:
|
31 |
st.error("Error: No se encontraron las credenciales de Cloudinary en secrets.toml")
|
32 |
st.session_state.cloudinary_initialized = False
|
33 |
+
logging.error(f"Error al inicializar Cloudinary: {e}")
|
34 |
|
35 |
|
36 |
def check_file_size(file, max_size_mb=10):
|
37 |
+
"""
|
38 |
+
Verifica que el tamaño del archivo no exceda el límite especificado (en MB).
|
39 |
+
"""
|
40 |
+
file.seek(0, io.SEEK_END)
|
41 |
file_size = file.tell() / (1024 * 1024)
|
42 |
file.seek(0)
|
43 |
return file_size <= max_size_mb
|
44 |
|
45 |
|
46 |
def cleanup_cloudinary():
|
47 |
+
"""
|
48 |
+
Limpia todos los recursos almacenados en Cloudinary.
|
49 |
+
"""
|
50 |
if not st.session_state.get('cloudinary_initialized', False):
|
51 |
return
|
|
|
52 |
try:
|
53 |
result = cloudinary.api.resources()
|
54 |
if 'resources' in result and result['resources']:
|
55 |
public_ids = [resource['public_id'] for resource in result['resources']]
|
56 |
if public_ids:
|
57 |
cloudinary.api.delete_resources(public_ids)
|
58 |
+
logging.info("Recursos de Cloudinary limpiados correctamente.")
|
59 |
except Exception as e:
|
60 |
st.error(f"Error al limpiar recursos: {e}")
|
61 |
+
logging.error(f"Error al limpiar recursos: {e}")
|
62 |
|
63 |
|
64 |
def process_image(image, width, height, dpr):
|
65 |
"""
|
66 |
+
Procesa la imagen utilizando Cloudinary aplicando una transformación definida.
|
67 |
+
|
68 |
+
Parámetros:
|
69 |
+
- image: Objeto tipo BytesIO con la imagen a procesar.
|
70 |
+
- width, height: Dimensiones deseadas.
|
71 |
+
- dpr: Escalado de resolución.
|
72 |
+
|
73 |
+
Retorna:
|
74 |
+
- Tuple con la imagen procesada en bytes y la extensión del archivo.
|
75 |
"""
|
76 |
if not st.session_state.get('cloudinary_initialized', False):
|
77 |
st.error("Cloudinary no está inicializado correctamente")
|
78 |
return None, None
|
79 |
|
80 |
try:
|
|
|
81 |
image.seek(0)
|
82 |
if not check_file_size(image, 10):
|
83 |
st.error("La imagen excede el límite de 10MB")
|
84 |
return None, None
|
85 |
|
86 |
image_content = image.read()
|
|
|
87 |
response = cloudinary.uploader.upload(
|
88 |
image_content,
|
89 |
transformation=[{
|
|
|
96 |
"flags": "preserve_transparency"
|
97 |
}]
|
98 |
)
|
|
|
99 |
processed_url = response['secure_url']
|
100 |
processed_image = requests.get(processed_url).content
|
101 |
+
file_format = response.get('format', 'jpg')
|
102 |
+
logging.info("Imagen procesada correctamente.")
|
103 |
return processed_image, file_format
|
104 |
except Exception as e:
|
105 |
st.error(f"Error procesando imagen: {e}")
|
106 |
+
logging.error(f"Error en process_image: {e}")
|
107 |
return None, None
|
108 |
|
109 |
|
110 |
def main():
|
111 |
+
"""
|
112 |
+
Función principal que configura la interfaz de la aplicación y coordina el flujo de procesamiento.
|
113 |
+
"""
|
114 |
init_cloudinary()
|
115 |
|
116 |
st.title("🤖 Cloudinary AI Background Generator")
|
117 |
|
118 |
with st.expander("📌 ¿Cómo usar esta herramienta?", expanded=True):
|
119 |
+
st.markdown(
|
120 |
+
"""
|
121 |
+
**Transforma tus imágenes automáticamente con IA:**
|
122 |
+
- 🔄 Redimensiona manteniendo la relación de aspecto
|
123 |
+
- 🎨 Genera fondos coherentes usando IA
|
124 |
+
- 📥 Descarga múltiples imágenes en un ZIP
|
125 |
+
|
126 |
+
**Formatos soportados:**
|
127 |
+
- PNG, JPG, JPEG, WEBP
|
128 |
+
|
129 |
+
**Pasos para usar:**
|
130 |
+
1. Define las dimensiones deseadas (ancho y alto)
|
131 |
+
2. Sube tus imágenes (hasta 10MB cada una)
|
132 |
+
3. Haz clic en "Procesar Imágenes"
|
133 |
+
4. Descarga los resultados finales
|
134 |
+
|
135 |
+
**Características clave:**
|
136 |
+
- Preserva transparencia en PNGs
|
137 |
+
- Soporte para formatos modernos (WEBP)
|
138 |
+
- Calidad ultra HD (DPR configurable entre 1 y 3)
|
139 |
+
- Procesamiento por lotes
|
140 |
+
- Fondo generado por IA adaptado al contexto
|
141 |
+
|
142 |
+
**Notas:**
|
143 |
+
- Las imágenes subidas se borran automáticamente después del procesamiento
|
144 |
+
- Para mejores resultados, usa imágenes con sujetos bien definidos
|
145 |
+
- El tiempo de procesamiento varía según el tamaño y cantidad de imágenes
|
146 |
+
"""
|
147 |
+
)
|
148 |
|
149 |
+
# Sección de parámetros de configuración
|
150 |
col1, col2, col3 = st.columns(3)
|
151 |
with col1:
|
152 |
width = st.number_input("Ancho (px)", value=1000, min_value=100, max_value=3000)
|
|
|
155 |
with col3:
|
156 |
dpr = st.number_input("DPR", value=3, min_value=1, max_value=3, step=1)
|
157 |
|
158 |
+
# Carga de archivos de imagen
|
159 |
uploaded_files = st.file_uploader(
|
160 |
"Sube tus imágenes (máx. 10MB por archivo)",
|
161 |
type=['png', 'jpg', 'jpeg', 'webp'],
|
|
|
165 |
if uploaded_files:
|
166 |
st.header("Imágenes Originales")
|
167 |
cols = st.columns(3)
|
|
|
168 |
original_images = []
|
169 |
for idx, file in enumerate(uploaded_files):
|
170 |
file_bytes = file.getvalue()
|
171 |
original_images.append((file.name, file_bytes))
|
172 |
with cols[idx % 3]:
|
173 |
+
st.image(file_bytes, caption=file.name, use_column_width=True)
|
174 |
|
175 |
if st.button("Procesar Imágenes"):
|
176 |
if not st.session_state.get('cloudinary_initialized', False):
|
|
|
181 |
progress_bar = st.progress(0)
|
182 |
|
183 |
for idx, (name, img_bytes) in enumerate(original_images):
|
|
|
184 |
img_io = io.BytesIO(img_bytes)
|
185 |
with st.spinner(f'Procesando imagen {idx + 1}/{len(original_images)}...'):
|
186 |
processed, file_format = process_image(img_io, width, height, dpr)
|
|
|
193 |
cols = st.columns(3)
|
194 |
for idx, (img_bytes, file_format) in enumerate(processed_images):
|
195 |
with cols[idx % 3]:
|
196 |
+
st.image(img_bytes, caption=f"Formato: {file_format}", use_column_width=True)
|
197 |
|
198 |
+
# Empaquetado en ZIP
|
199 |
zip_buffer = io.BytesIO()
|
200 |
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
|
201 |
for idx, (img_bytes, file_format) in enumerate(processed_images):
|
pages/2_Cloudinary_Crop.py
CHANGED
@@ -5,47 +5,67 @@ import cloudinary.api
|
|
5 |
import requests
|
6 |
import io
|
7 |
import zipfile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
|
10 |
def init_cloudinary():
|
11 |
-
"""
|
|
|
|
|
|
|
12 |
if 'cloudinary_initialized' not in st.session_state:
|
13 |
try:
|
14 |
cloudinary.config(url=st.secrets['CLOUDINARY_URL'])
|
15 |
st.session_state.cloudinary_initialized = True
|
16 |
-
cleanup_cloudinary()
|
|
|
17 |
except Exception as e:
|
18 |
st.error("Error: No se encontraron las credenciales de Cloudinary en secrets.toml")
|
19 |
st.session_state.cloudinary_initialized = False
|
|
|
20 |
|
21 |
|
22 |
def check_file_size(file, max_size_mb=10):
|
23 |
-
|
|
|
|
|
|
|
24 |
file_size = file.tell() / (1024 * 1024)
|
25 |
-
file.seek(0)
|
26 |
return file_size <= max_size_mb
|
27 |
|
28 |
|
29 |
def cleanup_cloudinary():
|
30 |
-
"""
|
|
|
|
|
31 |
if not st.session_state.get('cloudinary_initialized', False):
|
32 |
return
|
33 |
-
|
34 |
try:
|
35 |
result = cloudinary.api.resources()
|
36 |
if 'resources' in result and result['resources']:
|
37 |
public_ids = [resource['public_id'] for resource in result['resources']]
|
38 |
if public_ids:
|
39 |
cloudinary.api.delete_resources(public_ids)
|
|
|
40 |
except Exception as e:
|
41 |
st.error(f"Error al limpiar recursos: {e}")
|
|
|
42 |
|
43 |
|
44 |
def process_image(image, width, height, gravity_option, dpr):
|
45 |
"""
|
46 |
Procesa la imagen usando Cloudinary y retorna la imagen procesada.
|
47 |
-
|
48 |
-
|
49 |
"""
|
50 |
if not st.session_state.get('cloudinary_initialized', False):
|
51 |
st.error("Cloudinary no está inicializado correctamente")
|
@@ -60,31 +80,43 @@ def process_image(image, width, height, gravity_option, dpr):
|
|
60 |
|
61 |
image_content = image.read()
|
62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
response = cloudinary.uploader.upload(
|
64 |
image_content,
|
65 |
-
transformation=[
|
66 |
-
"width": width,
|
67 |
-
"height": height,
|
68 |
-
"crop": "fill",
|
69 |
-
"gravity": gravity_option,
|
70 |
-
"quality": 100,
|
71 |
-
"dpr": dpr,
|
72 |
-
"flags": "preserve_transparency" if image_name.lower().endswith('.png') else None
|
73 |
-
}]
|
74 |
)
|
75 |
|
76 |
-
processed_url = response
|
77 |
processed_image = requests.get(processed_url).content
|
78 |
|
79 |
# Limpia el recurso procesado en Cloudinary
|
80 |
-
cloudinary.api.delete_resources([response
|
|
|
81 |
return processed_image
|
|
|
82 |
except Exception as e:
|
83 |
st.error(f"Error procesando imagen: {e}")
|
|
|
84 |
return None
|
85 |
|
86 |
|
87 |
def main():
|
|
|
|
|
|
|
88 |
init_cloudinary()
|
89 |
|
90 |
st.title("✂️ Cloudinary Smart Crop")
|
@@ -109,6 +141,7 @@ def main():
|
|
109 |
4. 🚀 Procesa y descarga los resultados
|
110 |
""")
|
111 |
|
|
|
112 |
col1, col2, col3, col4 = st.columns(4)
|
113 |
with col1:
|
114 |
width = st.number_input("Ancho (px)", value=1000, min_value=100, max_value=3000)
|
@@ -123,6 +156,7 @@ def main():
|
|
123 |
with col4:
|
124 |
dpr = st.number_input("DPR", value=3, min_value=1, max_value=3, step=1)
|
125 |
|
|
|
126 |
uploaded_files = st.file_uploader(
|
127 |
"Sube tus imágenes (máx. 10MB por archivo)",
|
128 |
type=['png', 'jpg', 'jpeg', 'webp'],
|
@@ -137,27 +171,30 @@ def main():
|
|
137 |
file_bytes = file.getvalue()
|
138 |
original_images.append((file.name, file_bytes))
|
139 |
with cols[idx % 3]:
|
140 |
-
st.image(file_bytes, caption=file.name)
|
141 |
|
142 |
if st.button("✨ Procesar Imágenes"):
|
143 |
processed_images = []
|
144 |
progress_bar = st.progress(0)
|
|
|
145 |
|
146 |
for idx, (name, img_bytes) in enumerate(original_images):
|
147 |
st.write(f"Procesando: {name}")
|
148 |
img_io = io.BytesIO(img_bytes)
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
|
|
153 |
|
154 |
if processed_images:
|
155 |
st.header("Resultados Finales")
|
156 |
cols = st.columns(3)
|
157 |
for idx, (name, img_bytes) in enumerate(processed_images):
|
158 |
with cols[idx % 3]:
|
159 |
-
st.image(img_bytes, caption=name)
|
160 |
|
|
|
161 |
zip_buffer = io.BytesIO()
|
162 |
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
|
163 |
for name, img_bytes in processed_images:
|
@@ -167,8 +204,7 @@ def main():
|
|
167 |
label="📥 Descargar Todas",
|
168 |
data=zip_buffer.getvalue(),
|
169 |
file_name="imagenes_procesadas.zip",
|
170 |
-
mime="application/zip"
|
171 |
-
type="primary"
|
172 |
)
|
173 |
|
174 |
|
|
|
5 |
import requests
|
6 |
import io
|
7 |
import zipfile
|
8 |
+
import logging
|
9 |
+
|
10 |
+
# Configuración inicial de la página y logging
|
11 |
+
st.set_page_config(
|
12 |
+
page_title="✂️ Cloudinary Smart Crop",
|
13 |
+
page_icon="✂️",
|
14 |
+
layout="wide"
|
15 |
+
)
|
16 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
17 |
|
18 |
|
19 |
def init_cloudinary():
|
20 |
+
"""
|
21 |
+
Inicializa Cloudinary usando las credenciales definidas en st.secrets.
|
22 |
+
Realiza una limpieza inicial de recursos y registra el estado de inicialización.
|
23 |
+
"""
|
24 |
if 'cloudinary_initialized' not in st.session_state:
|
25 |
try:
|
26 |
cloudinary.config(url=st.secrets['CLOUDINARY_URL'])
|
27 |
st.session_state.cloudinary_initialized = True
|
28 |
+
cleanup_cloudinary() # Limpieza inicial de recursos
|
29 |
+
logging.info("Cloudinary inicializado correctamente.")
|
30 |
except Exception as e:
|
31 |
st.error("Error: No se encontraron las credenciales de Cloudinary en secrets.toml")
|
32 |
st.session_state.cloudinary_initialized = False
|
33 |
+
logging.error(f"Error al inicializar Cloudinary: {e}")
|
34 |
|
35 |
|
36 |
def check_file_size(file, max_size_mb=10):
|
37 |
+
"""
|
38 |
+
Verifica que el tamaño del archivo no exceda el límite especificado (en MB).
|
39 |
+
"""
|
40 |
+
file.seek(0, 2) # Mover al final del archivo
|
41 |
file_size = file.tell() / (1024 * 1024)
|
42 |
+
file.seek(0) # Regresar al inicio
|
43 |
return file_size <= max_size_mb
|
44 |
|
45 |
|
46 |
def cleanup_cloudinary():
|
47 |
+
"""
|
48 |
+
Limpia todos los recursos almacenados en Cloudinary.
|
49 |
+
"""
|
50 |
if not st.session_state.get('cloudinary_initialized', False):
|
51 |
return
|
|
|
52 |
try:
|
53 |
result = cloudinary.api.resources()
|
54 |
if 'resources' in result and result['resources']:
|
55 |
public_ids = [resource['public_id'] for resource in result['resources']]
|
56 |
if public_ids:
|
57 |
cloudinary.api.delete_resources(public_ids)
|
58 |
+
logging.info("Recursos de Cloudinary limpiados.")
|
59 |
except Exception as e:
|
60 |
st.error(f"Error al limpiar recursos: {e}")
|
61 |
+
logging.error(f"Error en cleanup_cloudinary: {e}")
|
62 |
|
63 |
|
64 |
def process_image(image, width, height, gravity_option, dpr):
|
65 |
"""
|
66 |
Procesa la imagen usando Cloudinary y retorna la imagen procesada.
|
67 |
+
Reinicia el puntero del stream y utiliza el atributo 'name' para determinar
|
68 |
+
si se debe preservar la transparencia en PNG.
|
69 |
"""
|
70 |
if not st.session_state.get('cloudinary_initialized', False):
|
71 |
st.error("Cloudinary no está inicializado correctamente")
|
|
|
80 |
|
81 |
image_content = image.read()
|
82 |
|
83 |
+
# Configurar la transformación para Cloudinary
|
84 |
+
transformation = {
|
85 |
+
"width": width,
|
86 |
+
"height": height,
|
87 |
+
"crop": "fill",
|
88 |
+
"gravity": gravity_option,
|
89 |
+
"quality": 100,
|
90 |
+
"dpr": dpr,
|
91 |
+
}
|
92 |
+
if image_name.lower().endswith('.png'):
|
93 |
+
transformation["flags"] = "preserve_transparency"
|
94 |
+
else:
|
95 |
+
transformation["flags"] = None
|
96 |
+
|
97 |
response = cloudinary.uploader.upload(
|
98 |
image_content,
|
99 |
+
transformation=[transformation]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
)
|
101 |
|
102 |
+
processed_url = response.get('secure_url')
|
103 |
processed_image = requests.get(processed_url).content
|
104 |
|
105 |
# Limpia el recurso procesado en Cloudinary
|
106 |
+
cloudinary.api.delete_resources([response.get('public_id')])
|
107 |
+
logging.info(f"Imagen {image_name} procesada correctamente.")
|
108 |
return processed_image
|
109 |
+
|
110 |
except Exception as e:
|
111 |
st.error(f"Error procesando imagen: {e}")
|
112 |
+
logging.error(f"Error en process_image para {image_name}: {e}")
|
113 |
return None
|
114 |
|
115 |
|
116 |
def main():
|
117 |
+
"""
|
118 |
+
Función principal que ejecuta la aplicación Streamlit.
|
119 |
+
"""
|
120 |
init_cloudinary()
|
121 |
|
122 |
st.title("✂️ Cloudinary Smart Crop")
|
|
|
141 |
4. 🚀 Procesa y descarga los resultados
|
142 |
""")
|
143 |
|
144 |
+
# Parámetros de configuración
|
145 |
col1, col2, col3, col4 = st.columns(4)
|
146 |
with col1:
|
147 |
width = st.number_input("Ancho (px)", value=1000, min_value=100, max_value=3000)
|
|
|
156 |
with col4:
|
157 |
dpr = st.number_input("DPR", value=3, min_value=1, max_value=3, step=1)
|
158 |
|
159 |
+
# Carga de imágenes
|
160 |
uploaded_files = st.file_uploader(
|
161 |
"Sube tus imágenes (máx. 10MB por archivo)",
|
162 |
type=['png', 'jpg', 'jpeg', 'webp'],
|
|
|
171 |
file_bytes = file.getvalue()
|
172 |
original_images.append((file.name, file_bytes))
|
173 |
with cols[idx % 3]:
|
174 |
+
st.image(file_bytes, caption=file.name, use_column_width=True)
|
175 |
|
176 |
if st.button("✨ Procesar Imágenes"):
|
177 |
processed_images = []
|
178 |
progress_bar = st.progress(0)
|
179 |
+
total_images = len(original_images)
|
180 |
|
181 |
for idx, (name, img_bytes) in enumerate(original_images):
|
182 |
st.write(f"Procesando: {name}")
|
183 |
img_io = io.BytesIO(img_bytes)
|
184 |
+
with st.spinner(f"Procesando {name}..."):
|
185 |
+
processed = process_image(img_io, width, height, gravity_option, dpr)
|
186 |
+
if processed:
|
187 |
+
processed_images.append((name, processed))
|
188 |
+
progress_bar.progress((idx + 1) / total_images)
|
189 |
|
190 |
if processed_images:
|
191 |
st.header("Resultados Finales")
|
192 |
cols = st.columns(3)
|
193 |
for idx, (name, img_bytes) in enumerate(processed_images):
|
194 |
with cols[idx % 3]:
|
195 |
+
st.image(img_bytes, caption=name, use_column_width=True)
|
196 |
|
197 |
+
# Crear archivo ZIP con las imágenes procesadas
|
198 |
zip_buffer = io.BytesIO()
|
199 |
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
|
200 |
for name, img_bytes in processed_images:
|
|
|
204 |
label="📥 Descargar Todas",
|
205 |
data=zip_buffer.getvalue(),
|
206 |
file_name="imagenes_procesadas.zip",
|
207 |
+
mime="application/zip"
|
|
|
208 |
)
|
209 |
|
210 |
|
pages/3_Image_Compression_Tool.py
CHANGED
@@ -4,57 +4,74 @@ import io
|
|
4 |
import zipfile
|
5 |
from datetime import datetime
|
6 |
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
|
9 |
def optimize_image(image_file):
|
10 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
try:
|
12 |
img = Image.open(image_file)
|
13 |
output_buffer = io.BytesIO()
|
14 |
|
15 |
-
# Configuración
|
16 |
if img.format == 'PNG':
|
17 |
-
# Para PNG, mantener transparencia si existe
|
18 |
if img.mode in ('RGBA', 'LA'):
|
19 |
img_to_save = img
|
20 |
-
save_params = {
|
21 |
-
'format': 'PNG',
|
22 |
-
'optimize': True
|
23 |
-
}
|
24 |
else:
|
25 |
img_to_save = img.convert('RGB')
|
26 |
-
save_params = {
|
27 |
-
'format': 'JPEG',
|
28 |
-
'quality': 85,
|
29 |
-
'optimize': True
|
30 |
-
}
|
31 |
else:
|
32 |
-
# Para otros formatos, convertir a JPEG
|
33 |
img_to_save = img.convert('RGB')
|
34 |
-
save_params = {
|
35 |
-
'format': 'JPEG',
|
36 |
-
'quality': 85,
|
37 |
-
'optimize': True
|
38 |
-
}
|
39 |
|
40 |
-
# Guardar imagen optimizada
|
41 |
img_to_save.save(output_buffer, **save_params)
|
42 |
output_buffer.seek(0)
|
43 |
|
44 |
-
# Validar reducción de tamaño
|
45 |
optimized_size = len(output_buffer.getvalue())
|
|
|
46 |
if optimized_size >= image_file.size:
|
|
|
47 |
return None, None
|
48 |
|
|
|
49 |
return output_buffer.getvalue(), save_params['format']
|
50 |
|
51 |
except Exception as e:
|
52 |
-
st.error(f"Error optimizando imagen: {
|
|
|
53 |
return None, None
|
54 |
|
55 |
|
56 |
def process_filename(original_name, optimized_format):
|
57 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
path = Path(original_name)
|
59 |
new_suffix = f".{optimized_format.lower()}" if optimized_format else '.jpg'
|
60 |
return f"{path.stem}_optimizado{new_suffix}"
|
@@ -64,16 +81,18 @@ def main():
|
|
64 |
st.title("📁 Image Compression Tool")
|
65 |
|
66 |
with st.expander("📌 Instrucciones de uso", expanded=True):
|
67 |
-
st.markdown(
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
|
|
|
|
77 |
|
78 |
uploaded_files = st.file_uploader(
|
79 |
"Sube tus imágenes (máx. 50MB por archivo)",
|
@@ -88,11 +107,13 @@ def main():
|
|
88 |
total_files = len(uploaded_files)
|
89 |
|
90 |
st.info("Optimizando imágenes...")
|
|
|
91 |
|
92 |
for idx, file in enumerate(uploaded_files):
|
93 |
try:
|
94 |
if file.size > 50 * 1024 * 1024:
|
95 |
-
st.error(f"
|
|
|
96 |
continue
|
97 |
|
98 |
original_size = file.size
|
@@ -107,18 +128,20 @@ def main():
|
|
107 |
processed_images.append((new_name, optimized_data, original_size, new_size))
|
108 |
|
109 |
st.write(f"✅ {file.name} optimizado ({reduction / 1024:.1f} KB ahorrados)")
|
|
|
110 |
else:
|
111 |
st.write(f"⚠️ {file.name} no se optimizó porque no se logró reducir el tamaño.")
|
|
|
112 |
|
113 |
progress_bar.progress((idx + 1) / total_files)
|
114 |
|
115 |
except Exception as e:
|
116 |
-
st.error(f"Error procesando {file.name}: {
|
|
|
117 |
|
118 |
st.success(f"¡Optimización completada! (Ahorro total: {total_reduction / 1024:.1f} KB)")
|
119 |
|
120 |
if processed_images:
|
121 |
-
# Mostrar resultados
|
122 |
st.subheader("Resultados de Optimización")
|
123 |
cols = st.columns(3)
|
124 |
with cols[0]:
|
@@ -129,13 +152,12 @@ def main():
|
|
129 |
avg_reduction = (total_reduction / len(processed_images)) / 1024
|
130 |
st.metric("Reducción promedio", f"{avg_reduction:.1f} KB")
|
131 |
|
132 |
-
#
|
133 |
zip_buffer = io.BytesIO()
|
134 |
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
135 |
for name, data, _, _ in processed_images:
|
136 |
zip_file.writestr(name, data)
|
137 |
|
138 |
-
# Botón de descarga
|
139 |
st.download_button(
|
140 |
label="📥 Descargar Todas las Imágenes",
|
141 |
data=zip_buffer.getvalue(),
|
|
|
4 |
import zipfile
|
5 |
from datetime import datetime
|
6 |
from pathlib import Path
|
7 |
+
import logging
|
8 |
+
|
9 |
+
# Configuración de la página y del logging
|
10 |
+
st.set_page_config(
|
11 |
+
page_title="📁 Image Compression Tool",
|
12 |
+
page_icon="📁",
|
13 |
+
layout="wide"
|
14 |
+
)
|
15 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
16 |
|
17 |
|
18 |
def optimize_image(image_file):
|
19 |
+
"""
|
20 |
+
Optimiza la imagen automáticamente manteniendo la máxima calidad visual.
|
21 |
+
|
22 |
+
Parámetros:
|
23 |
+
- image_file: Archivo de imagen (stream) subido por el usuario.
|
24 |
+
|
25 |
+
Retorna:
|
26 |
+
- Tuple con los datos de la imagen optimizada (bytes) y el formato usado.
|
27 |
+
- Si no se logra una reducción de tamaño, retorna (None, None).
|
28 |
+
"""
|
29 |
try:
|
30 |
img = Image.open(image_file)
|
31 |
output_buffer = io.BytesIO()
|
32 |
|
33 |
+
# Configuración de parámetros según el formato y modo de la imagen
|
34 |
if img.format == 'PNG':
|
|
|
35 |
if img.mode in ('RGBA', 'LA'):
|
36 |
img_to_save = img
|
37 |
+
save_params = {'format': 'PNG', 'optimize': True}
|
|
|
|
|
|
|
38 |
else:
|
39 |
img_to_save = img.convert('RGB')
|
40 |
+
save_params = {'format': 'JPEG', 'quality': 85, 'optimize': True}
|
|
|
|
|
|
|
|
|
41 |
else:
|
|
|
42 |
img_to_save = img.convert('RGB')
|
43 |
+
save_params = {'format': 'JPEG', 'quality': 85, 'optimize': True}
|
|
|
|
|
|
|
|
|
44 |
|
45 |
+
# Guardar la imagen optimizada en un buffer
|
46 |
img_to_save.save(output_buffer, **save_params)
|
47 |
output_buffer.seek(0)
|
48 |
|
|
|
49 |
optimized_size = len(output_buffer.getvalue())
|
50 |
+
# Se valida que la imagen optimizada sea menor que la original
|
51 |
if optimized_size >= image_file.size:
|
52 |
+
logging.info("No se logró reducir el tamaño de la imagen.")
|
53 |
return None, None
|
54 |
|
55 |
+
logging.info("Imagen optimizada exitosamente.")
|
56 |
return output_buffer.getvalue(), save_params['format']
|
57 |
|
58 |
except Exception as e:
|
59 |
+
st.error(f"Error optimizando imagen: {e}")
|
60 |
+
logging.error(f"Error en optimize_image: {e}")
|
61 |
return None, None
|
62 |
|
63 |
|
64 |
def process_filename(original_name, optimized_format):
|
65 |
+
"""
|
66 |
+
Genera un nuevo nombre de archivo para la imagen optimizada.
|
67 |
+
|
68 |
+
Parámetros:
|
69 |
+
- original_name: Nombre original del archivo.
|
70 |
+
- optimized_format: Formato de la imagen optimizada.
|
71 |
+
|
72 |
+
Retorna:
|
73 |
+
- String con el nuevo nombre (por ejemplo, 'foto_optimizado.jpg').
|
74 |
+
"""
|
75 |
path = Path(original_name)
|
76 |
new_suffix = f".{optimized_format.lower()}" if optimized_format else '.jpg'
|
77 |
return f"{path.stem}_optimizado{new_suffix}"
|
|
|
81 |
st.title("📁 Image Compression Tool")
|
82 |
|
83 |
with st.expander("📌 Instrucciones de uso", expanded=True):
|
84 |
+
st.markdown(
|
85 |
+
"""
|
86 |
+
**Optimización automática de imágenes con máxima calidad visual**
|
87 |
+
|
88 |
+
**Características:**
|
89 |
+
- 🔍 Compresión inteligente automática
|
90 |
+
- 🖼️ Mantiene transparencia en PNG
|
91 |
+
- 📉 Reducción de tamaño garantizada
|
92 |
+
- 🚀 Procesamiento por lotes
|
93 |
+
- 📥 Descarga múltiple en ZIP
|
94 |
+
"""
|
95 |
+
)
|
96 |
|
97 |
uploaded_files = st.file_uploader(
|
98 |
"Sube tus imágenes (máx. 50MB por archivo)",
|
|
|
107 |
total_files = len(uploaded_files)
|
108 |
|
109 |
st.info("Optimizando imágenes...")
|
110 |
+
logging.info(f"Se subieron {total_files} archivos para optimización.")
|
111 |
|
112 |
for idx, file in enumerate(uploaded_files):
|
113 |
try:
|
114 |
if file.size > 50 * 1024 * 1024:
|
115 |
+
st.error(f"El archivo {file.name} excede 50MB")
|
116 |
+
logging.warning(f"El archivo {file.name} excede el tamaño máximo permitido.")
|
117 |
continue
|
118 |
|
119 |
original_size = file.size
|
|
|
128 |
processed_images.append((new_name, optimized_data, original_size, new_size))
|
129 |
|
130 |
st.write(f"✅ {file.name} optimizado ({reduction / 1024:.1f} KB ahorrados)")
|
131 |
+
logging.info(f"{file.name} optimizado. Ahorro: {reduction / 1024:.1f} KB.")
|
132 |
else:
|
133 |
st.write(f"⚠️ {file.name} no se optimizó porque no se logró reducir el tamaño.")
|
134 |
+
logging.info(f"{file.name} no se optimizó, no hubo reducción de tamaño.")
|
135 |
|
136 |
progress_bar.progress((idx + 1) / total_files)
|
137 |
|
138 |
except Exception as e:
|
139 |
+
st.error(f"Error procesando {file.name}: {e}")
|
140 |
+
logging.error(f"Error procesando {file.name}: {e}")
|
141 |
|
142 |
st.success(f"¡Optimización completada! (Ahorro total: {total_reduction / 1024:.1f} KB)")
|
143 |
|
144 |
if processed_images:
|
|
|
145 |
st.subheader("Resultados de Optimización")
|
146 |
cols = st.columns(3)
|
147 |
with cols[0]:
|
|
|
152 |
avg_reduction = (total_reduction / len(processed_images)) / 1024
|
153 |
st.metric("Reducción promedio", f"{avg_reduction:.1f} KB")
|
154 |
|
155 |
+
# Crear archivo ZIP con las imágenes optimizadas
|
156 |
zip_buffer = io.BytesIO()
|
157 |
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
158 |
for name, data, _, _ in processed_images:
|
159 |
zip_file.writestr(name, data)
|
160 |
|
|
|
161 |
st.download_button(
|
162 |
label="📥 Descargar Todas las Imágenes",
|
163 |
data=zip_buffer.getvalue(),
|
pages/4_Image_Converter.py
ADDED
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from PIL import Image, UnidentifiedImageError
|
3 |
+
import io
|
4 |
+
import zipfile
|
5 |
+
import logging
|
6 |
+
|
7 |
+
# Configuración inicial de la página y logging
|
8 |
+
st.set_page_config(
|
9 |
+
page_title="Image Converter",
|
10 |
+
page_icon="🖼️",
|
11 |
+
layout="wide"
|
12 |
+
)
|
13 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
14 |
+
|
15 |
+
|
16 |
+
def convert_image(image_file, output_format):
|
17 |
+
"""
|
18 |
+
Verifica la integridad del archivo y lo convierte al formato de salida deseado.
|
19 |
+
|
20 |
+
Parámetros:
|
21 |
+
- image_file: objeto BytesIO del archivo de imagen.
|
22 |
+
- output_format: cadena con el formato de salida deseado ('png' o 'jpg').
|
23 |
+
|
24 |
+
Retorna:
|
25 |
+
- Los bytes de la imagen convertida o None si ocurre algún error.
|
26 |
+
"""
|
27 |
+
image_name = getattr(image_file, 'name', 'Imagen sin nombre')
|
28 |
+
try:
|
29 |
+
# Intentamos abrir y verificar la integridad de la imagen
|
30 |
+
image_file.seek(0)
|
31 |
+
image = Image.open(image_file)
|
32 |
+
image.verify() # Esto lanza una excepción si el archivo está dañado
|
33 |
+
logging.info(f"{image_name} verificado correctamente.")
|
34 |
+
|
35 |
+
# Reiniciamos el puntero y volvemos a abrir la imagen para poder manipularla
|
36 |
+
image_file.seek(0)
|
37 |
+
image = Image.open(image_file)
|
38 |
+
except (UnidentifiedImageError, Exception) as e:
|
39 |
+
st.error(f"El archivo {image_name} está dañado o es inválido: {e}")
|
40 |
+
logging.error(f"Error al verificar {image_name}: {e}")
|
41 |
+
return None
|
42 |
+
|
43 |
+
# Para JPG es necesario que la imagen esté en modo RGB
|
44 |
+
if output_format.lower() == "jpg" and image.mode != "RGB":
|
45 |
+
image = image.convert("RGB")
|
46 |
+
|
47 |
+
output_io = io.BytesIO()
|
48 |
+
try:
|
49 |
+
image.save(output_io, format=output_format.upper())
|
50 |
+
logging.info(f"{image_name} convertido a {output_format}.")
|
51 |
+
return output_io.getvalue()
|
52 |
+
except Exception as e:
|
53 |
+
st.error(f"Error al convertir {image_name}: {e}")
|
54 |
+
logging.error(f"Error al guardar {image_name}: {e}")
|
55 |
+
return None
|
56 |
+
|
57 |
+
|
58 |
+
def main():
|
59 |
+
st.title("Conversor de Formatos de Imagen")
|
60 |
+
st.markdown("Convierte tus imágenes al formato deseado y verifica la integridad de cada archivo.")
|
61 |
+
|
62 |
+
# Selección del formato de salida
|
63 |
+
output_format = st.selectbox(
|
64 |
+
"Formato de salida",
|
65 |
+
["png", "jpg"],
|
66 |
+
help="Selecciona el formato al que deseas convertir la imagen"
|
67 |
+
)
|
68 |
+
|
69 |
+
# Carga de imágenes
|
70 |
+
uploaded_files = st.file_uploader(
|
71 |
+
"Sube tus imágenes (Formatos soportados: PNG, JPG, JPEG, WEBP)",
|
72 |
+
type=['png', 'jpg', 'jpeg', 'webp'],
|
73 |
+
accept_multiple_files=True
|
74 |
+
)
|
75 |
+
|
76 |
+
if uploaded_files:
|
77 |
+
st.header("Vista Previa Original")
|
78 |
+
cols = st.columns(3)
|
79 |
+
original_images = []
|
80 |
+
for idx, file in enumerate(uploaded_files):
|
81 |
+
file_bytes = file.getvalue()
|
82 |
+
original_images.append((file.name, file_bytes))
|
83 |
+
with cols[idx % 3]:
|
84 |
+
st.image(file_bytes, caption=file.name, use_column_width=True)
|
85 |
+
|
86 |
+
if st.button("✨ Convertir Imágenes"):
|
87 |
+
converted_images = []
|
88 |
+
for name, file_bytes in original_images:
|
89 |
+
st.write(f"Procesando: {name}")
|
90 |
+
img_io = io.BytesIO(file_bytes)
|
91 |
+
output_bytes = convert_image(img_io, output_format)
|
92 |
+
if output_bytes:
|
93 |
+
converted_images.append((name, output_bytes))
|
94 |
+
|
95 |
+
if converted_images:
|
96 |
+
st.header("Imágenes Convertidas")
|
97 |
+
cols = st.columns(3)
|
98 |
+
for idx, (name, img_bytes) in enumerate(converted_images):
|
99 |
+
with cols[idx % 3]:
|
100 |
+
st.image(img_bytes, caption=f"{name} convertido a {output_format}", use_column_width=True)
|
101 |
+
|
102 |
+
# Empaquetar las imágenes convertidas en un ZIP
|
103 |
+
zip_buffer = io.BytesIO()
|
104 |
+
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
|
105 |
+
for name, img_bytes in converted_images:
|
106 |
+
# Se ajusta el nombre del archivo con la extensión deseada
|
107 |
+
base_name = name.rsplit('.', 1)[0]
|
108 |
+
zip_file.writestr(f"{base_name}.{output_format}", img_bytes)
|
109 |
+
|
110 |
+
st.download_button(
|
111 |
+
label="📥 Descargar todas las imágenes convertidas",
|
112 |
+
data=zip_buffer.getvalue(),
|
113 |
+
file_name="imagenes_convertidas.zip",
|
114 |
+
mime="application/zip"
|
115 |
+
)
|
116 |
+
|
117 |
+
|
118 |
+
if __name__ == "__main__":
|
119 |
+
main()
|