Technical Article

N-up-imponering och sidomordning med PDFium

Sammanslagning och uppdelning är de två sidoperationer som alla sträcker sig efter först, och de täcker en hel del. De täcker dock inte allt. Det finns en separat familj av uppgifter som ordnar om sidor snarare än att flytta hela filer: placera fyra diabilder på ett ark för ett utdelningsmaterial, dra en sida från baksidan av ett dokument till framsidan, eller hämta sidorna 3, 7 och 12 till ett kort utdrag utan att röra resten. PDFium exponerar tre metoder för just detta, och var och en av dem beter sig annorlunda än de sammanslagningar och uppdelningar du redan känner till. Denna artikel går igenom vad de gör, var utdatapoängen finns och en ägarskapsdetalj som har orsakat krascher i skarpt läge

De tre är ImportNPagesToOne för N-up-imponering, MovePages för omordning på plats och ImportPagesByIndex för utdrag av delmängder. Sammanslagning lägger dokument efter varandra och lämnar sidantalet lika med summan av indata. Uppdelning skriver flera utdatafiler från en indata. De tre operationerna här ligger däremellan: en av dem ändrar hur många källsidor som delar på ett ark, en av dem ändrar ordningen i ett enskilt dokument och en av dem kopierar en utvald handfull sidor till ett annat dokument. Att veta vilken som är vilken besparar dig från att tvinga fram en sammanslagnings- och raderingsdans där ett enda anrop räcker

Vad N-up-imponering faktiskt gör

Imponering (imposition) är prepress-termen för att ordna flera källsidor på ett större ark så att det tryckta och vikta resultatet kan läsas i rätt ordning. Vardagsversionen är 2-up-utdelningsmaterial, 4-up-häfte eller kontaktkartan som rymmer ett dussin miniatyrbilder på en sida. PDFium hanterar geometrin via ett anrop:

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

NumX och NumY beskriver rutnätet. Värdet 2, 1 placerar två källsidor sida vid sida; 2, 2 packar fyra i en kvadrantlayout; 4, 3 bygger ett tolv-up kontaktark. PDFium läser källsidorna i ordning, skalar ner var och en för att passa dess cell och fyller rutnätet från vänster till höger, uppifrån och ned, och påbörjar ett nytt utdataark så fort det aktuella rutnätet är fullt. Källsidorna ändras inte. Det du får tillbaka är ett nytt dokument vars sidor är kompositer

Utdatastorleken är i punkter, inte pixlar

OutputWidth och OutputHeight är PDF-användarenheter, och en PDF-användarenhet är en punkt, vilket är en sjuttiotvådel av en tum. Enheten anger den fysiska storleken på utdataarket och har ingenting att göra med skärmpixlar eller renderings-DPI. Detta är det absolut vanligaste stället att göra fel på vid imponering, eftersom en utvecklare som är van vid bitmappar tar till ett pixelantal och slutar med ett ark i storleken av ett frimärke eller en reklamtavla

Siffrorna som är värda att lägga på minnet är de två sidstorlekar du kommer att använda mest. US Letter är 612 gånger 792 punkter, eftersom 8,5 tum gånger 72 är 612 och 11 tum gånger 72 är 792. A4 är ungefär 595 gånger 842 punkter, baserat på dess dimensioner 210 gånger 297 millimeter. Bindningens egen rubrik anger regeln tydligt, att en enhet är en sjuttiotvådel av en tum, och enheten levererar en PointsPerInch-konstant lika med 72 om du hellre vill beräkna en storlek från tum i koden än att skriva det bokstavliga värdet

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;

Den returnerade pekaren är din att frigöra

Läs signaturen igen. ImportNPagesToOne returnerar en TPdf, inte en Boolean. Det returvärdet är en helt ny dokumentpekare, allokerad separat från källan, och anroparen äger den. Den käll-TPdf som du anropade metoden på är orörd och äger fortfarande sin egen pekare; kompositen är ett andra, oberoende objekt. Om du låter den returnerade TPdf gå ur omfånget utan att frigöra den läcker du ett helt PDFium-dokument

