La imposición (stitching) de páginas compone el contenido de páginas existentes en un lienzo compartido, lo cual es una operación diferente a la fusión de archivos. losLab PDF Library implementa la imposición de páginas mediante Form XObject y plantillas de marcas de agua compuestas para Delphi: StitchPageOverlay, StitchPagesSideBySide y StitchPagesVertically colocan las páginas capturadas a cualquier escala con total fidelidad vectorial, y TPDFlibCompositeWatermark apila marcas de agua de texto e imagen a lo largo de un rango de páginas en una sola llamada. Si lo que realmente necesita es anexar páginas completas de varios documentos en un solo archivo, esa es la familia de funciones de fusión, analizada en el artículo sobre la fusión rápida de PDF a nivel de bytes; este artículo trata sobre la combinación del contenido de las páginas dentro de un mismo espacio de coordenadas
Las solicitudes que conducen aquí son comunes y recurrentes. Una escuela quiere imponer dos páginas de examen en una sola hoja horizontal para reducir a la mitad el uso de papel. Un revisor de localización quiere la página en inglés y su traducción una al lado de la otra en una sola página, desplazándose juntas. Un equipo legal quiere que cada borrador se selle con tres capas a la vez: un logotipo en la esquina, la palabra CONFIDENCIAL en diagonal en el medio y una pequeña línea de pie de página, solo en las páginas 1 a 3. Los tres casos representan el mismo problema subyacente: colocar contenido de página ya maquetado en otro lugar y a otro tamaño sin degradarlo a un mapa de bits
¿Por qué falla la concatenación de flujos de contenido de PDF?
La imposición ingenua, que consiste en copiar el flujo /Contents de la página de origen en la página de destino, falla por tres razones estructurales. En primer lugar, las coordenadas entran en conflicto: ambos flujos de contenido asumen su propio origen en la parte inferior izquierda de su propio MediaBox, por lo que el contenido pegado queda encima del contenido existente en el lugar equivocado. En segundo lugar, los nombres de recursos entran en conflicto: cada página resuelve operadores como /F1 Tf y /Im0 Do frente a su propio diccionario /Resources, y es habitual que dos páginas vinculen el mismo nombre a diferentes fuentes o imágenes, lo que hace que una de ellas se renderice silenciosamente con el recurso incorrecto. En tercer lugar, la copia directa de un flujo no proporciona ningún tipo de escala; lo mejor que se puede hacer es trasladar
Los objetos Form XObject resuelven los tres problemas a la vez, y es por eso que losLab PDF Library basa su imposición en ellos. La norma ISO 32000-1 §8.10 define un Form XObject como un flujo de contenido autónomo con su propio diccionario /Resources y su propio espacio de coordenadas, que se coloca en una página invocando su nombre bajo cualquier matriz de transformación activa actual. CapturePage envuelve una página existente completa, contenido más recursos, en un objeto XObject /Subtype /Form, y las funciones de imposición lo posicionan con una matriz cm: escalando, trasladando o rotando a voluntad. Dado que el origen sigue siendo vectorial, el texto sigue siendo texto y las rutas siguen siendo rutas en cualquier nivel de zoom, la misma propiedad que hace que los gráficos vectoriales y los patrones de sombreado sean independientes de la resolución. No se rasteriza nada, y el diccionario de recursos aislado significa que la página capturada no puede entrar en conflicto con la página de destino por el nombre /F1
¿Cómo combinar dos páginas de PDF en una sola página?
StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) es la respuesta directa: captura ambas páginas de origen, crea una nueva página más ancha y coloca las dos capturas a la izquierda y a la derecha con un margen (gutter) entre ellas, cada una escalada para ajustarse a TargetHeight. La función devuelve el número de la nueva página (basado en 1) o 0 en caso de fallo, y ambas páginas de origen se capturan y se ocultan como parte de la operación. El caso del examen de dos páginas por hoja es un trabajo de tres líneas
var
Lib: TPDFlib;
NewPage: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('exam.pdf', '');
// Pages 1 and 2 on one new wider page with an 18pt gutter,
// both scaled to a 595pt output height (A4 landscape)
NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
if NewPage > 0 then
Lib.SaveToFile('exam-2up.pdf');
finally
Lib.Free;
end;
end;
Para la composición vertical, StitchPagesVertically(PageRanges, Gap, TargetWidth) apila un rango de páginas completo en una sola página alta estilo póster. PageRanges acepta la sintaxis de rango estándar, como '1-3,5'. Cada página de origen se escala para que su ancho coincida con TargetWidth, por lo que la contribución de altura de cada página es su propia altura multiplicada por su propio factor de escala, y la altura de la página de salida es la suma de estas más un espacio Gap entre cada par. Este es el formato adecuado para documentos de revisión con desplazamiento continuo o recibos largos ensamblados a partir de fuentes de tamaño carta
¿Cómo superponer una página sobre otra?
StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) dibuja una página existente sobre otra página existente, en lugar de crear una nueva. La página de origen se captura como un Form XObject y se dibuja sobre el destino en el rectángulo indicado, con una opacidad Opacity de 0 a 1 (1 es opaco). El ancho y el alto son parámetros libres, por lo que la colocación se estirará si su proporción difiere de la relación de aspecto de la propia página de origen; pase un par proporcional si desea una miniatura sin distorsiones del origen. Un uso típico es un documento cuyo bloque de aprobación se mantiene como una página independiente y se estampa en la portada
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('report.pdf', '');
// Page 5 holds the approval block; place it on page 1
// in a 260 x 180 box at 45% opacity. Page 5 is consumed:
// it is captured and hidden, so pages 6..n become 5..n-1
if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
Lib.SaveToFile('report-approved.pdf');
finally
Lib.Free;
end;
end;
¿Qué sucede con los números de página después de una captura?
CapturePage consume la página de origen: su contenido se mueve al objeto XObject y la página ahora vacía se oculta, por lo que cada página numerada por encima del origen se desplaza inmediatamente hacia abajo en una posición. Esta es la semántica más importante en toda la API de imposición. losLab PDF Library maneja la aritmética dentro de cada llamada de imposición: StitchPageOverlay ajusta el índice de destino cuando el destino está por encima del origen, y StitchPagesSideBySide captura primero la página con el número más alto para que el índice de la segunda captura siga siendo válido; sin embargo, su propio código debe tener en cuenta el desplazamiento entre llamadas. Dos reglas mantienen correcto el código de imposición múltiple: registre los tamaños de página o números de página que necesitará before de la primera captura, ya que cambian después. Y cuando capture varias páginas en un bucle, itere en orden descendente de páginas, de modo que ocultar una página posterior nunca altere el índice de una anterior que aún no haya capturado
¿Cómo aplicar una marca de agua multicapa a un rango de páginas?
La capa de plantilla de marca de agua en PDFlibWatermark.pas convierte la idea de "definir una vez, aplicar a muchas páginas" en objetos. TPDFlibWatermarkBase contiene lo que comparten todas las marcas de agua: una posición de nueve puntos Position (desde wpTopLeft hasta wpBottomRight, más wpCustom que utiliza coordenadas X/Y directas), un ángulo Angle en grados, una opacidad Opacity de 0 a 1 y una cadena de rango de páginas PageRange donde el valor vacío significa todas las páginas. TPDFlibTextWatermark añade texto, un ID de fuente (Helvetica por defecto), tamaño y un color RGB; TPDFlibImageWatermark añade un ID de imagen de una imagen añadida previamente, más un ancho y alto de colocación. TPDFlibCompositeWatermark contiene una lista de submarcas de agua y las aplica de forma recursiva, y TPDFlib.ApplyWatermark es el punto de entrada de una sola llamada que respeta el propio rango de páginas de la plantilla y devuelve el número de páginas estampadas. El sello legal de tres capas se ve así
var
Lib: TPDFlib;
Logo: TPDFlibImageWatermark;
Diagonal, Footer: TPDFlibTextWatermark;
Stack: TPDFlibCompositeWatermark;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('draft.pdf', '');
Stack := TPDFlibCompositeWatermark.Create;
try
Logo := TPDFlibImageWatermark.Create;
Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
Logo.Position := wpTopRight;
Logo.Width := 90;
Logo.Height := 32;
Stack.Add(Logo);
Diagonal := TPDFlibTextWatermark.Create;
Diagonal.Text := 'CONFIDENTIAL';
Diagonal.Position := wpCenter;
Diagonal.Angle := 45;
Diagonal.FontSize := 60;
Diagonal.Opacity := 0.15;
Stack.Add(Diagonal);
Footer := TPDFlibTextWatermark.Create;
Footer.Text := 'Internal review copy';
Footer.Position := wpBottomCenter;
Footer.FontSize := 9;
Footer.Opacity := 0.6;
Stack.Add(Footer);
Stack.PageRange := '1-3'; // children inherit this range
Lib.ApplyWatermark(Stack);
Lib.SaveToFile('draft-marked.pdf');
finally
Stack.Free; // composite frees its children
end;
finally
Lib.Free;
end;
end;
Vale la pena detallar dos aspectos de la composición. La herencia de rango es unidireccional y no destructiva: un elemento hijo con un rango PageRange vacío hereda temporalmente el rango del compuesto durante el método Apply, mientras que un hijo que define su propio rango lo conserva, de modo que se puede fijar el pie de página en cada página mientras que el sello diagonal cubre únicamente la sección de borrador. Y la propiedad sigue al compuesto: el destructor TPDFlibCompositeWatermark.Destroy libera a sus hijos, por lo que solo se debe liberar el compuesto una vez y nada más, como se muestra en el ejemplo anterior
Límites que vale la pena conocer
Se aplican dos límites sinceros, y ambos derivan de cómo funciona el mecanismo. En primer lugar, una captura envuelve únicamente el flujo de contenido y los recursos de la página. Las anotaciones, enlaces, campos de formulario y comentarios residen como objetos separados en el diccionario de la página, no en el flujo de contenido, por lo que no se transfieren al objeto XObject; un diseño impuesto conserva el contenido visible de la página pero no la capa interactiva de sus orígenes. En segundo lugar, el posicionamiento de texto de nueve puntos es una aproximación: sin un motor de maquetación completo, la biblioteca no puede conocer el ancho renderizado exacto de una cadena, por lo que estima la caja de texto en un 40% del ancho de la página al resolver posiciones como wpCenter. Esto es lo suficientemente preciso para centrar una etiqueta corta, pero para una colocación exacta al píxel de cadenas largas, cambie a wpCustom y calcule las coordenadas usted mismo
La escala no es una limitación práctica en este caso, ya que la captura y la imposición manipulan referencias a objetos en lugar de volver a codificar el contenido de la página; si sus fuentes son de cientos de megabytes, se aplica la misma disciplina descrita en la guía para la fusión y división de archivos grandes con acceso directo antes de realizar la imposición. Las funciones de imposición y las clases de plantilla de marca de agua que se muestran aquí se incluyen en la versión actual de losLab PDF Library para Delphi, C# y VB.NET, junto con las primitivas de nivel inferior CapturePage, DrawCapturedPage y DrawCapturedPageMatrix para cuando necesite un diseño que las tres llamadas de imposición predefinidas no cubren