# Agente PDF — Pablo

Agente para rellenar formularios PDF. Cubre todos los tipos: formularios rellenables (AcroForm), XFA, y PDFs planos/escaneados.

## Contexto

- **Servidor:** `ssh pablo` (se ejecuta localmente en Pablo o desde ARCHITECT via SSH)
- **Dependencias:** PyMuPDF (`pymupdf`), pypdf, PyPDFForm, pdftk-java
- **Archivos de trabajo:** `/tmp/` para PDFs temporales y previews

---

## Inicio de sesion

No requiere documentos de contexto adicionales. El agente es autocontenido.

Confirmar: **"Agente PDF listo."**

---

## Flujo de trabajo

```
1. Recibir PDF + datos a rellenar
2. Clasificar el PDF (AcroForm / XFA / plano)
3. Elegir estrategia según tipo
4. Rellenar
5. Verificar visualmente (render → imagen)
6. Entregar
```

---

## Paso 1 — Clasificar el PDF

```python
from pypdf import PdfReader

def clasificar_pdf(path: str) -> str:
    """Devuelve: 'acroform', 'xfa', o 'plano'"""
    reader = PdfReader(path)
    root = reader.trailer.get("/Root", {})

    if "/AcroForm" in root:
        acroform = root["/AcroForm"]
        if "/XFA" in acroform:
            return "xfa"
        fields = acroform.get("/Fields", [])
        if len(fields) > 0:
            return "acroform"

    # Verificar anotaciones widget
    for page in reader.pages:
        if page.annotations:
            for annot in page.annotations:
                obj = annot.get_object()
                if obj.get("/Subtype") == "/Widget":
                    return "acroform"

    return "plano"
```

---

## Estrategia A — AcroForm (PDF rellenable)

Usar **pypdf** para campos de texto y **PyMuPDF** para checkboxes/radios.

### Listar campos disponibles

```python
from pypdf import PdfReader

reader = PdfReader("formulario.pdf")
fields = reader.get_fields()
for nombre, campo in fields.items():
    tipo = campo.get("/FT")  # /Tx=texto, /Btn=checkbox/radio, /Ch=dropdown
    valor = campo.get("/V")
    print(f"  {nombre}: tipo={tipo}, valor_actual={valor}")
```

### Rellenar campos de texto

```python
from pypdf import PdfReader, PdfWriter

reader = PdfReader("formulario.pdf")
writer = PdfWriter()
writer.append(reader)

writer.update_page_form_field_values(
    writer.pages[0],
    {"nombre": "Pablo Valcarcel", "direccion": "Tai'an Lu 289"},
    auto_regenerate=False,  # SIEMPRE False para evitar corrupcion
)

with open("rellenado.pdf", "wb") as f:
    writer.write(f)
```

### Rellenar checkboxes

```python
import fitz  # PyMuPDF

doc = fitz.open("formulario.pdf")
for page in doc:
    for widget in page.widgets():
        if widget.field_type == fitz.PDF_WIDGET_TYPE_CHECKBOX:
            if widget.field_name == "acepto_condiciones":
                widget.field_value = widget.on_state()
                widget.update()
doc.save("rellenado.pdf")
```

### API simplificada con PyPDFForm

```python
from PyPDFForm import PdfWrapper

filled = PdfWrapper("formulario.pdf").fill({
    "nombre": "Pablo Valcarcel",
    "acepto": True,          # checkbox
    "pais": "Spain",         # dropdown
})
with open("rellenado.pdf", "wb+") as f:
    f.write(filled.read())
```

---

## Estrategia B — PDF plano (sin campos)

Para formularios escaneados o con fuentes custom. Se superpone texto con PyMuPDF.

### Lecciones aprendidas (caso real: solicitud consular)

1. **La extraccion de texto puede ser inutil.** PDFs con fuentes custom devuelven texto ilegible. No confiar en las coordenadas del texto extraido.
2. **Las coordenadas de los dibujos (drawings) SI son fiables.** Usar `page.get_drawings()` para encontrar casillas de verificacion y bordes de celdas.
3. **Calibrar con cuadricula visual.** Cuando las coordenadas no coinciden con lo visible, renderizar con regla superpuesta.
4. **Dibujar X con lineas, no con texto.** Las X de texto son dificiles de centrar. Las lineas cruzadas dentro del rectangulo de la casilla son precisas.
5. **Offset de 8pt por debajo de las etiquetas.** En formularios tipicos, las etiquetas estan arriba del campo. Sumar ~8pt al y para que el valor quede debajo.

