Samenvoegen en splitsen zijn de twee paginabewerkingen die iedereen als eerste gebruikt, en ze dekken veel terrein. Ze dekken echter niet alles. Er is een afzonderlijke categorie werkzaamheden die pagina's herschikt in plaats van hele bestanden te verplaatsen: leg vier dia's op één vel voor een hand-out, sleep een pagina van de achterkant van een document naar de voorkant, of haal de pagina's 3, 7 en 12 op in een kort uittreksel zonder de rest aan te raken. PDFium biedt exact hiervoor drie methoden, en elk daarvan gedraagt zich anders dan de samenvoegingen en splitsingen die u al kent. Dit artikel behandelt wat ze doen, waar de uitvoerpunten liggen en een eigendomsdetail dat in de praktijk tot een crash heeft geleid.
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.
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;
// 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;
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 werkt andersom. Onder water vraagt de methode PDFium om een nieuwe FPDF_DOCUMENT via FPDF_ImportNPagesToOne, en verpakt die rauwe handle vervolgens in de geretourneerde TPdf zodat de levensduur van de wrapper die van de handle bepaalt. Vanaf dat moment is er exact één eigenaar van de handle, en exact één plek waar deze moet worden gesloten: wanneer u het geretourneerde object vrijgeeft met Free. Een onzorgvuldig foutpad dat zowel de wrapper vrijgeeft als FPDF_CloseDocument aanroept op de verkregen rauwe handle, sluit hetzelfde PDFium-document tweemaal. Dat is een double-free, en dat is de specifieke bug waar een gebruiker hier ooit tegenaan liep. De regel om dit te voorkomen is simpel. Sluit het document op slechts één pad, door de TPdf die de methode u gaf vrij te geven, en grijp nooit achter de wrapper om de handle te sluiten die deze al heeft geadopteerd.
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.
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.
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;
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; // 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;
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.
If the work you actually have is combining whole files rather than rearranging pages, see merging multiple PDF files into one document. If it is the reverse, breaking one document into several files, see splitting PDF documents into multiple files. The imposition and reordering methods described here ship as part of the PDFium Component for Delphi and C++Builder, alongside the loading, rendering, and editing APIs covered elsewhere on this blog.