Artículo técnico

Splitting PDF Documents with PDFium Component in Delphi

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