Artículo técnico

Bloques PDF en Delphi: rellenos de texto, imagen y PDF

PDFlibPas (losLab PDF Library) rellena plantillas de bloques PDF desde Delphi: un bloque es un rectángulo con nombre en una página plantilla, tipado como texto, imagen o PDF, y FillTextBlock, FillImageBlock y FillPDFBlock dibujan datos variables en él. Los bloques se encuentran como anotaciones de página PDFlibBlock:Text, PDFlibBlock:Image y PDFlibBlock:PDF, o en un diccionario page-piece bajo /PieceInfo

El caso de uso es del tipo aburrido y de alto volumen. Un diseñador maqueta un certificado, una entrada de evento o una carta de cobranza una vez, y el programa estampa un nombre, un número de asiento, una imagen de firma o una página de código de barras pre-renderizada en ranuras fijas para diez mil registros. Los campos de formulario pueden hacer parte de eso, pero un campo AcroForm es un objeto interactivo con su propio flujo de apariencia y sus propias reglas, y normalmente termina aplanándolo de todos modos. Un bloque es más simple: un rectángulo más un puñado de pistas de estilo, consumido una vez y luego desechado

¿Qué es exactamente un bloque PDF en PDFlibPas?

Un bloque de PDFlibPas es una anotación de página cuyo /Subtype es PDFlibBlock:Text, PDFlibBlock:Image o PDFlibBlock:PDF, cuyo /Rect es el área del placeholder, y cuya cadena /Contents contiene name;key=value;key=value. El primer segmento es el nombre del bloque, todo lo demás es una lista de propiedades. TPDFlib.AddBlock escribe la anotación con /F 4 (Print) y sin flujo de apariencia, así que un bloque sin rellenar no ocupa píxeles en pantalla ni en papel

Anatomía de la plantilla de bloques de PDFlibPas para datos variables en Delphi: una anotación de página con /Subtype PDFlibBlock:Text, Image o PDF, un placeholder /Rect, una cadena /Contents con nombre y propiedades key=value, /F 4 Print y sin flujo de apariencia, con el diccionario page-piece /PieceInfo /PDFlib /Blocks como segunda fuente
Los bloques se enumeran en base 1 por página con las anotaciones primero y los bloques page-piece después; las entradas de arreglo se vuelven Block1 y Block2, las llaves /Custom se aplanan en propiedades, y cualquier cosa distinta de Image, PDF o Page se trata como texto

El lado de lectura tiene una segunda fuente. Después de enumerar las anotaciones de bloque, PDFlibPas recurre a la ruta del diccionario de página /PieceInfo << /PDFlib << /Blocks ... >> >> y su variante /PieceInfo << /PDFlib << /Private << /Blocks ... >> >> >>. Los diccionarios page-piece son PDF estándar, definidos en ISO 32000-1 §14.5 como el lugar donde una aplicación guarda datos privados adjuntos a una página, y ahí es donde viven normalmente las definiciones de bloques de herramientas externas de autoría de plantillas. El contenedor /Blocks puede ser un diccionario indexado por nombre de bloque o un arreglo simple; para la forma de arreglo PDFlibPas nombra las entradas Block1, Block2 y así sucesivamente a menos que un bloque traiga su propio /Name

Dentro de un bloque page-piece, el valor /Subtype (o /Type) Image se mapea a un bloque de imagen, PDF o Page a un bloque PDF, y cualquier otra cosa se trata como texto. Todas las demás llaves excepto /Type, /Subtype, /Name, /Rect y /Custom se vuelven propiedades, y las llaves de un diccionario /Custom anidado también se aplanan, así que una entrada /Custom << /department (Finance) >> se lee como GetBlockProperty(Page, Index, 'department'). Los índices de bloque son base 1 por página, bloques de anotación primero, bloques page-piece después

Autoría de una plantilla de bloques con AddBlock

TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) agrega un bloque a la página seleccionada actualmente y devuelve 1 en caso de éxito. Devuelve 0 para un nombre vacío, un ancho o alto cero o negativo, o un BlockType distinto de 'text', 'image' o 'pdf' (sin distinguir mayúsculas). Properties se anexa al nombre con un punto y coma, así que usa la misma sintaxis key=value;key=value que la cadena guardada

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // arte estático: borde, logo, texto fijo ...
    Lib.DrawText(72, 200, 'This certifies that');

    Lib.AddBlock('text', 'recipient', 72, 230, 450, 44,
      'fontname=Times-Bold;fontsize=30;fillcolor={cmyk 0 0.55 1 0.1};alignment=center');
    Lib.AddBlock('text', 'course', 72, 290, 450, 24,
      'fontname=Helvetica;fontsize=14;alignment=center;fitmethod=word');
    Lib.AddBlock('image', 'signature', 360, 600, 160, 60, '');
    Lib.AddBlock('pdf', 'seal', 72, 580, 100, 100, '');

    if Lib.GetBlockCount(1) <> 4 then
      raise Exception.Create('Template blocks missing');
    Lib.SaveToFile('certificate-template.pdf');
  finally
    Lib.Free;
  end;
end;

El parser de propiedades es deliberadamente flexible. Las llaves se comparan sin distinguir mayúsculas, los valores se recortan pero por lo demás se devuelven verbatim (un valor escrito como {top left} vuelve con sus llaves), y una llave faltante devuelve una cadena vacía. El único límite duro es el separador: un valor no puede contener punto y coma, porque el punto y coma termina el segmento

¿Cómo aplica FillTextBlock fuente, color y alineación?

TPDFlib.FillTextBlock(Page, BlockIndex, Text, Options) lee las propiedades del bloque, las aplica al estado de dibujo, ajusta el texto en el rectángulo del bloque con FitTextBox, y luego restaura la fuente, el tamaño de texto, el color de texto y la alineación del llamador en un bloque finally. Esa restauración importa en un bucle de mezcla: un bloque de encabezado rojo de 30 pt no puede filtrar su estilo en la siguiente llamada DrawText que usted haga en la misma página

  • fontname (o font): primero reutiliza una fuente ya cargada en el documento con ese nombre, luego mapea los nombres Standard 14 como Helvetica-Bold, Times-Roman o Courier, y por último intenta AddTrueTypeFont con el nombre
  • fontsize (o size): el tamaño de texto que se pasa al ajustador, que lo reduce si el texto no cabe
  • fillcolor (o textcolor, color): #RRGGBB, {rgb r g b}, {cmyk c m y k}, {gray g}, listas de 3 o 4 números sueltas, o los nombres black, white, red, green, blue y gray; los componentes RGB mayores que 1 se leen como 0–255
  • alignment, align y position se leen juntos: left, right, center (o centre) y justify fijan la alineación horizontal, top y bottom fijan la colocación vertical, y el valor por defecto es centrado verticalmente
  • fitmethod: word o nofitsplit evita que las palabras largas se partan, scale o expand deja que el tamaño crezca para llenar la caja

La ruta CMYK es la razón por la que las imprentas se interesan. {cmyk 0 0.55 1 0.1} llama a SetTextColorCMYK, así que el texto variable se emite en DeviceCMYK y pega en el color de marca en la prensa sin un viaje de ida y vuelta por RGB. FillTextBlock devuelve 1 cuando el texto se dibujó; devuelve 0 para un Text vacío, un bloque faltante, o un bloque que no es de texto, y en todos esos casos el bloque queda en su lugar

Orden de procesamiento de FillTextBlock en PDFlibPas para Delphi: las propiedades del bloque fontname, fontsize, fillcolor y alignment se aplican al estado de dibujo, el valor CMYK pasa por SetTextColorCMYK, el texto se ajusta con FitTextBox, y la fuente, el tamaño, el color y la alineación del llamador vuelven en un bloque finally
El estilo del bloque se aplica una vez y se revierte para que un encabezado rojo de 30 pt no se filtre en el siguiente DrawText; la llamada devuelve 1 cuando se dibujó texto y 0 con el bloque en su lugar para texto vacío o un bloque que no es de texto

Rellenar bloques de imagen y PDF, y los bits de Options

FillImageBlock(Page, BlockIndex, ImageID, Options) dibuja una imagen agregada previamente, y FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) importa una página de otro documento cargado como un Form XObject y la dibuja, lo que mantiene contenido vectorial como un sello, una página de código QR o un membrete totalmente vectorial. Ambos preservan la relación de aspecto del origen y centran el resultado dentro del rectángulo del bloque. Las tres funciones de relleno comparten los mismos bits de Options: el bit 1 (valor 1) elimina el bloque tras un relleno exitoso, ya sea borrando la anotación o quitando la entrada del contenedor /Blocks, y el bit 2 (valor 2) estira el contenido de imagen y PDF al rectángulo exacto en lugar de ajustarlo