Det farligare misstaget går åt andra hållet. Under ytan ber metoden PDFium om en ny FPDF_DOCUMENT via FPDF_ImportNPagesToOne, och slår sedan in den råa pekaren i den returnerade TPdf så att omslagets livstid styr pekarens. Från den tidpunkten finns det exakt en ägare av pekaren, och exakt ett ställe där den ska stängas: när du gör Free på det returnerade objektet. En slarvig felsökväg som både frigör omslaget och anropar FPDF_CloseDocument på den råa pekaren den fångade stänger samma PDFium-dokument två gånger. Det är en dubbelfrigöring, och det är just den bugg som drabbade en anropare här en gång. Regeln som förhindrar det är enkel. Stäng dokumentet på endast en sökväg, genom att frigöra den TPdf som metoden gav dig, och sträck dig aldrig förbi omslaget för att stänga den pekare som det redan har antagit

Två följdsatser härrör från detta. För det första returnerar metoden nil när PDFium avvisar argumenten, till exempel noll på någon av rutnätets axlar eller ett allokeringsfel, så en nil-kontroll hör hemma innan du rör vid resultatet. För det andra, initiera din utdatavariabel till nil före try och frigör den i finally, som exemplet ovan gör, så att ett misslyckande halvvägs inte kan leda till att du frigör en odefinierad referens eller hoppar över frigöringen helt

Praktiskt sammanhang

Imponering bygger ett nytt dokument. Omordning ändrar ett dokument på plats. MovePages lyfter en uppsättning sidor ur deras nuvarande positioner och släpper dem vid en destination, vilket förskjuter allt annat runt det flyttade blocket så att sidantalet förblir detsamma:

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

Indexen är nollbaserade. PageIndices listar sidorna som ska flyttas, i den ordning de ska hamna, och DestPageIndex is indexet där den först flyttade sidan landar efter att flytten har lagt sig. Eftersom PDFium flyttar sidorna snarare än att kopiera och komprimera om deras innehåll är operationen billig och förlustfri: sidospelet behåller sina strömmar, sina resurser och sin trohet. Detta är anropet bakom en dra-för-att-ordna-om-sidpanel, där en användare drar en miniatyrbild till en ny plats och du utför den nya ordningen med en enda flytt. Det returnerar False när ett index är utanför intervallet, så validera resultatet istället för att anta att omordningen lyckades

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;

Att hämta en delmängd efter index

Den tredje operationen kopierar en explicit uppsättning sidor från ett dokument till ett annat. ImportPagesByIndex tar källdokumentet och en nollbaserad index-array, och infogar dessa sidor i målet på en vald position:

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

Du anropar det på måldokumentet och skickar källan som det första argumentet. PageIndices anger källsidorna som ska hämtas, i den ordning du vill ha dem; InsertAt är den nollbaserade platsen i målet där den första importerade sidan hamnar, så 0 placerar dem före den befintliga första sidan och målets aktuella sidantal utökas. En tom array importerar varje sida, vilket gör anropet till en fullständig kopia när du behöver en. Det returnerar False om något index är utanför källans intervall

Det är här kontrasten till uppdelning spelar roll. Uppdelning skriver separata filer, en operation som producerar många utdata på disken. ImportPagesByIndex gör det motsatta arbetet: det samlar en vald uppsättning sidor i ett enda måldokument i minnet, som du sedan sparar en gång. När uppgiften är "ge mig sidorna 3, 7 och 12 som en kort PDF" är detta den direkta vägen, och den sveper runt FPDF_ImportPagesByIndex under huven

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;

Att sätta ihop det rent

Formen från början till slut är densamma för alla tre: öppna källan genom att ställa in FileName och ändra Active till True, utför operationen, spara med SaveAs och frigör det du äger. Den gren som behöver omsorg är vilka anrop som allokerar ett nytt dokument. MovePages muterar dokumentet du redan har, så det finns ett objekt att frigöra. ImportPagesByIndex skriver till ett mål du själv skapat, så du frigör källan och målet du öppnade. ImportNPagesToOne är avvikaren, eftersom det nya dokumentet är metodens returvärde snarare än något du konstruerat, och att glömma att det är en separat, anropar-ägd pekare är hur både läckan och dubbelfrigöringen sker. Initiera resultatet till nil, kontrollera det efter anropet och frigör det på en enda sökväg

Om arbetet du faktiskt har är att kombinera hela filer snarare än att ordna om sidor, se att slå samman flera PDF-filer till ett dokument. Om det är det omvända, att dela upp ett dokument i flera filer, se att dela upp PDF-dokument i flera filer. Imponerings- och omordningsmetoderna som beskrivs här levereras som en del av PDFium Component för Delphi och C++Builder, tillsammans med de API:er för laddning, rendering och redigering som beskrivs på andra ställen i denna blogg