PDFium Component le ofrece un método para la división de PDF: ImportPages. Todo lo demás, ya sea que aísle una sola página, corte en límites arbitrarios, o siga la propia estructura de marcadores del documento, son sólo formas diferentes de decidir qué números de página van en cada fichero de salida. La mecánica sigue siendo la misma. Entender esto desde el principio ahorra muchos caminos equivocados
Cómo funciona el bucle de división
El patrón es el mismo sin importar cómo divida el documento origen. Cree una instancia nueva de TPdf, llame a CreateDocument en ella para inicializar un PDF vacío en memoria, importe las páginas que desee con ImportPages, guarde el resultado, y luego restablezca Active a False antes de la siguiente iteración. Ese último paso es el que la gente pasa por alto: CreateDocument siempre inicia un documento nuevo, pero si Active sigue siendo True cuando vuelve a ejecutarse, descarta implícitamente el documento que todavía está en memoria, de modo que restablecerlo primero mantiene el estado limpio y bien definido. La instancia exterior TPdf se reutiliza a lo largo de todas las iteraciones, lo que mantiene baja la presión de asignación en trabajos grandes
Así es como se ve la división página por página reducida a lo esencial:
procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 1 to Source.PageCount do
begin
PdfOut.CreateDocument;
// Range is a 1-based page number string; insertion point 1 = first position
PdfOut.ImportPages(Source, IntToStr(I), 1);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
PdfOut.SaveAs(OutFile);
PdfOut.Active := False; // reset before next CreateDocument
end;
finally
PdfOut.Free;
end;
end;
El parámetro Range para ImportPages es el mismo formato de cadena que PDFium usa internamente: una lista de números de página separados por comas o rangos delimitados por guiones, todos en base 1. '3' importa la página 3. '1-5' importa las páginas de la 1 a la 5 en orden. '2,5,8' importa esas tres páginas. El tercer parámetro es la posición de inserción en base 1 en el documento destino; pasar 1 siempre coloca las páginas importadas al principio de un fichero por lo demás vacío, que es lo que usted quiere aquí
Dividir por rangos de páginas
Cuando el llamador suministra una lista como 1-12,13-24,25-36, usted la analiza en pares de inicio/fin y ejecuta el mismo bucle, construyendo la cadena de rango a partir de cada par:
procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(RangeList) do
begin
PdfOut.CreateDocument;
PdfOut.ImportPages(Source, RangeList[I], 1);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
PdfOut.SaveAs(OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
La validación antes de que alcance ImportPages importa aquí. ImportPages devuelve False cuando un número de página en la cadena de rango excede Source.PageCount, pero no lanza una excepción y no produce un fichero de salida parcial que pueda detectar sólo por el nombre. Compruebe el valor de retorno de SaveAs y registre los fallos por separado; un rango que produce un fichero de salida vacío no es evidentemente erróneo hasta que alguien lo abre
Dividir en los límites de los marcadores
El tercer enfoque utiliza la propia estructura del documento en lugar de una lista suministrada externamente. Cada marcador de nivel superior lleva un número de página de destino; la sección que define abarca desde esa página hasta una antes de la página del siguiente marcador, o hasta el final del documento para la última entrada
procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
Bm: TBookmarks;
I, StartPage, EndPage: Integer;
PdfOut: TPdf;
RangeStr, OutFile, SafeTitle: string;
begin
Bm := Source.Bookmarks;
if Length(Bm) = 0 then
Exit;
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(Bm) do
begin
StartPage := Bm[I].PageNumber;
if I < High(Bm) then
EndPage := Bm[I + 1].PageNumber - 1
else
EndPage := Source.PageCount;
if (StartPage < 1) or (EndPage < StartPage) then
Continue;
RangeStr := Format('%d-%d', [StartPage, EndPage]);
PdfOut.CreateDocument;
PdfOut.ImportPages(Source, RangeStr, 1);
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
PdfOut.SaveAs(OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
Un documento que no tiene marcadores no es una condición de error que valga la pena mostrar al usuario como tal; simplemente significa que este modo de división no tiene sobre qué trabajar. La guarda Length(Bm) = 0 maneja eso silenciosamente. Lo que sí vale la pena mostrar es cuando el número de página de un marcador está fuera del rango del documento, lo cual ocurre en ficheros mal formados donde el esquema nunca se actualizó después de que se borraron páginas. La comprobación de límites en StartPage y EndPage omite esas entradas en lugar de pasar un rango basura a ImportPages
Nomenclatura del fichero de salida y el restablecimiento de Active
La seguridad del nombre de fichero para nombres derivados de marcadores requiere atención explícita. Los títulos de los marcadores pueden contener caracteres que son válidos en una cadena de PDF pero no en una ruta de sistema de ficheros. Como mínimo, reemplace la barra diagonal, la barra invertida y los dos puntos antes de construir la ruta de salida. En Windows, *, ?, ", <, > y | también están prohibidos; un simple bucle sobre un conjunto fijo los cubre sin tener que usar expresiones regulares
La línea Active := False al final de cada iteración merece énfasis porque es el único requisito no obvio en el patrón. CreateDocument no cierra implícitamente lo que esté abierto. Si Active sigue siendo True cuando CreateDocument vuelve a ejecutarse, PDFium descarta el documento actual y comienza uno nuevo sin error, pero el comportamiento está definido por la implementación en casos extremos y la intención es más clara cuando usted lo restablece explícitamente. Piense en ello como el par de try/finally: el bloque finally libera el objeto exterior; el Active := False restablece el estado interno del documento entre las iteraciones del bucle
El uso de memoria a lo largo de un trabajo de división grande se mantiene plano con este enfoque porque nunca mantiene más de un documento de salida en memoria a la vez. El documento origen permanece abierto y de solo lectura durante todo el proceso; ImportPages copia los datos de la página al documento nuevo sin modificar el origen. Si el origen está encriptado, ábralo con su contraseña antes del bucle y las páginas copiadas en cada fichero de salida estarán desencriptadas, lo que suele ser el comportamiento correcto para resultados divididos distribuidos a distintos destinatarios
Una cosa más sobre SaveAs: devuelve un Boolean. Un directorio de salida que no existe, una ruta con caracteres que el SO rechaza, o una condición de disco lleno causarán todos que SaveAs devuelva False sin lanzar una excepción. En un trabajo por lotes que divide un documento de 200 páginas en 200 ficheros de una sola página, un fallo silencioso en la página 147 es fácil de pasar por alto. Compruebe el valor de retorno en cada llamada y cuente los éxitos frente al total esperado cuando termine el bucle
Los métodos ImportPages y CreateDocument mostrados aquí son parte de PDFium Component para Delphi y C++Builder