Articol tehnic

Împărțirea documentelor PDF cu PDFium în Delphi

PDFium Component vă dă o singură metodă pentru împărțirea PDF-urilor: ImportPages. Tot restul, fie că izolați o singură pagină, fie că tăiați pe granițe arbitrare sau urmați structura de marcaje a documentului, înseamnă doar moduri diferite de a decide ce numere de pagină intră în fiecare fișier de ieșire. Mecanica rămâne aceeași. Înțelegerea din timp a acestui lucru vă scutește de multe drumuri greșite

Cum funcționează bucla de împărțire

Tiparul este același indiferent cum împărțiți documentul sursă. Creați o instanță TPdf proaspătă, apelați pe ea CreateDocument ca să inițializați un PDF gol în memorie, importați paginile dorite cu ImportPages, salvați rezultatul, apoi readuceți Active la False înainte de iterația următoare. Ultimul pas este cel pe care lumea îl ratează: CreateDocument nu închide implicit documentul aflat încă în memorie, așa că trebuie să salvați rezultatul și să resetați explicit Active := False înainte de a-l apela din nou; resetarea în prealabil păstrează starea curată și bine definită. Instanța TPdf exterioară este reutilizată în toate iterațiile, ceea ce ține presiunea de alocare scăzută la lucrările mari

Diagramă a buclei de împărțire cu PDFium Component în Delphi: CreateDocument, ImportPages din sursa doar-citire, un SaveAs verificat și resetarea lui Active înaintea fiecărei iterații noi
Oricine ar decide grupurile, bucla rămâne identică: importați paginile, salvați cu rezultatul verificat, apoi resetați Active, ca următorul CreateDocument să pornească dintr-o stare curată

Iată cum arată împărțirea pagină cu pagină, redusă la esențial:

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 este un șir cu numere de pagină de la 1; punctul de inserare 1 = prima poziție
      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;   // resetează înainte de următorul CreateDocument
    end;
  finally
    PdfOut.Free;
  end;
end;

Parametrul Range al lui ImportPages are același format de șir pe care îl folosește PDFium intern: o listă separată prin virgulă de numere de pagină sau de intervale delimitate prin cratimă, toate începând de la 1. '3' importă pagina 3. '1-5' importă paginile de la 1 la 5, în ordine. '2,5,8' importă acele trei pagini. Al treilea parametru este poziția de inserare, numărată de la 1, în documentul destinație; transmiterea lui 1 plasează întotdeauna paginile importate la începutul unui fișier altfel gol, adică exact ce vreți aici

Împărțirea pe intervale de pagini

Când apelantul furnizează o listă precum 1-12,13-24,25-36, o parsați în perechi de început și sfârșit și rulați aceeași buclă, construind șirul de interval din fiecare pereche:

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;

Validarea dinaintea ajungerii la ImportPages contează aici. ImportPages returnează False când un număr de pagină din șirul de interval depășește Source.PageCount, dar nu ridică excepție și nu produce un fișier de ieșire parțial pe care să îl detectați doar după nume. Verificați valoarea returnată de SaveAs și înregistrați eșecurile separat; un interval care produce un fișier de ieșire gol nu pare greșit până când nu îl deschide cineva

Împărțirea la granițele marcajelor

A treia abordare folosește structura proprie a documentului, nu o listă furnizată din afară. Fiecare marcaj de nivel superior poartă un număr de pagină țintă; secțiunea pe care o definește merge de la acea pagină până la una înaintea paginii marcajului următor sau până la finalul documentului, pentru ultima intrare

Diagramă care asociază marcajele PDF de nivel superior cu intervalele de pagini calculate și cu fișierele de ieșire la împărțirea cu PDFium Component în Delphi, inclusiv un marcaj în afara intervalului, care este sărit
O secțiune merge de la pagina fiecărui marcaj de nivel superior până la o pagină înaintea marcajului următor, iar intrările care arată dincolo de final sunt sărite, nu transformate în fișiere goale
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;   // sari peste o secțiune malformată în loc să scrii un fișier gol
      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 document care nu are marcaje nu este o condiție de eroare pe care să merite să o arătați utilizatorului ca atare; înseamnă doar că acest mod de împărțire nu are de unde să pornească. Paza Length(Bm) = 0 se ocupă de asta în tăcere. Ce merită arătat este cazul în care numărul de pagină al unui marcaj se află în afara intervalului documentului, ceea ce se întâmplă în fișierele malformate, unde cuprinsul nu a fost actualizat după ștergerea unor pagini. Verificarea limitelor pe StartPage și EndPage sare peste acele intrări, în loc să transmită un interval de gunoi lui ImportPages

Numele fișierelor de ieșire și resetarea lui Active

Siguranța numelor de fișier pentru denumirile derivate din marcaje are nevoie de atenție explicită. Titlurile de marcaj pot conține caractere valide într-un șir PDF, dar nu și într-o cale din sistemul de fișiere. Cel puțin, înlocuiți bara oblică, bara oblică inversă și două puncte înainte să construiți calea de ieșire. Pe Windows sunt interzise și *, ?, ", <, > și |; o buclă simplă peste un set fix le acoperă fără să aduceți o expresie regulată

Rândul Active := False de la finalul fiecărei iterații merită subliniat, pentru că este singura cerință neevidentă din acest tipar. CreateDocument nu închide implicit ce este deschis. Dacă Active este încă True când CreateDocument rulează din nou, documentul aflat încă în memorie nu a fost niciodată închis sau salvat corespunzător, iar în acea stare nu vă puteți baza pe un comportament bine definit, așa că salvați și resetați explicit înainte să porniți documentul următor. Gândiți-vă la asta ca la perechea lui try/finally: blocul finally eliberează obiectul exterior, iar Active := False resetează starea documentului interior între iterațiile buclei

Consumul de memorie pe o lucrare mare de împărțire rămâne plat cu această abordare, pentru că nu țineți niciodată mai mult de un document de ieșire în memorie deodată. Documentul sursă rămâne deschis și doar-citire pe tot parcursul; ImportPages copiază datele paginilor în documentul nou fără să modifice sursa. Dacă sursa este criptată, deschideți-o cu parola ei înainte de buclă, iar paginile copiate în fiecare fișier de ieșire vor fi necriptate, ceea ce este de obicei comportamentul corect pentru un rezultat de împărțire distribuit unor destinatari diferiți

Încă un lucru despre SaveAs: returnează un Boolean. Un director de ieșire care nu există, o cale cu caractere pe care sistemul de operare le respinge sau un disc plin vor face toate ca SaveAs să returneze False fără să ridice o excepție. Într-o lucrare pe loturi care împarte un document de 200 de pagini în 200 de fișiere de câte o pagină, un eșec tăcut la pagina 147 este ușor de scăpat din vedere. Verificați valoarea returnată la fiecare apel și numărați reușitele față de totalul așteptat când bucla se termină

Metodele ImportPages și CreateDocument arătate aici fac parte din PDFium Component pentru Delphi și C++Builder