Dos minutos para copiar tres páginas de un PDF de 40 páginas no es un problema de ajuste de rendimiento. Es una señal de que se está utilizando la ruta API incorrecta. Cuando vi por primera vez este tiempo en un ejemplo de copia de página de HotPDF Component, mi instinto fue observar primero la estructura del documento y en segundo lugar el código. Ese orden resultó ser importante
Lo que realmente era lento
El PDF en cuestión era un documento de referencia de 40 páginas con un árbol de páginas nada trivial: múltiples nodos /Pages intermedios en lugar de un único arreglo plano. El código de ejemplo original llamaba a LoadFromFile, luego creaba un nuevo documento con BeginDoc, recorría los números de página seleccionados y, en cada iteración, cargaba el documento fuente nuevamente desde el disco para extraer una página. Ese es el costo del análisis completo multiplicado por la cantidad de páginas que se deseen. Un archivo de 12 MB accedió al disco seis veces para una extracción de tres páginas porque nadie se fijó en si el archivo debía permanecer abierto a lo largo de las iteraciones
El segundo contribuyente era invisible en el código: LoadFromFile de HotPDF resuelve la tabla de referencias cruzadas completa y descomprime cada flujo de objetos al cargarlo. Ese es el comportamiento correcto para un documento que está a punto de modificar, pero es más trabajo del necesario si solo desea el recuento de páginas y un subconjunto de estas. Para acceso de solo lectura a la estructura, DAOpenFileReadOnly evita deserializar el árbol de objetos completo, lo cual es importante en archivos comprimidos con recursos de imágenes grandes
Ninguno de estos es un error en la biblioteca. Ambos son casos de un usuario eligiendo la API diseñada para un trabajo y usándola para uno diferente
Uso de InsertPagesFromDocument para la extracción de páginas
La ruta correcta para copiar un rango de páginas de un documento HotPDF a otro es InsertPagesFromDocument, invocada después de LoadFromFile en la fuente. Se carga la fuente una vez, se carga o crea el destino una vez, se mueven las páginas y se guarda. La fuente permanece en la memoria en todas las inserciones de páginas:
procedure ExtractPages(const SourceFile, DestFile: string;
const PageRange: string);
var
Source, Dest: THotPDF;
begin
Source := THotPDF.Create(nil);
Dest := THotPDF.Create(nil);
try
// Load source once: full parse happens here and only here
Source.LoadFromFile(SourceFile);
// Build a minimal destination document
Dest.FileName := DestFile;
Dest.BeginDoc;
// Copy the requested range; '1-3' inserts pages 1 through 3
// starting at position 1 in the destination
Dest.InsertPagesFromDocument(Source, PageRange, 1);
Dest.EndDoc;
finally
Source.Free;
Dest.Free;
end;
end;
El parámetro PageRange acepta el mismo formato que la muestra de la línea de comandos: una lista separada por comas de números de página o rangos como '1-3' o '1,5,7-9'. Las páginas se indexan basándose en 1. InsertPagesFromDocument copia flujos de contenido, diccionarios de recursos y geometría de página sin tocar metadatos, marcadores o archivos adjuntos incrustados, a menos que se haga referencia a ellos desde las páginas copiadas. Para una extracción de tres páginas de un documento de 40 páginas, ese es un conjunto de trabajo pequeño
El tiempo en el mismo archivo de 12 MB que anteriormente demoraba dos minutos: menos de 1,5 segundos con este patrón. La mayor parte de ese tiempo es la única llamada a LoadFromFile. La estructura del documento es irrelevante una vez que la tabla de objetos se resuelve la primera vez
Cuando LoadFromFile es demasiado: la API de Direct File
Si solo necesita contar páginas, inspeccionar la información del documento o copiar un archivo sin tocar su contenido, la API de Direct File evita por completo el análisis completo. DAOpenFileReadOnly mapea la tabla de referencias cruzadas sin descomprimir los flujos de objetos, por lo que el conteo de páginas es O(tamaño de xref) en lugar de O(tamaño del archivo):
procedure InspectPDF(const FileName: string);
var
Pdf: THotPDF;
Handle, PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Handle := Pdf.DAOpenFileReadOnly(FileName, '');
if Handle <= 0 then
Exit;
try
PageCount := Pdf.DAGetPageCount(Handle);
Writeln('Pages: ', PageCount);
// DACopyFile is a byte-preserving copy, no re-serialization
Pdf.DACopyFile(FileName, 'archive-copy.pdf');
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
La advertencia: DAOpenFileReadOnly acepta un parámetro de contraseña pero recurre a un análisis completo para entradas cifradas, debido a que el descifrado requiere el árbol de objetos para resolver el diccionario de cifrado. Si sus archivos fuente están cifrados, descífrelos primero con DecryptFile para obtener una copia sin cifrar, luego abra dicha copia con la API de Direct File. La función DecryptFile a nivel de archivo toma una ruta de reescritura directa AES-256 para el cifrado estándar y es más rápida que LoadFromFile seguido de SaveLoadedDocument para archivos grandes, ya que no construye el modelo de objetos completo en memoria
Memoria durante el procesamiento por lotes grandes
Los trabajos por lotes que procesan docenas de archivos en un bucle tienen un patrón que parece correcto pero acumula memoria: crear THotPDF dentro del bucle, llamar a LoadFromFile, hacer el trabajo y llamar a Free. Eso está estructuralmente bien. El problema es cuando el trabajo interno asigna objetos provisionales (scratch), captura excepciones y deja esos objetos vivos en las rutas de error. El administrador de memoria de Delphi no compacta, por lo que un centenar de fugas de rutas de error en una ejecución por lotes puede elevar la memoria lo suficiente como para ralentizar la asignación de todo lo demás
La solución no es exótica. Cada THotPDF y cada TStream o TBitmap intermedio que participe en el trabajo con PDF pertenece a un bloque try/finally donde Free es la última declaración. Establezca los punteros locales en nil antes del try para que la rama finally pueda utilizar if Assigned(x) then x.Free de forma segura cuando la inicialización falle a la mitad. Esta es la disciplina de propiedad estándar en Delphi y es la historia completa para esta clase de problema
Una cosa más a verificar en contextos por lotes: AddImage registra imágenes en una lista interna que persiste durante la vida útil de la instancia de THotPDF. Si reutiliza una única instancia en muchos documentos llamando a LoadFromFile repetidamente, los registros de imágenes de documentos anteriores permanecen en la lista. O crea una instancia nueva por documento, o llama a la ruta que borra la lista de imágenes entre documentos
Medir antes de cambiar algo
Antes de recurrir a cualquiera de estos patrones, mida. El TStopwatch de Delphi desde System.Diagnostics envuelve a QueryPerformanceCounter y es lo suficientemente preciso para perfilar el tiempo de reloj del I/O de archivos. Envuelva solo a LoadFromFile y vea qué tanto de ese tiempo representa. Si es el 90 % del tiempo total, la solución es la API de Direct File o reducir la cantidad de veces que analiza el mismo archivo. Si es inferior al 20 %, el cuello de botella está en otra parte y está persiguiendo lo incorrecto
La extracción de dos minutos que inició esta publicación resultó ser enteramente del patrón de carga repetida. La estructura del documento no contribuyó en nada; un árbol de páginas plano se habría ejecutado de la misma manera. El cambiar a un solo LoadFromFile seguido de una llamada a InsertPagesFromDocument lo redujo a 1,3 segundos en el mismo hardware sin tocar nada más
La API de manipulación de páginas que se muestra aquí es parte de HotPDF Component para Delphi y C++Builder