### Encontrar posiciones de casillas (checkboxes)

```python
import fitz

doc = fitz.open("formulario.pdf")
page = doc[0]

# Buscar rectangulos cuadrados (~7x7 pt) = casillas de verificacion
paths = page.get_drawings()
for i, path in enumerate(paths):
    r = path["rect"]
    w, h = r.width, r.height
    if 5 < w < 15 and 5 < h < 15 and abs(w - h) < 3:
        print(f"Casilla #{i}: ({r.x0:.1f}, {r.y0:.1f}, {r.x1:.1f}, {r.y1:.1f})")
```

**Filtrar por zona.** Las casillas encontradas son muchas (glyphs de la fuente tambien aparecen). Filtrar por rango de coordenadas y por tamano ~6.9x6.9:

```python
# Ejemplo: buscar casillas en la zona de "Estado civil" (y: 610-660)
for path in paths:
    r = path["rect"]
    w, h = r.width, r.height
    if 6 < w < 8 and 6 < h < 8 and 610 < r.y0 < 660:
        print(f"Casilla: ({r.x0:.1f}, {r.y0:.1f}, {r.x1:.1f}, {r.y1:.1f})")
```

### Cuadricula de calibracion

Cuando las posiciones visuales no coinciden con las coordenadas del PDF:

```python
import fitz

doc = fitz.open("formulario.pdf")
page = doc[0]

shape = page.new_shape()
for y in range(0, int(page.rect.height), 25):
    shape.draw_line((0, y), (page.rect.width, y))
    page.insert_text((2, y + 10), str(y), fontsize=6, color=(1, 0, 0))
for x in range(0, int(page.rect.width), 50):
    shape.draw_line((x, 0), (x, page.rect.height))
    page.insert_text((x + 2, 12), str(x), fontsize=6, color=(0, 0, 1))
shape.finish(color=(0.8, 0.8, 0.8), width=0.3)
shape.commit()

pix = page.get_pixmap(matrix=fitz.Matrix(2, 2))
pix.save("/tmp/calibracion.png")
doc.close()
```

### Rellenar texto y marcar casillas

```python
import fitz

doc = fitz.open("formulario.pdf")
page = doc[0]
f = "helv"
c = (0, 0, 0.35)  # azul oscuro para distinguir del texto original
D = 8              # offset vertical debajo de etiquetas

# Insertar texto en campos
page.insert_text((150, 200 + D), "PABLO VALCARCEL", fontname=f, fontsize=12, color=c)

# Marcar casilla con X dibujada (lineas cruzadas)
# rect = coordenadas exactas del rectangulo de la casilla
rect = (306.9, 379.3, 313.8, 386.2)  # (x0, y0, x1, y1)
m = 1.2  # margen interior
shape = page.new_shape()
shape.draw_line((rect[0] + m, rect[1] + m), (rect[2] - m, rect[3] - m))
shape.draw_line((rect[0] + m, rect[3] - m), (rect[2] - m, rect[1] + m))
shape.finish(color=c, width=1.0)
shape.commit()

doc.save("rellenado.pdf")
```

### Caracteres especiales (n, tildes)

Los caracteres espanoles (n, a, e, i, o, u) estan en Latin-1, asi que las fuentes integradas de PyMuPDF los soportan:

```python
page.insert_text(
    (100, 200),
    u"Espa\u00f1a",          # Espana con ene
    fontname="helv",
    fontsize=10,
    color=(0, 0, 0),
    encoding=fitz.TEXT_ENCODING_LATIN,
)
```

Para caracteres fuera de Latin-1 (chino, arabe, etc.), usar un archivo TTF:

```python
page.insert_text(
    (100, 200),
    "texto en otros idiomas",
    fontfile="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
    fontname="dejavu",
    fontsize=10,
)
```

### Verificacion visual obligatoria

**SIEMPRE** renderizar y revisar antes de entregar:

```python
import fitz

doc = fitz.open("rellenado.pdf")
for i in range(doc.page_count):
    pix = doc[i].get_pixmap(matrix=fitz.Matrix(2, 2))
    pix.save(f"/tmp/preview_p{i}.png")
doc.close()
```

---

## Estrategia C — XFA

XFA esta deprecado en PDF 2.0 y tiene mal soporte en Python.

### Opcion 1: Convertir XFA a AcroForm

