El PDFium Component le ofrece un método para la división de PDFs: ImportPages. Todo lo demás (ya sea aislar una sola página, dividir bajo límites arbitrarios o seguir la estructura de marcadores del propio documento) son diferentes formas de decidir qué números de páginas corresponden a cada archivo de salida. La mecánica es la misma. Comprender esto desde el principio le evitará muchos errores
Cómo funciona el bucle de división
El modelo es el mismo independientemente de cómo divida el documento de origen. Cree una instancia nueva de TPdf, llame a CreateDocument para inicializar un PDF vacío en memoria, importe las páginas deseadas mediante ImportPages, guarde el resultado y luego restablezca Active en False antes de la siguiente iteración. Ese último paso es el que se suele omitir: CreateDocument no cierra de forma implícita el documento que permanece en memoria, por lo que debe guardar su salida y restablecer Active := False de manera explícitamente antes de llamarlo de nuevo; restablecerlo primero mantiene el estado limpio y bien definido. La instancia externa de TPdf se reutiliza en todas las iteraciones, lo que reduce la carga de asignación en tareas grandes
A continuación se muestra una 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
if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
raise Exception.CreateFmt('Failed to import page %d', [I]);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False; // reset before next CreateDocument
end;
finally
PdfOut.Free;
end;
end;
El parámetro Range para ImportPages utiliza el mismo formato de cadena que PDFium emplea internamente: una lista de números de páginas separada por comas o rangos delimitados por guiones, todos basados en 1. El valor '3' importa la página 3; '1-5' importa las páginas 1 a 5 en orden; '2,5,8' importa esas tres páginas. El tercer parámetro es la posición de inserción basada en 1 en el documento de destino; pasar 1 siempre coloca las páginas importadas al principio de un archivo por lo demás vacío, que es la acción requerida aquí
Dividir por rangos de páginas
Cuando el solicitante proporciona una lista como 1-12,13-24,25-36, usted la divide en pares de inicio y 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;
if not PdfOut.ImportPages(Source, RangeList[I], 1) then
raise Exception.Create('Invalid page range: ' + RangeList[I]);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
La validación previa a ImportPages es importante aquí. ImportPages devuelve False si un número de página en la cadena de rango supera a Source.PageCount, pero no genera una excepción ni produce un archivo de salida parcial que pueda detectar solo por el nombre. Verifique el valor de retorno de SaveAs y registre los fallos de manera independiente; un rango que genere un archivo de salida vacío no se detectará como erróneo hasta que alguien intente abrirlo
Dividir bajo los límites de marcadores
El tercer enfoque utiliza la propia estructura del documento en lugar de una lista externa. Cada marcador de nivel superior incluye un número de página de destino; la sección que define se extiende desde esa página hasta una antes de la página del siguiente marcador, o hasta el final del documento en el caso de 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;
if not PdfOut.ImportPages(Source, RangeStr, 1) then
begin
PdfOut.Active := False;
Continue; // skip a malformed section instead of writing an empty file
end;
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
Un documento que carece de marcadores no es una condición de error que deba notificarse al usuario; simplemente significa que este modo de división no tiene elementos con los que trabajar. La comprobación Length(Bm) = 0 gestiona ese caso de forma silenciosa. Lo que sí conviene reportar es cuando el número de página de un marcador está fuera del rango del documento, lo cual ocurre en archivos mal formados donde el índice nunca se actualizó tras eliminar páginas. La comprobación de límites en StartPage y EndPage descarta esas entradas en lugar de pasar un rango incorrecto a ImportPages
Nombres de archivos de salida y reinicio de Active
La seguridad de los nombres para archivos derivados de marcadores requiere atención específica. Los títulos de marcadores pueden contener caracteres válidos en una cadena PDF pero no en una ruta del sistema de archivos. Como mínimo, reemplace la barra diagonal, la barra diagonal inversa y los dos puntos antes de construir la ruta de salida. En Windows, los caracteres *, ?, ", <, > y | también están prohibidos; un bucle simple sobre un conjunto fijo los resuelve sin necesidad de utilizar expresiones regulares (regex)
La línea Active := False al final de cada iteración merece especial atención porque es el único requisito no evidente en este modelo. CreateDocument no cierra de forma implícita lo que esté abierto. Si Active sigue en True cuando CreateDocument se ejecuta de nuevo, el documento en memoria nunca se cerró ni guardó correctamente, y no se puede confiar en obtener un comportamiento predecible en ese estado, por lo que debe guardar y restablecer explícitamente antes de iniciar el siguiente documento. Considérelo como el complemento de try/finally: el bloque finally libera el objeto externo; Active := False restablece el estado del documento interno entre las iteraciones del bucle
El uso de memoria durante una tarea de división grande se mantiene estable con este enfoque porque nunca conserva más de un documento de salida en memoria a la vez. El documento de origen permanece abierto y en modo de solo lectura; ImportPages copia los datos de la página al nuevo documento sin modificar el origen. Si el origen está cifrado, ábralo con su contraseña antes del bucle y las páginas copiadas en cada archivo de salida quedarán descifradas, lo cual suele ser el comportamiento esperado para salidas divididas que se distribuyen a diferentes destinatarios
Un detalle adicional sobre SaveAs: devuelve un valor Boolean. Un directorio de salida que no existe, una ruta con caracteres que el sistema operativo rechaza o una condición de disco lleno harán que SaveAs devuelva False sin generar una excepción. En una tarea por lotes que divide un documento de 200 páginas en 200 archivos de una sola página, es fácil pasar por alto un fallo silencioso en la página 147. Verifique el valor de retorno en cada llamada y compare los casos exitosos frente al total esperado cuando finalice el bucle
Los métodos ImportPages y CreateDocument descritos aquí forman parte del PDFium Component para Delphi y C++Builder