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
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(ofont): primero reutiliza una fuente ya cargada en el documento con ese nombre, luego mapea los nombres Standard 14 comoHelvetica-Bold,Times-RomanoCourier, y por último pruebaAddTrueTypeFontcon el nombrefontsize(osize): el tamaño de texto pasado al ajustador, que lo encoge si el texto no cabefillcolor(otextcolor,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–255alignment,alignypositionse 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 verticalmentefitmethod:wordonofitsplitevita que las palabras largas se partan,scaleoexpanddeja 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
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
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
/PieceInfoes 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