Technisch artikel

N-up inslag en paginaherschikking met PDFium

Samenvoegen en splitsen zijn de twee paginaoperaties waar iedereen het eerst naar grijpt, en ze dekken veel grond. Ze dekken niet alles. Er is een aparte familie werk dat pagina's herschikt in plaats van hele bestanden te verplaatsen: vier dia's op één vel leggen voor een handout, een pagina van achterin het document naar voren slepen, of pagina 3, 7 en 12 in een kort uittreksel trekken zonder de rest aan te raken. PDFium stelt daarvoor precies drie methoden bloot, en elke gedraagt zich anders dan het samenvoegen en splitsen dat u al kent. Dit artikel loopt door wat ze doen, waar de uitvoerverwijzingen staan, en één eigendomsdetail dat makkelijk misgaat

De drie zijn ImportNPagesToOne voor N-up inslag (imposition), MovePages voor paginaherschikking ter plaatse, en ImportPagesByIndex voor selectie-extractie. Samenvoegen stapelt documenten achter elkaar en laat het aantal pagina's gelijk aan de som van de invoer. Splitsen schrijft meerdere uitvoerbestanden vanuit één invoer. De drie bewerkingen hier zitten daartussenin: een ervan wijzigt hoeveel bronpagina's een vel delen, een wijzigt de volgorde binnen een enkel document, en een kopieert een geselecteerd aantal pagina's naar een ander document. Weten welke welke is, voorkomt dat u een ingewikkelde samenvoeg-en-verwijder-dans moet uitvoeren waar een enkele aanroep zou volstaan

Wat N-up inslag daadwerkelijk doet

Inslag (imposition) is de drukkersterm voor het rangschikken van meerdere bronpagina's op één groter vel, zodat het gedrukte en gevouwen resultaat in de juiste volgorde leest. De alledaagse versie is the 2-up hand-out, the 4-up katernindeling of het contactvel dat een dozijn miniaturen op een pagina past. PDFium handelt de geometrie af via één enkele aanroep:

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

NumX en NumY beschrijven het raster. Een waarde van 2, 1 plaatst twee bronpagina's naast elkaar; 2, 2 verpakt er vier in een kwadrantlay-out; 4, 3 bouwt een twaalf-up contactvel. PDFium leest de bronpagina's op volgorde, schaalt elke pagina om in zijn cel te passen, en vult het raster van links naar rechts, van boven naar beneden, waarbij een nieuw uitvoervel wordt gestart zodra het huidige raster vol is. De bronpagina's worden niet gewijzigd. Wat u terugkrijgt is een nieuw document waarvan de pagina's samengesteld zijn

Diagram van PDFium N-up-impositie die zes bronpagina's verpakt op twee-up en vier-up US Letter-composietvellen
ImportNPagesToOne propt NumX maal NumY bronpagina's op vellen gemeten in punten, gevuld van links naar rechts en van boven naar beneden voordat een vers vel begint

De uitvoergrootte is in punten, niet in pixels

OutputWidth en OutputHeight zijn PDF-gebruikersunits, en een PDF-gebruikersunit is één punt, wat gelijk staat aan een tweeënzeventigste van een inch. De eenheid definieert de fysieke grootte van het uitvoervel en heeft niets te maken met schermpixels of render-DPI. Dit is de meest voorkomende plek waar een inslag misgaat, omdat een ontwikkelaar die gewend is aan bitmaps een pixelwaarde kiest en eindigt met een vel ter grootte van een postzegel of een reclamebord

