Technical Article

Spajanje više PDF datoteka u jedan dokument pomoću PDFium VCL-a

PDFium VCL omogućuje spajanje PDF-ova putem jedne metode: ImportPages. Uzorak je uvijek isti: stvorite prazan ciljni dokument, otvorite svaku izvornu datoteku, pozovite ImportPages za kopiranje stranica, zatvorite izvor i ponovite postupak. Kada petlja završi, SaveAs zapisuje rezultat na disk. Ne postoji poseban način spajanja niti konfiguracija koju treba mijenjati. Složenost leži u rubnim slučajevima, a postoji nekoliko njih koji mogu stvoriti probleme bez upozorenja.

Glavna petlja

Sve što trebate su dvije instance klase TPdf. Jedna drži ciljni dokument stvoren kao prazan pomoću CreateDocument. Druga otvara svaku izvornu datoteku pojedinačno. Ispod je procedura koja prima popis putanja datoteka i zapisuje spojeni izlaz na jednu putanju:

procedure MergeFiles(const FileList: TStrings; const OutputPath: string);
var
  PdfDest, PdfSrc: TPdf;
  InsertAt, I: Integer;
begin
  PdfDest := TPdf.Create(nil);
  PdfSrc  := TPdf.Create(nil);
  try
    PdfDest.CreateDocument;
    InsertAt := 1;  // ImportPages uses 1-based destination position

    for I := 0 to FileList.Count - 1 do
    begin
      PdfSrc.FileName := FileList[I];
      PdfSrc.Active   := True;

      if not PdfSrc.Active then
        raise Exception.CreateFmt('Cannot open: %s', [FileList[I]]);

      PdfDest.ImportPages(
        PdfSrc,
        '1-' + IntToStr(PdfSrc.PageCount),  // full document range
        InsertAt);

      Inc(InsertAt, PdfSrc.PageCount);
      PdfSrc.Active := False;
    end;

    PdfDest.SaveAs(OutputPath);
  finally
    PdfSrc.Free;
    PdfDest.Free;
  end;
end;

Dvije stvari u tom kodu lako je previdjeti na prvo čitanje. Prva je način na koji PDFium prijavljuje pogreške pri učitavanju. Active := True nikada ne podiže iznimku: ako datoteka nedostaje, oštećena je ili zaštićena lozinkom, PDFium interno hvata pogrešku i ostavlja Active na False. Bez eksplicitne provjere, neispravna datoteka bi tiho ispala iz spajanja bez ikakvih naznaka u izlaznom rezultatu. Konačni PDF imao bi manje stranica od očekivanog, a ne biste znali koja je datoteka uzrok.

Druga stvar je brojač InsertAt. Treći argument metode ImportPages je 1-bazirana pozicija u ciljnom dokumentu na koju dolazi prva uvezena stranica. Početak od 1 smješta prvi izvorni dokument na početak inače prazne datoteke. Nakon svakog izvora, brojač se uvećava za PdfSrc.PageCount, tako da se sljedeći skup stranica dodaje nakon posljednjeg. Ako zaboravite uvećati ovaj brojač, svaki sljedeći izvor će prepisati stranice na poziciji 1, ostavljajući vam samo posljednji dokument s popisa.

Odabir raspona stranica

Ne morate preuzeti svaku stranicu iz izvora. Znakovni niz raspona koji se prosljeđuje kao drugi argument slijedi jednostavan format s zarezom i crticom: "1-3" uzima stranice od 1 do 3, "2,4,6" odabire tri specifične stranice, a "1-" označava stranicu 1 do kraja dokumenta. Rasponi se mogu kombinirati u jednom nizu, pa tako "1-3,5,7-" preskače stranice 4 i 6. Ovdje je važna jedna pojedinost: brojevi se uvijek odnose na stranice u izvornom dokumentu, počevši od 1, bez obzira na to gdje te stranice završavaju u ciljnom dokumentu. Ako želite stranice od 40 do 50 iz kataloga od 200 stranica, niz raspona je "40-50", a ne pozicija u odnosu na ono što se već nalazi u ciljnom dokumentu.

// Extract cover plus a three-page executive summary from a long report
PdfSrc.FileName := 'annual-report.pdf';
PdfSrc.Active   := True;
if PdfSrc.Active then
begin
  // Page 1 is the cover; pages 3-5 are the summary
  PdfDest.ImportPages(PdfSrc, '1,3-5', InsertAt);
  Inc(InsertAt, 4);  // 1 cover + 3 summary pages = 4 pages added
  PdfSrc.Active := False;
end;

