Artículo técnico

HotPDF en Free Pascal: deflate, AES y límites de códecs

HotPDF compila y corre bajo Free Pascal 3.2.2 con Lazarus, y el resumen honesto de ese port son dos frases. La creación de documentos, la carga, el guardado, la compresión, la descompresión, el cifrado y el descifrado funcionan todos sobre backends solo en Pascal, así que una aplicación Lazarus puede producir y consumir PDF real sin ninguna dependencia de C. Los códecs de imagen nativos opcionales no, porque los objetos Win64 preconstruidos usan una variante COFF que ninguno de los dos enlazadores de Free Pascal puede consumir, así que en esa toolchain los puntos de entrada se resuelven a stubs que fallan cerrados

Mapa de capacidades de HotPDF en Free Pascal: deflate, AES y backends de documentos funcionando en Pascal junto a stubs de códecs de imagen que fallan cerrados
Las funciones de documento, compresión y cifrado corren sobre backends solo en Pascal, mientras que los códecs de imagen nativos se resuelven a stubs que fallan cerrados

Pasar de "compila" a "funciona" tomó un conjunto específico de arreglos, y cada uno de ellos es una trampa que encontrará a cualquier otra base de código Delphi que migre a Free Pascal. Vale la pena anotarlas en el orden en que duelen

Por qué una unidad que compila no prueba nada

Porque una unidad de Pascal puede referenciar un símbolo que nunca va a hacer nada útil y aun así satisfacer al compilador. Cuando las 113 unidades de la biblioteca compilaron limpias bajo Free Pascal, los manejadores de contenedores de archivo genuinamente funcionaban, verificados por una prueba de humo que abría un CBZ y lo convertía a PDF. El aplanado de formularios XFA no funcionaba en absoluto, porque aplanar tiene que inflar el flujo del paquete /XFA comprimido y el punto de entrada de deflate seguía siendo un stub. Nada en la salida de la compilación distinguía esos dos casos

La regla que salió de ahí es corta. Antes de escribir en una nota de versión que una función funciona en una toolchain nueva, escriban una sonda de runtime que ejercite la función de punta a punta en esa toolchain. La cobertura de compilación es un prerrequisito, nunca evidencia. El panorama más amplio de lo que cubre el port está en las notas de soporte Win64 de Free Pascal y Lazarus

Un raise dentro de un stub cdecl no llega a quien llama

Este merece su propia sección porque el síntoma es tan engañoso. Las unidades stub exponen puntos de entrada de C como lo haría una biblioteca estática, así que un stub se ve así

// Parece razonable. No lo es.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

En Free Pascal para Win64 esa excepción no se propaga a quien llama. No hay ningún manejador try..except que la vea, porque desenrollar a través de una frontera cdecl declarada de esta manera no lleva el marco de excepción de Pascal; el proceso termina con código de salida 217. Desde el lado de la aplicación no hay error, ni mensaje, ni línea de registro, solo un programa que desaparece. Eso es estrictamente peor que una respuesta equivocada, porque una respuesta equivocada se puede manejar

Por qué una excepción lanzada dentro de un stub cdecl termina un proceso Free Pascal con código de salida 217 y cómo poner una guarda en el punto de entrada Pascal lo corrige
El marco de excepción de Pascal no puede desenrollarse a través de la frontera cdecl, así que el proceso muere en silencio; el arreglo pone la guarda antes de llegar al stub

El arreglo tentador es hacer que el stub devuelva un código de fallo en su lugar, y para inflate eso es correcto porque zlib tiene un retorno de error bien definido. En general está mal: un stub de jpeg_read_header que devuelve cero le dice a quien llama que siga con una estructura que nadie inicializó. El arreglo durable es poner una guarda en el punto de entrada de Pascal en lugar de dentro del stub con forma de C, usando la convención de fallo que esa API ya tenga

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Rechazar antes de llegar alguna vez al stub, con la convención
  // de fallo propia de esta API y no una excepción a través de cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib no es zlib, y la diferencia son dos clases de documentos

La implementación de deflate en Pascal disponible en Free Pascal maneja dos enmarcados: el envoltorio zlib y el deflate crudo. No maneja el enmarcado gzip, que zlib selecciona mediante valores de windowBits de 16 a 31, ni maneja el modo de detección automática que seleccionan los valores de 32 a 47. HotPDF necesita ambos. La vía segura de importación SVG pide 31, y el cargador tiene una escalera de respaldo que pide 47 cuando el enmarcado de un flujo es ambiguo. Saltarse cualquiera de los dos y toda una familia de documentos deja de abrirse, con un error de decodificación que apunta al flujo y no al enmarcado ausente

Cobertura de windowBits de paszlib frente a zlib: rangos de enmarcado gzip y autodetección ausentes para la importación SVG de HotPDF y el respaldo del cargador
paszlib maneja el envoltorio zlib y el deflate crudo, pero HotPDF también necesita windowBits 31 y 47, así que el shim debe suministrar el enmarcado gzip por sí mismo

