Fusionar y dividir son las dos operaciones de página que todos buscan primero, y cubren mucho terreno. Pero no lo cubren todo. Hay una familia de trabajo separada que reorganiza páginas en lugar de mover archivos enteros: colocar cuatro diapositivas en una hoja para un folleto, arrastrar una página desde el final de un documento al principio, o extraer las páginas 3, 7 y 12 en un fragmento corto sin tocar 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 explica qué hacen, dónde residen los puntos de salida y un detalle de propiedad que ha provocado fallos en el terreno
Qué hace realmente la imposición N-up
La imposición es el término de preimpresión para organizar varias páginas fuente 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 2-up (dos páginas por hoja), la signatura de cuadernillo 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 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 fuente lado a lado; 2, 2 agrupa cuatro en un diseño de cuadrante; 4, 3 construye una hoja de contactos de doce páginas. PDFium lee las páginas fuente en orden, reduce la escala de cada una para que encaje en su celda y llena la cuadrícula de izquierda a derecha, de arriba a abajo, comenzando una hoja de salida nueva cada vez que la cuadrícula actual está llena. Las páginas fuente no se modifican. Lo que obtiene a cambio es un documento nuevo cuyas páginas son compuestos
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 una setenta y dosava parte de una 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 los DPI de renderizado. Este es el lugar más común para cometer un error 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 un panel publicitario
Los números que vale la pena memorizar son los dos tamaños de página que usará más. La carta estadounidense (US Letter) es de 612 por 792 puntos, porque 8.5 pulgadas por 72 es 612 y 11 pulgadas por 72 es 792. El formato A4 es de aproximadamente 595 por 842 puntos, por sus dimensiones de 210 por 297 milímetros. El propio encabezado de la vinculación (binding) establece la regla claramente, que una unidad es una setenta y dosava parte de una 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 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 identificador devuelto le corresponde a usted liberarlo
Lea la firma nuevamente. ImportNPagesToOne devuelve un TPdf, no un booleano. Ese valor de retorno es un identificador (handle) de documento completamente nuevo, asignado por separado de la fuente, y el llamador es su propietario. El TPdf fuente sobre el que llamó al método permanece intacto y aún es dueño de su propio identificador; el compuesto es un segundo objeto independiente. Si deja que el TPdf devuelto salga del alcance sin liberarlo, perderá un documento PDFium entero (memory leak)
El error más peligroso funciona al revés. Internamente, el método le pide a PDFium un FPDF_DOCUMENT nuevo a través de FPDF_ImportNPagesToOne, y luego envuelve ese identificador sin procesar (raw handle) dentro del TPdf devuelto para que el ciclo de vida de la envoltura gobierne el del identificador. Desde ese momento, hay exactamente un propietario del identificador, y exactamente un lugar donde debe cerrarse: cuando hace Free del objeto devuelto. Una ruta de error descuidada que libera la envoltura y también llama a FPDF_CloseDocument en el identificador que capturó, cierra el mismo documento PDFium dos veces. Esa es una doble liberación (double-free), y es el error específico que afectó a un usuario aquí una vez. La regla que lo previene es corta. Cierre el documento solo por una vía, liberando el TPdf que le entregó el método, y nunca pase por alto la envoltura para cerrar el identificador que 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 un fallo de asignación, por lo que corresponde hacer una comprobación de nil antes de tocar el resultado. Segundo, inicialice su variable de salida a nil antes del try y libérela en el finally, como lo hace el ejemplo anterior, de modo que un fallo a mitad de camino no lo deje liberando una referencia indefinida u omitiendo la liberación por completo
Reordenar páginas sin reescribirlas
La imposición construye un documento nuevo. El reordenamiento cambia un documento en su 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 conteo de páginas siga siendo el mismo:
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 establece el movimiento. Debido a que PDFium reubica las páginas en lugar de copiar y recomprimir su contenido, la operación es económica y sin pérdida de calidad: los objetos de página conservan sus flujos, sus recursos y su fidelidad. Esta es la llamada que se oculta detrás de un panel de páginas de arrastrar para reordenar, donde un usuario arrastra una miniatura a un nuevo espacio 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 tuvo lugar
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;
Extraer 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 fuente y una matriz de índices basada en cero, e inserta esas páginas en el destino en una posición elegida:
function ImportPagesByIndex(
Source : TPdf;
const PageIndices: array of Integer;
InsertAt : Integer= 0): Boolean;
Lo llama sobre el documento destino y pasa la fuente como primer argumento. PageIndices nombra las páginas de la fuente que se van a extraer, en el orden en que las desea; InsertAt es el espacio (basado 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 se agrega el conteo de páginas actual del destino. Una matriz vacía importa todas las páginas, lo que hace que la llamada sea una copia completa cuando la necesita. Devuelve False si algún índice está fuera de rango en la fuente
Aquí es donde importa el contraste con la división. Dividir (split) escribe archivos separados, siendo una operación que produce muchas salidas en el disco. ImportPagesByIndex hace la forma opuesta de trabajo: reúne un conjunto elegido de páginas en un solo documento de destino en memoria, que luego se guarda una vez. Cuando el trabajo es "dame las páginas 3, 7 y 12 como un PDF corto", esta es la ruta directa, y envuelve a 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;
Ensamblándolo de manera limpia
La forma de principio a fin es la misma en las tres: abrir la fuente configurando FileName y cambiando Active a True, realizar la operación, guardar con SaveAs y liberar lo que posee. La única rama que necesita cuidado es qué llamadas asignan un documento nuevo. MovePages muta el documento que ya tiene, por lo que hay un objeto que liberar. ImportPagesByIndex escribe en un destino que usted mismo creó, por lo que libera la fuente 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 construyó, y olvidar que es un identificador separado propiedad del llamador es la forma en que ocurren tanto las pérdidas de memoria (leaks) como las liberaciones dobles. Inicialice el resultado a nil, compruébelo después de la llamada y libérelo en una sola ruta
Si el trabajo que realmente tiene es combinar archivos enteros en lugar de reorganizar páginas, consulte fusionar múltiples archivos PDF en un solo documento. Si es a la inversa, dividir un documento en varios archivos, consulte dividir documentos PDF en múltiples archivos. Los métodos de imposición y reordenamiento descritos aquí se envían como parte del Componente PDFium para Delphi y C++Builder, junto con las API de carga, renderizado y edición cubiertas en otras partes de este blog