function FillCertificate(const TemplateFile, SealFile, SignatureFile,
  OutFile: WideString; Values: TStrings): Boolean;
var
  Lib: TPDFlib;
  SealID, SignatureID, I: Integer;
  BlockName, BlockType: WideString;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(SealFile, '') <> 1 then Exit;
    SealID := Lib.SelectedDocument;          // el ID real del documento
    if Lib.LoadFromFile(TemplateFile, '') <> 1 then Exit;
    // la plantilla es ahora el documento seleccionado
    SignatureID := Lib.AddImageFromFile(SignatureFile, 0);

    // recorra hacia atrás: el bit 1 elimina cada bloque, lo que renumera los posteriores
    for I := Lib.GetBlockCount(1) downto 1 do
    begin
      BlockName := Lib.GetBlockName(1, I);
      BlockType := Lib.GetBlockType(1, I);
      if BlockType = 'text' then
        Lib.FillTextBlock(1, I, Values.Values[BlockName], 1)
      else if (BlockType = 'image') and (SignatureID <> 0) then
        Lib.FillImageBlock(1, I, SignatureID, 1)
      else if BlockType = 'pdf' then
        Lib.FillPDFBlock(1, I, SealID, 1, 1);
    end;
    Result := Lib.SaveToFile(OutFile) = 1;
  finally
    Lib.Free;
  end;
end;

Dos detalles en ese bucle son fáciles de equivocar. El bucle corre desde el índice más alto hacia abajo porque eliminar el bloque 2 convierte el viejo bloque 3 en bloque 2; contar hacia arriba con el bit 1 activo se saltea un bloque de por medio. Y el documento de origen para FillPDFBlock se carga primero y su ID se captura antes de cargar la plantilla, porque los IDs de imagen pertenecen al documento seleccionado, así que AddImageFromFile tiene que correr mientras la plantilla está seleccionada. Para corridas por lotes donde el sello o el membrete cambia por registro, el cosido de páginas con Form XObject y las plantillas de marca de agua componibles en PDFlibPas usan el mismo mecanismo de capturar y dibujar sobre páginas enteras

¿Por qué las coordenadas de los bloques salen invertidas?

Los rectángulos de bloque salen invertidos cuando el código mezcla los dos sistemas de coordenadas con los que PDFlibPas trabaja. Un /Rect de anotación se guarda en el espacio de usuario de PDF, medido desde la parte inferior de la página (ISO 32000-1 §12.5.2), mientras que la API de dibujo de TPDFlib mide desde la parte superior. AddBlock toma entrada basada en superior y guarda el borde inferior como PageHeight - Top - Height; GetBlockRect y las tres funciones de relleno leen el rectángulo de vuelta y lo convierten con PageHeight - URy

En la práctica eso significa: use GetBlockRect(Page, BlockIndex, Left, Top, Width, Height) cuando necesite la geometría de la ranura, por ejemplo para dibujar un marco de depuración o colocar una leyenda junto a un bloque, y no lea el /Rect mediante una llamada genérica de anotación para luego alimentarlo directo a DrawText. Un bloque page-piece con /Rect [40 700 240 730] en una página carta de 792 pt reporta Top = 62, no 700. El bug que esto previene es el clásico donde cada valor rellenado cae espejado cerca del margen inferior y nada da error

Inversión de coordenadas del rectángulo de bloque en PDFlibPas para Delphi: los valores /Rect viven en el espacio de usuario de PDF basado en inferior mientras AddBlock toma entrada basada en superior y guarda PageHeight menos Top menos Height, así que /Rect 40 700 240 730 en una página carta de 792 pt se lee como Top 62 a través de GetBlockRect y su conversión PageHeight menos URy
Mezclar los dos sistemas de coordenadas espeja cada valor rellenado hacia el margen inferior sin un solo error; lea la geometría de la ranura mediante GetBlockRect en lugar de alimentar el /Rect directo a DrawText

Trampas de las que vale la pena aprender

El bug más instructivo en el código de bloques fue un desliz de un carácter. Las anotaciones de bloque se reconocen con una prueba de prefijo sobre el subtipo, y 'PDFlibBlock:' tiene 12 caracteres. Una versión temprana comparaba Copy(Subtype, 1, 13) contra ese literal de 12 caracteres, lo que nunca puede ser igual, así que GetBlockCount devolvía 0 para cada plantilla y cada relleno reportaba "no such block". Nada se colgó y nada quedó registrado. La lección se generaliza a cualquier motor de plantillas: aserte el conteo de bloques de una plantilla conocida en una prueba, porque una enumeración silenciosa sin coincidencias se ve exactamente como una página vacía

