Teknisk artikel

Opdeling af PDF-dokumenter med PDFium-komponenten i Delphi

PDFium-komponenten giver dig én metode til PDF-opdeling: ImportPages. Alt andet, uanset om du isolerer en enkelt side, skærer på vilkårlige (arbitrary) grænser eller følger dokumentets egen bogmærkestruktur, er bare forskellige måder at beslutte, hvilke sidetal der skal i hver output-fil. Mekanikken forbliver den samme. At forstå det tidligt sparer mange forkerte sving

Hvordan opdelingsløkken (the split loop) fungerer

Mønsteret er det samme, uanset hvordan du opdeler kildedokumentet. Opret en frisk TPdf instans, kald CreateDocument på den for at initialisere en tom PDF i hukommelsen, importer de sider, du ønsker, med ImportPages, gem resultatet, nulstil derefter Active til False før den næste iteration. Det sidste trin er det, folk overser: CreateDocument lukker ikke implicit dokumentet, der stadig er i hukommelsen, så du skal gemme dit output og nulstille Active := False eksplicit, før du kalder den igen; nulstilling først holder tilstanden (the state) ren og veldefineret. Den ydre TPdf instans genbruges på tværs af alle iterationer, hvilket holder allokeringspresset lavt på store job

Her er, hvordan side-for-side opdeling ser ud, skåret ned til det væsentlige:

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 strengformat, som PDFium bruger internt: en komma-separeret liste over sidetal eller bindestreg-afgrænsede områder, alle 1-baserede. '3' importerer side 3. '1-5' importerer siderne 1 til 5 i rækkefølge. '2,5,8' importerer de tre sider. Den tredje parameter er den 1-baserede indsættelsesposition i destinationsdokumentet; at overgive (passing) 1 placerer altid importerede sider i begyndelsen af en ellers tom fil, hvilket er det, du ønsker her

Opdeling efter sideområder (page ranges)

Når kalderen leverer en liste som 1-12,13-24,25-36, parser du den i start/slut-par og kører den samme løkke og konstruerer områdestrengen 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, betyder noget her. ImportPages returnerer False, når et sidetal i områdestrengen overstiger Source.PageCount, men det kaster ikke en undtagelse, og det producerer ikke en delvis outputfil, du kan opdage ved hjælp af navnet alene. Tjek SaveAs's returværdi og log fejl separat; et område, der producerer en tom outputfil, er ikke åbenlyst forkert, før nogen åbner den

Opdeling ved bogmærkegrænser

Den tredje tilgang bruger dokumentets egen struktur frem for en eksternt leveret liste. Hvert top-niveau bogmærke (top-level bookmark) bærer et målsidetal; den sektion, det definerer, løber fra den side til én før det næste bogmærkes side, eller til slutningen af dokumentet for den sidste indtastning (entry)

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, der ikke har nogen bogmærker, er ikke en fejltilstand, der er værd at bringe frem til brugeren som én; det betyder bare, at denne opdelingstilstand ikke har noget at arbejde ud fra. Length(Bm) = 0 vagten (guard) håndterer det i stilhed. Det, der er værd at bringe frem, er, når et bogmærkes sidetal er uden for dokumentets område, hvilket sker i misdannede (malformed) filer, hvor oversigten (the outline) aldrig blev opdateret, efter at sider blev slettet. Grænsetjekket (bounds check) på StartPage og EndPage springer de indtastninger over frem for at sende et affaldsområde (garbage range) til ImportPages

Navngivning af outputfil og nulstilling af Active

Filnavnssikkerhed for bogmærke-afledte navne kræver eksplicit opmærksomhed. Bogmærketitler kan indeholde tegn, der er gyldige i en PDF-streng, men ikke i en filsystemsti. Som minimum skal du erstatte skråstreg, omvendt skråstreg og kolon, før du bygger outputstien. På Windows er *, ?, ", <, > og | også forbudt; en simpel løkke over et fast sæt dækker dem uden at trække en regex ind

Linjen Active := False i slutningen af hver iteration fortjener fremhævelse, fordi det er det eneste ikke-åbenlyse krav i mønsteret. CreateDocument lukker ikke implicit, hvad der måtte være åbent. Hvis Active stadig er True, når CreateDocument kører igen, blev dokumentet, der stadig er i hukommelsen, aldrig ordentligt lukket eller gemt, og du kan ikke stole på veldefineret adfærd i den tilstand, så gem og nulstil eksplicit, før du starter det næste dokument. Tænk på det som parret til try/finally: finally-blokken frigør det ydre objekt; Active := False nulstiller den indre dokumenttilstand mellem løkkeiterationer

Hukommelsesforbruget (Memory use) på tværs af et stort opdelingsjob forbliver fladt (flat) med denne tilgang, fordi du aldrig holder mere end ét outputdokument i hukommelsen ad gangen. Kildedokumentet forbliver åbent og skrivebeskyttet (read-only) hele vejen igennem; ImportPages kopierer sidedata over i det nye dokument uden at ændre kilden. Hvis kilden er krypteret, skal du åbne den med dens adgangskode (password) før løkken, og de kopierede sider i hver outputfil vil være ukrypterede, hvilket normalt er den rigtige adfærd for splittet output distribueret til forskellige modtagere

En ting mere om SaveAs: det returnerer en Boolean. En output-mappe, der ikke eksisterer, en sti med tegn, som OS afviser, eller en disk-fuld tilstand vil alle forårsage, at SaveAs returnerer False uden at kaste en undtagelse. I et batchjob, der opdeler et 200-siders dokument i 200 enkeltsides filer, er en stille fejl på side 147 let at overse. Tjek returværdien på hvert kald og tæl succeser i forhold til det forventede total, når løkken slutter

ImportPages og CreateDocument metoderne vist her er en del af PDFium-komponenten til Delphi og C++Builder