Artículo técnico

Backends de codificador JBIG2 y el enlazador de Free Pascal

PDFlibPas puede codificar imágenes bilevel como JBIG2 a través de dos backends distintos. Uno es un codificador MMR nativo en Object Pascal que está siempre presente. El otro es un codificador externo de diccionarios de símbolos que produce una salida sustancialmente menor en texto escaneado, y es opcional: un proyecto tiene que enlazar la unidad del backend para que exista siquiera. Esa distinción es el origen de la sorpresa más común con esta característica, así que conviene enunciarla primero: DefaultJBIG2EncodeOptions pide el codificador externo por defecto, y cuando la unidad del backend no está enlazada la petición cae en silencio a la ruta Pascal MMR

En Delphi y C++Builder el backend externo es un conjunto de objetos estáticos precompilados. En Free Pascal tuvo que convertirse en una DLL, y el camino hasta esa conclusión es una historia de enlazado útil para cualquiera que haya intentado enlazar objetos C++ en un programa Free Pascal

El registro es el contrato

La unidad del backend se registra desde su sección de inicialización llamando a RegisterJBIG2EncoderBackend. Los clientes la piden o bien mediante el bit de opciones PDF_JBIG2_OPTION_EXTERNAL_ENCODER, que vale 4, o bien mediante el parámetro UseExternalEncoder de los puntos de entrada extendidos de imagen. El paraguas de la biblioteca deliberadamente no arrastra la unidad del backend, porque cargar un conjunto grande de objetos debe ser decisión de cada proyecto; en el árbol de C++Builder, por ejemplo, la incluyen explícitamente los proyectos que la quieren

La consecuencia para quienes llaman es que pedir el codificador externo es una preferencia, no una garantía, y una compilación que olvida la unidad produce ficheros mayores en lugar de un error. Si el tamaño de la salida importa lo bastante para pedir el mejor codificador, importa lo bastante para comprobar que lo habéis recibido

Flujo de petición de codificación JBIG2 de PDFlibPas donde la preferencia del codificador externo cae en silencio a la ruta Pascal MMR nativa sin la unidad del backend
Pedir el codificador externo de diccionario de símbolos es una preferencia: enlazado, la salida se encoge; sin enlazar, la ruta Pascal MMR corre en silencio con ficheros mayores
uses
  PDFlibrary,
{$IFDEF FPC}
  PDFlibJBIG2EncDLL;    // backend dinámico para Free Pascal
{$ELSE}
  PDFlibJBIG2EncC;      // conjunto de objetos estáticos para Delphi / C++Builder
{$ENDIF}

var
  Pdf: TPDFlib;
  ImageId: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.NewPage;
    // Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
    // BlackDotSize, LossyLevel
    ImageId := Pdf.AddImageJBIG2FromFileEx('scan-page-1.tif',
      0, 1, 1, 0, 0, 0);
    if ImageId = 0 then
      raise Exception.Create('JBIG2 encoding failed');
    Pdf.SaveToFile('archive.pdf');
  finally
    Pdf.Free;
  end;
end;

Compilar la unidad fueron dos líneas. Los símbolos fueron el trabajo

Hacer que la propia unidad del backend compilara bajo Free Pascal tomó exactamente dos cambios: fijar el dialecto del ensamblador y sustituir un constructor de ajustes de formato basado en record por la variable global por defecto. Es un reflejo justo de lo portable que es el Pascal directo entre ambos compiladores

El lado de los símbolos fue el trabajo de verdad. El conjunto de objetos referencia 176 símbolos C. De ellos, 128 ya tenían implementaciones Pascal dentro de la unidad y solo necesitaron que se les adjuntara el nombre de exportación, porque Delphi usa el nombre de la función como nombre del símbolo mientras que Free Pascal exige una declaración explícita de nombre público. Veintisiete se compartían con el códec JPEG 2000 y hubo que exportarlos desde exactamente un sitio, porque definirlos dos veces rompe cualquier programa que enlace ambos. Los 21 restantes eran entradas de plataforma y del runtime de C, dieciséis funciones de ficheros Win32 más un puñado de llamadas de biblioteca estándar, y fueron a parar a una nueva unidad de compatibilidad

Nada de eso es conceptualmente difícil, y todo es necesario antes de que el enlazador siquiera lo intente. En el enlazador es donde se paró

Tres rutas de enlazado, tres callejones sin salida

El enlazador interno de Free Pascal no puede leer los ficheros objeto, porque los produjo un compilador que emite secciones COMDAT asociativas y el enlazador interno informa de que no las soporta. Es un rechazo rotundo, no un aviso

Cambiar a un enlazador externo parecía la respuesta. El enlazador binutils que trae Free Pascal se cuelga directamente al aplicar garbage collection de secciones a este archivo, y esa bandera es parte del conjunto fijo de parámetros que Free Pascal pasa para el destino Windows de 64 bits, así que no puede quitarse de la línea de comandos; los switches documentados para suprimirla se ignoran en esta ruta. Proporcionar un binutils mucho más nuevo falla de otra manera: no puede procesar en absoluto el script de enlazado de Free Pascal, produciendo una salida vacía sin el script y una pared de errores de reubicación con él

