Estampar una marca de agua o un logotipo en cada página de un documento parece un trabajo de cinco minutos hasta que abre el resultado en un inspector de tamaño de archivo. El enfoque obvio es recorrer las páginas y, en cada una, construir de nuevo los mismos objetos de texto o imagen. Eso funciona visualmente, y es derrochador de una manera que se acumula. Una marca de agua diagonal "DRAFT" dibujada directamente en un informe de cien páginas son cien copias de la misma trayectoria y de los mismos datos de texto alojados en los flujos de contenido, y el archivo guardado carga con todas ellas
Un Form XObject es el mecanismo que PDF ofrece para evitar exactamente esto. Envuelve un fragmento de contenido reutilizable, una página entera o una pequeña plantilla, en un único objeto con nombre que se puede pintar muchas veces en muchas posiciones. El contenido vive en el archivo una sola vez. Cada página que quiere el sello lleva una breve instrucción que dice "pinta el XObject N aquí, con esta transformación". Una marca de agua en cien páginas añade entonces un objeto de contenido al archivo, no cien, y esa es la diferencia entre un documento que crece linealmente con su número de páginas y uno que no. Marcas de agua, sellos de logotipo, plantillas de número de página y sellos oficiales son todos el mismo tipo de problema, y el Form XObject es la herramienta adecuada para todos ellos
Por qué un objeto guardado vence a un centenar de redibujados
El ahorro es estructural, no cosmético. Una página de PDF se renderiza ejecutando su flujo de contenido, una secuencia de operadores de dibujo. Cuando redibuja un sello por página, está añadiendo la secuencia completa de operadores de 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 sola vez en el documento. La referencia que conserva cada página individual es pequeña: apila una matriz de transformación, invoca el XObject y restaura el estado. El número de páginas ya no multiplica el coste del arte
Esto importa más cuando el sello es pesado. Un sello vectorial con cientos de segmentos de trayectoria, o un logotipo en mapa de bits, es costoso de almacenar. Almacenado una vez y referenciado, la parte pesada se paga una sola vez y la sobrecarga por página son unos pocos bytes de invocación. El resultado visual en la página es idéntico a un redibujado directo, que es justamente el objetivo. El lector no puede notar la diferencia; el tamaño del archivo sí, y mucho
Capturando una página para convertirla 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 pequeño PDF de una sola página que no contiene más que el arte de su marca de agua, o una página concreta 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, el que está estampando
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := 'Report.pdf';
Dest.Active := True;
Stamp.FileName := 'Watermark.pdf'; // una página de arte
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// Captura la página 0 del documento del sello en un manejador reutilizable que
// pertenece a Dest. El origen debe estar Active; el índice empieza en cero.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... colóquelo, y libérelo antes de cerrar Stamp (véase más abajo) ...
La firma es CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. El método lanza una excepción si el documento de origen no está Active, y devuelve nil en lugar de lanzar una excepción cuando PDFium no puede construir el objeto, así que la comprobación explícita de arriba no es opcional. El manejador que se devuelve es un TPdfXObject que usted posee, y las dos restricciones de vida útil asociadas a él son la parte de todo este ejercicio que sorprende a la gente, así que tienen su propia sección más abajo
Colocar el sello en una página
Un XObject capturado no hace nada por sí solo. Para que aparezca, inserta una copia de él en la página actual del documento, la seleccionada por la propiedad PageNumber basada en 1, con InsertFormObjectFromXObject. Esa llamada devuelve el objeto de página subyacente, un FPDF_PAGEOBJECT, y el manejador devuelto es cómo se posiciona la colocación. Sin una transformación, el sello aterriza en el origen, en las propias coordenadas de la página de origen, que rara vez es donde lo quiere
Como InsertFormObjectFromXObject inserta una copia por llamada y devuelve un objeto de página nuevo cada vez, puede pintar el mismo XObject varias veces en una página con transformaciones distintas, y el contenido almacenado sigue contando una sola vez en el archivo. Un logotipo en una esquina y una tenue marca de agua a página completa pueden proceder del mismo objeto capturado
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
begin
// La página actual de Dest recibe una copia del XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Posiciónelo: mueva 200 unidades a la derecha, 500 hacia arriba, al 70% de escala.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
RawM := M.Handle;
if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
raise Exception.Create('Cannot assign the stamp matrix');
finally
M.Free;
end;
Dest.UpdatePage; // confirma las ediciones de esta página en su flujo de contenido
// if not Dest.SaveAs(...) then ... cuando todas las páginas estén hechas.
end;
Dos detalles de mantenimiento hacen esto seguro. Primero, 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 hizo. Eso es lo que permite que funcione el orden crear-colocar-liberar descrito más abajo. Segundo, insertar y posicionar solo cambia la lista de objetos de la página en memoria; UpdatePage es lo que serializa esa lista de vuelta al flujo de contenido de la página, así que una página que edite sin llamarlo se guarda como si el sello nunca se hubiera colocado
La regla de vida útil del manejador que muerde a la gente
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 desde el documento de origen vivo, así que ese documento y su página tienen que estar abiertos y ser válidos cuando se construye el manejador. Segundo, y esta es la que sorprende a la gente, el manejador debe liberarse antes de que se cierre la página de origen, y en la práctica antes de cerrar o liberar el documento de origen del que procede
La razón es que el XObject es una referencia hacia una estructura que el documento de origen todavía posee. No es una copia desprendida y autocontenida que se pueda llevar consigo después de que el origen desaparezca. Cierre el origen primero y el manejador queda apuntando a contenido que ha sido desmontado, así que liberarlo más tarde, o cualquier otro uso de él, opera sobre memoria que ya no es válida. El síntoma es el clásico de un manejador colgante: una violación de acceso al cerrar, o una corrupción intermitente que se mueve según el 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. Construya el XObject, insértelo en cada página que lo necesite, libere el XObject y solo entonces cierre el documento de origen. El destructor de TPdfXObject libera por usted el manejador subyacente de PDFium, así que liberar el envoltorio en el momento correcto 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 usa 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 desde el espacio propio del objeto al espacio de la página:
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : escala horizontal y vertical
// b, c : los términos de cizalladura / rotación
// e, f : traslación (dónde aterriza el origen en la página)
Puede rellenar esos seis valores a mano, pero componerlos a mano es donde la rotación sale mal, porque la rotación mezcla los cuatro valores a, b, c, d a la vez. El envoltorio TPdfMatrix, de la unidad FPdfMatrix, compone las operaciones habituales por usted y va posmultiplicando a medida que avanza, así que Translate, Scale y Rotate se encadenan en el orden en que los llame. Una marca de agua diagonal es una rotación seguida de una traslación para recentrarla; un logotipo en una esquina es una escala seguida de una traslación. Cuando la matriz esté lista, copie su valor bruto, la propiedad Handle de tipo FS_MATRIX, en una variable local y páselo a FPDFPageObj_SetMatrix; la importación declara la matriz como un parámetro var, así que no se le puede entregar una propiedad directamente, y su resultado es 0 en caso de fallo. La función de nivel más bajo FPDFPageObj_Transform, que toma los seis valores directamente como doubles, está disponible cuando prefiera pasar números en lugar de construir un envoltorio
Estampar todas las páginas, en el orden correcto
El patrón completo une las piezas con el orden que exige la regla de vida útil. Abra ambos documentos, capture el sello una vez, recorra las páginas de destino fijando por turno el PageNumber basado en 1 e insertando más posicionando una copia, confirmando cada página con UpdatePage, después libere el XObject, después guarde con SaveAs, y deje que el documento de origen se cierre el último
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
I: Integer;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := ASource;
Dest.Active := True;
Stamp.FileName := AStamp;
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// 1. Captura el arte una vez. Stamp está Active aquí.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Coloca una copia en cada página de Dest. PageNumber empieza en 1.
for I := 1 to Dest.PageCount do
begin
Dest.PageNumber := I; // hace que la página I sea la actual
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // marca de agua diagonal
M.Translate(150, 100); // ajusta a la posición
RawM := M.Handle;
FPDFPageObj_SetMatrix(PageObj, RawM);
finally
M.Free;
end;
Dest.UpdatePage; // confirma las ediciones de esta página
end;
finally
XObject.Free; // 3. libera ANTES de que Stamp se cierre
end;
// 4. Escribe el resultado mientras Dest sigue abierto.
if not Dest.SaveAs(AOutput) then
raise Exception.Create('Could not save ' + AOutput);
finally
Stamp.Free; // el origen se cierra el último
Dest.Free;
end;
end;
La forma de los bloques try es lo que hace el trabajo real. El finally interior libera el XObject antes de que el control pueda llegar al finally exterior que libera Stamp, así que el manejador siempre se libera mientras su origen sigue vivo, incluso si una excepción salta a mitad del bucle. Consiga ese anidamiento correcto y la regla de vida útil se cuida sola
Estampar es una esquina de un conjunto de herramientas más amplio para construir y editar contenido de página. Si su sello es en sí mismo una imagen en lugar de una página capturada, convertir imágenes a documentos PDF con PDFium cubre cómo meter primero ese mapa de bits en un documento. Y cuando lo que quiere llevar junto al sello visible es un archivo en lugar de tinta sobre la página, trabajar con adjuntos de PDF en Delphi muestra el lado del archivo incrustado. Todo esto se incluye con el PDFium Component para Delphi y C++Builder, junto a las API de renderizado, edición y documento cubiertas en otras partes de este blog