Technisch artikel

Meerdere PDF-bestanden samenvoegen tot één document met de PDFium-component

De PDFium-component biedt de mogelijkheid om PDF's samen te voegen via een enkele methode: ImportPages. Het patroon is altijd hetzelfde: maak een leeg doeldocument aan, open elk bronbestand, roep ImportPages aan om de pagina's te kopiëren, sluit de bron, en herhaal. Wanneer de lus eindigt, schrijft SaveAs het resultaat naar de schijf. Er is geen speciale samenvoegmodus, geen configuratie om om te schakelen. De complexiteit zit in de randgevallen, en er zijn er een paar die zonder waarschuwing de kop opsteken

De kernlus

Twee TPdf instanties zijn alles wat u nodig heeft. Eén bevat het doeldocument, dat leeg is gemaakt met CreateDocument. De ander opent elk bronbestand op zijn beurt. Hieronder volgt een procedure die een lijst van bestandspaden neemt en de samengevoegde uitvoer naar een enkel pad schrijft:

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;

Twee dingen in die code zijn gemakkelijk over het hoofd te zien bij een eerste keer lezen. Het eerste is hoe PDFium laadfouten rapporteert. Active := True werpt nooit een uitzondering op: als het bestand ontbreekt, beschadigd is of met een wachtwoord beveiligd is, vangt PDFium de fout intern op en laat Active op False staan. Zonder de expliciete controle op regel 10 zou een slecht bestand stilzwijgend uit de samenvoeging wegvallen zonder enige indicatie in de uitvoer. De uiteindelijke PDF zou minder pagina's hebben dan verwacht en u zou niet weten welk bestand de boosdoener was

Het tweede is de InsertAt teller. Het derde argument voor ImportPages is de op 1 gebaseerde positie in de bestemming waar de eerste geïmporteerde pagina belandt. Beginnen bij 1 plaatst het eerste brondocument aan het begin van een verder leeg bestand. Na elke bron wordt de teller verhoogd met PdfSrc.PageCount, zodat de volgende batch pagina's na de laatste wordt toegevoegd. Vergeet dit te verhogen en elke volgende bron overschrijft pagina's op positie 1, waardoor u het laatste document in de lijst krijgt en niets anders

Selectieve paginabereiken

U hoeft niet elke pagina van een bron over te nemen. De bereiktekenreeks die als tweede argument wordt doorgegeven volgt een eenvoudig formaat met komma's en koppeltekens: "1-3" neemt pagina's 1 tot en met 3, "2,4,6" kiest drie specifieke pagina's, en "1-" betekent pagina 1 tot het einde van het document. Bereiken kunnen in een enkele tekenreeks worden gecombineerd, dus "1-3,5,7-" slaat pagina's 4 en 6 over. Eén subtiliteit is hierbij van belang: de getallen verwijzen altijd naar pagina's in het brondocument, beginnend bij 1, ongeacht waar deze pagina's in de bestemming terechtkomen. Als u pagina's 40 tot en met 50 uit een catalogus van 200 pagina's wilt halen, is de bereiktekenreeks "40-50", niet een positie relatief aan wat al in de bestemming staat

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

Bereken bij het bepalen van de toename voor InsertAt de pagina's die u daadwerkelijk hebt geïmporteerd, niet het aantal pagina's van de bron. Als u '1,3-5' doorgeeft, hebt u 4 pagina's geïmporteerd, dus verhoog met 4. Verhoging met PdfSrc.PageCount zou een gat laten met lege bestemmingsposities en het volgende brondocument verder in het bestand plaatsen dan de bedoeling is

Wat ImportPages behoudt en wat niet

Pagina's gekopieerd met ImportPages behouden hun zichtbare inhoud intact. Tekst, vectorafbeeldingen, rasterafbeeldingen, ingesloten lettertypen en formulier XObjects worden allemaal overgedragen als onderdeel van de paginainhoudsstromen. Annotaties op paginaniveau, waaronder opmerkingen, markeringen en inktstreken, komen ook over, omdat ze zijn opgeslagen in de paginadictionary in plaats van op documentniveau

Metagegevens op documentniveau zijn een ander verhaal. De tekenreeksen voor titel, auteur, onderwerp en trefwoorden in de Info-dictionary van de bron blijven achter. Het doeldocument begint met lege metagegevens na CreateDocument, dus als de samengevoegde uitvoer deze velden ingevuld moet hebben, moet u ze direct aan PdfDest toewijzen voordat u SaveAs aanroept. De eigenschappen Title, Author, Subject, Keywords, en Creator op TPdf accepteren gewone tekenreeksen en schrijven naar de Info-dictionary bij het opslaan

Interactieve formuliervelden zijn gecompliceerder. Definities van AcroForm-velden bevinden zich in een dictionary op documentniveau, in plaats van binnenin individuele paginastromen. Wanneer ImportPages een pagina kopieert die formuliervelden bevat, wordt de visuele weergave van die velden overgedragen omdat deze wordt gerenderd in de paginainhoudsstroom, maar de veldwidgets die ze interactief maken, maken deel uit van de AcroForm-structuur en volgen niet. In een typische samenvoeging zal een tekstveld uit een brondocument de waarde tonen die het had op het moment van import, maar het zal niet bewerkbaar zijn in het samengevoegde bestand. Als u de velden invulbaar wilt houden, maak ze dan in elk brondocument plat voordat u ze importeert: dat voegt de huidige waarden samen in de inhoudsstroom en verwijdert de interactieve overlay, waardoor u een schoon visueel resultaat krijgt zonder kapotte widgets in de uitvoer

Versleutelde bronbestanden

Met een wachtwoord beveiligde brondocumenten openen op dezelfde manier als niet-versleutelde documenten, met één extra eigenschap om eerst in te stellen. Wijs het wachtwoord toe aan PdfSrc.Password voordat u Active := True omschakelt, en PDFium zal dit tijdens het openen gebruiken:

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;

Een verkeerd wachtwoord veroorzaakt hetzelfde stille Active = False resultaat als een ontbrekend bestand, dus de expliciete controle is hier net zo noodzakelijk. De versleuteling wordt niet overgedragen naar de bestemming: pagina's geïmporteerd vanuit een beveiligde bron komen in de bestemming terecht als onbeveiligde inhoud. Als de samengevoegde uitvoer ook versleuteling nodig heeft, configureer dit dan op PdfDest voordat u SaveAs aanroept

Het resultaat opslaan

SaveAs op TPdf accepteert een bestandspad of een TStream. Voor de meeste samenvoegingen wilt u de bestands-overload:

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

Het optionele tweede argument is een TSaveOption die de opslagmodus regelt. De standaard, saNone, schrijft een incrementele update als het document uit een bestand was geladen of een volledige herschrijving als het vers was aangemaakt. Aangezien een bestemming die is opgebouwd met CreateDocument altijd vers is, zal de uitvoer een compact bestand met een enkele revisie zijn. Het derde argument, TPdfVersion, laat u de PDF-versieheader vastzetten wanneer u downstream-consumenten heeft die een specifieke versie vereisen; het op pvUnknown laten staan laat PDFium kiezen op basis van de inhoud

De methoden ImportPages en SaveAs die hier worden getoond, maken deel uit van de PDFium-component voor Delphi en C++Builder