```bash
# Con pdftk (elimina la capa XFA, conserva AcroForm si existe)
pdftk input.pdf output limpio.pdf drop_xfa
```

```python
# Con Aspose.PDF (comercial)
import aspose.pdf as ap
doc = ap.Document("xfa.pdf")
doc.form.type = ap.forms.FormType.STANDARD
doc.save("acroform.pdf")
```

### Opcion 2: Tratar como PDF plano

Si la conversion falla, usar la estrategia B (superponer texto).

---

## Alternativa CLI: pdftk

Para AcroForm, pdftk es la herramienta mas robusta para rellenar y aplanar:

```python
import subprocess

# Generar FDF
def generar_fdf(campos: dict) -> str:
    header = "%FDF-1.2\n1 0 obj<</FDF<</Fields["
    body = ""
    for k, v in campos.items():
        if isinstance(v, bool):
            v = "Yes" if v else "Off"
        body += f"<</T({k})/V({v})>>"
    footer = "]>>>>\nendobj\ntrailer<</Root 1 0 R>>\n%%EOF"
    return header + body + footer

fdf = generar_fdf({"nombre": "Pablo", "acepto": "Yes"})
with open("/tmp/datos.fdf", "w") as f:
    f.write(fdf)

subprocess.run([
    "pdftk", "formulario.pdf",
    "fill_form", "/tmp/datos.fdf",
    "output", "rellenado.pdf",
    "flatten"
], check=True)
```

---

## Librerias — referencia rapida

| Libreria | Estrellas | Uso principal | Instalacion |
|----------|-----------|---------------|-------------|
| **PyMuPDF** | ~8.7k | Todo: leer, escribir, overlay, widgets | `pip install pymupdf` |
| **pypdf** | ~9k | AcroForm, merge, split (Python puro) | `pip install pypdf` |
| **PyPDFForm** | ~1.2k | API simple para rellenar formularios | `pip install PyPDFForm` |
| **pdfplumber** | ~6.5k | Extraer texto con coordenadas | `pip install pdfplumber` |
| **ReportLab** | ~4.3k | Generar PDFs, crear overlays | `pip install reportlab` |
| **pikepdf** | ~2.6k | Manipulacion bajo nivel | `pip install pikepdf` |
| **fpdf2** | ~1.1k | Generacion rapida con Unicode | `pip install fpdf2` |
| **pdftk** | CLI | Rellenar + aplanar (mas robusto) | `apt install pdftk-java` |

GitHub curado: [py-pdf/awesome-pdf](https://github.com/py-pdf/awesome-pdf)

---

## Errores comunes

| Error | Causa | Solucion |
|-------|-------|----------|
| Texto no aparece donde se espera | Fuente custom distorsiona coordenadas | Calibrar con cuadricula visual |
| X desalineada de la casilla | Se usaron coordenadas de glyphs de texto, no las casillas reales | Buscar rectangulos 6-8pt con `get_drawings()` |
| Caracteres como "?" o cuadrado | Fuente no soporta el caracter | Usar `encoding=fitz.TEXT_ENCODING_LATIN` o cargar TTF |
| Campos no se ven al imprimir | Formulario no aplanado | Aplanar con pdftk o `doc.bake()` |
| `auto_regenerate` corrompe apariencia | pypdf regenera mal las apariencias | Usar `auto_regenerate=False` siempre |
| Texto sobre las etiquetas | Falta offset vertical | Sumar D=8pt al eje Y |

---

## Sistemas de coordenadas

| Libreria | Origen | Y crece hacia |
|----------|--------|---------------|
| **PyMuPDF** | Esquina superior izquierda | Abajo |
| **ReportLab** | Esquina inferior izquierda | Arriba |
| **pdfplumber** | Esquina superior izquierda | Abajo |
| **Especificacion PDF** | Esquina inferior izquierda | Arriba |

Conversion entre sistemas:

```python
# De PyMuPDF/pdfplumber (top-left) a ReportLab (bottom-left)
y_reportlab = page_height - y_pymupdf

# De ReportLab (bottom-left) a PyMuPDF (top-left)
y_pymupdf = page_height - y_reportlab
```

---

## Cuando ejecutar

- Cuando el usuario necesita rellenar un formulario PDF (consular, bancario, administrativo)
- Cuando se recibe un PDF con campos a completar
- NO usar para generar PDFs desde cero (usar ReportLab directamente)