Artículo técnico

Plantillas de bloques PDF en Delphi: 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 de plantilla, tipado como texto, imagen o PDF, y FillTextBlock, FillImageBlock y FillPDFBlock dibujan datos variables dentro. 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 de los aburridos de gran volumen. Un diseñador maqueta un certificado, una entrada de evento o una carta de reclamación 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 prerenderizada en huecos fijos para diez mil registros. Los campos de formulario pueden hacer parte de eso, pero un campo AcroForm es un objeto interactivo con su propio appearance stream y sus propias reglas, y normalmente acabáis aplanándolo de todos modos. Un bloque es más simple: un rectángulo más un puñado de pistas de estilo, que se consume una vez y luego se tira

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 hueco, y cuyo string de /Contents contiene name;key=value;key=value. El primer segmento es el nombre del bloque, todo lo que va después es una lista de propiedades. TPDFlib.AddBlock escribe la anotación con /F 4 (Print) y sin appearance stream, 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 /Rect de hueco, un string /Contents de nombre y propiedades key=value, /F 4 Print y sin appearance stream, con el diccionario page-piece /PieceInfo /PDFlib /Blocks como segunda fuente
Los bloques se enumeran 1-based por página con las anotaciones primero y los bloques page-piece después; las entradas de array se convierten en Block1 y Block2, las claves /Custom se aplanan en propiedades, y todo lo que no sea Image, PDF o Page se trata como texto

La lectura tiene una segunda fuente. Tras enumerar las anotaciones de bloque, PDFlibPas recurre a la ruta de diccionario de página /PieceInfo << /PDFlib << /Blocks ... >> >> y a 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 suelen vivir las definiciones de bloques de herramientas externas de maquetación de plantillas. El contenedor /Blocks puede ser un diccionario indexado por nombre de bloque o un array plano; en la forma de array PDFlibPas nombra las entradas Block1, Block2 y así sucesivamente salvo que un bloque traiga su propio /Name

Dentro de un bloque page-piece, el valor /Subtype (o /Type) Image mapea a un bloque de imagen, PDF o Page mapea a un bloque PDF, y cualquier otra cosa se trata como texto. Todas las demás claves excepto /Type, /Subtype, /Name, /Rect y /Custom se convierten en propiedades, y las claves 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 1-based por página, primero los bloques de anotación y después los page-piece

Crear una plantilla de bloques con AddBlock

TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) añade un bloque a la página seleccionada actualmente y devuelve 1 en caso de éxito. Devuelve 0 para un nombre vacío, una anchura o altura cero o negativa, o un BlockType distinto de 'text', 'image' o 'pdf' (sin distinguir mayúsculas). Properties se añade al nombre con un punto y coma, así que usa la misma sintaxis key=value;key=value que el string guardado

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 laxo. Las claves se comparan sin distinguir mayúsculas, los valores se recortan pero por lo demás se devuelven tal cual (un valor escrito como {top left} vuelve con sus llaves), y una clave ausente devuelve un string vacío. 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 al rectángulo del bloque con FitTextBox y después 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 merge: un bloque de encabezado rojo de 30 pt no puede filtrar su estilo en la siguiente llamada a DrawText que hagáis 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 prueba AddTrueTypeFont con el nombre
  • fontsize (o size): el tamaño de texto pasado al ajustador, que lo encoge si el texto no cabe
  • fillcolor (o textcolor, color): #RRGGBB, {rgb r g b}, {cmyk c m y k}, {gray g}, listas peladas de 3 o 4 números, o los nombres black, white, red, green, blue y gray; los componentes RGB por encima de 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 hasta 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 el color de marca en la máquina 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 ausente o un bloque que no es de texto, y en todos esos casos el bloque se deja en su sitio

Orden de procesado 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, así que un encabezado rojo de 30 pt no puede filtrarse en el siguiente DrawText; la llamada devuelve 1 cuando se dibujó texto y 0 con el bloque en su sitio 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 añadida previamente, y FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) importa una página de otro documento cargado como Form XObject y la dibuja, lo que mantiene el contenido vectorial como un sello, una página de código QR o un membrete completamente vectorial. Ambas 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 con éxito, 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);

    // ir 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 de ese bucle son fáciles de torcer. El bucle corre del índice más alto hacia abajo porque eliminar el bloque 2 convierte el antiguo bloque 3 en el bloque 2; contar hacia arriba con el bit 1 activo se salta un bloque de cada dos. 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 con la plantilla seleccionada. Para lotes donde el sello o el membrete cambian por registro, el page stitching 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é salen volteadas las coordenadas de los bloques

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

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

Volteo de coordenadas del rectángulo de bloque en PDFlibPas para Delphi: los valores /Rect viven en el espacio de usuario PDF basado en abajo mientras AddBlock toma entrada basada en arriba y guarda PageHeight menos Top menos Height, así que /Rect 40 700 240 730 en una página letter de 792 pt se lee como Top 62 vía GetBlockRect y su conversión de PageHeight menos URy
Mezclar los dos sistemas de coordenadas espeja cada valor rellenado hacia el margen inferior sin un solo error; leed la geometría del hueco vía GetBlockRect en lugar de meter el /Rect directamente en DrawText

Trampas de las que vale la pena aprender

El bug más instructivo del código de bloques fue un desliz de un carácter. Las anotaciones de bloque se reconocen por una prueba de prefijo sobre el subtype, y 'PDFlibBlock:' tiene 12 caracteres. Una versión temprana comparaba Copy(Subtype, 1, 13) contra ese literal de 12 caracteres, que nunca puede ser igual, así que GetBlockCount devolvía 0 para cada plantilla y cada relleno informaba «no such block». Nada crasheaba y nada se registraba. La lección se generaliza a cualquier motor de plantillas: afirmad el recuento de bloques de una plantilla conocida en una prueba, porque una enumeración silenciosa sin coincidencias se parece exactamente a 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 fallo, 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. Llamad a SelectedDocument justo después de una carga con éxito para obtener el ID real, y a SelectDocument para volver a la plantilla si cargasteis el origen después

Una más pequeña: FillTextBlock con un string vacío no hace nada y conserva el bloque. Para un mail merge con campos opcionales, como una segunda línea de dirección ausente, eso suele ser lo que queréis, pero la anotación sin rellenar se queda entonces en el fichero de salida. Eliminadla explícitamente con DeleteAnnotation o rellenad un solo espacio con el bit 1 si la salida tiene que quedar 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 vuestra 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 característica 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 los trata como anotaciones desconocidas. Como el flag Invisible está a cero y no hay appearance stream, los visores conformes no dibujan nada por ellas, pero algunos visores pueden seguir listándolas en un panel de comentarios, y otras herramientas PDF no entenderán ni preservarán su semántica

  • Rellenad con el bit 1 activo para cualquier cosa que salga de vuestro sistema, así el fichero final solo contiene 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 mantener en una plantilla, ya que /PieceInfo es estándar y los consumidores ignoran los datos que no son suyos
  • Los bloques no son campos de formulario: no hay interactividad, no hay valor de campo en el fichero ni exportación a FDF o XFDF; para datos que deban seguir editables ved 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 imprenta, los bloques son la herramienta equivocada. Para eso existe 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 array, con un /Rect de cuatro números. GetBlockCount los cuenta después de cualquier anotación PDFlibBlock:*, y las claves custom se exponen vía 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?

Cargad el PDF de origen, guardad SelectedDocument como su ID, volved a seleccionar el documento de plantilla si hace falta, y llamad a FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1). La página se importa como Form XObject, así que se queda vectorial y se ajusta al bloque con su relación de aspecto preservada salvo que añadáis el bit 2

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