Files
fn_registry/python/functions/datascience/add_pdf_internal_links.md
T
egutierrez d1a3d58a6b feat(eda): motor AutomaticEDA fase 4a — render fixes + keep-together + glosario clicable
Mejoras transversales del motor de render (no del contenido de capítulos):

1. Fix negrita pisa texto (PDF): _place_rich_lines mide el ancho REAL de cada
   span con las métricas de fuente del renderer (peso correcto) en vez del
   grid de ancho medio; negrita y normal en la misma línea ya no se solapan.
2. Zebra striping: filas pares sombreadas (#f6f8fa) en DataTable (PDF + PPTX),
   coherente al partir tablas largas (índice de fila lógico, no por página).
3. Keep-together: bloque Group nuevo; el renderer mide el grupo entero y lo
   mueve completo a la página/slide siguiente si no cabe, y encoge la figura
   (height_in) para dejar sitio a su título y texto. num_distr lo usa.
4. Caption siempre visible en toda figura PPTX (fallback al heading); la figura
   reserva el alto de su caption para que ambos quepan en el mismo slide.
5. Portada construida al final (con resumen agregado del análisis vía
   ctx['document_summary']) pero colocada primera por build_document.
6. Glosario: capítulo nuevo (último) + GlossaryCollector en ctx; los capítulos
   registran términos y marcan apariciones con [[term:key]]...[[/term]]. Links
   clicables reales: PDF (PyMuPDF, link GOTO) y PPTX (slide-jump nativo).
   Enganchado "entropía" en cat_distr como ejemplo end-to-end.

Funciones reutilizables delegadas a fn-constructor (tag eda):
- add_pdf_internal_links_py_datascience (PyMuPDF)
- pptx_link_run_to_slide_py_datascience (slide-jump)

Contrato docs/automatic_eda_contract.md actualizado (§1/§3/§5 + §11 nueva) con
la API de glosario, keep-together y zebra para la siguiente fase. PyMuPDF
declarado en pyproject. Suite verde (90 tests); golden titanic verificado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:35:19 +02:00

5.8 KiB

name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, params, output, tested, tests, test_file_path, file_path
name kind lang domain version purity signature description tags uses_functions uses_types returns returns_optional error_type imports params output tested tests test_file_path file_path
add_pdf_internal_links function py datascience 1.0.0 impure def add_pdf_internal_links(pdf_path: str, links: list) -> dict Postprocesa un PDF YA escrito insertando link annotations internos de tipo GOTO ('ir a') con PyMuPDF (import fitz). Pensado para PDFs generados por matplotlib PdfPages, que NO soporta hyperlinks internos: tras escribir el PDF se reabre y, por cada entrada de `links`, se añade una anotacion clicable desde un rectangulo de una pagina origen (src_page + src_rect en puntos top-left) hasta un punto de una pagina destino (dst_page + dst_point). Caso de uso tipico del grupo eda: hacer clicables los terminos de un AutomaticEDA que apuntan a su entrada en el glosario al final del documento. Estilo dict-no-throw: NUNCA lanza; valida cada link y SALTA (n_skipped++) los malformados o fuera de rango en vez de fallar. Guarda de forma segura escribiendo a un temporal en el mismo directorio y haciendo os.replace atomico (evita corromper el original). Devuelve {status:ok,n_links,n_skipped} o {status:error,error}; si pymupdf no esta disponible o el archivo no existe devuelve status error.
eda
datascience
pdf
links
glossary
pymupdf
fitz
postprocess
python
false error_go_core
name desc
pdf_path ruta al PDF existente (str no vacio). Se reescribe IN SITU (in-place) tras añadir los links: se guarda a un temporal `.<base>.tmp_links` en el mismo directorio y se reemplaza atomicamente con os.replace. Si no es str o no existe el archivo -> {status:error}.
name desc
links lista de dicts, uno por link a insertar. Cada dict: src_page (int 0-based de la pagina origen), src_rect ([x0,y0,x1,y1] del rectangulo clicable en PUNTOS PDF 1/72" con origen ARRIBA-IZQUIERDA), dst_page (int 0-based de la pagina destino), dst_point ([x,y] punto destino, mismos puntos top-left). Las entradas que no son dict, con page fuera de rango [0,page_count), src_rect que no tenga 4 numeros o dst_point que no tenga 2 numeros se SALTAN (n_skipped++), no lanzan. None se trata como lista vacia.
dict (NUNCA lanza): en exito {"status":"ok","n_links":int,"n_skipped":int} con n_links = anotaciones GOTO insertadas y n_skipped = entradas invalidas saltadas. En fallo {"status":"error","error":str}: pymupdf no disponible, pdf_path no es str / no existe, links no es lista, o cualquier excepcion global (el PDF original queda intacto porque el replace solo ocurre tras un save correcto). true
test_add_goto_link_basico
test_links_invalidos_se_saltan
test_archivo_inexistente_devuelve_error
python/functions/datascience/add_pdf_internal_links_test.py python/functions/datascience/add_pdf_internal_links.py

Ejemplo

import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from datascience import add_pdf_internal_links

# Tienes un PDF ya escrito por matplotlib PdfPages (sin hyperlinks internos).
# Quieres que el texto "Margen bruto" de la pagina 0 (rectangulo en puntos
# top-left) salte a su entrada del glosario en la ultima pagina (indice 7).
res = add_pdf_internal_links(
    "reports/eda.pdf",
    [
        {"src_page": 0, "src_rect": [72, 120, 180, 134], "dst_page": 7, "dst_point": [72, 200]},
        {"src_page": 0, "src_rect": [72, 140, 180, 154], "dst_page": 7, "dst_point": [72, 260]},
    ],
)
# res == {"status": "ok", "n_links": 2, "n_skipped": 0}

Cuando usarla

Justo DESPUES de escribir un PDF con matplotlib PdfPages (o cualquier motor que no genere hyperlinks internos) cuando necesitas que ciertos terminos o referencias sean clicables y salten a otra pagina del mismo documento — el caso canonico es enlazar los terminos de un AutomaticEDA con su entrada de glosario al final. Es un paso de postproceso: primero generas el PDF y calculas en que rectangulo quedo cada termino (en puntos PDF), luego pasas esa lista a esta funcion para inyectar las anotaciones GOTO.

Gotchas

  • Impura — reescribe el archivo IN SITU. El PDF en pdf_path se reemplaza por la version con los links. El guardado es seguro: escribe a un temporal .<base>.tmp_links en el MISMO directorio y hace os.replace atomico tras cerrar el documento, asi un fallo a mitad no corrompe el original. Aun asi, conserva una copia si el PDF es valioso.
  • Sistema de coordenadas: puntos top-left, igual que matplotlib. PyMuPDF y matplotlib (PdfPages) usan ambos PUNTOS PDF (1/72") con el origen ARRIBA- IZQUIERDA, asi que los rectangulos/puntos COINCIDEN: el src_rect que calcules con la geometria de la figura matplotlib se pasa tal cual, sin invertir el eje Y. (Ojo: el espacio de datos de matplotlib SI tiene el origen abajo; lo que coincide es el espacio de la PAGINA en puntos.)
  • Indices de pagina 0-based. src_page / dst_page son indices base 0 (la primera pagina es 0). Fuera del rango [0, page_count) el link se SALTA (cuenta en n_skipped), no lanza.
  • dict-no-throw, validacion por-link. Las entradas malformadas (no dict, page fuera de rango, src_rect sin 4 numeros, dst_point sin 2 numeros) se saltan individualmente e incrementan n_skipped; el resto de links validos se insertan igual. La funcion solo devuelve {status:error} ante fallos globales (pymupdf ausente, archivo inexistente, links no es lista).
  • error_type: error_go_core es metadata del registry, no comportamiento. Toda funcion impura debe declararlo y el indexer lo exige, pero el codigo NUNCA lanza esa excepcion: degrada al dict de estado.
  • Requiere PyMuPDF (import fitz). Si no esta instalado devuelve {"status":"error","error":"pymupdf no disponible: ..."}. En el registry el venv python/.venv ya lo trae.