De getallen die het waard zijn om te onthouden zijn de twee paginaformaten die u het meest zult gebruiken. US Letter is 612 bij 792 punten, omdat 8,5 inch vermenigvuldigd met 72 gelijk is aan 612 en 11 inch vermenigvuldigd met 72 gelijk is aan 792. A4 is ongeveer 595 bij 842 punten, afgeleid van de afmetingen 210 bij 297 millimeter. De header van de binding zelf vermeldt de regel duidelijk: één eenheid is een tweeënzeventigste van een inch, en de unit levert een PointsPerInch-constante van 72 voor het geval u een grootte in code wilt berekenen in plaats van de waarde hard te coderen

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;

    // Vier bronpagina's per Letter-vel, raster van 2 bij 2
    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;   // zie de volgende sectie: dit is verplicht
    Source.Free;
  end;
end;

De geretourneerde handle moet u zelf vrijgeven

Lees de signatuur nogmaals. ImportNPagesToOne retourneert een TPdf, geen Boolean. Die retourwaarde is een gloednieuwe documenthandle, afzonderlijk van de bron gealloceerd, en de aanroeper bezit deze. De bron-TPdf waarop u de methode aanriep is ongewijzigd en bezit nog steeds zijn eigen handle; het composiet is een tweede, onafhankelijk object. Als u de geretourneerde TPdf buiten scope laat gaan zonder deze vrij te geven, lekt u een volledig PDFium-document

De gevaarlijkere fout loopt de andere kant op. Onder de motorkap vraagt de methode PDFium om een verse FPDF_DOCUMENT via FPDF_ImportNPagesToOne, en pakt die rauwe handle vervolgens in de teruggegeven TPdf, zodat de levensduur van de wrapper die van de handle bepaalt. Vanaf dat moment is er precies één eigenaar van de handle, en precies één plek waar hij gesloten hoort te worden: wanneer u het teruggegeven object met Free vrijgeeft. Een achteloos foutpad dat zowel de wrapper vrijgeeft als ook FPDF_CloseDocument aanroept op de rauwe handle die het heeft vastgehouden, sluit hetzelfde PDFium-document twee keer. Dat is een double-free. De regel die dat voorkomt is kort. Sluit het document op één pad alleen, door de TPdf vrij te geven die de methode u aanreikte, en reik nooit langs de wrapper om de handle die hij al heeft geadopteerd te sluiten

Hieruit vloeien twee logische gevolgen voort. Ten eerste retourneert de methode nil wanneer PDFium de argumenten afwijst, zoals een nul op een van de rasterassen of een allocatiefout, dus een nil-controle is vereist voordat u het resultaat gebruikt. Ten tweede: initialiseer uw uitvoervariabele op nil vóór de try en geef deze vrij in de finally, zoals in het bovenstaande voorbeeld, zodat een fout halverwege er niet toe leidt dat u een ongedefinieerde referentie vrijgeeft of het vrijgeven helemaal overslaat

Levenscyclusdiagram van de TPdf-handle teruggegeven door PDFium ImportNPagesToOne, met het veilige enkele free-pad versus het double-free-crashpad
De teruggegeven TPdf is een tweede, aanroeper-eigen document: initialiseer het op nil, controleer het, en geef de wrapper op één pad vrij

Pagina's herschikken zonder ze te herschrijven

Inslag bouwt een nieuw document. Herschikken wijzigt een document ter plaatse. MovePages tilt een set pagina's uit hun huidige posities en plaatst ze op een bestemming, waarbij al het andere rond het verplaatste blok verschuift zodat het aantal pagina's gelijk blijft:

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

De indexen zijn zero-based. PageIndices geeft de te verplaatsen pagina's weer, in de volgorde waarin ze moeten eindigen, en DestPageIndex is de index waar de eerste verplaatste pagina landt nadat de verplaatsing is voltooid. Omdat PDFium de pagina's verplaatst in plaats van hun inhoud te kopiëren en opnieuw te comprimeren, is de bewerking goedkoop en verliesvrij: de pagina-objecten behouden hun streams, bronnen en getrouwheid. Dit is de aanroep achter een drag-to-reorder paginapaneel, waar een gebruiker een miniatuur naar een nieuw slot sleept en u de nieuwe volgorde met één verplaatsing vastlegt. Het retourneert False wanneer een index buiten bereik is, dus valideer het resultaat in plaats van aan te nemen dat de herschikking is gelukt

