Teknisk artikkel

Slå sammen flere PDF-filer til ett dokument med PDFium Component

PDFium Component gjør PDF-sammenslåing tilgjengelig gjennom én enkelt metode: ImportPages. Fremgangsmåten er alltid den samme: Opprett et tomt måldokument, åpne hver kildefil, kall ImportPages for å kopiere sidene, lukk kilden og gjenta. Når løkken er ferdig, skriver SaveAs resultatet til disk. Det finnes ingen egen sammenslåingsmodus og ingen innstilling som må aktiveres. Utfordringene ligger i grensetilfellene, og noen av dem kan gi uventede problemer

Den grunnleggende løkken

To forekomster av TPdf er alt du trenger. Den ene inneholder måldokumentet, som opprettes tomt med CreateDocument. Den andre åpner hver kildefil etter tur. Prosedyren nedenfor tar en liste med filbaner og skriver det sammenslåtte resultatet til én filbane:

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;

To detaljer i denne koden er lette å overse ved første gjennomlesning. Den første er hvordan PDFium rapporterer innlastingsfeil. Active := True utløser aldri et unntak: Hvis filen mangler, er skadet eller er passordbeskyttet, håndterer PDFium feilen internt og lar Active forbli False. Uten den uttrykkelige kontrollen på linje 10 ville en ugyldig fil ganske enkelt bli utelatt fra sammenslåingen. Den ferdige PDF-filen ville inneholde færre sider enn forventet, uten at du visste hvilken fil som forårsaket problemet

Den andre detaljen er telleren InsertAt. Det tredje argumentet til ImportPages er den 1-baserte posisjonen i måldokumentet der den første importerte siden plasseres. Startverdien 1 legger det første kildedokumentet først i en ellers tom fil. Etter hver kilde økes telleren med PdfSrc.PageCount, slik at neste sidegruppe legges til etter den siste siden. Hvis telleren ikke økes, overskriver hver påfølgende kilde sidene fra posisjon 1. Da står du igjen med det siste dokumentet i listen og ingenting annet

Valgte sideområder

Du trenger ikke å importere alle sidene fra en kilde. Områdestrengen i det andre argumentet bruker et enkelt format med komma og bindestrek: "1-3" henter side 1 til 3, "2,4,6" velger tre bestemte sider, og "1-" betyr fra side 1 til slutten av dokumentet. Områder kan kombineres i én streng, slik at "1-3,5,7-" hopper over side 4 og 6. Her er én viktig detalj: Tallene viser alltid til sidene i kildedokumentet og begynner på 1, uavhengig av hvor sidene plasseres i måldokumentet. Hvis du vil hente side 40 til 50 fra en katalog på 200 sider, bruker du områdestrengen "40-50", ikke en posisjon relativt til innholdet som allerede ligger i måldokumentet

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

Når du beregner hvor mye InsertAt skal økes, må du telle sidene som faktisk ble importert, ikke alle sidene i kilden. Hvis du angir '1,3-5', importeres 4 sider, og telleren skal derfor økes med 4. Hvis du i stedet bruker PdfSrc.PageCount, oppstår det et tomrom mellom plasseringene i måldokumentet, og det neste kildedokumentet legges inn lenger ut i filen enn planlagt

Hva ImportPages bevarer, og hva som ikke følger med

Sider som kopieres med ImportPages, beholder det synlige innholdet. Tekst, vektorgrafikk, rasterbilder, innebygde skrifter og form XObjects overføres som en del av sidens innholdsstrømmer. Merknader på sidenivå, blant annet kommentarer, uthevinger og frihåndsstreker, følger også med fordi de lagres i sideordboken og ikke på dokumentnivå

Metadata på dokumentnivå behandles annerledes. Tittel, forfatter, emne og nøkkelord fra kildens Info-ordbok blir ikke med. Måldokumentet har tomme metadata etter CreateDocument. Hvis det sammenslåtte resultatet skal inneholde disse feltene, må du tilordne dem direkte til PdfDest før du kaller SaveAs. Egenskapene Title, Author, Subject, Keywords og Creator i TPdf tar vanlige strenger og skriver dem til Info-ordboken ved lagring

Interaktive skjemafelt er mer kompliserte. AcroForm-feltdefinisjoner ligger i en ordbok på dokumentnivå, ikke i de enkelte sideinnholdsstrømmene. Når ImportPages kopierer en side med skjemafelt, følger det visuelle utseendet med fordi det gjengis i sideinnholdet. Feltkontrollene som gjør feltene interaktive, er derimot en del av AcroForm-strukturen og følger ikke med. Ved en vanlig sammenslåing vil et tekstfelt fra kildedokumentet vise verdien det hadde under importen, men det kan ikke redigeres i den sammenslåtte filen. Hvis feltene fortsatt skal kunne fylles ut, bør du flate dem ut i hvert kildedokument før import. Da bygges de gjeldende verdiene inn i innholdsstrømmen, og det interaktive laget fjernes, slik at resultatet blir visuelt korrekt uten ødelagte feltkontroller

Krypterte kildefiler

Passordbeskyttede kildedokumenter åpnes på samme måte som ukrypterte dokumenter, men én ekstra egenskap må angis først. Tilordne passordet til PdfSrc.Password før du setter Active := True, så bruker PDFium det under åpningen:

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;

Et feil passord gir det samme tause resultatet Active = False som en manglende fil, så den uttrykkelige kontrollen er like viktig her. Krypteringen overføres ikke til måldokumentet: Sider som importeres fra en beskyttet kilde, legges inn som ubeskyttet innhold. Hvis det sammenslåtte resultatet også skal krypteres, må krypteringen konfigureres på PdfDest før du kaller SaveAs

Lagre resultatet

SaveAs i TPdf godtar enten en filbane eller en TStream. For de fleste sammenslåinger er filoverlastingen det naturlige valget:

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

Det valgfrie andre argumentet er en TSaveOption som styrer lagringsmodusen. Standardverdien saNone skriver en trinnvis oppdatering hvis dokumentet ble lastet fra en fil, eller en fullstendig omskriving hvis det ble opprettet fra grunnen av. Et måldokument som er bygd med CreateDocument, er alltid nytt, så resultatet blir en kompakt fil med én revisjon. Det tredje argumentet, TPdfVersion, lar deg låse PDF-versjonen i filhodet når etterfølgende systemer krever en bestemt versjon. Hvis du lar verdien være pvUnknown, velger PDFium versjon ut fra innholdet

Metodene ImportPages og SaveAs som vises her, er en del av PDFium Component for Delphi og C++Builder