Fusionar y dividir son las dos operaciones de página a las que todos recurren primero y abarcan mucho terreno. No lo cubren todo. Existe una familia independiente 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 el final de un documento hacia el frente o extraer las páginas 3, 7 y 12 en un breve fragmento sin alterar el resto. PDFium expone tres métodos exactamente para esto, y cada uno se comporta de manera diferente a la fusión y división que ya conoce. Este artículo analiza qué hacen, dónde se encuentran los puntos de salida y un detalle de propiedad que ha causado bloqueos en entornos de producción.
Los tres son ImportNPagesToOne para la imposición N-up, MovePages para el reordenamiento en el lugar e ImportPagesByIndex para la extracción de subconjuntos. Fusión apila documentos de extremo a extremo y deja el recuento de páginas igual a la suma de las entradas. División escribe varios archivos de salida a partir de una entrada. Las tres operaciones aquí descritas se sitúan en un punto intermedio: una de ellas cambia cuántas páginas de origen comparten una hoja, otra cambia el orden dentro de un mismo documento y otra copia un grupo seleccionado de páginas en otro documento. Saber cuál es cuál le evita forzar una rutina de fusionar y eliminar cuando una sola llamada sería suficiente.
Qué hace realmente 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 (2-up), la signatura de cuadernillo de 4 páginas por hoja (4-up) 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 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 agrupa cuatro en un diseño de cuadrantes; 4, 3 genera una hoja de contactos de doce páginas. PDFium lee las páginas de origen en orden, reduce la escala de cada una para que quepa en su celda y llena la cuadrícula de izquierda a derecha, de arriba a abajo, iniciando una nueva hoja de salida cada vez que la cuadrícula actual se llena. Las páginas de origen no se modifican. Lo que recibe de vuelta es un nuevo documento cuyas páginas son composiciones.
El tamaño de salida está 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 ni con la resolución 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 recurre a un recuento de píxeles y termina con una hoja del tamaño de una estampilla postal o una cartelera espectacular.
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 multiplicadas por 72 es 612 y 11 pulgadas multiplicadas por 72 es 792. A4 mide 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, indicando que una unidad es 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 numérico directo.
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 identificador devuelto es suyo para liberarlo
Lea la firma de nuevo. ImportNPagesToOne devuelve un TPdf, no un Booleano. Ese valor de retorno es un identificador de documento completamente nuevo, asignado por separado de la fuente, y el llamador es su propietario. El objeto TPdf de origen sobre el que llamó al método permanece intacto y sigue poseyendo su propio identificador; el compuesto es un segundo objeto independiente. Si permite que el TPdf devuelto quede fuera del alcance sin liberarlo, filtrará un documento de 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 identificador directo dentro del TPdf devuelto, de modo que el ciclo de vida del contenedor rige al del identificador. Desde ese momento, existe exactamente un único propietario del identificador y un solo lugar donde debe cerrarse: al liberar (Free) el objeto devuelto. Una ruta de error descuidada que libera el contenedor y también llama a FPDF_CloseDocument en el identificador directo que capturó cierra el mismo documento de PDFium dos veces. Eso es una doble liberación, y es el error específico que afectó a un usuario aquí en una ocasión. La regla que lo previene es simple. Cierre el documento por una sola ruta, liberando el TPdf que el método le entregó, y nunca acceda a través del contenedor para cerrar el identificador que este ya adoptó.
De esto se desprenden dos corolarios. Primero, el método devuelve nil cuando PDFium rechaza los argumentos (como un cero en cualquiera de los ejes de la cuadrícula o una falla de asignación), por lo que corresponde realizar una verificación de nil antes de interactuar con el resultado. Segundo, inicialice su variable de salida en nil antes del bloque try y libérela en finally, tal como lo hace el ejemplo anterior, de modo que una falla a mitad de camino no lo deje intentando liberar una referencia indefinida u omitiendo la liberación por completo.
Reordenamiento de páginas sin reescribirlas
El reordenamiento modifica un documento en el lugar. MovePages extrae 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 tienen base cero. PageIndices enumera las páginas a mover, en el orden en que deberían quedar, y DestPageIndex es el índice en el que aterriza la primera página movida una vez establecido 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 mueve una miniatura a un espacio nuevo 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ó correctamente.
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 un arreglo de índices con base cero, e inserta esas páginas en el destino en la posición elegida:
function ImportPagesByIndex(
Source : TPdf;
const PageIndices: array of Integer;
InsertAt : Integer= 0): Boolean;
Lo invoca 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 el espacio con base 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 se anexa al recuento actual de páginas del destino. Un arreglo vacío importa cada página, lo que convierte la llamada en una copia completa cuando la necesita. Devuelve False si algún índice está fuera de rango en el origen.
Aquí es donde importa el contraste con la división. División escribe archivos separados (una sola operación que produce muchas salidas en el disco). ImportPagesByIndex realiza la estructura de trabajo opuesta: reúne un conjunto seleccionado de páginas en un único documento de destino en la memoria, el cual luego guarda una sola vez. Cuando la tarea es "entrégame las páginas 3, 7 y 12 como un único PDF corto", este es el camino directo, y envuelve internamente a FPDF_ImportPagesByIndex.
var
Source, Excerpt: TPdf;
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 estructurarlo todo limpiamente
La estructura 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 cuidado es cuáles llamadas asignan un documento nuevo. MovePages modifica el documento que ya posee, por lo que hay un solo objeto a 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 identificador independiente propiedad del llamador es cómo ocurren tanto la filtración como la doble liberación. Inicialice el resultado en nil, verifíquelo después de la llamada y libérelo por una única ruta.
Si la tarea que realmente tiene es combinar archivos completos en lugar de reorganizar páginas, consulte cómo fusionar múltiples archivos PDF en un solo documento. Si es lo contrario, dividir un documento en varios archivos, consulte cómo dividir 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, renderizado y edición cubiertas en otras partes de este blog.