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
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
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