Más allá del CRUD: Cómo arquitecté un Motor Dinámico de Compaginación PDF utilizando Go

# go# softwaredevelopment# architecture
Más allá del CRUD: Cómo arquitecté un Motor Dinámico de Compaginación PDF utilizando GoGabriel Bernardo Paredes Abreu

Decisiones de diseño, manejo de I/O pesado de binarios y el cálculo de algoritmos para...

Decisiones de diseño, manejo de I/O pesado de binarios y el cálculo de algoritmos para paginaciones predecibles.

Introducción

Llega un punto en la trayectoria de todo ingeniero backend donde nos preguntamos: ¿Todo en la web se resume a guardar y extraer datos de una base de datos? Afortunadamente, no. Las aplicaciones operativas serias requieren procesos heavy-computation.
Recientemente, desarrollé un Sistema de Gestión Documental corporativo.
El dominio requería que los usuarios estructuraran árboles jerárquicos de información (Documento → Múltiples Secciones → Múltiples Insumos/PDFs preexistentes). La "magia" residía en que el sistema, a nivel de backend, debía calcular el peso, generar índices visuales dinámicos, compaginar físicamente e inyectar numeración a cientos de páginas en un solo stream. Acá expongo sobre cómo Go facilitó el proceso.

El Diseño del Sistema: Del Dominio al Binario

Construir una maraña de mergeos secuenciales hubiese reventado el consumo de memoria I/O y agotado el File System. Dividí la carga en tres componentes fundamentales:

  • La Jerarquía de Dominio: Base de datos estructurada que mapea virtualmente el documento vía API usando el clásico trío Handlers-Services-Repositories.
  • La Planificación (PlanConstruccion): Antes de mover un solo bit real, un Pipeline recorre el árbol mapeando en memoria lo que sería el esqueleto.
  • El Motor PDF (Aislado): Una caja negra completamente abstracta (pdfcpu y gofpdf) que solo obedece a nuestro esqueleto inmutable, operando bajo los siguientes pasos calculados: Validar -> Limpiar -> Calcular Offsets -> Renderizar Índice Visual -> Merge -> Estampar Numeración.

Decisiones Técnicas e Infraestructura

Elegí fervientemente Go por sus superpoderes concurrentes, su excelente latencia en acceso al disco y su binario ejecutable predecible. En segundo lugar, quité toda referencia de librerías PDF dentro de la capa del Dominio; si el día de mañana deseamos llevar la creación a un Microservicio o Cola de Eventos (Event-Driven), el paquete Motor se migraría sin cambiar una sola línea de lógica comercial.

Retos de Algoritmia Encontrados

  1. La Paradoja del Índice Visual: Para saber exactamente en qué página de fondo arranca el "Capítulo 4", necesitamos saber la numeración de offset. Pero… ¿cómo sé cuánto offset ocupa el Índice si el Índice depende del número de las secciones? Lo resolvimos calculando de antemano el dibujado matemático del canvas en memoria (gofpdf) permitiendo apartar la cantidad exacta de "hojas hipotéticas".
  2. Compaginado a Doble Cara (Impresión Física): En el formato libro, un bloque denso no puede darse el lujo de iniciar en una página impar izquierda, arruinando la legibilidad física de la institución. Añadí un condicional robusto (MoverPaginaDerecha); que frente a escenarios pares, inyecta buffers limpios en blanco al Merge para obligar el salto natural de carro.

Para mantener el bajo acoplamiento, creamos un planificador algorítmico. Su tarea es barrer la colección inyectando hojas virtuales si el comportamiento comercial así lo requiere, mucho antes de tocar siquiera los módulos pesado de combinación (Merge).
package document_engine

import "fmt"

// PDFNode representa una estructura simplificada de nuestra jerarquía de negocio
type PDFNode struct {
 Name             string
 TotalPages       int
 RequiresRightAlignment bool   // Regla comercial: Si debe imprimirse a la derecha (Página impar)
 InjectBlankPage  bool         // Bandera de buffer virtual 
 CalculatedOffset int          // Cuál será su página física de arranque real
}

// CalculatePagination offsets procesa el árbol y devuelve el recuento matemático
func CalculatePaginationOffsets(nodes []PDFNode) int {
 currentPage := 1 // En la realidad de impresión, siempre arrancamos en 1

 for i := range nodes {
  // Validamos si la regla de negocio física choca con la página actual
  if nodes[i].RequiresRightAlignment && currentPage%2 == 0 {
   fmt.Printf("-> [Lógica] Colisión par. Inyectando buffer en blanco previo a '%s'\n", nodes[i].Name)

   // Mutamos el estado y forzamos a una página impar para resolver el choque
   nodes[i].InjectBlankPage = true
   currentPage++ 
  }

  // Asignamos el offset real de dónde empezará este componente en el reporte master
  nodes[i].CalculatedOffset = currentPage

  // Sumamos su peso para el siguiente nodo del ciclo
  currentPage += nodes[i].TotalPages
 }

 totalAssignedPages := currentPage - 1
 return totalAssignedPages
}

func main() {
    // Ejemplo de simulación
 nodes := []PDFNode{
  {Name: "Portada", TotalPages: 1, RequiresRightAlignment: false},
  {Name: "Capitulo_1_Intro", TotalPages: 2, RequiresRightAlignment: true}, // Arrancará en 3, salta la 2
  {Name: "Capitulo_2_Data", TotalPages: 5, RequiresRightAlignment: true},  // Arrancará en 5
 }

 total := CalculatePaginationOffsets(nodes)
 fmt.Printf("Total de planchas lógicas estimadas: %d\n", total)
}
Enter fullscreen mode Exit fullscreen mode