Estampar una marca de agua o un logo 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, volver a construir los mismos objetos de texto o de imagen. Eso funciona visualmente, y es un derroche que se va acumulando. Una marca de agua diagonal "DRAFT" dibujada directamente sobre un informe de cien páginas son cien copias de los mismos datos de trazado y de texto dentro de los flujos de contenido, y el archivo guardado carga con todas ellas
Un Form XObject es la construcción que PDF ofrece para evitar exactamente esto. Envuelve una pieza de contenido reutilizable, una página entera o una plantilla pequeña, en un único objeto con nombre que se puede pintar muchas veces en muchas posiciones. El contenido vive una sola vez en el archivo. Cada página que quiere el sello guarda una instrucción corta que dice "pinta aquí el XObject N, con esta transformación." Una marca de agua de cien páginas agrega entonces un objeto de contenido al archivo en lugar de cien, y esa es la diferencia entre un documento que crece linealmente con su cantidad de páginas y uno que no. Marcas de agua, sellos de logo, plantillas de numeración y precintos son todos la misma forma de problema, y el Form XObject es la herramienta correcta para cada uno de ellos
Por qué un objeto guardado le gana 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 redibuja un sello por página, está agregando 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 mueve esos operadores a un flujo guardado una sola vez en el documento. La referencia que conserva una página individual es pequeña: empuja una matriz de transformación, invoca el XObject y restaura el estado. La cantidad de páginas ya no multiplica el costo del arte
Esto importa sobre todo cuando el sello es pesado. Un precinto vectorial con cientos de segmentos de trazado, o un logo de mapa de bits, es caro de almacenar. Guardado 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 al de un redibujado directo, que es justamente el objetivo. El lector no nota la diferencia; el tamaño del archivo la nota muchísimo
Capturar una página en un XObject
PDFium construye el objeto reutilizable a partir de una página existente. La fuente es una página de algún documento que tenga abierto, un PDF pequeño de una sola página que no contenga 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 fuente en un identificador 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 identificador
// reutilizable de Dest. La fuente debe estar Active; el índice arranca 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 (vea más abajo) ...
La firma es CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. El método lanza una excepción si el documento fuente no está Active, y devuelve nil en lugar de lanzar cuando PDFium no puede construir el objeto, así que la comprobación explícita de arriba no es opcional. El identificador que vuelve es un TPdfXObject que le pertenece, y las dos restricciones de tiempo de vida 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 que selecciona la propiedad PageNumber basada en 1, con InsertFormObjectFromXObject. Esa llamada devuelve el objeto de página subyacente, un FPDF_PAGEOBJECT, y ese identificador devuelto es la vía para posicionar la colocación. Sin una transformación, el sello cae en el origen, en las coordenadas propias de la página fuente, que rara vez es donde lo quiere
Como InsertFormObjectFromXObject inserta una copia por llamada y entrega un objeto de página nuevo cada vez, puede pintar el mismo XObject varias veces en una página con distintas transformaciones, y el contenido guardado sigue contando una sola vez en el archivo. Un logo de esquina y una marca de agua tenue a página completa pueden salir 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');
// Colóquelo: 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 listas.
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 adelante no invalida las colocaciones que ya hizo. Eso es lo que permite que funcione el orden crear-colocar-liberar descrito 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 tiempo de vida del identificador que muerde
Dos restricciones gobiernan el identificador del XObject, e ignorar cualquiera de las dos produce una falla que parece no tener relación con su causa. Primero, el documento fuente debe estar activo en el momento en que llama a CreateXObjectFromPage. La captura lee el contenido de la página fuente desde el documento fuente vivo, así que ese documento y su página tienen que estar abiertos y ser válidos cuando se construye el identificador. Segundo, y esta es la que sorprende a la gente, el identificador debe liberarse antes de que se cierre la página fuente y, en la práctica, antes de que cierre o libere el documento fuente del que provino
La razón es que el XObject es una referencia a una estructura que el documento fuente todavía posee. No es una copia desprendida y autocontenida que pueda llevar consigo después de que la fuente desaparezca. Cierre primero la fuente y el identificador queda apuntando a contenido que ya fue desmantelado, así que liberarlo después, o cualquier otro uso, opera sobre memoria que ya no es válida. El síntoma es el clásico de un identificador 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 y no 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 fuente. El destructor de TPdfXObject libera por usted el identificador de PDFium subyacente, 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 sesgo / rotación
// e, f : traslación (dónde cae el origen en la página)
Puede rellenar esos seis valores a mano, pero componerlos a mano es donde la rotación se tuerce, porque la rotación mezcla los cuatro términos a, b, c, d entre sí. El envoltorio TPdfMatrix, de la unidad FPdfMatrix, compone por usted las operaciones comunes y posmultiplica sobre la marcha, así que 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 recentrarla; un logo de esquina es una escala seguida de una traslación. Cuando la matriz está lista, copie su valor crudo, la propiedad Handle de tipo FS_MATRIX, en una variable local y pase esa variable a FPDFPageObj_SetMatrix; la importación declara la matriz como parámetro var, así que no se le puede entregar una propiedad directamente, y su resultado es 0 en caso de falla. La función de más bajo nivel FPDFPageObj_Transform, que toma los seis valores directamente como dobles, está disponible cuando prefiera pasar números en lugar de construir un envoltorio
Estampar cada página, en el orden correcto
El patrón completo junta 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 fijando por turno el PageNumber basado en 1 e insertando y posicionando una copia, confirme cada página con UpdatePage, luego libere el XObject, luego guarde con SaveAs, y deje que el documento fuente se cierre al final
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. Capture el arte una sola vez. Aquí Stamp está Active.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Coloque una copia en cada página de Dest. PageNumber arranca en 1.
for I := 1 to Dest.PageCount do
begin
Dest.PageNumber := I; // vuelve actual la página I
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // marca de agua diagonal
M.Translate(150, 100); // ajuste fino de 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. libere ANTES de que Stamp cierre
end;
// 4. Escriba el resultado mientras Dest sigue abierto.
if not Dest.SaveAs(AOutput) then
raise Exception.Create('Could not save ' + AOutput);
finally
Stamp.Free; // la fuente cierra al final
Dest.Free;
end;
end;
La forma de los bloques try es la que hace el trabajo de verdad. El finally interno libera el XObject antes de que el control pueda llegar al finally externo que libera Stamp, así que el identificador siempre se suelta mientras su fuente sigue viva, incluso si salta una excepción a mitad del bucle. Acierte ese anidamiento y la regla de tiempo de vida se cuida sola
Estampar es un rincón de un conjunto de herramientas más amplio para construir y editar contenido de páginas. Si su sello es en sí una imagen y no una página capturada, convertir imágenes a documentos PDF con PDFium cubre primero cómo meter 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 PDF en Delphi muestra el lado de los archivos incrustados. Todo esto viene con el PDFium Component para Delphi y C++Builder, junto con las APIs de renderizado, edición y documentos cubiertas en otras partes de este blog