Skip to content

Commit 597a7cb

Browse files
committed
Merge PR: Compatibilidad con macOS y correccion de brechas de seguridad
2 parents 7a85408 + 485b1e6 commit 597a7cb

42 files changed

Lines changed: 993 additions & 324 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,4 @@ assets/installer/
7474
installer/
7575
notes/
7676
Compilar-Buildear-Pasos.txt
77-
index.html
77+
index.html

app/app.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from app.utils.paths import resource_path
2121
from app.ui.main_window import MainWindow
2222
from app.translations import t
23-
from app.modules.image_utils import init_heif_support
23+
from app.utils.image_utils import init_heif_support
2424

2525
logger = logging.getLogger(__name__)
2626

@@ -60,16 +60,24 @@ def __init__(self):
6060
# Actualizar para asegurar que la geometria se aplique antes de maximizar
6161
self.update()
6262

63-
# Maximizar ventana al iniciar (Windows/Linux)
64-
try:
65-
# Windows y algunos WM
66-
self.state('zoomed')
67-
except Exception:
63+
# Maximizar ventana al iniciar
64+
if sys.platform == 'darwin':
65+
self.update_idletasks()
66+
w = self.winfo_screenwidth()
67+
h = self.winfo_screenheight()
68+
self.geometry(f'{w}x{h}+0+0')
69+
elif sys.platform == 'win32':
70+
try:
71+
self.state('zoomed')
72+
except Exception:
73+
self.update_idletasks()
74+
w = self.winfo_screenwidth()
75+
h = self.winfo_screenheight()
76+
self.geometry(f'{w}x{h}+0+0')
77+
else:
6878
try:
69-
# Linux (algunos window managers)
7079
self.attributes('-zoomed', True)
7180
except Exception:
72-
# Fallback: usar tamaño de pantalla
7381
self.update_idletasks()
7482
w = self.winfo_screenwidth()
7583
h = self.winfo_screenheight()
@@ -78,42 +86,60 @@ def __init__(self):
7886
# Establecer tamano minimo de la ventana
7987
self.minsize(900, 600)
8088

89+
# En macOS, restaurar la ventana al hacer click en el icono del Dock
90+
if sys.platform == 'darwin':
91+
self._setup_dock_reopen()
92+
8193
# Configurar icono de la ventana segun plataforma
8294
self._setup_icon()
8395

8496
# Crear ventana principal con sidebar y area de contenido
8597
self.main_window = MainWindow(self)
8698
self.main_window.pack(fill='both', expand=True)
8799

100+
def _setup_dock_reopen(self):
101+
"""Registra el handler para restaurar la ventana desde el Dock en macOS."""
102+
try:
103+
self.createcommand('tk::mac::ReopenApplication', self._on_dock_click)
104+
except Exception as e:
105+
logger.debug("No se pudo registrar ReopenApplication: %s", e)
106+
107+
def _on_dock_click(self):
108+
"""Restaura la ventana cuando el usuario hace click en el icono del Dock."""
109+
self.deiconify()
110+
self.lift()
111+
self.focus_force()
112+
88113
def _setup_icon(self):
89114
"""
90115
Configura el icono de la ventana segun la plataforma.
91116
92-
En Windows usa icon.ico, en otras plataformas usa icon.png.
93-
Registra warnings si los iconos no se encuentran.
117+
Windows: icon.ico | macOS: icon.png (icon.icns para bundle) | Linux: icon.png
94118
"""
95-
# Rutas de los archivos de icono
96119
icon_ico = resource_path('assets/icon.ico')
120+
icon_icns = resource_path('assets/icon.icns')
97121
icon_png = resource_path('assets/icon.png')
98122

