Tekninen artikkeli

PDF-asiakirjojen jakaminen useiksi tiedostoiksi PDFium Componentilla Delphissä

PDFium Component antaa PDF:n jakamiseen yhden metodin: ImportPages. Kaikki muu, olitpa eristämässä yhtä sivua, katkomassa tiedostoa mielivaltaisista kohdista tai seuraamassa asiakirjan omaa kirjanmerkkirakennetta, on vain erilaisia tapoja päättää mitkä sivunumerot menevät kuhunkin ulostulotiedostoon. Mekaniikka pysyy samana. Tämän ymmärtäminen varhain säästää monelta väärältä suunnalta

Miten jakosilmukka toimii

Malli on sama riippumatta siitä, miten lähdeasiakirja jaetaan. Luo uusi TPdf-instanssi, kutsu sillä CreateDocument-metodia alustamaan tyhjä PDF muistiin, tuo haluamasi sivut ImportPages-kutsulla, tallenna tulos ja aseta sitten Active arvoon False ennen seuraavaa iteraatiota. Juuri tuo viimeinen askel jää ihmisiltä usein tekemättä: CreateDocument ei sulje muistissa yhä olevaa asiakirjaa implisiittisesti, joten ulostulo täytyy tallentaa ja Active := False asettaa eksplisiittisesti ennen seuraavaa kutsua; nollaus ensin pitää tilan puhtaana ja hyvin määriteltynä. Ulompi TPdf-instanssi käytetään uudelleen kaikkien iteraatioiden ajan, mikä pitää allokointipaineen pienenä suurissa töissä

Tältä sivukohtainen jako näyttää riisuttuna olennaisimpiin osiin

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-parametri, joka annetaan ImportPages-metodille, käyttää samaa merkkijonomuotoa jota PDFium käyttää sisäisesti: pilkuilla erotettu lista sivunumeroita tai yhdysviivalla ilmaistuja alueita, kaikki 1-pohjaisina. '3' tuo sivun 3. '1-5' tuo sivut 1–5 järjestyksessä. '2,5,8' tuo nuo kolme sivua. Kolmas parametri on 1-pohjainen lisäyskohta kohdeasiakirjassa; arvon 1 antaminen sijoittaa tuodut sivut aina muuten tyhjän tiedoston alkuun, mikä on juuri sitä mitä tässä halutaan

Jakaminen sivualueiden mukaan

Kun kutsuja toimittaa listan kuten 1-12,13-24,25-36, se jäsennetään alku/loppu-pareiksi ja ajetaan sama silmukka, joka rakentaa jokaisesta parista alue-merkkijonon

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;

Tässä validointi ennen kuin päästään ImportPages-kutsuun on tärkeää. ImportPages palauttaa False, kun alue-merkkijonossa oleva sivunumero ylittää arvon Source.PageCount, mutta se ei heitä poikkeusta eikä tuota osittaista ulostulotiedostoa, jota voisi päätellä pelkästä tiedostonimestä. Tarkista SaveAs-kutsun paluuarvo ja kirjaa epäonnistumiset erikseen; alue, joka tuottaa tyhjän ulostulotiedoston, ei näytä ilmeisen väärältä ennen kuin joku avaa sen

Jakaminen kirjanmerkkirajojen kohdalta

Kolmas lähestymistapa käyttää asiakirjan omaa rakennetta ulkopuolelta syötetyn listan sijasta. Jokaisella ylimmän tason kirjanmerkillä on kohdesivunumero; sen määrittelemä osio ulottuu kyseisestä sivusta seuraavan kirjanmerkin sivua edeltävään sivuun asti tai viimeisen merkinnän tapauksessa asiakirjan loppuun

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;

Asiakirja, jossa ei ole yhtään kirjanmerkkiä, ei ole virhetila jonka takia käyttäjälle pitäisi nostaa virhe; se tarkoittaa vain, ettei tällä jakotilalla ole mitään mihin tarttua. Tarkistus Length(Bm) = 0 hoitaa tämän hiljaa. Se mikä kannattaa nostaa esiin on tilanne, jossa kirjanmerkin sivunumero on asiakirjan rajojen ulkopuolella, mitä tapahtuu rikkinäisissä tiedostoissa joissa sisällysluetteloa ei päivitetty sivujen poistamisen jälkeen. Raja-arvotarkistus kentille StartPage ja EndPage ohittaa tällaiset merkinnät sen sijaan että syöttäisi roska-alueen ImportPages-kutsulle

Ulostulotiedostojen nimeäminen ja Active-tilan nollaus

Kirjanmerkeistä johdettujen nimien tiedostonimiturvallisuus vaatii erillistä huomiota. Kirjanmerkkien otsikot voivat sisältää merkkejä, jotka ovat sallittuja PDF-merkkijonossa mutta eivät tiedostojärjestelmän polussa. Vähintään etukeno, takakeno ja kaksoispiste kannattaa korvata ennen ulostulopolun rakentamista. Windowsissa myös *, ?, ", <, > ja | ovat kiellettyjä; yksinkertainen silmukka kiinteän merkkijoukon yli kattaa nämä ilman että tarvitaan säännöllisiä lausekkeita

Rivi Active := False jokaisen iteraation lopussa ansaitsee painotuksen, koska se on mallin ainoa epäitsestään selvä vaatimus. CreateDocument ei sulje mitään jo avattua implisiittisesti. Jos Active on edelleen True, kun CreateDocument ajetaan uudelleen, muistissa ollut asiakirja ei koskaan sulkeutunut eikä tallentunut kunnolla, eikä tuollaisessa tilassa kannata luottaa hyvin määriteltyyn käyttäytymiseen, joten tallenna ja nollaa tila eksplisiittisesti ennen seuraavan asiakirjan aloittamista. Ajattele sitä vastinparina try/finally-rakenteelle: finally lohko vapauttaa ulomman olion; Active := False nollaa sisemmän asiakirjatilan silmukan iteraatioiden välissä

Muistinkäyttö pysyy tasaisena suurissa jakotöissä tällä lähestymistavalla, koska muistissa ei pidetä koskaan enempää kuin yhtä ulostuloasiakirjaa kerrallaan. Lähdeasiakirja pysyy auki ja vain luku -tilassa koko ajan; ImportPages kopioi sivudatan uuteen asiakirjaan muuttamatta lähdettä. Jos lähde on salattu, avaa se salasanallaan ennen silmukkaa ja jokaisen ulostulotiedoston kopioidut sivut ovat salaamattomia, mikä on tavallisesti oikea käyttäytyminen, kun jaettu ulostulo menee eri vastaanottajille

Yksi lisähuomio SaveAs-metodista: se palauttaa Boolean-arvon. Ulostulohakemisto, jota ei ole olemassa, polku jossa on käyttöjärjestelmän hylkäämiä merkkejä, tai tilanne jossa levy on täynnä, saa SaveAs-kutsun palauttamaan False ilman poikkeusta. Eräajossa, joka jakaa 200-sivuisen asiakirjan 200 yksisivuiseksi tiedostoksi, hiljainen epäonnistuminen sivulla 147 on helppo ohittaa. Tarkista jokaisen kutsun paluuarvo ja vertaa onnistumisten määrää odotettuun kokonaismäärään, kun silmukka päättyy

Tässä näytetyt ImportPages- ja CreateDocument-metodit ovat osa Delphiä ja C++Builderia varten tarjottavaa PDFium Component -tuotetta