Artículo técnico

JBIG2: backends externos y el enlazador de Free Pascal

PDFlibPas puede codificar imágenes bilevel como JBIG2 mediante dos backends distintos. Uno es un codificador MMR nativo en Object Pascal que está siempre presente. El otro es un codificador externo de diccionario de símbolos que produce resultados sustancialmente menores en texto escaneado, y es opcional: un proyecto debe enlazar la unidad del backend para que exista. Esa distinción es la fuente de la sorpresa más común con esta función, así que conviene enunciarla primero: DefaultJBIG2EncodeOptions solicita el codificador externo por defecto, y cuando la unidad del backend no está enlazada la solicitud cae en silencio a la vía MMR de Pascal

En Delphi y C++Builder el backend externo es un conjunto de objetos estáticos preconstruidos. En Free Pascal tuvo que convertirse en una DLL, y el camino hasta esa conclusión es una historia de enlazador que le sirve a 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. Quienes llaman la solicitan por medio del bit de opciones, PDF_JBIG2_OPTION_EXTERNAL_ENCODER, que vale 4, o por medio del parámetro UseExternalEncoder de los puntos de entrada extendidos de imagen. El paraguas de la biblioteca deliberadamente no incluye 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, los proyectos que la quieren la incluyen de forma explícita

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 archivos más grandes en lugar de un error. Si el tamaño de la salida importa lo suficiente para pedir el mejor codificador, importa lo suficiente para comprobar que lo recibieron

Flujo de solicitud de codificación JBIG2 de PDFlibPas donde la preferencia del codificador externo cae en silencio a la vía MMR nativa de Pascal sin la unidad del backend
Pedir el codificador externo de diccionario de símbolos es una preferencia: enlazado, la salida se reduce; sin enlazar, la vía MMR de Pascal corre en silencio con archivos más grandes
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 reemplazar un constructor de ajustes de formato basado en record por la variable global por defecto. Eso refleja bien lo portable que es el Pascal directo entre ambos compiladores

El lado de los símbolos fue el verdadero trabajo. El conjunto de objetos referencia 176 símbolos de C. De ellos, 128 ya tenían implementaciones en Pascal dentro de la unidad y solo necesitaban 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 de nombre público explícita. Veintisiete estaban compartidos con el códec JPEG 2000 y debían exportarse desde un único lugar, ya que 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 archivos Win32 más una serie de llamadas de la 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. El enlazador fue donde se detuvo todo

Tres rutas de enlazado, tres callejones sin salida

El enlazador interno de Free Pascal no puede leer los archivos objeto, porque fueron producidos por un compilador que emite secciones COMDAT asociativas y el enlazador interno informa de que no las soporta. Es un rechazo rotundo, no una advertencia

Cambiar a un enlazador externo parecía la respuesta. El enlazador binutils que viene con Free Pascal se cuelga sin más al aplicar la recolección de basura 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 se puede quitar de la línea de comandos; los interruptores documentados para suprimirla se ignoran en esta ruta. Suministrar un binutils mucho más nuevo falla de otra manera: no puede procesar el script de enlace de Free Pascal en absoluto, y produce una salida vacía sin el script y un muro de errores de reubicación con él

Un límite descubierto en el camino vale la pena conocerlo aunque nunca se topen con el problema del enlazador. El enlazador externo resuelve las rutas de los archivos objeto en relación con el directorio de salida del ejecutable y no con el árbol de fuentes, así que una directiva de inclusión de objeto relativa solo funciona cuando el directorio de salida coincide por casualidad con el directorio de trabajo en tiempo de compilación. Una biblioteca no puede asumir eso del proyecto de quien la consume, lo que por sí solo es una razón para preferir una biblioteca enlazada a objetos sueltos

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

Por qué otro compilador de C++ no ayuda

La idea obvia siguiente es reconstruir 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 interruptores. Una unidad de traducción mínima de C++ que contiene una plantilla, compilada con todas las funciones 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ímbolo de plano. El sentido inverso 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 una DLL, que es lo que ocurrió: el codificador y su dependencia de procesamiento de imágenes se construyen en una biblioteca que expone dos puntos de entrada planos en C, y la unidad del backend de Free Pascal los enlaza dinámicamente y se registra exactamente igual que el backend estático. La vía de Delphi y C++Builder no se tocó en absoluto, que es el resultado correcto; un problema de portabilidad en una toolchain no debe perturbar la toolchain que ya funcionaba

La polaridad es lo único que muerde de verdad

Entre un mapa de bits bilevel de Windows y un codificador JBIG2 hay un desajuste de convención que ningún sistema de tipos atrapará. Una scanline de un mapa de bits independiente del dispositivo con un bit por píxel trata un bit encendido como blanco. El codificador trata un bit encendido como negro. Entreguen las scanlines sin cambios y obtendrán un flujo JBIG2 perfectamente válido del negativo fotográfico de su 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, corregido invirtiendo cada byte
Los mismos bytes con significado opuesto: sin invertir cada byte el codificador produce un flujo JBIG2 válido del negativo fotográfico
// DIB de un bit: bit encendido significa blanco. Codificador JBIG2:
// bit encendido significa negro. Inviertan 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 flujos comprimidos no les dice nada, porque una imagen negativa comprime a un tamaño similar. Mirar la página solo prueba que no está invertida de forma evidente. La comprobación confiable es renderizar la salida de ambas vías de codificación, la nativa de Pascal y la externa, a PNG y compararlas byte a byte: ambos codificadores son sin pérdidas sobre la misma imagen de origen, así que cualquier cosa que no sea una coincidencia exacta es un error en uno de los dos. Esa comparación es ahora una prueba de regresión permanente, y es el tipo de aserción que vale la pena construir siempre que se supone que dos implementaciones coinciden exactamente

Qué backend usar

Para contenido bilevel general, medios tonos tramados, arte lineal y gráficos mixtos, el codificador MMR nativo de Pascal es suficiente y no cuesta nada en el despliegue. Para texto escaneado, que es el caso para el que JBIG2 fue diseñado, el codificador externo de diccionario de símbolos es donde vive la reducción de tamaño, porque factoriza las formas de glifos repetidas en un diccionario en lugar de recodificar cada aparición. Si producen archivos de documentos escaneados, esa diferencia es lo bastante grande para cambiar la planificación de almacenamiento

La pregunta de origen, cómo se produce la imagen bilevel en primer lugar, importa tanto como el tamaño de la salida; el renderizado monocromo por regiones se trata en el artículo de renderizado de regiones monocromas, y la estrategia de tamaño de todo el documento en optimización del tamaño de PDF y subconjunto de fuentes. Para lotes de escaneo con páginas repetidas, la deduplicación suele superar a la 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 del producto losLab PDF Developer Library