De afmetingen van PDF-pagina's staan vast op het moment dat een pagina wordt aangemaakt, dus je kunt niet simpelweg de inhoud ter plekke herschalen zoals je een afbeelding van grootte zou veranderen. Het bibliotheekmodel dat krimpen praktisch maakt, is vastleggen-en-opnieuw-tekenen (capture-and-redraw): licht de inhoud van elke pagina uit het document naar een handle, creëer een nieuwe blanco pagina op het oorspronkelijke mediaformaat, en teken vervolgens de vastgelegde inhoud terug in een verkleind selectiekader (bounding box). De omringende witruimte wordt de marge. Bij een schaal van 70% op een A4-pagina, bijvoorbeeld, valt 15% van de breedte aan elke kant en dezelfde fractie aan de boven- en onderkant, wat precies is wat de onderstaande randberekeningen opleveren
Hoe CapturePage werkt
CapturePage neemt een paginanummer, promoveert de inhoud van die pagina naar een in-memory capture-object, en verwijdert de pagina uit de paginaboom (page tree) van het document. Die verwijdering is opzettelijk en is de reden waarom de lus altijd pagina 1 selecteert, ongeacht de iteratie-index: zodra pagina 1 is vastgelegd en verwijderd, wordt wat pagina 2 was de nieuwe pagina 1, enzovoort. Als je de paginaselector samen met de lusteller (loop counter) ophoogt, sla je om de andere pagina over en eindig je met de helft van de verwachte uitvoer
De capture-handle die door CapturePage wordt geretourneerd, is geen paginaverwijzing; het is meer zoiets als een snapshot van de inhoud. Het blijft geldig totdat je DrawCapturedPage aanroept of het expliciet vrijgeeft. DrawCapturedPage neemt die handle plus een bestemmingsrechthoek, opgegeven als linker offset, onderste offset, breedte en hoogte, allemaal in punten. De bibliotheek schaalt de vastgelegde inhoud om precies in die rechthoek te passen, waarbij de beeldverhouding (aspect ratio) alleen behouden blijft als je rechthoek toevallig overeenkomt met de oorspronkelijke verhoudingen. Voor uniforme schaling wil je dat de rechthoek de oorspronkelijke grootte is, vermenigvuldigd met de schaalfactor, gecentreerd op de pagina
De centreerberekeningen
Met een schaalfactor van 70% wordt de resterende 30% van elke dimensie gelijkmatig verdeeld over de twee zijden. De horizontale inzet is dus pageWidth * (1.0 - 0.70) / 2, wat 15% van de breedte is, en de verticale inzet volgt dezelfde formule met behulp van de paginahoogte. De bestemmingsrechthoek voor DrawCapturedPage begint dan op (horizBorder, vertBorder) en beslaat pageWidth - 2 * horizBorder bij pageHeight - 2 * vertBorder. Die rekenkunde is niet bibliotheekspecifiek; het is gewoon de geometrie van het symmetrisch passen van een kleinere rechthoek in een grotere
Eén ding om op te merken: SetOrigin(1) plaatst de oorsprong (origin) van de coördinaten linksboven in plaats van linksonder. De randwaarden die je aan DrawCapturedPage doorgeeft, worden gemeten vanaf de oorsprong die je hebt ingesteld, dus als je tussen het laden en tekenen van oorsprongsmodus wisselt, zal de centrering niet kloppen
C# voorbeeld
De volgende code verwerkt elke pagina van Pages.pdf via de capture-and-redraw cyclus en schrijft het resultaat naar newpages.pdf. PDFL is het ActiveX/COM wrapper-object dat aan het project is toegevoegd vanuit PDFlibDLL64.dll
private void ScalePages_Click(object sender, EventArgs e)
{
File.Delete("newpages.pdf");
double pageWidth, pageHeight, horizBorder, vertBorder;
double scaleFactor = 0.70;
int capturedPageId, ret;
PDFL.LoadFromFile("Pages.pdf", "");
PDFL.SetOrigin(1);
int numPages = PDFL.PageCount();
for (int i = 1; i <= numPages; i++)
{
// Always select page 1: CapturePage removes the page, so page 2
// becomes page 1 on the next iteration.
PDFL.SelectPage(1);
pageWidth = PDFL.PageWidth();
pageHeight = PDFL.PageHeight();
horizBorder = pageWidth * (1.0 - scaleFactor) / 2;
vertBorder = pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId = PDFL.CapturePage(1);
PDFL.NewPage();
PDFL.SetPageDimensions(pageWidth, pageHeight);
ret = PDFL.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
}
PDFL.SaveToFile("newpages.pdf");
}
Delphi voorbeeld
De Delphi-versie gebruikt TPDFlib rechtstreeks in plaats van via de COM-laag, maar de aanroepvolgorde is identiek. Eén praktisch verschil is de bewaker van het uitvoerbestand (output file guard): FileExists plus DeleteFile in plaats van File.Delete, omdat SaveToFile zal mislukken als de bestemming is vergrendeld door een eerdere run die nog openstaat in een viewer
procedure TForm1.ScalePagesClick(Sender: TObject);
var
PDFLib: TPDFlib;
pageWidth, pageHeight, horizBorder, vertBorder: Double;
scaleFactor: Double;
capturedPageId, ret, numPages, i: Integer;
begin
if FileExists('newpages.pdf') then
DeleteFile('newpages.pdf');
scaleFactor := 0.70;
PDFLib := TPDFlib.Create;
try
PDFLib.LoadFromFile('Pages.pdf', '');
PDFLib.SetOrigin(1);
numPages := PDFLib.PageCount();
for i := 1 to numPages do
begin
PDFLib.SelectPage(1);
pageWidth := PDFLib.PageWidth();
pageHeight := PDFLib.PageHeight();
horizBorder := pageWidth * (1.0 - scaleFactor) / 2;
vertBorder := pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId := PDFLib.CapturePage(1);
PDFLib.NewPage();
PDFLib.SetPageDimensions(pageWidth, pageHeight);
ret := PDFLib.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
end;
PDFLib.SaveToFile('newpages.pdf');
finally
PDFLib.Free;
end;
end;
Wat de schaalfactor eigenlijk regelt
De waarde 0,70 hier betekent dat de weergegeven inhoud 70% van elke paginadimensie beslaat, niet dat het bestand 70% van zijn oorspronkelijke bytegrootte is. De bestandsgrootte na deze bewerking hangt af van de complexiteit van de oorspronkelijke inhoud; een pagina met grote afbeeldingen zal niet proportioneel krimpen, omdat de pixeldata op dezelfde resolutie opnieuw in een kleiner gebied wordt getekend. Als compressie op byteniveau het doel is, is de juiste aanpak LinearizeFile of opnieuw opslaan met streamcompressie, geen geometrische schaling
Het getal van 70% is ook geen harde limiet. Elke waarde tussen 0,0 en 1,0 werkt, en waarden boven de 1,0 vergroten de inhoud tot voorbij de oorspronkelijke paginagrens, die wordt afgekapt aan de rand van de mediabox, tenzij je ook de pagina-afmetingen vergroot. Documenten van gemengd formaat worden op natuurlijke wijze verwerkt, omdat PageWidth en PageHeight per pagina worden opgevraagd vóór de randberekening, dus een document waarvan de oneven pagina's A4 zijn en de even pagina's A3, zal zonder enige speciale behandeling een correct gecentreerde uitvoer produceren op elk paginaformaat
Waar dingen mis kunnen gaan
In de praktijk komen er twee soorten fouten voor. De eerste is een uitvoerbestand dat open is blijven staan in een PDF-viewer na een eerdere run: SaveToFile zal mislukken of nul bytes schrijven afhankelijk van het platform, en de nieuwe uitvoer landt nooit. De 'file-delete'-bewaker bovenaan de functie handelt dat af voor ontwikkeling, maar in een productie-pijplijn is het veiliger om naar een tijdelijk pad te schrijven en het na succes te hernoemen
De tweede is een niet-overeenkomend aantal pagina's. Omdat CapturePage pagina's uit het document verwijdert terwijl het ze verwerkt, is het aantal dat je uit PageCount() leest vóór de lus de juiste limiet om tegen te itereren. PageCount() aanroepen binnen de lus zou bij elke passage een afnemend getal retourneren en vroegtijdig afsluiten, waardoor de laatste pagina's onverwerkt blijven. De lusvariabele (loop variable) in de voorbeelden dient alleen als een teller voor resterende iteraties; het wordt nooit gebruikt om een pagina te selecteren, omdat de te selecteren pagina altijd 1 is, om de eerder uitgelegde reden
De hier getoonde paginamanipulatie-aanroepen, waaronder CapturePage, DrawCapturedPage en SetPageDimensions, maken deel uit van de losLab PDF-bibliotheek voor Delphi, C#, VB.NET en C++