Technical Article

Sellos de página reutilizables mediante Form XObjects con PDFium

Estampar una marca de agua o un logotipo en cada página de un documento parece un trabajo de cinco minutos hasta que se abre el resultado en un inspector de tamaño de archivo. El enfoque obvio es recorrer las páginas y, en cada una, construir los mismos objetos de texto o imagen nuevamente. Eso funciona visualmente, y es un desperdicio que se acumula. Una marca de agua diagonal "BORRADOR" dibujada directamente en un informe de cien páginas representa cien copias de los mismos datos de ruta y texto sentados en los flujos de contenido, y el archivo guardado lleva cada una de ellas.

Un Form XObject es el constructo que PDF proporciona para evitar exactamente esto. Envuelve una pieza de contenido reutilizable, una página completa o una plantilla pequeña, en un único objeto con nombre que se puede pintar muchas veces en muchas posiciones. El contenido vive en el archivo una vez. Cada página que desea el sello contiene una instrucción corta que dice "pinta el XObject N aquí, con esta transformación". Una marca de agua de cien páginas añade entonces un objeto de contenido al archivo en lugar de cien, y esa es la diferencia entre un documento que crece linealmente con su recuento de páginas y uno que no. Las marcas de agua, los sellos de logotipos, las plantillas de número de página y los sellos son la misma forma de problema, y el 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 representa ejecutando su flujo de contenido, una secuencia de operadores de dibujo. Cuando vuelve a dibujar un sello por página, está añadiendo 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 mueve esos operadores a un único flujo almacenado una vez en el documento. La referencia que mantiene una página individual es pequeña: empuja una matriz de transformación, invoca el XObject y restaura el estado. El recuento de páginas ya no multiplica el costo de la obra de arte.

Esto importa más cuando el sello es pesado. Un sello vectorial con cientos de segmentos de ruta, o un mapa de bits de logotipo, es costoso de almacenar. Almacenado una vez y referenciado, la parte pesada se paga una sola vez y la sobrecarga por página es de unos pocos bytes de invocación. El resultado visual en la página es idéntico a un redibujado directo, que es el punto. El lector no puede notar la diferencia; el tamaño del archivo definitivamente 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 pequeño PDF de una página que no contiene nada 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 identificador reutilizable que pertenece al documento de destino, el que está estampando.

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, inserta una copia en la página actual del documento con InsertFormObjectFromXObject. Esa llamada devuelve el objeto de página subyacente, un FPDF_PAGEOBJECT, y el identificador devuelto es cómo 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 desea.

Porque 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 diferentes transformaciones, y el contenido almacenado sigue contando una sola vez en el archivo. Un logotipo de esquina y una marca de agua tenue de 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;

La regla de duración del identificador que causa problemas

Dos restricciones gobiernan el identificador de 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 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 de origen y, en la práctica, antes de cerrar o liberar el documento de origen del que proviene.

La razón es que el XObject es una referencia a la estructura que el documento de origen todavía posee. No es una copia independiente y autónoma que pueda llevar consigo después de que el origen haya desaparecido. Cierre el origen primero y el identificador quedará apuntando a un contenido que ha sido desmantelado, por lo que liberarlo más tarde, o cualquier otro uso del mismo, opera sobre memoria que ya no es válida. El síntoma es el clásico para un identificador huérfano: una infracción de acceso al apagar, o una corrupción intermitente que se mueve 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. 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 TPdfXObject libera el identificador PDFium subyacente por usted, por lo que liberar el contenedor en el momento adecuado es toda su responsabilidad.

La matriz y lo que 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 el contenido (ISO 32000-1, sección 8.3.4). Son seis números, escritos como a, b, c, d, e, f, y PDFium los expone como el registro FS_MATRIX. Mapean un punto desde el propio espacio 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 a mano es donde la rotación sale mal, porque la rotación mezcla los cuatro términos a, b, c, d. El contenedor TPdfMatrix compone las operaciones comunes por usted y las multiplica a medida que avanza, de modo que Translate, Scale y Rotate se encadenan en el orden en que los 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 sin procesar a FPDFPageObj_SetMatrix(PageObj, M.Handle), donde M.Handle is el `FS_MATRIX` subyacente. El nivel inferior `FPDFPageObj_Transform`, que toma los seis valores directamente como doubles, está disponible cuando prefiere pasar números en lugar de construir un contenedor.

Estampar cada página, en el orden correcto

El patrón completo une las piezas con el orden que exige la regla de duración. 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 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 forma de los bloques try es la que realiza el trabajo real. El finally interno libera el XObject antes de que el control pueda llegar al finally externo que libera Stamp, por lo que el identificador siempre se libera mientras su origen está vivo, incluso si se lanza una excepción a mitad del bucle. Entienda bien ese anidamiento y la regla de duración se resolverá sola. (Use cualquier selector de página actual que exponga su compilación; el cuerpo del bucle es el mismo en cualquier caso).

El estampado es una esquina de un conjunto de herramientas más grande para construir y editar contenido de páginas. Si su sello es una imagen en sí misma en lugar de una página capturada, la conversión de imágenes a documentos PDF con PDFium cubre cómo llevar ese mapa de bits a un documento primero. Y cuando lo que desea llevar junto con el sello visible es un archivo en lugar de tinta en la página, el trabajo con archivos adjuntos PDF en Delphi muestra el lado del archivo incrustado. Todo ello se incluye con el Componente PDFium para Delphi y C++Builder, junto con las API de representación, edición y documentos cubiertas en otras partes de este blog.