CollateDocumentsEx en la librería PDF PDFlibPas para Delphi combina varios documentos abiertos en un documento intercalado. Agrega GroupSize páginas de cada fuente por ronda, acepta una lista de rangos de páginas por fuente, y trata un rango descendente como 3-1 como una inversión de esa fuente. Una sola llamada convierte una pila frontal y una pila reversa invertida en orden de lectura
El escenario detrás de esa API es mundano y extremadamente común. Un escáner alimentado por hojas con una ruta de un solo lado procesa toda la pila boca abajo, luego el operador voltea la pila y la corre de nuevo. Terminas con dos PDFs: frentes en orden, reversos en orden inverso. El resultado que el usuario quiere es un solo archivo, página 1 frente, página 1 reverso, página 2 frente, y así sucesivamente. Este artículo trata sobre el problema de ordenamiento y la trampa de duplicación de recursos que subyace a él. Si tu interés es el rendimiento de concatenación pura, consulta fusión rápida de PDF mediante desplazamiento de referencias a nivel de bytes; si las entradas son demasiado grandes para caber en memoria, consulta fusión y división de PDFs de gigabytes con acceso directo
El escáner produce dos pilas, una de ellas al revés
La intercalación no es una fusión. Una fusión concatena rangos de páginas; una intercalación las entreteje, y el patrón de entretejido es una propiedad del dispositivo físico que produjo la entrada. Si el patrón está mal, el archivo no queda ligeramente mal, queda ilegible: cada segunda página pertenece a una hoja distinta. Tres variables describen casi todos los casos reales: cuántas fuentes están en la rotación, cuántas páginas vienen de cada fuente por ronda, y si alguna fuente necesita leerse al revés. CollateDocuments cubre las primeras dos con un arreglo simple de handles de documento y un entero GroupSize. CollateDocumentsEx agrega la tercera aceptando una lista de rangos de páginas separada por punto y coma, un segmento por fuente, donde un segmento vacío significa todas las páginas de esa fuente y un rango descendente la invierte. Ambas funciones agregan al final del documento actualmente seleccionado y devuelven 1 en éxito, 0 en cualquier rechazo
¿Por qué la intercalación ingenua multiplica el tamaño del archivo?
Porque el mapa de importación que asocia números de objeto de la fuente con números de objeto del destino se reconstruye en cada llamada de copia, y cualquier cosa accesible desde más de un fragmento se importa una vez por fragmento. Dentro de PDFlibPas, TPDFDocument.CopyPagesFromDoc reinicia su NewIndObjList al comienzo de cada invocación. Esa lista es la única memoria que el copiador tiene de lo que ya trajo. Llámalo una vez con un rango de diez páginas y una fuente compartida por las diez páginas se incrusta una vez. Llámalo diez veces con una página cada vez y esa misma fuente se incrusta diez veces. Esto importa mucho más en escaneos que en documentos de texto, porque una página escaneada es un único XObject de imagen grande y los objetos compartidos son los que tienen peso real: un perfil ICC incrustado, una cadena /DecodeParms compartida, un sello o marca de agua como XObject de formulario aplicado a cada hoja, la fuente de la capa de texto OCR. La forma obvia de escribir una intercalación round-robin es un bucle sobre rondas, y ese bucle es exactamente el caso patológico
// Do not do this. Each CopyPageRanges call rebuilds the import map,
// so anything the two sources share internally is imported once per
// round instead of once per source.
var
RoundIndex: Integer;
begin
for RoundIndex := 1 to 12 do
begin
PDF.CopyPageRanges(Fronts, IntToStr(RoundIndex));
PDF.CopyPageRanges(Backs, IntToStr(13 - RoundIndex));
end;
end;
Doce rondas, dos fuentes, veinticuatro mapas de importación. Nada te avisa. El orden de páginas es correcto, cada página se renderiza, y el único síntoma es un archivo varias veces más grande que la suma de sus entradas. En un trabajo por lotes de 300 páginas el multiplicador no es un error de redondeo, es la diferencia entre un archivo que cabe en el presupuesto de retención y uno que no
Importar una vez, luego reordenar el árbol de páginas
La solución es separar las dos preocupaciones que el bucle ingenuo había fusionado. Copiar decide qué objetos existen en el destino; ordenar decide dónde se ubican las páginas en el árbol de páginas. CollateDocumentsEx copia cada fuente exactamente una vez, en una sola llamada a CopyPagesFromDoc con el rango completo de esa fuente, así que cada fuente obtiene un mapa de importación y los recursos compartidos se escriben una sola vez. Solo después de que cada fuente ha aterrizado ocurre el entrelazado, y ocurre enteramente a través de TPDFPageTree.MovePage
Los movimientos de página son gratuitos en el sentido que importa aquí. ISO 32000-1 §7.7.3 define el árbol de páginas como una estructura balanceada de diccionarios de nodo cuyos arreglos /Kids contienen referencias indirectas, con /Count llevando el total de hojas en cada nodo. Reubicar una página significa quitar una referencia indirecta de un arreglo /Kids, insertarla en otro, ajustar ambos valores de /Count, y redirigir el /Parent de la página. No se toca ningún flujo de contenido, no se duplica ningún recurso, no se crea ningún objeto. El objeto de página conserva su número de objeto, que es también por qué los números de objeto permanecen estables como en reemplazo de páginas que preserva los números de objeto. Hay un detalle adicional que un movimiento de página ingenuo hace mal y que MovePage no. ISO 32000-1 §7.7.3.4 permite que /Resources, /MediaBox, /CropBox y /Rotate se hereden de un nodo ancestro en vez de estar declarados en la página. Una página que hereda sus recursos del nodo A y luego se mueve bajo el nodo B hereda silenciosamente algo distinto, o nada en absoluto. MovePage por lo tanto resuelve el valor heredado y lo escribe en el diccionario de la página antes de la reubicación, así que la página lleva consigo sus propios atributos a través del movimiento
¿Qué hace en realidad el paso de reordenamiento?
Ejecuta una ordenación por selección contra semántica de insertar-en. El orden relativo al bloque deseado se calcula primero: recorre las fuentes en rotación, toma hasta GroupSize índices de cada una, salta una fuente que ya se agotó, repite hasta que cada página esté colocada. Eso produce una permutación sobre el bloque agregado. Aplicarla es la parte incómoda, porque MovePage es una inserción, no un intercambio, así que cada movimiento desplaza en uno todo lo que hay entre la posición vieja y la nueva
La implementación mantiene un arreglo Current que modela dónde está actualmente cada página agregada, escanea hacia adelante desde la posición K en busca de la página que pertenece ahí, emite el movimiento, luego desliza las entradas del arreglo para reflejar lo que el movimiento hizo al árbol. Es O(n al cuadrado) en operaciones de arreglo y cero en copias de objetos, que es el intercambio correcto para esta carga de trabajo: una intercalación de 500 páginas son un cuarto de millón de mezclas de enteros y ni un byte de datos de imagen duplicados. Los rangos descendentes y las páginas repetidas no necesitan manejo especial en este paso porque PLParsePageRangeList se llama con el ordenamiento deshabilitado y duplicados permitidos, así que el orden solicitado sobrevive intacto al análisis
Rangos invertidos y la fusión dúplex de una sola llamada
Con la inversión expresada como un rango, el caso de doble pasada en un escáner plano se colapsa en una sola llamada. Los frentes quieren su orden natural y los reversos quieren 12-1, y el primer segmento vacío antes del punto y coma indica que la primera fuente aporta todas sus páginas
var
PDF: TPDFlib;
Target, Fronts, Backs: Integer;
begin
PDF := TPDFlib.Create;
try
Target := PDF.NewDocument;
if PDF.LoadFromFile('fronts.pdf', '') <> 1 then
Exit;
Fronts := PDF.SelectedDocument;
if PDF.LoadFromFile('backs.pdf', '') <> 1 then
Exit;
Backs := PDF.SelectedDocument;
PDF.SelectDocument(Target);
// fronts 1..12 in order, backs scanned in reverse: F1 B12 F2 B11 ...
if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 1 then
PDF.SaveToFile('duplex.pdf');
finally
PDF.Free;
end;
end;
Dos comportamientos en ese fragmento vale la pena mencionar explícitamente. Las páginas intercaladas se agregan al documento seleccionado, así que un documento creado con NewDocument aporta su página en blanco inicial antes de ellas y deberías eliminarla si no la quieres. Y las fuentes pueden ser desiguales: con GroupSize 2 sobre una fuente de tres páginas y una de cinco páginas, las rondas salen A1 A2 B1 B2, luego A3 B3 B4 una vez que A casi se agota, luego B5 sola, porque una fuente agotada simplemente se salta en vez de rellenarse
Reversión, campos de formulario, y qué no viene incluido
Cada argumento se valida antes de tocar el destino. Un handle de documento faltante, el documento seleccionado listado como su propia fuente, un GroupSize menor a uno, un conteo de segmentos que no coincide con el conteo de fuentes, un rango que nombra una página que la fuente no tiene: todos estos devuelven 0 con el destino sin cambios. Un fallo durante la copia es el caso más difícil, y se maneja a través del DeletePages público en vez del PageTree.DeletePages crudo. La razón es específica. La copia se ejecuta con MergeFormData habilitado, así que los campos de formulario de la fuente ya se agregaron al arreglo /AcroForm /Fields del destino para cuando falla una fuente posterior. Eliminar las páginas al nivel del árbol de páginas eliminaría las páginas con widgets y dejaría esas referencias de campo colgando; la ruta pública desvincula las referencias de campo, de esquema y de hilo de artículo junto con las páginas
if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 0 then
// Nothing was appended and the target is byte-identical to before.
// 412 is the copy failure; 0 means the arguments were rejected
// during validation, before any page was touched.
Log(Format('collate rejected, LastErrorCode=%d', [PDF.LastErrorCode]));
Sé honesto con tus usuarios sobre los límites. La intercalación transporta páginas, sus anotaciones y sus campos de formulario, y fusiona la lista de campos de AcroForm, el arreglo de orden de cálculo y el diccionario de recursos por defecto. No transporta marcadores de la fuente: el árbol de esquemas de una pila de escaneos frontales casi siempre está vacío, así que no se pierde nada en el caso dúplex, pero si intercalas dos documentos redactados sus esquemas se quedan atrás y tienes que reconstruir la navegación tú mismo. Los destinos con nombre que vivían solo en el catálogo de la fuente están en la misma situación. Planifica eso antes de prometerle a un cliente una intercalación sin pérdidas
PDFlibPas incluye las funciones de intercalación junto con el resto de su superficie de ensamblaje de páginas, así que el flujo de trabajo del escáner, la extracción basada en rangos y las rutas para archivos grandes están todos detrás de un solo componente en Delphi y C++Builder. La referencia completa de la API y una versión de prueba están en la página del producto losLab Delphi PDF library