HotPDF compila y se ejecuta bajo Free Pascal 3.2.2 con Lazarus, y el resumen honesto de ese port son dos frases. La creación, carga, guardado, compresión, descompresión, cifrado y descifrado de documentos funcionan sobre backends solo Pascal, así que una aplicación Lazarus puede producir y consumir PDF real sin dependencia C alguna. Los códecs nativos opcionales de imagen no, porque los objetos Win64 precompilados usan una variante COFF que ninguno de los dos enlazadores de Free Pascal puede consumir, así que en esa toolchain los puntos de entrada resuelven a stubs que fallan de forma cerrada
Pasar de «compila» a «funciona» tomó un conjunto concreto de arreglos, y cada uno es una trampa que acabará encontrando cualquier otra base de código Delphi que se pase a Free Pascal. Merece anotarlas en el orden en que dolieron
Por qué una unidad que compila no prueba nada?
Porque una unidad Pascal puede referenciar un símbolo que nunca hará 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 funcionaban de verdad, verificado 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 stream de paquetes comprimido de /XFA y el punto de entrada 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, escribid una sonda en tiempo de ejecución que ejercite la función de principio a fin en esa toolchain. La cobertura de compilación es un prerrequisito, nunca una 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 de stubs exponen puntos de entrada 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 el desenrollo a través de una frontera cdecl declarada de este modo no lleva el marco de excepción Pascal; el proceso termina con el código de salida 217. Desde la aplicación no hay error, ni mensaje, ni línea de log, solo un programa que desaparece. Eso es estrictamente peor que una respuesta equivocada, porque una respuesta equivocada se puede gestionar
El arreglo tentador es hacer que el stub devuelva un código de fallo, 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 continúe con una estructura que nadie inicializó. El arreglo duradero es cortar el paso en el punto de entrada Pascal en lugar de dentro del stub con forma 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 siquiera al stub, con la convención de
// fallo de esta API en lugar de 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 Pascal de deflate disponible en Free Pascal maneja dos enmarcados: el wrapper zlib y el deflate crudo. No maneja el enmarcado gzip, que zlib selecciona mediante valores de windowBits de 16 a 31, ni el modo de detección automática que seleccionan los valores de 32 a 47. HotPDF necesita ambos. La ruta segura de importación SVG pide 31, y el loader tiene una escalera de fallback que pide 47 cuando el enmarcado de un stream 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 stream y no al enmarcado ausente
Hay una segunda incompatibilidad más afilada. El record z_stream que declara paszlib no tiene la misma disposición de memoria que el de C: su campo msg es un short string 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, por tanto, no puede pasarse directamente. El acuerdo 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 hacia dentro y hacia fuera en cada llamada. El CRC de gzip y el tráiler de longitud de ocho bytes se contabilizan en esa misma capa de shim, que es su lugar natural porque ya es dueña de la decisión de enmarcado
Pasar un array dinámico a un parámetro var sin tipo
Este es el error con más probabilidad de estar ahora mismo en vuestro código. Cuando pasáis un array dinámico a un parámetro var sin tipo, lo que recibe el destinatario es la dirección de la variable del array, que es la dirección de un puntero, no la dirección de la carga útil. Así que una lectura hacia él sobrescribe la variable misma y lo que tenga al lado
var
FBuffer: TBytes;
begin
SetLength(FBuffer, 65536);
// Equivocado: entrega la dirección de la variable FBuffer
FStream.Read(FBuffer, Length(FBuffer));
// Correcto: entrega la dirección del primer byte de carga útil
FStream.Read(FBuffer[0], Length(FBuffer));
end;
En Delphi la forma equivocada con frecuencia parece funcionar, porque lo que corrompe es una casilla adyacente de la pila que nada lee después. En Free Pascal la misma línea da un segmentation fault al primer uso. Lo que hace tan difícil verlo a simple vista es que los arrays estáticos no tienen ese problema, ya que una variable de array estático es su propia carga útil, así que ambas escrituras son correctas en el mismo fichero según la declaración que esté a unos centenares de líneas
Contenedores ZIP sin System.Zip
Free Pascal no tiene equivalente de la unidad zip de la RTL, y la alternativa disponible tiene tanto una superficie de API distinta como falta de soporte para el cifrado legacy que formatos de contenedor antiguos aún usan, de modo que un pequeño lector interno 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 comprobación de la cabecera de cifrado. Su duodécimo byte es normalmente el byte alto del CRC, pero cuando el bit 3 del flag de propósito general está activo, lo que significa que los tamaños viven en un data descriptor final y el CRC aún no se conoce, el byte de comprobación sale del byte alto de la hora de modificación. Implementad solo la forma 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 offsets fijos funciona en los archivos que probasteis y falla en el siguiente. Analizadlos posicionalmente según qué campos de 32 bits estén saturados
Una comodidad que merece conocerse: el stream de descompresión de Free Pascal acepta un segundo argumento de constructor que se salta la cabecera zlib, que es justo lo que necesitan las entradas ZIP ya que almacenan deflate crudo. Esa ruta no toca en absoluto el shim zlib de la biblioteca, así que no le afecta la ausencia del backend C
Transparencia de glifos en color bajo la LCL
Leer el canal alfa de un glifo en color rasterizado es el único detalle gráfico 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 en color llega completamente opaco y se compone con una caja negra detrás. La ruta que funciona es la imagen de interfaz: crearla desde el PNG y leer los píxeles a través del accesor de color, recordando que sus componentes son de 16 bits y necesitan desplazarse ocho para convertirse en bytes. Esa superficie también usa el orden natural de filas de arriba abajo, así que la inversión Height - 1 - Y que necesita el código de scanlines de la VCL hay que quitarla, no portarla
Dos notas de sistema de compilación antes de abrir un bug
Una recompilación completa de vez en cuando falla con un símbolo sin definir cuyo nombre acaba en un sufijo $crc y un valor hexadecimal. Ese sufijo se calcula a partir de los tipos de 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
Segunda: Free Pascal 3.2.2 no tiene métodos anónimos, así que donde la biblioteca usaba closures para cablear un pipeline paralelo, la compilación Free Pascal toma un fallback serie determinista. La salida es idéntica, el rendimiento no; si dependéis del renderizado paralelo de páginas, esa es una razón para quedarse en Delphi por el momento, 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 de producto de HotPDF Delphi PDF component