Hay una segunda incompatibilidad más aguda. El record z_stream que declara paszlib no tiene la misma disposición de memoria que el de C: su campo msg es un shortstring en lugar de un puntero, y total_in y total_out son de 64 bits donde la ABI de C tiene palabras de máquina. Un record de quien llama no puede pasarse directo. El arreglo que funciona es mantener el estado de paszlib detrás del puntero state que el record público ya reserva, y copiar los campos públicos dentro y fuera en cada llamada. El CRC gzip y el tráiler de longitud de ocho bytes se contabilizan en esa misma capa de shim, que es el lugar natural para ellos ya que ella ya decide el enmarcado

Pasar un arreglo dinámico a un parámetro var sin tipo

Este es el error con más probabilidad de estar sentado en su código ahora mismo. Cuando pasan un arreglo dinámico a un parámetro var sin tipo, lo que recibe quien atiende la llamada es la dirección de la variable del arreglo, que es la dirección de un puntero, no la dirección de la carga útil. Así que una lectura hacia dentro sobrescribe la variable misma y lo que esté junto a ella

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // Incorrecto: entrega la dirección de la variable FBuffer
  FStream.Read(FBuffer, Length(FBuffer));

  // Correcto: entrega la dirección del primer byte de la carga útil
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

En Delphi la forma incorrecta con frecuencia parece funcionar, porque lo que corrompe es una ranura adyacente de la pila que nada lee después. En Free Pascal la misma línea produce un fallo de segmentación al primer uso. Lo que hace tan difícil verlo a simple vista es que los arreglos estáticos no tienen ese problema, ya que la variable de un arreglo estático es su propia carga útil, así que ambas escrituras son correctas en el mismo archivo según la declaración que esté a unos cientos de líneas de distancia

Contenedores ZIP sin System.Zip

Free Pascal no tiene equivalente de la unidad zip del RTL, y la alternativa disponible tiene tanto una superficie de API distinta como ninguna compatibilidad con el cifrado antiguo que los formatos de contenedor viejos aún usan, así que un pequeño lector dentro de la biblioteca resultó más corto que adaptarse a ella. Dos detalles del formato costaron tiempo y son fáciles de hacer mal

El primero es el byte de verificación de la cabecera de cifrado. Su duodécimo byte es normalmente el byte alto del CRC, pero cuando el bit 3 de la bandera de propósito general está encendido, lo que significa que los tamaños viven en un descriptor de datos final y el CRC aún no se conoce, el byte de verificación sale del byte alto de la hora de modificación. Implementen solo la forma del CRC y todo archivo escrito en modo streaming rechazará una contraseña correcta. El segundo es el campo extra ZIP64: sus tres campos de 64 bits aparecen en un orden fijo pero solo se escriben cuando el campo de 32 bits correspondiente está saturado, así que leerlos en desplazamientos fijos funciona en los archivos que probaron y falla en el siguiente. Analícenlos posicionalmente según qué campos de 32 bits estén saturados

Una comodidad que vale conocer: el flujo de descompresión de Free Pascal acepta un segundo argumento de constructor que se salta la cabecera zlib, que es exactamente lo que necesitan las entradas ZIP ya que almacenan deflate crudo. Esa vía no toca en absoluto el shim zlib de la biblioteca, así que no le afecta el backend de C ausente

Transparencia de glifos a color bajo la LCL

Leer el canal alfa de un glifo a color rasterizado es el único detalle de gráficos sin traducción directa. La clase PNG de la LCL no tiene ningún accesor de scanline que exponga el alfa, y asignar un PNG a un bitmap lo descarta, así que un emoji a color llega totalmente opaco y se compone con una caja negra detrás. La vía que funciona es la imagen de interfaz: crearla desde el PNG y después leer píxeles por el accesor de color, recordando que sus componentes son de 16 bits y necesitan desplazarse ocho bits a la derecha para volverse bytes. Esa superficie también usa el orden natural de filas de arriba hacia abajo, así que la inversión Height - 1 - Y que el código de scanlines de la VCL necesita debe eliminarse en lugar de portarse

Dos notas del sistema de compilación antes de reportar un error

Una recompilación completa en ocasiones falla con un símbolo indefinido cuyo nombre termina en un sufijo $crc y un valor hexadecimal. Ese sufijo se calcula a partir de los tipos de los parámetros, y deja de coincidir cuando una compilación construye una unidad contra dos versiones de interfaz distintas en la misma pasada. Volver a ejecutar la compilación lo despeja; la firma no está mal

Segundo, Free Pascal 3.2.2 no tiene métodos anónimos, así que en cualquier lugar donde la biblioteca usó cierres para cablear un pipeline paralelo, la compilación Free Pascal toma en su lugar un respaldo serial determinista. La salida es idéntica, el rendimiento no; si dependen del renderizado paralelo de páginas, esa es una razón para quedarse en Delphi por ahora, y el diseño del pipeline se describe en el artículo del pipeline de renderizado paralelo. La situación de los códecs de imagen es el otro lugar donde la elección de toolchain cambia la capacidad y no solo la velocidad, así que un despliegue Lazarus debería planificar sus formatos de imagen en consecuencia; la matriz actual por toolchain está en la página del producto HotPDF Delphi PDF component