99-
# Configuracion especifica para Windows
100123
if sys.platform == 'win32':
101-
# Usar .ico en Windows (formato nativo)
102124
if icon_ico.exists():
103125
self.iconbitmap(str(icon_ico))
104-
# Fallback a PNG si .ico no existe
105126
elif icon_png.exists():
106127
self._setup_icon_png(icon_png)
107128
else:
108-
# Registrar warning si no hay icono disponible
129+
logger.warning("Icono no encontrado en assets/")
130+
elif sys.platform == 'darwin':
131+
if icon_png.exists():
132+
self._setup_icon_png(icon_png)
133+
elif icon_icns.exists():
134+
try:
135+
self.iconbitmap(str(icon_icns))
136+
except Exception as e:
137+
logger.warning(f'No se pudo aplicar icon.icns: {e}')
138+
else:
109139
logger.warning("Icono no encontrado en assets/")
110140
else:
111-
# En otras plataformas solo usar PNG
112141
if icon_png.exists():
113142
self._setup_icon_png(icon_png)
114-
elif icon_ico.exists():
115-
# .ico no es compatible con macOS/Linux
116-
logger.warning("icon.ico no es compatible con esta plataforma")
117143
else:
118144
logger.warning("Icono no encontrado en assets/")
119145

app/modules/compress.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515

1616
from PIL import Image
1717

18-
from app.modules.image_utils import normalize_common, ensure_rgb_for_jpeg
19-
from app.modules.output import unique_output_path
18+
from app.utils.image_utils import normalize_common, ensure_rgb_for_jpeg
19+
from app.utils.output import unique_output_path
2020

2121
logger = logging.getLogger(__name__)
2222

app/modules/convert.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
from PIL import Image
1414

15-
from app.modules.image_utils import normalize_common, ensure_rgb_for_jpeg
16-
from app.modules.output import unique_output_path
15+
from app.utils.image_utils import normalize_common, ensure_rgb_for_jpeg
16+
from app.utils.output import unique_output_path
1717

1818
logger = logging.getLogger(__name__)
1919

