Estampar una marca de agua o un logotipo en cada página de un documento parece una tarea de cinco minutos, hasta que se abre el resultado en un inspector de tamaño de archivo. El enfoque obvio consiste en recorrer las páginas y, en cada una, volver a construir los mismos objetos de texto o imagen. Eso funciona visualmente, pero es ineficiente de una manera acumulativa: una marca de agua diagonal de \"BORRADOR\" dibujada directamente en un informe de cien páginas representa cien copias de los mismos datos de ruta y texto en los flujos de contenido, y el archivo guardado contiene cada una de ellas.
Un Form XObject es el recurso que proporciona el formato PDF para evitar precisamente esto. Envuelve una sección de contenido reutilizable, una página completa o una plantilla pequeña, en un único objeto nombrado que se puede pintar muchas veces en muchas posiciones. El contenido reside en el archivo una sola vez; cada página que requiere el sello incluye una instrucción corta que dice \"pinte XObject N aquí, con esta transformación\". Una marca de agua en cien páginas agrega entonces un único objeto de contenido al archivo en lugar de cien, y esa es la diferencia entre un documento que crece de forma lineal con su cantidad de páginas y uno que no lo hace. Las marcas de agua, los sellos de logotipo, las plantillas de numeración de páginas y los sellos de seguridad son todos el mismo tipo de problema, y Form XObject es la herramienta adecuada para cada uno de ellos.
Por qué un objeto almacenado supera a cien redibujados
El ahorro es estructural, no cosmético. Una página PDF se renderiza ejecutando su flujo de contenido, una secuencia de operadores de dibujo. Cuando vuelve a dibujar un sello en cada página, está anexando la secuencia completa de operadores para ese sello al flujo de cada página, y los bytes se duplican tantas veces como páginas tenga. Un Form XObject traslada esos operadores a un único flujo almacenado una vez en el documento. La referencia que conserva cada página es pequeña: carga una matriz de transformación, invoca al XObject y restablece el estado. La cantidad de páginas ya no multiplica el costo de almacenamiento de la obra de arte.
Esto es especialmente importante cuando el sello es pesado. Un sello vectorial con cientos de segmentos de ruta, o un mapa de bits de logotipo, resulta costoso de almacenar. Almacenado una vez y referenciado, la sección pesada se paga una sola vez y la sobrecarga por página es de solo unos pocos bytes de invocación. El resultado visual en la página es idéntico a un redibujado directo, lo cual es el objetivo: el lector no puede notar la diferencia; el tamaño del archivo sí.
Capturar una página en un XObject
PDFium construye el objeto reutilizable a partir de una página existente. El origen es una página en algún documento que tenga abierto (un PDF pequeño de una sola página que no contenga más que su diseño de marca de agua, o una página particular de un archivo más grande). CreateXObjectFromPage captura el contenido de esa página de origen en un manejador reutilizable que pertenece al documento de destino, es decir, el que está sellando.
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
Colocar el sello en una página
Un XObject capturado no hace nada por sí solo. Para que aparezca, se debe insertar una copia de él en la página actual del documento con InsertFormObjectFromXObject. Esa llamada devuelve el objeto de página subyacente, un FPDF_PAGEOBJECT, y el manejador devuelto es la forma en que se posiciona la colocación. Sin una transformación, el sello aparece en el origen en las coordenadas propias de la página de origen, lo cual rara vez es donde se desea.
Debido a que InsertFormObjectFromXObject inserta una copia por llamada y devuelve un nuevo objeto de página cada vez, se puede pintar el mismo XObject varias veces en una página con diferentes transformaciones, y el contenido almacenado se cuenta como un solo elemento en el archivo. Un logotipo en una esquina y una marca de agua tenue a página completa pueden provenir del mismo objeto capturado.
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;
Un detalle de propiedad hace que la limpieza sea segura
Un detalle de propiedad hace que la limpieza sea segura. Una vez insertado, el objeto de página pertenece a la página, no al XObject. Liberar el XObject más tarde no invalida las colocaciones que ya realizó. Eso es lo que permite que funcione el orden de creación-colocación-liberación descrito a continuación.
La regla de tiempo de vida del manejador que causa problemas
Dos restricciones gobiernan el manejador del XObject, e ignorar cualquiera de ellas produce un fallo que parece no tener relación con su causa. Primero, el documento de origen debe estar activo en el momento en que llama a CreateXObjectFromPage. La captura lee el contenido de la página de origen del documento de origen activo, por lo que ese documento y su página deben estar abiertos y ser válidos cuando se construye el manejador. Segundo, y esta es la que sorprende a las personas, el manejador debe liberarse antes de que se cierre la página de origen y, en la práctica, antes de que cierre o libere el documento de origen del que provino.
La razón es que el XObject es una referencia a una estructura que el documento de origen aún posee. No es una copia independiente y separada que pueda llevar consigo una vez que el origen ha desaparecido. Si cierra el origen primero, el manejador se quedará apuntando a contenido que ha sido destruido, por lo que liberarlo más tarde, o cualquier otro uso de él, operará en memoria que ya no es válida. El síntoma es el clásico de un manejador suelto: una violación de acceso al apagar el sistema, o corrupción intermitente que se desplaza dependiendo del orden de asignación, con una pila que apunta al código de limpieza en lugar de a la línea que realmente causó el problema. La solución es el orden, no la programación defensiva: construir el XObject, insertarlo en cada página que lo necesite, liberar el XObject y solo entonces cerrar el documento de origen. El destructor de TPdfXObject libera el manejador de PDFium subyacente por usted, por lo que liberar el contenedor en el momento adecuado es toda su responsabilidad.
La matriz, y qué significan sus seis números
La colocación es una transformación afín 2D, la misma que PDF utiliza en todas partes para posicionar contenido (ISO 32000-1, sección 8.3.4). Son seis números, escritos a, b, c, d, e, f, y PDFium los expone como el registro FS_MATRIX. Mapean un punto del espacio propio del objeto al espacio de la página:
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
Puede llenar esos seis valores a mano, pero componerlos manualmente es donde la rotación falla, porque la rotación mezcla los cuatro componentes a, b, c, d entre sí. El contenedor TPdfMatrix compone las operaciones comunes por usted y realiza la multiplicación a medida que avanza, por lo que las instrucciones Translate, Scale y Rotate se encadenan en el orden en que las llama. Una marca de agua diagonal es una rotación seguida de una traslación para volver a centrarla; un logotipo de esquina es una escala seguida de una traslación. Cuando la matriz esté lista, entregue su valor bruto a FPDFPageObj_SetMatrix(PageObj, M.Handle), donde M.Handle es el FS_MATRIX subyacente. La instrucción de nivel inferior FPDFPageObj_Transform, que toma los seis valores directamente como doubles, está disponible cuando prefiere pasar números directamente en lugar de construir un contenedor.
Sellar cada página, en el orden correcto
El patrón completo reúne las piezas con el orden que exige la regla de tiempo de vida. Abra ambos documentos, capture el sello una vez, recorra las páginas de destino seleccionando cada una a su vez e insertando más posicionando una copia, luego libere el XObject, guarde el archivo y deje que el documento de origen se cierre al final.
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 1. Capture the artwork once. Stamp is active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := i; // make page i current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
La estructura de los bloques try es la que realiza el trabajo real. El bloque finally interno libera el XObject antes de que el control pueda llegar al finally externo que libera Stamp, por lo que el manejador siempre se libera mientras su origen está activo, incluso si ocurre una excepción en medio del bucle. Defina correctamente ese anidamiento y la regla de tiempo de vida se resolverá por sí sola. (Use cualquier selector de página actual que exponga su entorno; el cuerpo del bucle es idéntico en cualquier caso.)
El sellado es solo una sección de un conjunto de herramientas más grande para construir y editar contenido de página. Si su sello es una imagen en lugar de una página capturada, la conversión de imágenes a documentos PDF con PDFium cubre la incorporación de ese mapa de bits en un documento primero. Y cuando el elemento que desea transportar junto con el sello visible es un archivo en lugar de tinta en la página, el artículo sobre cómo trabajar con archivos adjuntos PDF en Delphi muestra el lado del archivo incrustado. Todo esto se distribuye con el PDFium Component para Delphi y C++Builder, junto con las API de renderizado, edición y documentos tratadas en otras secciones de este blog.