Un límite descubierto por el camino merece conocerse aunque nunca topéis con el problema del enlazador. El enlazador externo resuelve las rutas de los ficheros objeto relativas al directorio de salida del ejecutable y no al árbol de fuentes, de modo que una directiva relativa de inclusión de objetos solo funciona cuando el directorio de salida coincide, por casualidad, con el directorio de trabajo de compilación. Una biblioteca no puede asumir eso del proyecto de un cliente, lo cual es por sí solo una razón para preferir una biblioteca enlazada a objetos sueltos

Tres rutas de enlazado fallidas para los objetos del codificador JBIG2 C++ bajo Free Pascal y la DLL que expone dos puntos de entrada C planos que lo resolvieron
Las secciones COMDAT vencen al enlazador interno y ambos enlazadores externos fallan, así que el codificador C++ se distribuye como una DLL que la unidad del backend enlaza dinámicamente

Por qué un compilador C++ distinto no ayuda

La idea obvia siguiente es recompilar el lado C++ con un compilador cuyos objetos Free Pascal pueda leer. Tampoco funciona, y la razón es fundamental antes que cuestión de switches. Una unidad de traducción C++ mínima que contenga una plantilla, compilada con todas las características de generación de código desactivadas, sigue emitiendo símbolos externos débiles, porque la instanciación de plantillas e inline los produce por construcción. Free Pascal rechaza esa clase de símbolos rotundamente. La dirección inversa también falla: un enlazador C++ convencional no puede consumir objetos del otro compilador por el mismo manejo de secciones COMDAT

Así que el código C++ no puede entregarse como objetos a Free Pascal por ninguna ruta disponible. Sí puede entregarse como DLL, que es lo que ocurrió: el codificador y su dependencia de procesamiento de imágenes se construyen en una única biblioteca que expone dos puntos de entrada C planos, y la unidad del backend de Free Pascal los enlaza dinámicamente y se registra exactamente igual que el backend estático. La ruta de Delphi y C++Builder no se tocó en absoluto, que es el resultado correcto; un problema de portabilidad en una toolchain no debería perturbar a la toolchain que ya funcionaba

La polaridad es lo único que os va a morder

Entre un bitmap bilevel de Windows y un codificador JBIG2 hay un choque de convenciones que ningún sistema de tipos detectará. Una scanline de bitmap independiente de dispositivo de un bit por píxel trata un bit encendido como blanco. El codificador trata un bit encendido como negro. Entregad las scanlines sin cambios y obtendréis un stream JBIG2 perfectamente válido del negativo fotográfico de vuestra página

Convenciones de polaridad del DIB de un bit y de JBIG2, donde un bit encendido es blanco en la scanline y negro en el codificador, arreglado invirtiendo cada byte
Mismos bytes, significado opuesto: sin invertir cada byte el codificador produce un stream JBIG2 válido del negativo fotográfico
// DIB de un bit: bit encendido significa blanco. Codificador JBIG2:
// bit encendido significa negro. Invertid cada byte a la entrada
for I := 0 to RowBytes - 1 do
  Row[I] := Row[I] xor $FF;

El método de verificación importa tanto como el arreglo. Comparar longitudes de streams comprimidos no os dice nada, porque una imagen en negativo comprime a un tamaño similar. Mirar la página solo prueba que no está invertida de forma evidente. La comprobación fiable es renderizar a PNG la salida de ambas rutas de codificación, la Pascal nativa y la externa, y compararlas byte a byte: ambos codificadores son sin pérdida sobre la misma imagen de origen, así que cualquier cosa que no sea una coincidencia exacta es un error en uno de ellos. Esa comparación es ahora un test de regresión permanente, y es el tipo de aserción que merece construirse siempre que dos implementaciones se supone que coinciden exactamente

Qué backend usar

Para contenido bilevel general, semitonos con dithering, line art, gráficos mixtos, el codificador MMR nativo en Pascal es adecuado y no tiene coste de despliegue. Para texto escaneado, que es el caso para el que JBIG2 se diseñó, el codificador externo de diccionario de símbolos es donde vive la reducción de tamaño, porque factoriza las formas de glifo repetidas en un diccionario en lugar de recodificar cada aparición. Si estáis produciendo archivos de documentos escaneados, esa diferencia es lo bastante grande como para cambiar la planificación de almacenamiento

La pregunta aguas arriba, cómo se produce en primer lugar la imagen bilevel, importa igual de mucho para el tamaño de la salida; el renderizado monocromo por regiones se cubre en el artículo de renderizado de regiones monocromo, y la estrategia de tamaño de todo el documento en optimización de tamaño de PDF y subsetting de fuentes. Para conjuntos de escaneos con páginas repetidas, la deduplicación suele superar a una mejor compresión, que es el tema de deduplicación perceptual de imágenes. La disponibilidad de toolchains y backends por plataforma está en la página de producto de la losLab PDF Developer Library