Technical Article

N-up imposisjon og sideoppretting med PDFium

Fletting og splitting er de to sideoperasjonene alle tyr til først, og de dekker mye. De dekker imidlertid ikke alt. Det finnes en helt annen gruppe operasjoner som omorganiserer sider i stedet for å flytte hele filer: Legge fire lysbilder på ett ark for et utdelingsark, dra en side fra baksiden av et dokument til forsiden, eller hente ut sidene 3, 7 og 12 til et lite utdrag uten å berøre resten. PDFium tilbyr tre metoder for akkurat dette, og hver av dem oppfører seg annerledes enn flettingen og splittingen du allerede kjenner. Denne artikkelen går gjennom hva de gjør, hvor utdatapunktet ligger, og en eierskapsdetalj som har forårsaket krasj ute i feltet.

De tre er ImportNPagesToOne for N-up-imposisjon, MovePages for omorganisering på stedet, og ImportPagesByIndex for uttrekking av utvalg. Fletting stabler dokumenter etter hverandre og gjør at sidetallet blir summen av inndataene. Splitting skriver flere utdatafiler fra én inndata. De tre operasjonene her sitter imellom: Én av dem endrer hvor mange kildesider som deler et ark, én av dem endrer rekkefølgen i et enkelt dokument, og én av dem kopierer et valgt knippe sider over i et annet dokument. Å vite hva som er hva, sparer deg fra å tvinge frem en flette-og-slette-dans der et enkelt kall ville være nok.

Hva N-up-imposisjon faktisk gjør

Imposisjon er trykkeribegrepet for å arrangere flere kildesider på ett større ark, slik at det trykte og brettede resultatet kan leses i riktig rekkefølge. Hverdagsversjonen er 2-up-utdelingsark, 4-up-heftebretting, eller kontaktarket som har plass til et dusin miniatyrbilder på en side. PDFium håndterer geometrien via ett kall:

function ImportNPagesToOne(
  OutputWidth, OutputHeight: Single;
  NumX, NumY               : Cardinal): TPdf;

NumX og NumY beskriver rutenettet. En verdi på 2, 1 plasserer to kildesider ved siden av hverandre; 2, 2 pakker fire inn i en kvadrant-layout; 4, 3 bygger et tolv-up-kontaktark. PDFium leser kildesidene i rekkefølge, skalerer hver enkelt ned for å passe i cellen, og fyller rutenettet fra venstre til høyre, ovenfra og ned, og starter et nytt utdataark hver gang det gjeldende rutenettet er fullt. Kildesidene blir ikke endret. Det du får tilbake er et nytt dokument der sidene er sammensatte.

Utdatastørrelsen er i punkter, ikke piksler

OutputWidth og OutputHeight er PDF-brukerenheter, og en PDF-brukerenhet er ett punkt, som er en toogsyttiendedels tomme. Enheten angir den fysiske størrelsen på utdataarket, og den har ingenting å gjøre med skjermpiksler eller rendrings-DPI. Dette er det vanligste stedet man gjør feil med imposisjon, fordi en utvikler som er vant til bitmapelementer ber om et pikselantall og ender opp med et ark på størrelse med et frimerke eller en reklametavle.

Tallene som er verdt å huske, er de to sidestørrelsene du vil bruke mest. US Letter er 612 ganger 792 punkter, fordi 8,5 tommer ganger 72 er 612, og 11 tommer ganger 72 er 792. A4 er omtrent 595 ganger 842 punkter, ut fra målene på 210 ganger 297 millimeter. Bindingens egen header angir regelen tydelig: Én enhet er en toogsyttiendedels tomme, og enheten leveres med en PointsPerInch-konstant lik 72 hvis du heller vil beregne en størrelse fra tommer i koden i stedet for å skrive den bokstavelige verdien.

const
  LetterW = 612.0;   // 8.5 in * 72
  LetterH = 792.0;   // 11  in * 72
var
  Source, Composite: TPdf;
begin
  Source := TPdf.Create(nil);
  Composite := nil;
  try
    Source.FileName := 'slides.pdf';
    Source.Active := True;

    // Four source pages per Letter sheet, 2 by 2 grid.
    Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
    if Composite = nil then
      raise Exception.Create('PDFium rejected the imposition arguments');

    Composite.SaveAs('slides-4up.pdf');
  finally
    Composite.Free;   // see the next section: this is mandatory
    Source.Free;
  end;
end;

Det returnerte håndtaket er ditt å frigjøre

Les signaturen en gang til. ImportNPagesToOne returnerer en TPdf, ikke en Boolean. Returverdien er et helt nytt dokumenthåndtak, tildelt separat fra kilden, og kaller eier det. Kildens TPdf du kalte metoden på er urørt og eier fortsatt sitt eget håndtak; komposisjonen er et sekundært, uavhengig objekt. Hvis du lar den returnerte TPdf gå ut av scope uten å frigjøre den, du lekker et helt PDFium-dokument.

Den farligere feilen går den andre veien. Under panseret ber metoden PDFium om et nytt FPDF_DOCUMENT via FPDF_ImportNPagesToOne, og pakker deretter dette rå håndtaket inn i den returnerte TPdf slik at innpakningsobjektets levetid styrer håndtakets. Fra det tidspunktet er det nøyaktig én eier av håndtaket, og nøyaktig ett sted det skal lukkes: Når du kaller Free på det returnerte objektet. En uaktsom feilbane som både frigjør innpakningsobjektet og kaller FPDF_CloseDocument på det rå håndtaket den fanget opp, lukker det samme PDFium-dokumentet to ganger. Det er en dobbelt-frigjøring, og det er den spesifikke feilen som rammet en kaller her en gang. Regelen som forhindrer det er kort. Lukk dokumentet på bare én bane, ved å frigjøre den TPdf metoden ga deg, og prøv aldri å gå forbi innpakningsobjektet for å lukke håndtaket det allerede har overtatt.

