Technical Article

Imposición N-up y reordenamiento de páginas con PDFium

Fusionar y dividir son las dos operaciones de página a las que todos recurren primero, y cubren mucho terreno. No lo cubren todo. Existe otra familia de tareas que reorganiza páginas en lugar de mover archivos completos: colocar cuatro diapositivas en una sola hoja para un folleto, arrastrar una página desde la parte posterior de un documento hacia el frente, o extraer las páginas 3, 7 y 12 en un breve fragmento sin tocar el resto. PDFium expone tres métodos para hacer exactamente esto, y cada uno se comporta de manera diferente a la fusión y división que ya conoce. Este artículo recorre lo que hacen, dónde se encuentran los puntos de salida y un detalle de propiedad que ha causado caídas en producción

Los tres métodos son ImportNPagesToOne para la imposición N-up, MovePages para el reordenamiento en el lugar, e ImportPagesByIndex para la extracción de subconjuntos. Fusionar apila documentos de extremo a extremo y deja el recuento de páginas igual a la suma de las entradas. Dividir escribe varios archivos de salida a partir de una sola entrada. Las tres operaciones aquí se sitúan en medio: una de ellas cambia cuántas páginas de origen comparten una hoja, otra cambia el orden dentro de un solo documento y la última copia un puñado de páginas seleccionadas en otro documento. Saber cuál es cuál le evita verse forzado a realizar una danza de fusión y eliminación cuando bastaría con una sola llamada

Lo que realmente hace la imposición N-up

Imposición es el término de preimpresión para organizar varias páginas de origen en una hoja más grande de modo que el resultado impreso y doblado se lea en el orden correcto. La versión cotidiana es el folleto de 2 páginas por hoja, el folleto plegado de 4 páginas o la hoja de contactos que acomoda una docena de miniaturas en una página. PDFium maneja la geometría a través de una sola llamada

function ImportNPagesToOne(
  OutputWidth, OutputHeight: Single;
  NumX, NumY               : Cardinal): TPdf;

NumX y NumY describen la cuadrícula. Un valor de 2, 1 coloca dos páginas de origen una al lado de la otra; 2, 2 empaqueta cuatro en un diseño de cuadrante; 4, 3 construye una hoja de contactos de doce páginas. PDFium lee las páginas de origen en orden, reduce cada una para que quepa en su celda y llena la cuadrícula de izquierda a derecha, de arriba a abajo, comenzando una nueva hoja de salida cada vez que se llena la cuadrícula actual. Las páginas de origen no se modifican. Lo que obtiene de vuelta es un nuevo documento cuyas páginas son compuestas

El tamaño de salida se mide en puntos, no en píxeles

OutputWidth y OutputHeight son unidades de usuario de PDF, y una unidad de usuario de PDF es un punto, que equivale a un setenta y dosavo de pulgada. La unidad declara el tamaño físico de la hoja de salida y no tiene nada que ver con los píxeles de la pantalla o los DPI de renderizado. Este es el lugar más común para equivocarse en una imposición, porque un desarrollador acostumbrado a los mapas de bits busca un recuento de píxeles y termina con una hoja del tamaño de un sello postal o de una valla publicitaria

Los números que vale la pena memorizar son los dos tamaños de página que más utilizará. US Letter es de 612 por 792 puntos, porque 8.5 pulgadas por 72 es 612 y 11 pulgadas por 72 es 792. A4 es de aproximadamente 595 por 842 puntos, a partir de sus dimensiones de 210 por 297 milímetros. El propio encabezado de la vinculación establece la regla claramente, que una unidad equivale a un setenta y dosavo de pulgada, y la unidad incluye una constante PointsPerInch igual a 72 si prefiere calcular un tamaño a partir de pulgadas en el código en lugar de escribir el valor literal

const
  LetterW = 612.0;   // 8.5 in * 72
  LetterH = 792.0;   // 11  in * 72
var
  Source, Composite: TPdf;
begin
  Source := TPdf.Create(nil);
  Composite := nil;
  try
    Source.FileName := 'slides.pdf';
    Source.Active := True;

    // Four source pages per Letter sheet, 2 by 2 grid.
    Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
    if Composite = nil then
      raise Exception.Create('PDFium rejected the imposition arguments');

    Composite.SaveAs('slides-4up.pdf');
  finally
    Composite.Free;   // see the next section: this is mandatory
    Source.Free;
  end;
end;

El controlador devuelto es suyo para liberar

Lea la firma nuevamente. ImportNPagesToOne devuelve un TPdf, no un Boolean. Ese valor de retorno es un controlador de documento completamente nuevo, asignado por separado del origen, y el llamador lo posee. El TPdf de origen en el que llamó al método permanece intacto y aún posee su propio controlador; el compuesto es un segundo objeto independiente. Si deja que el TPdf devuelto quede fuera del alcance sin liberarlo, filtrará un documento PDFium completo

