Teknisk artikel

Slå samman flera PDF-filer till ett dokument med PDFium Component

PDFium Component exponerar PDF-sammanslagning genom en enda metod: ImportPages. Mönstret är alltid detsamma: skapa ett tomt måldokument, öppna varje källfil, anropa ImportPages för att kopiera sidorna, stäng källan och repetera. När loopen är klar skriver SaveAs resultatet till disk. Det finns inget speciellt sammanslagningsläge, ingen konfiguration att ändra. Komplexiteten ligger i specialfallen, och det finns några som biter ifrån utan förvarning

Kärnloopen

Två TPdf-instanser är allt du behöver. En håller måldokumentet, skapat tomt med CreateDocument. Den andra öppnar varje källfil i tur och ordning. Nedan följer en procedur som tar en lista med filsökvägar och skriver ut den sammanslagna utdatan till en enda sökväg:

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;

Två saker i koden ovan är lätta att förbise vid en första genomläsning. Det första är hur PDFium rapporterar laddningsfel. Active := True kastar aldrig ett undantag: om filen saknas, är skadad eller lösenordsskyddad fångar PDFium felet internt och lämnar Active som False. Utan den uttryckliga kontrollen på rad 10 skulle en felaktig fil tyst falla bort från sammanslagningen utan någon indikation i utdatan. Den slutliga PDF:en skulle ha färre sidor än förväntat och du skulle inte veta vilken fil som var den skyldige

Det andra är InsertAt-räknaren. Det tredje argumentet till ImportPages är den 1-baserade positionen i destinationen där den första importerade sidan hamnar. Att börja på 1 placerar det första källdokumentet i början av en annars tom fil. Efter varje källa flyttas räknaren fram med PdfSrc.PageCount, så nästa sats av sidor läggs till efter den sista. Glöm att öka den och varje efterföljande källa skriver över sidor på position 1, vilket ger dig det sista dokumentet i listan och inget annat

Selektiva sidintervall

Du behöver inte ta med varje sida från en källa. Intervallsträngen som skickas som det andra argumentet följer ett enkelt format med kommatecken och bindestreck: "1-3" tar sidorna 1 till och med 3, "2,4,6" väljer tre specifika sidor, och "1-" betyder sida 1 till slutet av dokumentet. Intervall kan kombineras i en enda sträng, så "1-3,5,7-" hoppar över sidorna 4 och 6. En finess är viktig här: siffrorna refererar alltid till sidor i källdokumentet, med början på 1, oavsett var de sidorna hamnar i destinationen. Om du vill ha sidorna 40 till och med 50 från en 200-sidig katalog, är intervallsträngen "40-50", inte en position relativ till vad som redan finns i destinationen

// 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 beräknar ökningen till InsertAt, räkna de sidor du faktiskt importerade, inte källans sidantal. Om du skickar '1,3-5' importerade du 4 sidor, så öka med 4. Att öka med PdfSrc.PageCount skulle lämna ett mellanrum av tomma destinationspositioner och placera nästa källdokument längre in i filen än avsett

Vad ImportPages bevarar och vad det inte gör

Sidor kopierade med ImportPages för med sig sitt synliga innehåll intakt. Text, vektorgrafik, rasterbilder, inbäddade teckensnitt och formulär-XObjects överförs alla som en del av sidans innehållsströmmar. Sidnivå-annoteringar, inklusive kommentarer, markeringar och bläckstreck, följer också med, eftersom de lagras inuti sidordlistan istället för på dokumentnivå

Dokumentnivå-metadata är en annan historia. Titeln, författaren, ämnet och nyckelordssträngarna i källans Info-ordlista blir kvar. Måldokumentet startar med tom metadata efter CreateDocument, så om den sammanslagna utdatan behöver dessa fält ifyllda måste du tilldela dem till PdfDest direkt innan du anropar SaveAs. Egenskaperna Title, Author, Subject, Keywords och CreatorTPdf tar emot vanliga strängar och skriver in i Info-ordlistan vid sparning

Interaktiva formulärfält är mer komplicerade. AcroForm-fältdefinitioner lever i en dokumentnivå-ordlista istället för inuti individuella sidströmmar. När ImportPages kopierar en sida som innehåller formulärfält, överförs det visuella utseendet för dessa fält eftersom det renderas in i sidans innehållsström, men de fält-widgets som gör dem interaktiva är en del av AcroForm-strukturen och följer inte med. I en typisk sammanslagning kommer ett textfält från ett källdokument att visa det värde det hade vid importtillfället, men det kommer inte att vara redigerbart i den sammanslagna filen. Om du behöver att fälten förblir ifyllbara, förenkla (flatten) dem i varje källdokument innan du importerar: det bakar in de aktuella värdena i innehållsströmmen och tar bort det interaktiva överlägget, vilket ger dig ett rent visuellt resultat utan trasiga widgets i utdatan

Krypterade källfiler

Lösenordsskyddade källdokument öppnas på samma sätt som okrypterade, med en extra egenskap att ställa in först. Tilldela lösenordet till PdfSrc.Password innan du växlar Active := True, och PDFium kommer att använda det vid öppnandet:

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;

Ett felaktigt lösenord orsakar samma tysta Active = False-utfall som en saknad fil, så den uttryckliga kontrollen är precis lika nödvändig här. Krypteringen överförs inte till destinationen: sidor importerade från en skyddad källa landar i destinationen som oskyddat innehåll. Om den sammanslagna utdatan också behöver kryptering, konfigurera det på PdfDest innan du anropar SaveAs

Spara resultatet

SaveAsTPdf accepterar antingen en filsökväg eller en TStream. För de flesta sammanslagningar är fil-överlagringen vad du vill ha:

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

Det valfria andra argumentet är ett TSaveOption som styr sparläget. Standardvärdet, saNone, skriver en inkrementell uppdatering om dokumentet laddades från en fil eller en komplett omskrivning om det skapades från grunden. Eftersom en destination byggd med CreateDocument alltid är ny, blir utdatan en kompakt fil med en enda revision. Det tredje argumentet, TPdfVersion, låter dig fästa PDF-versionshuvudet när du har nedströmskonsumenter som kräver en specifik version; om du lämnar det på pvUnknown låter du PDFium välja baserat på innehållet

Metoderna ImportPages och SaveAs som visas här är en del av PDFium Component för Delphi och C++Builder