To følger av dette er: For det første returnerer metoden nil når PDFium avviser argumentene, for eksempel en null på en av rutenettaksene eller en tildelingsfeil, så en nil-sjekk hører hjemme før du berører resultatet. For det andre bør du initialisere utdatavariabelen til nil før try-blokken og frigjøre den i finally, slik eksempelet over gjør, så en feil underveis ikke fører til at du frigjør en udefinert referanse eller hopper over frigjøringen fullstendig.

Endre siderekkefølge uten å skrive dem på nytt

Imposisjon bygger et nytt dokument. Omorganisering endrer ett dokument på stedet. MovePages løfter et sett med sider ut av deres nåværende posisjoner og slipper dem på en destinasjon, og forskyver alt annet rundt den flyttede blokken slik at sidetallet forblir det samme:

function MovePages(
  const PageIndices: array of Integer;
  DestPageIndex    : Integer): Boolean;

Indeksene er nullbaserte. PageIndices lister opp sidene som skal flyttes, i den rekkefølgen de skal ende opp, og DestPageIndex er indeksen den første flyttede siden lander på etter at flyttingen er fullført. Fordi PDFium flytter sidene i stedet for å kopiere og komprimere innholdet på nytt, operasjonen er rimelig og tapsfri: Sideobjektene beholder strømmene sine, ressursene sine og nøyaktigheten sin. Dette er kallet bak et panel for å dra-og-endre siderekkefølge, der en bruker drar et miniatyrbilde til et nytt spor og du utfører den nye rekkefølgen med ett flytt. Det returnerer False når en indeks er utenfor området, så valider resultatet i stedet for å anta at omorganiseringen lyktes.

var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Move the last page (index 4 in a 5-page file) to the very front.
    if not Doc.MovePages([4], 0) then
      raise Exception.Create('MovePages rejected the index');

    Doc.SaveAs('report-reordered.pdf');
  finally
    Doc.Free;
  end;
end;

Hente et utvalg etter indeks

Den tredje operasjonen kopierer et eksplisitt sett med sider fra ett dokument over i et annet. ImportPagesByIndex tar kildedokumentet og en nullbasert indeks-array, og setter disse sidene inn i målet ved en valgt posisjon:

function ImportPagesByIndex(
  Source           : TPdf;
  const PageIndices: array of Integer;
  InsertAt         : Integer= 0): Boolean;

Du kaller den på måldokumentet og sender kilden som det første argumentet. PageIndices navngir kildedene som skal hentes, i den rekkefølgen du ønsker dem; InsertAt er det nullbaserte sporet i målet der den første importerte siden plasseres, slik at 0 plasserer dem før den eksisterende første siden, og målets gjeldende sidetall legges til. En tom array importerer hver side, noe som gjør kallet til en student kopi når du trenger det. Det returnerer False hvis en indeks er utenfor området i kilden.

Det er her kontrasten med splitting er viktig. Splitting skriver separate filer, én operasjon som produserer mange utdata på disken. ImportPagesByIndex gjør det motsatte arbeidet: Den samler et valgt sett med sider inn i et enkelt måldokument i minnet, som du deretter lagrer én gang. Når jobben er "gi meg sidene 3, 7 og 12 som én kort PDF", er dette den direkte ruten, og den pakker inn FPDF_ImportPagesByIndex under panseret.

var
  Source, Excerpt: TPdf;
begin
  Source := TPdf.Create(nil);
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // start an empty target

    // Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
    if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
      raise Exception.Create('A requested page index is out of range');

    Excerpt.SaveAs('manual-excerpt.pdf');
  finally
    Excerpt.Free;
    Source.Free;
  end;
end;

Sette det rent sammen

Formen fra start til slutt er lik for alle tre: Åpne kilden ved å angi FileName og sette Active til True, utfør operasjonen, lagre med SaveAs og frigjør det du eier. Den ene grenen som krever forsiktighet, er hvilke kall som tildeler et nytt dokument. MovePages endrer dokumentet du allerede har, så det er ett objekt å frigjøre. ImportPagesByIndex skriver inn i et mål du har opprettet selv, så du frigjør kilden og målet du åpnet. ImportNPagesToOne er unntaket, fordi det nye dokumentet er metodens returverdi i stedet for noe du konstruerte, og å glemme at det er et separat, kaller-eid håndtak er hvordan både lekkasjen og dobbelt-frigjøringen skjer. Initialiser resultatet til nil, sjekk det etter kallet, og frigjør det på én enkelt bane.

Hvis arbeidet du faktisk har går ut på å slå sammen hele filer i stedet for å omorganisere sider, se å slå sammen flere PDF-filer til ett dokument. Hvis det er det motsatte, å dele ett dokument opp i flere filer, se å splitte PDF-dokumenter til flere filer. Imposisjons- og omorganiseringsmetodene beskrevet her leveres som en del av PDFium-komponenten for Delphi og C++Builder, sammen med API-ene for lasting, rendring og redigering som er dekket andre steder på denne bloggen.