Prilikom izračunavanja uvećanja za InsertAt, brojite stranice koje ste stvarno uvezli, a ne ukupan broj stranica izvora. Ako proslijedite '1,3-5', uvezli ste 4 stranice, pa se pomaknite za 4. Pomicanje za PdfSrc.PageCount ostavilo bi prazna mjesta u ciljnom dokumentu i smjestilo sljedeći izvorni dokument dalje u datoteku nego što je planirano.

Što ImportPages čuva, a što ne

Stranice kopirane pomoću ImportPages prenose svoj vidljivi sadržaj netaknut. Tekst, vektorska grafika, rasterske slike, ugrađeni fontovi i form XObjects prenose se kao dio strujanja sadržaja stranice (page content streams). Bilješke na razini stranice, uključujući komentare, isticanja i poteze tintom, također se prenose jer su pohranjene unutar rječnika stranice (page dictionary), a ne na razini dokumenta.

Metapodatci na razini dokumenta su druga priča. Naslov, autor, predmet i ključne riječi u izvornom Info rječniku ostaju iza. Ciljni dokument počinje s praznim metapodadcima nakon CreateDocument, pa ako spojeni izlaz treba imati popunjena ta polja, morate ih dodijeliti izravno objektu PdfDest prije pozivanja metode SaveAs. Svojstva Title, Author, Subject, Keywords i Creator klase TPdf primaju obične znakovne nizove i zapisuju se u Info rječnik prilikom spremanja.

Interaktivna polja obrazaca su složenija. Definicije polja AcroForm nalaze se u rječniku na razini dokumenta, a ne unutar pojedinačnih strujanja stranica. Kada ImportPages kopira stranicu koja sadrži polja obrazaca, vizualni izgled tih polja se prenosi jer je iscrtan u strujanju sadržaja stranice, ali widgeti polja koji ih čine interaktivnima dio su AcroForm strukture i ne prenose se. U tipičnom spajanju, tekstualno polje iz izvornog dokumenta prikazat će vrijednost koju je imalo u trenutku uvoza, ali se neće moći uređivati u spojenoj datoteci. Ako trebate da polja ostanu ispunjiva, spljoštite ih (flatten) u svakom izvornom dokumentu prije uvoza: to ugrađuje trenutne vrijednosti u strujanje sadržaja i uklanja interaktivni sloj, dajući čist vizualni rezultat bez neispravnih widgeta u izlazu.

Šifrirane izvorne datoteke

Izvorni dokumenti zaštićeni lozinkom otvaraju se na isti način kao i nešifrirani, uz jedno dodatno svojstvo koje je potrebno prvo postaviti. Dodijelite lozinku svojstvu PdfSrc.Password prije postavljanja Active := True, i PDFium će je koristiti prilikom otvaranja:

PdfSrc.Password := 'user-password';
PdfSrc.FileName := 'protected.pdf';
PdfSrc.Active   := True;
if not PdfSrc.Active then
  raise Exception.Create('Wrong password or file cannot be opened');

PdfDest.ImportPages(PdfSrc, '1-' + IntToStr(PdfSrc.PageCount), InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;

Pogrešna lozinka uzrokuje isti tihi ishod Active = False kao i nedostajuća datoteka, pa je eksplicitna provjera i ovdje jednako nužna. Šifriranje se ne prenosi na odredište: stranice uvezene iz zaštićenog izvora dolaze na odredište kao nezaštićeni sadržaj. Ako spojeni izlaz također zahtijeva šifriranje, konfigurirajte ga na objektu PdfDest prije pozivanja metode SaveAs.

Spremanje rezultata

Metoda SaveAs na klasi TPdf prihvaća ili putanju datoteke ili TStream. Za većinu spajanja, preopterećena verzija s datotekom je ono što želite:

PdfDest.SaveAs('merged-output.pdf');

Izborni drugi argument je TSaveOption koji kontrolira način spremanja. Zadana vrijednost, saNone, zapisuje inkrementalno ažuriranje ako je dokument učitan iz datoteke ili potpuno ponovno zapisivanje ako je stvoren nanovo. Budući da je odredište izgrađeno pomoću CreateDocument uvijek novo, izlaz će biti kompaktna datoteka s jednom revizijom. Treći argument, TPdfVersion, omogućuje vam fiksiranje zaglavlja verzije PDF-a kada imate daljnje korisnike koji zahtijevaju određenu verziju; ostavljanje na pvUnknown omogućuje PDFiumu da odabere na temelju sadržaja.

Metode ImportPages i SaveAs prikazane ovdje dio su komponente PDFium VCL Component za Delphi i C++Builder.