PDFium Component: MovePages-herschikkingskaart die PDF-pagina vier verplaatst naar index nul van een rapport met vijf pagina's, terwijl de resterende pagina's één plek naar rechts schuiven
MovePages tilt pagina 4 naar index 0 terwijl de overige pagina's één plek schuiven en het paginantal op vijf blijft
var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Verplaats de laatste pagina (index 4 in een bestand van 5 pagina's) helemaal naar voren
    if not Doc.MovePages([4], 0) then
      raise Exception.Create('MovePages rejected the index');

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

Een selectie ophalen per index

De derde bewerking kopieert een expliciete set pagina's van het ene document naar het andere. ImportPagesByIndex accepteert het brondocument en een zero-based index-array, en voegt die pagina's in het target in op een gekozen positie:

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

U roept dit aan op het doeldocument (target) en geeft de bron (source) door als eerste argument. PageIndices specificeert de te kopiëren bronpagina's, in de volgorde waarin u ze wilt; InsertAt is de zero-based positie in het doeldocument waar de eerste geïmporteerde pagina wordt geplaatst. Waarde 0 plaatst ze vóór de bestaande eerste pagina, en het huidige aantal pagina's van het target wordt verlengd. Een lege array importeert elke pagina, wat de aanroep tot een volledige kopie maakt wanneer u die nodig hebt. Het retourneert False als een index in de bron buiten bereik is

Dit is waar het contrast met splitsen belangrijk is. Splitsen schrijft afzonderlijke bestanden, waarbij één bewerking vele fysieke bestanden op de schijf produceert. ImportPagesByIndex doet het omgekeerde: het verzamelt een gekozen set pagina's in één enkel doeldocument in het geheugen, dat u vervolgens één keer opslaat. Wanneer de taak is "geef me pagina's 3, 7 en 12 als één korte PDF", is dit de directe route, die onder de motorkap FPDF_ImportPagesByIndex aanroept

var
  Source, Excerpt: TPdf;
begin
  Source := TPdf.Create(nil);
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // begin met een leeg doel

    // Trek pagina 3, 7 en 12 (nulgebaseerd 2, 6, 11) in het 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;

Alles netjes samenvoegen

De algehele structuur is voor alle drie hetzelfde: open de bron door FileName in te stellen en Active op True te zetten, voer de bewerking uit, sla op met SaveAs en geef vrij wat u bezit. Het enige punt dat zorgvuldigheid vereist, is bepalen welke aanroepen een nieuw document alloceren. MovePages wijzigt het document dat u al bezit, dus er is één object om vrij te geven. ImportPagesByIndex schrijft in een target dat u zelf hebt gemaakt, dus u geeft zowel de bron als het geopende target vrij. ImportNPagesToOne is de uitzondering, omdat het nieuwe document de retourwaarde is van de methode en niet iets wat u zelf hebt geconstrueerd. Vergeten dat dit een afzonderlijke handle in eigendom van de aanroeper is, is hoe zowel het lek als de double-free ontstaan. Initialiseer het resultaat op nil, controleer het na de aanroep en geef het via exact één pad vrij

Is het werk dat u werkelijk hebt het samenvoegen van hele bestanden in plaats van het herschikken van pagina's, zie dan meerdere PDF-bestanden in één document samenvoegen. Is het andersom, één document opsplitsen in meerdere bestanden, zie dan PDF-documenten in meerdere bestanden opsplitsen. Die twee dekken de alledaagse gevallen, en dit artikel is voor de indelingen en herschikkingen waar ze niet komen. De hier beschreven impositie- en herschikkingsmethoden worden geleverd als onderdeel van de PDFium Component voor Delphi en C++Builder, naast de laad-, render- en bewerkings-API's die elders op deze blog worden behandeld