El error más peligroso funciona en la otra dirección. Internamente, el método solicita a PDFium un FPDF_DOCUMENT nuevo a través de FPDF_ImportNPagesToOne, luego envuelve ese controlador sin procesar dentro del TPdf devuelto de modo que el ciclo de vida del contenedor gobierna al del controlador. A partir de ese momento, hay exactamente un propietario del controlador y exactamente un lugar donde debe cerrarse: cuando libera (Free) el objeto devuelto. Una ruta de error descuidada que libere el contenedor y también llame a FPDF_CloseDocument en el controlador sin procesar que capturó cerrará el mismo documento PDFium dos veces. Eso representa una doble liberación, y es el error específico que afectó a un desarrollador aquí una vez. La regla que lo evita es corta. Cierre el documento en una sola ruta, liberando el TPdf que el método le entregó, y nunca acceda más allá del contenedor para cerrar el controlador que ya adoptó

De esto se desprenden dos corolarios. Primero, el método devuelve nil cuando PDFium ya no acepta los argumentos, como un cero en cualquiera de los ejes de la cuadrícula o un fallo de asignación, por lo que corresponde realizar una comprobación de nil antes de tocar el resultado. Segundo, inicialice su variable de salida en nil antes del try y libérela en finally, como hace el ejemplo anterior, para que un fallo a mitad de camino no le deje liberando una referencia indefinida o saltándose la liberación por completo

Reordenamiento de páginas sin reescribirlas

La imposición crea un nuevo documento. El reordenamiento cambia un documento en el lugar. MovePages eleva un conjunto de páginas de sus posiciones actuales y las coloca en un destino, desplazando todo lo demás alrededor del bloque movido para que el recuento de páginas se mantenga igual

function MovePages(
  const PageIndices: array of Integer;
  DestPageIndex    : Integer): Boolean;

Los índices se basan en cero. PageIndices enumera las páginas a mover, en el orden en que deben quedar, y DestPageIndex es el índice en el que aterriza la primera página movida después de que se asiente el movimiento. Debido a que PDFium reubica las páginas en lugar de copiar y volver a comprimir su contenido, la operación es económica y sin pérdidas: los objetos de página conservan sus flujos, sus recursos y su fidelidad. Esta es la llamada detrás de un panel de páginas de arrastrar para reordenar, donde un usuario arrastra una miniatura a una nueva ranura y usted confirma el nuevo orden con un solo movimiento. Devuelve False cuando un índice está fuera de rango, así que valide el resultado en lugar de asumir que la reorganización se realizó

var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Move the last page (index 4 in a 5-page file) to the very front.
    if not Doc.MovePages([4], 0) then
      raise Exception.Create('MovePages rejected the index');

    Doc.SaveAs('report-reordered.pdf');
  finally
    Doc.Free;
  end;
end;

Extracción de un subconjunto por índice

La tercera operación copia un conjunto explícito de páginas de un documento a otro. ImportPagesByIndex toma el documento de origen y una matriz de índices basada en cero, e inserta esas páginas en el destino en una posición seleccionada

function ImportPagesByIndex(
  Source           : TPdf;
  const PageIndices: array of Integer;
  InsertAt         : Integer= 0): Boolean;

Lo llama en el documento de destino y pasa el origen como primer argumento. PageIndices indica las páginas de origen a extraer, en el orden en que las desea; InsertAt es la ranura basada en cero en el destino donde va la primera página importada, por lo que 0 las coloca antes de la primera página existente y el recuento actual del destino se añade al final. Una matriz vacía importa cada página, lo que convierte la llamada en una copia completa cuando necesita una. Devuelve False si algún índice está fuera de rango en el origen

Aquí es donde importa el contraste con dividir. Dividir escribe archivos separados, una sola operación que produce muchas salidas en disco. ImportPagesByIndex realiza la forma opuesta de trabajo: recopila un conjunto seleccionado de páginas en un solo documento de destino en memoria, el cual luego guarda una vez. Cuando la tarea es "entrégame las páginas 3, 7 y 12 como un único PDF corto", esta es la ruta directa, y envuelve FPDF_ImportPagesByIndex internamente

var
  Source, Excerpt: TPdf;
begin
  Source := TPdf.Create(nil);
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // start an empty target

    // Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
    if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
      raise Exception.Create('A requested page index is out of range');

    Excerpt.SaveAs('manual-excerpt.pdf');
  finally
    Excerpt.Free;
    Source.Free;
  end;
end;

Cómo ensamblarlo todo limpiamente

La forma de principio a fin es la misma en las tres operaciones: abra el origen configurando FileName y cambiando Active a True, realice la operación, guarde con SaveAs y libere lo que posee. La única rama que requiere atención es cuáles llamadas asignan un nuevo documento. MovePages muta el documento que ya posee, por lo que hay un solo objeto para liberar. ImportPagesByIndex escribe en un destino que usted mismo creó, por lo que libera el origen y el destino que abrió. ImportNPagesToOne es el caso atípico, porque el nuevo documento es el valor de retorno del método en lugar de algo que usted haya construido, y olvidar que es un controlador separado propiedad del llamador es como ocurren tanto el goteo como la doble liberación. Inicialice el resultado en nil, compruébelo después de la llamada y libérelo en una sola ruta

Si el trabajo que realmente tiene consiste en combinar archivos completos en lugar de reordenar páginas, consulte la fusión de múltiples archivos PDF en un solo documento. Si es lo contrario, dividir un documento en varios archivos, consulte la división de documentos PDF en múltiples archivos. Los métodos de imposición y reordenamiento descritos aquí se distribuyen como parte de PDFium Component para Delphi y C++Builder, junto con las API de carga, procesamiento y edición cubiertas en otras secciones de este blog