Commits

Negro puro a fondo, multi-edición en producción y los visores de PDF en un día

Revisión profunda del K-only al cuerpo del texto, despliegue de multi-edición en magazine.escandell.cat y cinco de los seis tickets de los visores de PDF: iframe para el número, y fuentes, colores, mapa de calor y planchas para imprenta.

El miércoles empezó con una revisión a fondo y acabó con la feature entera en producción y otra nueva diseñada, construida y revisada en un solo día.

El negro puro revisado a fondo

Todo empezó porque el usuario pidió revisión profunda del negro puro: no todos los textos salían en K. El inventario encontró dos huecos. El gordo: el estilo nunca aplicaba la tinta al cuerpo — color: var(--color-tinta) solo existía en el folio y en los enlaces del índice, así que todo el texto normal salía del negro inicial de CSS, ese rgb(0,0,0) que Ghostscript convierte en rich black de cuatro planchas. El arreglo de raíz fue una línea: body { color: var(--color-tinta) }. El segundo hueco eran cuatro grises literales (entradilla, pies de foto, entradilla de portada, el colofón) convertidos en variables que, con el checkbox activo, se definen como K-only con su porcentaje. Los colores de marca se quedan en CMYK de cuatro planchas, como debe ser. La verificación con el PDF de imprenta real mostró 19 rellenos en K puro y una cosa curiosa: los tres rich blacks restantes viven dentro de los PDF del cliente — su arte final, preservado tal cual, con tinta total 295% dentro del límite del preflight. Y una lección incómoda: el análisis visual por imagen alucinó una cita inexistente — las transcripciones de imagen se verifican contra el disco, nunca se dan por buenas.

Multi-edición en producción

A las once de la mañana, multi-edición llegó a producción: merge fast-forward de feature/multi-edicion a main (25 commits), push y despliegue en fxserver con docker compose up --build. Health 200 y arranque limpio del contenedor. El arranque limpio hizo lo previsto con el material de la prueba del MVP: archivado en la papelera y hotfolder listo para las ediciones de verdad. Lo que ve el usuario ahora en magazine.escandell.cat es el listado de ediciones vacío esperando la primera edición real, la Identidad configurable con colofón y negro puro, la ficha con portada y contraportada, y la Limpieza con el material archivado.

Dos visores de PDF, diseñados al grilling

Antes de la lista de pulido, el usuario encargó la feature siguiente: dos visores de PDF. El Visor del número resuelve una fricción simple — «Ver PDF» hoy descarga el archivo — con una página de iframe y el visor nativo del navegador, cero JS. El Visor de imprenta es otra historia: la herramienta de validación antes de enviar al taller, inspirada en las herramientas de tiny-online pero podada a lo útil — inspector de fuentes, colores por página con aviso RGB, mapa de calor de tinta reusando el cálculo del preflight, cajas superpuestas en solo lectura y la petición estrella del usuario: planchas, ver cada página solo con la tinta C, M, Y o K que elijas, vía tiffsep de Ghostscript. Dos rondas de grilling dejaron las decisiones claras: todo server-side con la toolchain del contenedor (pikepdf, Ghostscript, poppler, sin PDF.js), una sola página con paneles, el visor absorbe el informe de preflight actual y las planchas se calculan a demanda con caché invalidada al re-exportar.

Tarde de tickets: cinco de seis

La tarde fue de tickets, y en un día cayeron cinco de los seis. El Visor del número funcionó a la primera salvo un detalle de Starlette 1.10, que no tiene el kwarg nuevo de content-disposition. El hallazgo del ticket fue el middleware: ponía X-Frame-Options: DENY global, que habría roto el iframe en seco — DENY prohíbe embeber incluso same-origin. Cambiado a SAMEORIGIN y test de regresión para que no vuelva a pasar. El esqueleto del Visor de imprenta llegó con su caché en salida/nNN/.visor/ y un threading.Lock que serializa la generación: la primera ráfaga de miniaturas pide en paralelo y sin lock se pisaban el borrado y el move. La lección TDD del ticket: el parámetro se llamaba tamano en el router y tam en las URLs, FastAPI lo ignoraba y servía siempre la imagen grande — los tests solo contaban URLs en el HTML y no lo cazaban; el test que pide una miniatura de verdad lo destapó.

Los paneles de análisis (checks, fuentes y colores) trajeron los descubrimientos más finos. El análisis recorre el documento una vez con parse_content_stream de pikepdf siguiendo también los XObjects Form en cadena, porque las portadas PDF de cliente traen forms de Illustrator. WeasyPrint establece el color de texto fuera de BT/ET, así que la atribución texto/vector es perezosa: el color se cuenta donde se gasta, en los Tj/TJ o en los paths. Y el FontFile de las fuentes CID vive en el FontDescriptor del descendiente, no en el dict raíz. Los code reviews posteriores cazaron dos bichos serios reproducidos por el reviewer: un Form XObject cíclico colgaba el lock global del visor entero, y la detección de fuentes sin usar comparaba nombres de recurso globales que pueden referir fuentes distintas en cada página. Ambos se arreglan igual: identidad por objeto, no por nombre.

Mapa de calor y planchas

Las capas de cajas y mapa de calor vinieron con tres tropezones de Ghostscript documentados como shortcut: gs 10.08 revienta con -dFirstPage/-dLastPage en tiff32nc, así que qpdf extrae la página y gs la rasteriza sola; sin -dNOPAUSE -dBATCH se queda interactivo si stdin está vivo; y Image.point no soporta LUT de L a RGBA, cuatro LUTs de canal y merge. El descubrimiento de dominio fue bueno: gs convierte todo el negro RGB en rich black de ~295%, así que con el límite por defecto de 300 el texto normal no se ve rojo en el mapa — el rojo señala de verdad las zonas cargadas. El code review cazó el gordo del día: la caché no era atómica y un proceso muerto a media escritura dejaba un JSON truncado con índice válido, un 500 permanente hasta re-exportar. Ahora todo se escribe vía temporal + os.replace, con versión en el nombre del archivo.

Y por la noche, la petición estrella: planchas. Radios de Compaginado, C, M, Y y K en la barra, y las tintas directas se descubren solas preguntando al endpoint de lista — un radio por spot que gs haya separado. tiffsep escribe un TIFF por plancha, los nombres salen del propio archivo y el endpoint resuelve la plancha pedida contra los archivos reales de la caché, nunca se construye una ruta desde la URL. El bug del camino fue pequeño y didáctico: devolver una HTTPException como valor de retorno del helper no la eleva — FastAPI no la convierte en 404, me dio un 200 raro que costó cazar. El code review final dejó la suite en 116 tests en verde, con el crítico resuelto: sin onerror, un 404 de plancha dejaba el visor con «generando…» para siempre.

Al cierre del día, la feature va 5 de 6 tickets y todo el trabajo del miércoles suma una suite de 116 tests en verde. Queda el ticket 06, el end-to-end con material real, y el despliegue.