app/modules/lqip.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,39 @@
77
"""
88

99
import base64
10+
import html
1011
import io
1112
import json
1213
import logging
14+
import re
15+
import unicodedata
1316
from pathlib import Path
1417

1518
from PIL import Image, ImageFilter
1619

1720
logger = logging.getLogger(__name__)
1821

22+
23+
def _nombre_css_seguro(nombre_archivo):
24+
"""Convierte el nombre original a una clase CSS segura."""
25+
nombre = Path(nombre_archivo).stem.strip().lower()
26+
nombre = unicodedata.normalize('NFKD', nombre).encode('ascii', 'ignore').decode('ascii')
27+
nombre = re.sub(r'\s+', '-', nombre)
28+
nombre = re.sub(r'[^a-z0-9_-]+', '-', nombre)
29+
nombre = re.sub(r'-{2,}', '-', nombre).strip('-_')
30+
31+
if not nombre:
32+
return 'img'
33+
if nombre[0].isdigit():
34+
return f'img-{nombre}'
35+
return nombre
36+
37+
38+
def _comentario_seguro(nombre_archivo):
39+
"""Evita cierres de comentario o saltos de linea en exportes TXT."""
40+
return str(nombre_archivo).replace('*/', '* /').replace('\r', ' ').replace('\n', ' ')
41+
42+
1943
def imagen_a_base64(ruta, calidad=85):
2044
"""
2145
Convierte una imagen completa a string base64.
@@ -89,13 +113,14 @@ def _construir_resultado(datos_b64, nombre_archivo):
89113
Diccionario con data_uri, html_tag, css_bg.
90114
"""
91115
data_uri = f'data:image/jpeg;base64,{datos_b64}'
92-
nombre_css = Path(nombre_archivo).stem.replace(' ', '-').lower()
116+
nombre_css = _nombre_css_seguro(nombre_archivo)
117+
alt = html.escape(Path(nombre_archivo).stem, quote=True)
93118

94119
return {
95120
'nombre': nombre_archivo,
96121
'base64': datos_b64,
97122
'data_uri': data_uri,
98-
'html_tag': f'<img src="{data_uri}" alt="{Path(nombre_archivo).stem}" />',
123+
'html_tag': f'<img src="{data_uri}" alt="{alt}" />',
99124
'css_bg': f'.{nombre_css} {{\n background-image: url("{data_uri}");\n}}',
100125
}
101126

@@ -151,7 +176,7 @@ def exportar_txt(resultados, ruta_salida, campo='data_uri'):
151176
logger.info("Exportar LQIP TXT: %s resultados -> %s", len(resultados), ruta_salida)
152177
lineas = []
153178
for res in resultados:
154-
lineas.append(f'/* {res["nombre"]} */')
179+
lineas.append(f'/* {_comentario_seguro(res["nombre"])} */')
155180
lineas.append(res.get(campo, ''))
156181
lineas.append('')
157182

app/modules/metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from PIL import Image, ImageOps
1515
import piexif
1616

17-
from app.modules.output import unique_output_path
17+
from app.utils.output import unique_output_path
1818

1919
logger = logging.getLogger(__name__)
2020

app/modules/ocr.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
import cv2
1515
from PIL import features
1616

17-
from app.modules.image_utils import load_cv_image
17+
from app.utils.image_utils import load_cv_image
1818

1919
logger = logging.getLogger(__name__)
2020
_READER_CACHE = {}
2121
_READER_LOCK = threading.Lock()
22+
MAX_IMAGENES = 10
2223

2324

2425
def _idiomas_key(idiomas):
@@ -184,7 +185,7 @@ def batch_procesar(rutas, idiomas=['es', 'en']):
184185
textos = {}
185186
ok = 0
186187
errores = 0
187-
for ruta in rutas[:10]: # L?mite de 10 im?genes
188+
for ruta in rutas[:MAX_IMAGENES]:
188189
try:
189190
texto = procesar_imagen(ruta, reader)
190191
textos[str(ruta)] = texto

app/modules/remove_bg.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414

1515
from PIL import Image, ImageOps
1616

17-
from app.modules.output import unique_output_path
17+
from app.utils.output import unique_output_path
1818

1919
logger = logging.getLogger(__name__)
2020

2121
MODELO = 'u2netp'
2222
FORMATOS_SALIDA = ['PNG', 'WEBP', 'TIFF']
2323
_FMT_A_EXT = {'PNG': '.png', 'WEBP': '.webp', 'TIFF': '.tiff'}
24+
INSTALL_COMMAND = 'pip install --upgrade rembg onnxruntime pymatting'
2425
_MODEL_READY = False
26+
MAX_IMAGENES = 10
2527

2628

2729
def _ensure_stdio():
@@ -53,16 +55,17 @@ def quitar_fondo(ruta_entrada, ruta_salida, formato_salida='PNG'):
5355
try:
5456
from rembg import remove as rembg_remove, new_session
5557
except ImportError:
56-
raise ImportError('rembg no est? instalado. Ejecut?: pip install rembg')
58+
raise ImportError(f'rembg no está disponible. Ejecutá: {INSTALL_COMMAND}')
5759

5860
logger.info(
5961
"remove_bg: quitar_fondo inicio (entrada=%s, salida=%s, formato=%s)",
6062
ruta_entrada, ruta_salida, formato_salida
6163
)
6264
_ensure_stdio()
65+
tam_original = Path(ruta_entrada).stat().st_size
66+
6367
imagen = Image.open(ruta_entrada)
6468
imagen = ImageOps.exif_transpose(imagen)
65-
tam_original = Path(ruta_entrada).stat().st_size
6669

6770
logger.info("remove_bg: crear_sesion (modelo=%s)", MODELO)
6871
sesion = new_session(MODELO)
@@ -111,7 +114,7 @@ def batch_quitar_fondo(rutas, carpeta_salida, formato_salida='PNG'):
111114
try:
112115
from rembg import remove as rembg_remove, new_session
113116
except ImportError:
114-
raise ImportError('rembg no está instalado.')
117+
raise ImportError(f'rembg no está disponible. Ejecutá: {INSTALL_COMMAND}')
115118

116119
logger.info(
117120
"remove_bg: batch_inicio (cantidad=%s, carpeta=%s, formato=%s)",
@@ -126,12 +129,13 @@ def batch_quitar_fondo(rutas, carpeta_salida, formato_salida='PNG'):
126129
errores = 0
127130
conflictos = 0
128131

129-
for ruta in rutas:
132+
for ruta in list(rutas)[:MAX_IMAGENES]:
130133
try:
131134
logger.debug("remove_bg: procesar_imagen (ruta=%s)", ruta)
135+
tam_original = Path(ruta).stat().st_size
136+
132137
imagen = Image.open(ruta)
133138
imagen = ImageOps.exif_transpose(imagen)
134-
tam_original = Path(ruta).stat().st_size
135139

136140
resultado_raw = rembg_remove(imagen, session=sesion)
137141
if isinstance(resultado_raw, Image.Image):
@@ -193,15 +197,21 @@ def ensure_model():
193197
logger.info("remove_bg: ensure_model listo")
194198

195199

196-
def rembg_disponible():
197-
"""Verifica si rembg esta instalado."""
200+
def estado_rembg():
201+
"""Verifica si rembg puede importarse y retorna detalle del error si falla."""
198202
try:
199203
import rembg # noqa: F401
200-
logger.debug("remove_bg: rembg_disponible=True")
201-
return True
204+
return True, None
202205
except Exception as exc:
203-
logger.warning("rembg no disponible: %s", exc)
204-
return False
206+
detalle = str(exc).strip() or type(exc).__name__
207+
logger.warning("rembg no disponible: %s", detalle)
208+
return False, detalle
209+
210+
211+
def rembg_disponible():
212+
"""Verifica si rembg esta disponible en el entorno actual."""
213+
disponible, _ = estado_rembg()
214+
return disponible
205215

206216

207217
def modelo_descargado():

app/modules/rename.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,34 @@
77
"""
88