La segunda trampa son los IDs de documento. TPDFlib.LoadFromFile devuelve un código de estado, 1 para éxito y 0 para falla, no el ID del documento que acaba de cargar. Pasar ese 1 como SourceDocumentID a FillPDFBlock falla porque 1 no es un ID de documento válido. Llame a SelectedDocument justo después de una carga exitosa para obtener el ID real, y a SelectDocument para volver a la plantilla si cargó el origen después

Una más pequeña: FillTextBlock con una cadena vacía no hace nada y conserva el bloque. Para una mezcla de correspondencia con campos opcionales, como una segunda línea de dirección faltante, eso normalmente es lo que usted quiere, pero la anotación sin rellenar se queda entonces en el archivo de salida. Elimínela explícitamente con DeleteAnnotation o rellene un espacio con el bit 1 si la salida debe estar limpia. Todas las llamadas de bloque guardan y restauran la página seleccionada, así que un relleno en la página 3 no cambia adónde va su siguiente llamada de dibujo

¿Cuáles son los límites de interoperabilidad de las anotaciones de bloque?

Las anotaciones de bloque son una convención privada, no una función estándar de PDF. PDFlibBlock:Text y sus hermanos no están entre los tipos de anotación listados en ISO 32000-1 §12.5.6.1, así que un visor las trata como anotaciones desconocidas. Como el flag Invisible está apagado y no hay flujo de apariencia, los visores conformes no dibujan nada por ellas, pero algunos visores pueden listarlas en un panel de comentarios, y otras herramientas PDF no entenderán ni preservarán su semántica

  • Rellene con el bit 1 activo para cualquier cosa que salga de su sistema, así el archivo final contiene solo contenido de página ordinario
  • PDF/A no permite tipos de anotación fuera de la especificación PDF (ISO 19005-1 §6.5.2), así que una salida archivada no debe llevar anotaciones de bloque sin rellenar
  • Los bloques page-piece son más seguros de conservar en una plantilla, ya que /PieceInfo es estándar y los consumidores ignoran los datos que no les pertenecen
  • Los bloques no son campos de formulario: no hay interactividad, no hay valor de campo en el archivo ni exportación a FDF o XFDF; para datos que deben permanecer editables vea el intercambio de datos de formulario FDF, XFDF y XFA en PDFlibPas

Si la plantilla tiene que declarar sus campos variables de forma estandarizada y verificable por un validador para un flujo de impresión, los bloques son la herramienta equivocada. Para eso sirve PDF/VCR-1, y el manifiesto de placeholders de plantilla PDF/VCR-1 en PDFlibPas cubre cómo viven esos placeholders en el árbol de estructura en lugar de en anotaciones

FAQ

¿Puede PDFlibPas rellenar plantillas de bloques creadas por otras herramientas?

Sí, cuando la herramienta guarda los bloques bajo /PieceInfo /PDFlib /Blocks o /PieceInfo /PDFlib /Private /Blocks, en forma de diccionario o arreglo, con un /Rect de cuatro números. GetBlockCount los cuenta después de cualquier anotación PDFlibBlock:*, y las llaves personalizadas se exponen a través de GetBlockProperty

¿FillTextBlock cambia la fuente que seleccioné antes de la llamada?

No. FillTextBlock guarda la fuente, el tamaño de texto, el color de texto y la alineación seleccionados, aplica las propiedades fontname, fontsize, fillcolor y alignment del bloque, y restaura el estado original después, incluso si el ajuste falla

¿Cómo coloco un logo vectorial o una página de código de barras en un bloque?

Cargue el PDF de origen, guarde SelectedDocument como su ID, seleccione de nuevo el documento plantilla si hace falta, y llame a FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1). La página se importa como un Form XObject, así que se mantiene vectorial y se ajusta al bloque con su relación de aspecto preservada a menos que agregue el bit 2

Las plantillas de bloques, la importación de páginas como Form XObject y el resto del kit de datos variables vienen en PDFlibPas, la losLab PDF Library for Delphi, que cubre creación, edición y producción de impresión de PDF para proyectos Delphi y C++Builder