Artículo técnico

Sellos de página reutilizables a través de objetos XObjects de formulario 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 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

Diagrama contrastando dibujar operadores de marca de agua en cada página PDF frente a almacenarlos una vez en un Form XObject con PDFium
Redibujar el sello en cada página duplica sus bytes por cada flujo de contenido, mientras que un Form XObject almacena la obra una vez y deja que cada página la referencie

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

Diagrama de ciclo de vida ordenado para los sellos de página de PDFium mostrando captura, colocación, liberación del handle TPdfXObject y cierre del documento de sello al final
Capture el sello una vez, colóquelo en cada página, libere el XObject mientras el documento del sello sigue abierto, y después guarde y cierre el origen al final

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

Anatomía de FS_MATRIX mostrando los seis coeficientes afines que PDFium usa para escalar, rotar y trasladar un Form XObject estampado en la página
Seis números mapean las coordenadas del sello al espacio de página, y TPdfMatrix compone Scale, Rotate y Translate en orden de llamada para asentar una marca de agua diagonal

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