99
import logging
10+
import re
1011
from datetime import datetime
1112
from pathlib import Path
1213

1314
logger = logging.getLogger(__name__)
1415

16+
_INVALID_FILENAME_CHARS = re.compile(r'[<>:"/\\|?*\x00-\x1f]+')
17+
18+
19+
def _sanitizar_prefijo(prefijo):
20+
"""Limpia el prefijo para evitar rutas inseguras en el renombrado."""
21+
if not isinstance(prefijo, str):
22+
return ''
23+
24+
prefijo = _INVALID_FILENAME_CHARS.sub('_', prefijo.strip())
25+
prefijo = re.sub(r'_+', '_', prefijo).strip(' ._')
26+
return prefijo
27+
28+
29+
def _nombre_es_seguro(nombre):
30+
"""Verifica que el nombre no contenga separadores ni componentes de ruta."""
31+
if not nombre or '\x00' in nombre:
32+
return False
33+
if '/' in nombre or '\\' in nombre:
34+
return False
35+
return Path(nombre).name == nombre
36+
37+
1538
def generar_nombres_preview(rutas, opciones):
1639
"""
1740
Genera preview de nuevos nombres sin modificar archivos.
@@ -49,7 +72,7 @@ def _aplicar_transformaciones(nombre, indice, opciones):
4972

5073
# Paso 1 - Prefijo + numeracion
5174
if opciones.get('numeracion_activa'):
52-
prefijo = opciones.get('prefijo', '').strip()
75+
prefijo = _sanitizar_prefijo(opciones.get('prefijo', ''))
5376
numero = str(indice + inicio).zfill(3)
5477
if prefijo:
5578
nombre_resultado = f'{prefijo}_{numero}'
@@ -108,6 +131,11 @@ def renombrar_archivos(rutas, opciones):
108131
ok += 1
109132
continue
110133

134+
if not _nombre_es_seguro(nombre_nuevo):
135+
logger.warning("Nombre de destino inseguro rechazado: %s", nombre_nuevo)
136+
errores += 1
137+
continue
138+
111139
ruta_nueva = ruta_archivo.parent / (nombre_nuevo + extension)
112140

113141
if ruta_nueva.exists() and ruta_nueva != ruta_archivo:

app/modules/resize.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
from PIL import Image
1414

15-
from app.modules.image_utils import normalize_common
16-
from app.modules.output import unique_output_path
15+
from app.utils.image_utils import normalize_common
16+
from app.utils.output import unique_output_path
1717

1818
from math import gcd
1919

0 commit comments

Comments
 (0)