Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# Versión 3.
|
| 2 |
import gradio as gr
|
| 3 |
import pandas as pd
|
| 4 |
import config
|
|
@@ -15,14 +15,11 @@ engine = PDFEngine()
|
|
| 15 |
|
| 16 |
# --- SEGURIDAD: GARBAGE COLLECTOR ---
|
| 17 |
# NOTA PARA DESARROLLADORES / DUPLICADORES DEL ESPACIO:
|
| 18 |
-
# Esta función limpia los archivos cada minuto. Por defecto está en modo SILENCIOSO
|
| 19 |
-
#
|
| 20 |
-
# Si quieres ver qué está pasando, DESCOMENTA las líneas que empiezan por "#print"
|
| 21 |
-
# y elimina los "pass" si es necesario.
|
| 22 |
def cleanup_cron():
|
| 23 |
"""
|
| 24 |
Revisa cada minuto. Borra archivos Y carpetas mayores a 5 minutos.
|
| 25 |
-
Modo Producción: Logs desactivados por defecto.
|
| 26 |
"""
|
| 27 |
print("[INIT] 🛡️ Sistema de seguridad y auto-borrado ACTIVO.")
|
| 28 |
|
|
@@ -33,7 +30,6 @@ def cleanup_cron():
|
|
| 33 |
LIMIT_MINUTES = 5
|
| 34 |
cutoff = time.time() - (LIMIT_MINUTES * 60)
|
| 35 |
|
| 36 |
-
# Cabecera de ronda
|
| 37 |
#print(f"--- [SEGURIDAD] Ronda de limpieza: {time.strftime('%H:%M:%S')} ---")
|
| 38 |
|
| 39 |
if os.path.exists(config.TEMP_DIR):
|
|
@@ -49,48 +45,41 @@ def cleanup_cron():
|
|
| 49 |
for filename in items:
|
| 50 |
filepath = os.path.join(config.TEMP_DIR, filename)
|
| 51 |
|
| 52 |
-
# Ofuscación visual
|
| 53 |
if len(filename) > 8:
|
| 54 |
masked = f"{filename[:4]}****{os.path.splitext(filename)[1]}"
|
| 55 |
else:
|
| 56 |
masked = filename
|
| 57 |
|
| 58 |
-
# Calcular edad
|
| 59 |
try:
|
| 60 |
file_time = os.path.getmtime(filepath)
|
| 61 |
age_sec = time.time() - file_time
|
| 62 |
-
age_str = f"{int(age_sec // 60)}m {int(age_sec % 60)}s"
|
| 63 |
-
|
| 64 |
is_expired = file_time < cutoff
|
| 65 |
except FileNotFoundError:
|
| 66 |
continue
|
| 67 |
|
| 68 |
-
#
|
| 69 |
if os.path.isfile(filepath):
|
| 70 |
if is_expired:
|
| 71 |
try:
|
| 72 |
os.remove(filepath)
|
| 73 |
-
#print(f"❌ [BORRADO] Archivo {masked}
|
| 74 |
except Exception as e:
|
| 75 |
-
#print(f"⚠️ [ERROR] No se pudo borrar archivo {masked}: {e}")
|
| 76 |
pass
|
| 77 |
else:
|
| 78 |
-
#print(f"✅ [VIGENTE] Archivo {masked}
|
| 79 |
pass
|
| 80 |
|
| 81 |
-
#
|
| 82 |
elif os.path.isdir(filepath):
|
| 83 |
if is_expired:
|
| 84 |
try:
|
| 85 |
shutil.rmtree(filepath)
|
| 86 |
-
#print(f"🗑️ [LIMPIEZA] Carpeta {masked} eliminada
|
| 87 |
except Exception as e:
|
| 88 |
-
#print(f"⚠️ [ERROR] No se pudo borrar carpeta {masked}: {e}")
|
| 89 |
pass
|
| 90 |
else:
|
| 91 |
-
#print(f"📂 [VIGENTE] Carpeta {masked}
|
| 92 |
pass
|
| 93 |
-
|
| 94 |
else:
|
| 95 |
#print("[INFO] Carpeta temporal aún no creada.")
|
| 96 |
pass
|
|
@@ -98,20 +87,35 @@ def cleanup_cron():
|
|
| 98 |
except Exception as e:
|
| 99 |
print(f"[CRITICAL] Error en limpieza: {e}")
|
| 100 |
|
| 101 |
-
# Iniciar hilo en segundo plano
|
| 102 |
threading.Thread(target=cleanup_cron, daemon=True).start()
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
# --- WRAPPERS ---
|
| 105 |
def update_file_list(files):
|
| 106 |
if not files: return pd.DataFrame(), ""
|
| 107 |
data = [[i, f.split("/")[-1]] for i, f in enumerate(files)]
|
| 108 |
return pd.DataFrame(data, columns=["ID", "Archivo"]), ",".join([str(i) for i in range(len(files))])
|
| 109 |
|
| 110 |
-
|
|
|
|
| 111 |
if not files: return None
|
| 112 |
try:
|
| 113 |
indices = [int(x.strip()) for x in order_str.split(",") if x.strip().isdigit()]
|
| 114 |
-
return engine.merge_pdfs(files, indices)
|
| 115 |
except Exception as e: raise gr.Error(str(e))
|
| 116 |
|
| 117 |
def load_info(f):
|
|
@@ -179,6 +183,17 @@ def process_text(f):
|
|
| 179 |
try: return engine.extract_text(f)
|
| 180 |
except Exception as e: raise gr.Error(str(e))
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
# WRAPPERS OFFICE
|
| 183 |
def process_word(f):
|
| 184 |
if not f: return None
|
|
@@ -207,44 +222,40 @@ with gr.Blocks(title=config.APP_TITLE, theme=gr.themes.Soft()) as demo:
|
|
| 207 |
|
| 208 |
gr.Markdown(f"# {config.APP_TITLE}")
|
| 209 |
gr.Markdown("""
|
|
|
|
| 210 |
Los archivos se procesan en memoria y se **autodestruyen tras 5 minutos**.
|
| 211 |
""")
|
| 212 |
|
| 213 |
-
gr.HTML(
|
| 214 |
-
<div style="display: flex; gap: 10px; margin-bottom: 20px;">
|
| 215 |
-
<span>⚡ ¿Funciona lento o quieres seguridad extra? Duplica este espacio para tener tu propia instancia privada.</span>
|
| 216 |
-
</div>
|
| 217 |
-
""")
|
| 218 |
|
| 219 |
with gr.Tabs():
|
| 220 |
-
|
| 221 |
-
# 1. UNIR
|
| 222 |
with gr.TabItem("Unir"):
|
| 223 |
with gr.Row():
|
| 224 |
with gr.Column(scale=1):
|
| 225 |
m_files = gr.File(file_count="multiple", label="Archivos", file_types=[".pdf"])
|
| 226 |
with gr.Column(scale=2):
|
| 227 |
m_tbl = gr.Dataframe(headers=["ID", "Archivo"], interactive=False)
|
| 228 |
-
m_ord = gr.Textbox(label="Orden
|
| 229 |
-
|
|
|
|
|
|
|
| 230 |
m_out = gr.File(label="Resultado")
|
| 231 |
m_files.change(update_file_list, m_files, [m_tbl, m_ord])
|
| 232 |
-
|
|
|
|
| 233 |
|
| 234 |
# 2. DIVIDIR / REORDENAR
|
| 235 |
with gr.TabItem("Dividir / Reordenar"):
|
| 236 |
-
|
| 237 |
-
# BLOQUE SUPERIOR
|
| 238 |
dr_f = gr.File(label="PDF Origen", file_types=[".pdf"])
|
| 239 |
dr_inf = gr.Markdown("")
|
| 240 |
dr_pg = gr.State(0)
|
| 241 |
-
|
| 242 |
-
# BLOQUE INFERIOR
|
| 243 |
with gr.Tabs():
|
| 244 |
with gr.Tab("Extraer"):
|
|
|
|
| 245 |
with gr.Row():
|
| 246 |
with gr.Column():
|
| 247 |
-
s_rng = gr.Textbox(label="
|
| 248 |
with gr.Row():
|
| 249 |
s_prv = gr.Button("Preview")
|
| 250 |
s_btn = gr.Button("Dividir (ZIP)", variant="primary")
|
|
@@ -253,7 +264,6 @@ with gr.Blocks(title=config.APP_TITLE, theme=gr.themes.Soft()) as demo:
|
|
| 253 |
s_out = gr.File(label="ZIP")
|
| 254 |
s_prv.click(update_split_preview, [dr_f, s_rng, dr_pg], s_gal)
|
| 255 |
s_btn.click(process_split, [dr_f, s_rng], s_out)
|
| 256 |
-
|
| 257 |
with gr.Tab("Reordenar"):
|
| 258 |
gr.Markdown("Crea un PDF con nuevo orden.")
|
| 259 |
with gr.Row():
|
|
@@ -263,7 +273,6 @@ with gr.Blocks(title=config.APP_TITLE, theme=gr.themes.Soft()) as demo:
|
|
| 263 |
with gr.Column():
|
| 264 |
r_out = gr.File(label="PDF Reordenado")
|
| 265 |
r_btn.click(process_reorder, [dr_f, r_ord], r_out)
|
| 266 |
-
|
| 267 |
dr_f.change(load_info, dr_f, [dr_inf, dr_pg, s_out])
|
| 268 |
|
| 269 |
# 3. COMPRIMIR
|
|
@@ -272,7 +281,7 @@ with gr.Blocks(title=config.APP_TITLE, theme=gr.themes.Soft()) as demo:
|
|
| 272 |
with gr.Column():
|
| 273 |
c_f = gr.File(label="PDF Original", file_types=[".pdf"])
|
| 274 |
c_l = gr.Radio(["Baja (Máxima calidad)", "Media (Recomendado - eBook)", "Alta (Pantalla - 72dpi)"], label="Nivel", value="Media (Recomendado - eBook)")
|
| 275 |
-
c_b = gr.Button("Comprimir
|
| 276 |
with gr.Column():
|
| 277 |
c_out = gr.File(label="PDF Comprimido")
|
| 278 |
c_b.click(process_compress, [c_f, c_l], c_out)
|
|
@@ -324,8 +333,8 @@ with gr.Blocks(title=config.APP_TITLE, theme=gr.themes.Soft()) as demo:
|
|
| 324 |
co = gr.File(label="Informe PDF")
|
| 325 |
cb_btn.click(process_compare, [ca, cb], co)
|
| 326 |
|
| 327 |
-
# 6. EXTRAS
|
| 328 |
-
with gr.TabItem("Extras
|
| 329 |
with gr.Tab("Rotar"):
|
| 330 |
with gr.Row():
|
| 331 |
with gr.Column():
|
|
@@ -338,6 +347,29 @@ with gr.Blocks(title=config.APP_TITLE, theme=gr.themes.Soft()) as demo:
|
|
| 338 |
rf.change(update_rot_preview, [rf, ra], rp)
|
| 339 |
ra.change(update_rot_preview, [rf, ra], rp)
|
| 340 |
rb.click(process_rotate, [rf, ra], ro)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
|
| 342 |
with gr.Tab("Proteger"):
|
| 343 |
with gr.Row():
|
|
@@ -348,7 +380,7 @@ with gr.Blocks(title=config.APP_TITLE, theme=gr.themes.Soft()) as demo:
|
|
| 348 |
with gr.Column():
|
| 349 |
po = gr.File(label="Protegido")
|
| 350 |
pb.click(process_protect, [pf, pp], po)
|
| 351 |
-
|
| 352 |
with gr.Tab("Info/Texto"):
|
| 353 |
with gr.Row():
|
| 354 |
with gr.Column():
|
|
|
|
| 1 |
+
# Versión 3.6: App Pública Completa (Watermark, Reparar, Numeración)
|
| 2 |
import gradio as gr
|
| 3 |
import pandas as pd
|
| 4 |
import config
|
|
|
|
| 15 |
|
| 16 |
# --- SEGURIDAD: GARBAGE COLLECTOR ---
|
| 17 |
# NOTA PARA DESARROLLADORES / DUPLICADORES DEL ESPACIO:
|
| 18 |
+
# Esta función limpia los archivos cada minuto. Por defecto está en modo SILENCIOSO.
|
| 19 |
+
# Si quieres ver qué está pasando, DESCOMENTA las líneas que empiezan por "#print".
|
|
|
|
|
|
|
| 20 |
def cleanup_cron():
|
| 21 |
"""
|
| 22 |
Revisa cada minuto. Borra archivos Y carpetas mayores a 5 minutos.
|
|
|
|
| 23 |
"""
|
| 24 |
print("[INIT] 🛡️ Sistema de seguridad y auto-borrado ACTIVO.")
|
| 25 |
|
|
|
|
| 30 |
LIMIT_MINUTES = 5
|
| 31 |
cutoff = time.time() - (LIMIT_MINUTES * 60)
|
| 32 |
|
|
|
|
| 33 |
#print(f"--- [SEGURIDAD] Ronda de limpieza: {time.strftime('%H:%M:%S')} ---")
|
| 34 |
|
| 35 |
if os.path.exists(config.TEMP_DIR):
|
|
|
|
| 45 |
for filename in items:
|
| 46 |
filepath = os.path.join(config.TEMP_DIR, filename)
|
| 47 |
|
|
|
|
| 48 |
if len(filename) > 8:
|
| 49 |
masked = f"{filename[:4]}****{os.path.splitext(filename)[1]}"
|
| 50 |
else:
|
| 51 |
masked = filename
|
| 52 |
|
|
|
|
| 53 |
try:
|
| 54 |
file_time = os.path.getmtime(filepath)
|
| 55 |
age_sec = time.time() - file_time
|
|
|
|
|
|
|
| 56 |
is_expired = file_time < cutoff
|
| 57 |
except FileNotFoundError:
|
| 58 |
continue
|
| 59 |
|
| 60 |
+
# CASO 1: ARCHIVO
|
| 61 |
if os.path.isfile(filepath):
|
| 62 |
if is_expired:
|
| 63 |
try:
|
| 64 |
os.remove(filepath)
|
| 65 |
+
#print(f"❌ [BORRADO] Archivo {masked}")
|
| 66 |
except Exception as e:
|
|
|
|
| 67 |
pass
|
| 68 |
else:
|
| 69 |
+
#print(f"✅ [VIGENTE] Archivo {masked}")
|
| 70 |
pass
|
| 71 |
|
| 72 |
+
# CASO 2: CARPETA
|
| 73 |
elif os.path.isdir(filepath):
|
| 74 |
if is_expired:
|
| 75 |
try:
|
| 76 |
shutil.rmtree(filepath)
|
| 77 |
+
#print(f"🗑️ [LIMPIEZA] Carpeta {masked} eliminada")
|
| 78 |
except Exception as e:
|
|
|
|
| 79 |
pass
|
| 80 |
else:
|
| 81 |
+
#print(f"📂 [VIGENTE] Carpeta {masked}")
|
| 82 |
pass
|
|
|
|
| 83 |
else:
|
| 84 |
#print("[INFO] Carpeta temporal aún no creada.")
|
| 85 |
pass
|
|
|
|
| 87 |
except Exception as e:
|
| 88 |
print(f"[CRITICAL] Error en limpieza: {e}")
|
| 89 |
|
|
|
|
| 90 |
threading.Thread(target=cleanup_cron, daemon=True).start()
|
| 91 |
|
| 92 |
+
# --- GENERADOR DE ENLACE ---
|
| 93 |
+
def get_duplicate_html():
|
| 94 |
+
space_id = os.environ.get('SPACE_ID', None)
|
| 95 |
+
url = f"https://huggingface.co/spaces/{space_id}?duplicate=true" if space_id else "https://huggingface.co/spaces?duplicate=true"
|
| 96 |
+
return f"""
|
| 97 |
+
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 20px; background-color: #f9fafb; padding: 10px; border-radius: 8px; border: 1px solid #e5e7eb;">
|
| 98 |
+
<a href="{url}" target="_blank">
|
| 99 |
+
<img src="https://huggingface.co/datasets/huggingface/badges/raw/main/duplicate-this-space-sm.svg" alt="Duplicate Space" style="height: 30px;">
|
| 100 |
+
</a>
|
| 101 |
+
<span style="font-size: 0.9em; color: #4b5563;">
|
| 102 |
+
⚡ <b>¿Va lento?</b> Haz clic para duplicar este espacio y tener tu propia instancia privada (y activar logs si quieres).
|
| 103 |
+
</span>
|
| 104 |
+
</div>
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
# --- WRAPPERS ---
|
| 108 |
def update_file_list(files):
|
| 109 |
if not files: return pd.DataFrame(), ""
|
| 110 |
data = [[i, f.split("/")[-1]] for i, f in enumerate(files)]
|
| 111 |
return pd.DataFrame(data, columns=["ID", "Archivo"]), ",".join([str(i) for i in range(len(files))])
|
| 112 |
|
| 113 |
+
# MODIFICADO: Acepta use_numbering
|
| 114 |
+
def process_merge(files, order_str, use_numbering):
|
| 115 |
if not files: return None
|
| 116 |
try:
|
| 117 |
indices = [int(x.strip()) for x in order_str.split(",") if x.strip().isdigit()]
|
| 118 |
+
return engine.merge_pdfs(files, indices, use_numbering)
|
| 119 |
except Exception as e: raise gr.Error(str(e))
|
| 120 |
|
| 121 |
def load_info(f):
|
|
|
|
| 183 |
try: return engine.extract_text(f)
|
| 184 |
except Exception as e: raise gr.Error(str(e))
|
| 185 |
|
| 186 |
+
# NUEVOS WRAPPERS
|
| 187 |
+
def process_watermark(f, t):
|
| 188 |
+
if not f or not t: return None
|
| 189 |
+
try: return engine.add_watermark(f, t)
|
| 190 |
+
except Exception as e: raise gr.Error(str(e))
|
| 191 |
+
|
| 192 |
+
def process_repair(f):
|
| 193 |
+
if not f: return None
|
| 194 |
+
try: return engine.repair_pdf(f)
|
| 195 |
+
except Exception as e: raise gr.Error(str(e))
|
| 196 |
+
|
| 197 |
# WRAPPERS OFFICE
|
| 198 |
def process_word(f):
|
| 199 |
if not f: return None
|
|
|
|
| 222 |
|
| 223 |
gr.Markdown(f"# {config.APP_TITLE}")
|
| 224 |
gr.Markdown("""
|
| 225 |
+
### 🛡️ Suite PDF: Libre, Gratuita y Privada
|
| 226 |
Los archivos se procesan en memoria y se **autodestruyen tras 5 minutos**.
|
| 227 |
""")
|
| 228 |
|
| 229 |
+
gr.HTML(get_duplicate_html())
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
with gr.Tabs():
|
| 232 |
+
# 1. UNIR (Actualizado con Numeración)
|
|
|
|
| 233 |
with gr.TabItem("Unir"):
|
| 234 |
with gr.Row():
|
| 235 |
with gr.Column(scale=1):
|
| 236 |
m_files = gr.File(file_count="multiple", label="Archivos", file_types=[".pdf"])
|
| 237 |
with gr.Column(scale=2):
|
| 238 |
m_tbl = gr.Dataframe(headers=["ID", "Archivo"], interactive=False)
|
| 239 |
+
m_ord = gr.Textbox(label="Orden", placeholder="Ej: 0, 2, 1")
|
| 240 |
+
# NUEVO CHECKBOX
|
| 241 |
+
m_nums = gr.Checkbox(label="Numerar páginas (1 de X)", value=False)
|
| 242 |
+
m_btn = gr.Button("Unir PDF", variant="primary")
|
| 243 |
m_out = gr.File(label="Resultado")
|
| 244 |
m_files.change(update_file_list, m_files, [m_tbl, m_ord])
|
| 245 |
+
# Se pasa el valor del checkbox a la funcion
|
| 246 |
+
m_btn.click(process_merge, [m_files, m_ord, m_nums], m_out)
|
| 247 |
|
| 248 |
# 2. DIVIDIR / REORDENAR
|
| 249 |
with gr.TabItem("Dividir / Reordenar"):
|
|
|
|
|
|
|
| 250 |
dr_f = gr.File(label="PDF Origen", file_types=[".pdf"])
|
| 251 |
dr_inf = gr.Markdown("")
|
| 252 |
dr_pg = gr.State(0)
|
|
|
|
|
|
|
| 253 |
with gr.Tabs():
|
| 254 |
with gr.Tab("Extraer"):
|
| 255 |
+
gr.Markdown("Separa páginas en un ZIP.")
|
| 256 |
with gr.Row():
|
| 257 |
with gr.Column():
|
| 258 |
+
s_rng = gr.Textbox(label="Rango", placeholder="Ej: 1-3, 5")
|
| 259 |
with gr.Row():
|
| 260 |
s_prv = gr.Button("Preview")
|
| 261 |
s_btn = gr.Button("Dividir (ZIP)", variant="primary")
|
|
|
|
| 264 |
s_out = gr.File(label="ZIP")
|
| 265 |
s_prv.click(update_split_preview, [dr_f, s_rng, dr_pg], s_gal)
|
| 266 |
s_btn.click(process_split, [dr_f, s_rng], s_out)
|
|
|
|
| 267 |
with gr.Tab("Reordenar"):
|
| 268 |
gr.Markdown("Crea un PDF con nuevo orden.")
|
| 269 |
with gr.Row():
|
|
|
|
| 273 |
with gr.Column():
|
| 274 |
r_out = gr.File(label="PDF Reordenado")
|
| 275 |
r_btn.click(process_reorder, [dr_f, r_ord], r_out)
|
|
|
|
| 276 |
dr_f.change(load_info, dr_f, [dr_inf, dr_pg, s_out])
|
| 277 |
|
| 278 |
# 3. COMPRIMIR
|
|
|
|
| 281 |
with gr.Column():
|
| 282 |
c_f = gr.File(label="PDF Original", file_types=[".pdf"])
|
| 283 |
c_l = gr.Radio(["Baja (Máxima calidad)", "Media (Recomendado - eBook)", "Alta (Pantalla - 72dpi)"], label="Nivel", value="Media (Recomendado - eBook)")
|
| 284 |
+
c_b = gr.Button("Comprimir", variant="primary")
|
| 285 |
with gr.Column():
|
| 286 |
c_out = gr.File(label="PDF Comprimido")
|
| 287 |
c_b.click(process_compress, [c_f, c_l], c_out)
|
|
|
|
| 333 |
co = gr.File(label="Informe PDF")
|
| 334 |
cb_btn.click(process_compare, [ca, cb], co)
|
| 335 |
|
| 336 |
+
# 6. EXTRAS (Actualizado con Marca de Agua y Reparar)
|
| 337 |
+
with gr.TabItem("Extras"):
|
| 338 |
with gr.Tab("Rotar"):
|
| 339 |
with gr.Row():
|
| 340 |
with gr.Column():
|
|
|
|
| 347 |
rf.change(update_rot_preview, [rf, ra], rp)
|
| 348 |
ra.change(update_rot_preview, [rf, ra], rp)
|
| 349 |
rb.click(process_rotate, [rf, ra], ro)
|
| 350 |
+
|
| 351 |
+
# NUEVO: Marca de Agua
|
| 352 |
+
with gr.Tab("Marca de Agua"):
|
| 353 |
+
gr.Markdown("Añade una marca de agua diagonal en todas las páginas.")
|
| 354 |
+
with gr.Row():
|
| 355 |
+
with gr.Column():
|
| 356 |
+
wf = gr.File(label="PDF")
|
| 357 |
+
wt = gr.Textbox(label="Texto Marca de Agua", placeholder="Ej: CONFIDENCIAL")
|
| 358 |
+
wb = gr.Button("Estampar", variant="primary")
|
| 359 |
+
with gr.Column():
|
| 360 |
+
wo = gr.File(label="PDF Marcado")
|
| 361 |
+
wb.click(process_watermark, [wf, wt], wo)
|
| 362 |
+
|
| 363 |
+
# NUEVO: Reparar
|
| 364 |
+
with gr.Tab("Reparar"):
|
| 365 |
+
gr.Markdown("Intenta arreglar PDFs corruptos o dañados reescribiéndolos con Ghostscript.")
|
| 366 |
+
with gr.Row():
|
| 367 |
+
with gr.Column():
|
| 368 |
+
repf = gr.File(label="PDF Dañado")
|
| 369 |
+
repb = gr.Button("Reparar", variant="primary")
|
| 370 |
+
with gr.Column():
|
| 371 |
+
repo = gr.File(label="PDF Reparado")
|
| 372 |
+
repb.click(process_repair, repf, repo)
|
| 373 |
|
| 374 |
with gr.Tab("Proteger"):
|
| 375 |
with gr.Row():
|
|
|
|
| 380 |
with gr.Column():
|
| 381 |
po = gr.File(label="Protegido")
|
| 382 |
pb.click(process_protect, [pf, pp], po)
|
| 383 |
+
|
| 384 |
with gr.Tab("Info/Texto"):
|
| 385 |
with gr.Row():
|
| 386 |
with gr.Column():
|