Teknisk artikkel

Dele PDF-dokumenter med PDFium-komponent i Delphi

PDFium-komponenten gir deg én metode for PDF-deling: ImportPages. Alt annet, enten du isolerer en enkelt side, klipper på vilkårlige grenser, eller følger dokumentets egen bokmerkestruktur, er bare forskjellige måter å bestemme hvilke sidetall som går inn i hver utdatafil. Mekanikken forblir den samme. Å forstå det tidlig sparer deg for mange feilskjær

Hvordan deleløkken fungerer

Mønsteret er det samme uavhengig av hvordan du deler kildedokumentet. Opprett en fersk TPdf-forekomst (instance), kall CreateDocument på den for å initialisere en tom PDF i minnet, importer sidene du ønsker med ImportPages, lagre resultatet, og tilbakestill (reset) Active til False før neste iterasjon. Det siste trinnet er det folk går glipp av: CreateDocument lukker ikke implisitt dokumentet som fortsatt er i minnet, så du må lagre utdataene dine og tilbakestille Active := False eksplisitt før du kaller den igjen; å tilbakestille først holder tilstanden ren og veldefinert. Den ytre TPdf-forekomsten gjenbrukes på tvers av alle iterasjoner, noe som holder tildelingspresset (allocation pressure) lavt på store jobber

Slik ser side-for-side-deling ut strippet til det essensielle:

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 is a 1-based page number string; insertion point 1 = first position
      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;   // reset before next CreateDocument
    end;
  finally
    PdfOut.Free;
  end;
end;

Range-parameteren til ImportPages er det samme strengformatet PDFium bruker internt: en kommadelt liste over sidetall eller bindestrek-delte intervaller, alle 1-baserte. '3' importerer side 3. '1-5' importerer sidene 1 til og med 5 i rekkefølge. '2,5,8' importerer de tre sidene. Den tredje parameteren er den 1-baserte innsettingsposisjonen i måldokumentet; å sende 1 plasserer alltid importerte sider i begynnelsen av en ellers tom fil, som er det du vil ha her

Deling etter sideintervaller

Når oppringeren (caller) oppgir en liste som 1-12,13-24,25-36, analyserer (parse) du den i start/slutt-par og kjører den samme løkken, og konstruerer intervallstrengen (range string) fra hvert par:

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;

Validering før du når ImportPages er viktig her. ImportPages returnerer False når et sidetall i intervallstrengen overstiger Source.PageCount, men den kaster ikke et unntak (raise an exception), og den produserer ikke en delvis utdatafil du kan oppdage bare ved navn. Sjekk SaveAs sin returverdi og loggfør feil separat; et intervall som produserer en tom utdatafil er ikke åpenbart feil før noen åpner den

Deling ved bokmerkegrenser

Den tredje tilnærmingen bruker dokumentets egen struktur snarere enn en eksternt levert liste. Hvert bokmerke på toppnivå bærer et målsidetall; seksjonen det definerer løper fra den siden til én før det neste bokmerkets side, eller til slutten av dokumentet for den siste oppføringen

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;   // skip a malformed section instead of writing an empty file
      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;

Et dokument som ikke har noen bokmerker, er ikke en feiltilstand verdt å vise (surfacing) til brukeren som en; det betyr bare at denne delingsmodusen ikke har noe å jobbe ut fra. Length(Bm) = 0-vakten håndterer det i det stille. Det som er verdt å vise frem er når et bokmerkes sidetall er utenfor dokumentets intervall, noe som skjer i feilformaterte (malformed) filer der oversikten aldri ble oppdatert etter at sider ble slettet. Grensesjekken på StartPage og EndPage hopper over disse oppføringene i stedet for å sende et søppelintervall til ImportPages

Navngivning av utdatafil og Active-tilbakestillingen

Filsikkerhet (filename safety) for bokmerke-utledede navn trenger eksplisitt oppmerksomhet. Bokmerketitler kan inneholde tegn som er gyldige i en PDF-streng, men ikke i en filsystemsti. Bytt ut som et minimum skråstrek (forward slash), omvendt skråstrek (backslash) og kolon før du bygger utdatastien. På Windows er også *, ?, ", <, >, og | forbudt; en enkel løkke over et fast sett dekker dem uten å trekke inn et regulært uttrykk (regex)

Active := False-linjen på slutten av hver iterasjon fortjener vektlegging fordi det er det eneste ikke-åpenbare kravet i mønsteret. CreateDocument lukker ikke implisitt det som er åpent. Hvis Active fortsatt er True når CreateDocument kjører igjen, ble dokumentet som fortsatt er i minnet aldri lukket eller lagret ordentlig, og du kan ikke stole på veldefinert adferd i den tilstanden, så lagre og tilbakestill (reset) eksplisitt før du starter det neste dokumentet. Tenk på det som paret til try/finally: finally-blokken frigjør det ytre objektet; Active := False tilbakestiller den indre dokumenttilstanden mellom løkkeiterasjoner

Minnebruk over en stor delingsjobb forblir flat med denne tilnærmingen fordi du aldri holder mer enn ett utdatadokument i minnet på en gang. Kildesidokumentet forblir åpent og skrivebeskyttet (read-only) gjennom det hele; ImportPages kopierer sidedata inn i det nye dokumentet uten å endre kilden. Hvis kilden er kryptert, åpner du den med passordet dens før løkken, og de kopierte sidene i hver utdatafil vil være ukrypterte, noe som vanligvis er den riktige adferden for delte utdata som distribueres til forskjellige mottakere

Én ting til om SaveAs: den returnerer en Boolean. En utdatakatalog som ikke eksisterer, en sti med tegn OS-et avviser, eller en disk-full-tilstand vil alle føre til at SaveAs returnerer False uten å kaste et unntak. I en satsvis jobb (batch job) som deler et 200-siders dokument inn i 200 enkeltsidige filer, er en stille feil på side 147 lett å overse. Sjekk returverdien på hvert kall og tell suksesser mot det forventede totaltallet når løkken er ferdig

Metodene ImportPages og CreateDocument vist her er en del av PDFium-komponenten for Delphi og C++Builder