Layout de firmas (2 columnas)¶
El modulo app/layout.py implementa el algoritmo de posicionamiento automatico de firmas
en documentos PDF, usando un layout de 2 columnas.
Diagrama del layout¶
ULTIMA PAGINA
+-------------------------------------+
| Contenido documento |
| "end-text" | <-- Punto de referencia
| |
| [Firma 1] [Firma 2] | X=50 (impar) | X=270 (par)
| [Firma 3] [Firma 4] |
| [Firma 5] [Firma 6] | Crece hacia abajo
| ... ... |
| |
| ---- MIN_SIGNATURE_Y = 100 ----- | <-- Limite inferior
+-------------------------------------+
Constantes de configuracion¶
Definidas en app/config.py:
| Constante | Valor | Descripcion |
|---|---|---|
FIRST_SIGNATURE_X |
50 | Columna izquierda (firmas impares) |
SECOND_SIGNATURE_X |
270 | Columna derecha (firmas pares) |
SIGNATURE_WIDTH |
200 | Ancho de cada firma (pts) |
SIGNATURE_HEIGHT |
80 | Alto de cada firma (pts) |
ROW_SPACING |
20 | Espaciado entre filas (pts) |
SIGNATURE_OFFSET_BELOW |
100 | Distancia debajo del "end-text" (pts) |
MIN_SIGNATURE_Y |
100 | Posicion Y minima (margen inferior) |
PAGE_WIDTH |
612 | Ancho pagina Letter (pts) |
PAGE_HEIGHT |
792 | Alto pagina Letter (pts) |
Sistemas de coordenadas¶
Dos sistemas diferentes
PyMuPDF y ReportLab usan sistemas de coordenadas opuestos:
- PyMuPDF: Origen
(0,0)en esquina superior izquierda, Y crece hacia abajo - ReportLab: Origen
(0,0)en esquina inferior izquierda, Y crece hacia arriba
La conversion es: reportlab_y = page_height - pymupdf_y
Algoritmo paso a paso¶
1. Buscar "end-text"¶
def find_end_text_positions(pdf_content: bytes) -> List[Tuple[float, float]]:
doc = fitz.open(stream=pdf_content, filetype="pdf")
last_page = doc[-1]
text_instances = last_page.search_for("end-text")
positions = [(rect.x0, rect.y0) for rect in text_instances]
return positions
Solo busca en la ultima pagina del documento. Si no encuentra "end-text", el proceso falla con LayoutError.
2. Calcular posicion Y de la primera firma¶
def get_first_signature_y_position(pdf_content: bytes) -> Optional[float]:
# Encontrar "end-text" en coordenadas PyMuPDF
text_pymupdf_y = text_rect.y0
# Mover 100pts hacia abajo (en PyMuPDF)
signature_pymupdf_y = text_pymupdf_y + SIGNATURE_OFFSET_BELOW
# Convertir a coordenadas ReportLab
reportlab_y = page_height - signature_pymupdf_y
return reportlab_y
3. Calcular posicion de la N-esima firma¶
def calculate_signature_position(
existing_signatures_count: int = 0,
pdf_content: bytes = None
) -> Tuple[float, float]:
# Posicion Y base (primera firma)
first_signature_y = get_first_signature_y_position(pdf_content)
# Numero de la nueva firma
signature_number = existing_signatures_count + 1
# Columna X: impares a la izquierda, pares a la derecha
if signature_number % 2 == 1:
x = FIRST_SIGNATURE_X # 50
else:
x = SECOND_SIGNATURE_X # 270
# Fila Y: cada 2 firmas baja una fila
row = (signature_number - 1) // 2
y = first_signature_y - (row * (SIGNATURE_HEIGHT + ROW_SPACING))
# Validar espacio
if not validate_signature_space(x, y):
raise LayoutError("FULLPAGE")
return (x, y)
4. Ejemplo numerico¶
Con end-text encontrado en PyMuPDF y=500 y pagina Letter (height=792):
| Firma | Numero | Columna | X | Fila | Y (ReportLab) |
|---|---|---|---|---|---|
| 1ra | 1 | Izquierda | 50 | 0 | 192 |
| 2da | 2 | Derecha | 270 | 0 | 192 |
| 3ra | 3 | Izquierda | 50 | 1 | 92 |
| 4ta | 4 | Derecha | 270 | 1 | 92 |
| 5ta | 5 | - | - | 2 | Error FULLPAGE |
Calculo primera firma: page_height - (text_y + offset) = 792 - (500 + 100) = 192
Deteccion de firmas existentes¶
def count_existing_signatures(pdf_content: bytes) -> int:
# 1. Contar firmas por texto visual
visual_count = page_text.count(SIGNATURE_DETECTION_TEXT)
# Patrones de respaldo
other_patterns = [
"Firmado digitalmente por:",
"Digitally signed by:",
"FIRMA DIGITAL",
"DIGITAL SIGNATURE"
]
# 2. Contar firmas PAdES embebidas
pades_count = count_pades_signatures(pdf_content)
# Usar el mayor de ambos
return max(visual_count, pades_count)
Deteccion dual
El sistema detecta tanto firmas visuales (por patron de texto) como firmas PAdES (por metadatos embebidos). Usa el mayor para evitar superposicion en cualquier caso.
Validacion de espacio¶
def validate_signature_space(x: float, y: float) -> bool:
# Margenes horizontales
if x < SIGNATURE_MARGIN_LEFT:
return False
if x + SIGNATURE_WIDTH > PAGE_WIDTH - SIGNATURE_MARGIN_RIGHT:
return False
# Limite inferior (critico)
if y - SIGNATURE_HEIGHT < MIN_SIGNATURE_Y:
return False
# Limite superior
if y > PAGE_HEIGHT - SIGNATURE_MARGIN_TOP:
return False
return True
Si la validacion falla, se lanza LayoutError("FULLPAGE") indicando que no hay mas espacio
para firmas en la pagina.