Technisch artikel

PDF-pagina's schalen naar 70% met de losLab PDF-bibliotheek

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

PDF: Vastleg-en-herteken-pijplijn: elke PDF-pagina wordt vastgelegd in een handle en teruggetekend op een verse blanco pagina die op de originele mediagrootte blijft
Capture-and-redraw tilt de inhoud van elke pagina in een handle en tekent haar opnieuw op verminderde grootte op een verse pagina van de originele afmetingen

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 CapturePage teruggeeft is geen paginaverwijzing; het is meer zoiets als een snapshot van de inhoud. Hij blijft geldig na het tekenen, dus dezelfde capture kan meer dan één keer worden geplaatst, totdat je hem vrijgeeft met ReleaseCapturedPage of het document verdwijnt. DrawCapturedPage neemt die handle plus een bestemmingsrechthoek, opgegeven als linkerrand, bovenrand, breedte en hoogte, allemaal standaard in punten. De bibliotheek schaalt de vastgelegde inhoud om precies in die rechthoek te passen, waarbij de beeldverhouding alleen behouden blijft als je rechthoek toevallig overeenkomt met de oorspronkelijke verhoudingen. Voor uniforme schaling wil je dat de rechthoek de oorspronkelijke grootte maal de schaalfactor is, 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

PDF: Bij 70 procent schaal zit de bestemmingsrechthoek gecentreerd op de originele pagina met een rand van 15 procent berekend voor elke zijde
Schalen naar 70% laat een rand van 15 procent per zijde over, en DrawCapturedPage ontvangt die gecentreerde rechthoek in punten

Eén ding om op te merken: SetOrigin(1) plaatst de oorsprong van de coördinaten linksboven in plaats van linksonder. De tweede waarde die je aan DrawCapturedPage doorgeeft is de bovenrand van de rechthoek, gemeten vanaf de oorsprong die je hebt ingesteld. Daarom roepen de voorbeelden eerst SetOrigin(1) aan: met de default-oorsprong linksonder zou dezelfde vertBorder de bovenrand vlak bij de onderrand van de pagina leggen en het merendeel van de content van de pagina duwen

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 een instantie van de PDFlibrary-importklasse die bij de DLL-editie hoort, aangemaakt met new PDFlibrary("PDFlibDLL64.dll"); de ActiveX-editie biedt dezelfde methoden aan via PDFlibAX.PDFlibrary

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++)
    {
        // Selecteer altijd pagina 1: CapturePage verwijdert de pagina, dus pagina 2
        // wordt pagina 1 bij de volgende iteratie.
        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);
        // 0 = niets vastgelegd, bijv. een bron met één pagina: de enige pagina kan niet worden verwijderd
        if (capturedPageId == 0)
            throw new InvalidOperationException("CapturePage failed on source page " + i);

        PDFL.NewPage();
        PDFL.SetPageDimensions(pageWidth, pageHeight);

        ret = PDFL.DrawCapturedPage(
            capturedPageId,
            horizBorder, vertBorder,
            pageWidth  - 2 * horizBorder,
            pageHeight - 2 * vertBorder);
        if (ret != 1)
            throw new InvalidOperationException("DrawCapturedPage failed on source page " + i);
    }

    PDFL.SaveToFile("newpages.pdf");
}

Delphi voorbeeld

De Delphi-versie gebruikt TPDFlib rechtstreeks in plaats van via de DLL-importklasse, maar de aanroepvolgorde is identiek. Eén praktisch verschil is de bewaker van het uitvoerbestand: 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);
      // 0 = niets vastgelegd, bijv. een bron met één pagina: de enige pagina kan niet worden verwijderd
      if capturedPageId = 0 then
        raise Exception.CreateFmt('CapturePage failed on source page %d', [i]);

      PDFLib.NewPage();
      PDFLib.SetPageDimensions(pageWidth, pageHeight);

      ret := PDFLib.DrawCapturedPage(
        capturedPageId,
        horizBorder, vertBorder,
        pageWidth  - 2 * horizBorder,
        pageHeight - 2 * vertBorder);
      if ret <> 1 then
        raise Exception.CreateFmt('DrawCapturedPage failed on source page %d', [i]);
    end;

    PDFLib.SaveToFile('newpages.pdf');
  finally
    PDFLib.Free;
  end;
end;

Wat de schaalfactor eigenlijk regelt

De waarde 0,70 betekent hier 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 krimpt niet proportioneel, omdat de pixeldata op dezelfde resolutie opnieuw in een kleiner gebied wordt getekend. Is een kleiner bestand het doel, kijk dan naar CompressContent voor page content streams die ongecomprimeerd naar buiten gingen en naar het opnieuw comprimeren van afbeeldingen (OptimizeImages in de Delphi-editie), niet naar geometrische schaling. LinearizeFile is een ander gereedschap: hij herschikt het bestand voor pagina-voor-pagina-laden (Fast Web View), wat viewers helpt grote bestanden over een netwerk te openen maar ze niet kleiner maakt

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 pagina-aantal. Omdat CapturePage pagina's uit het document verwijdert terwijl hij ze verwerkt, is het aantal dat je vóór de lus uit PageCount() leest de juiste grens om tegen te itereren. PageCount() binnen de lus aanroepen helpt ook niet: elke doorgang verwijdert één pagina en voegt er één toe, dus het aantal blijft vlak en zegt niets over hoeveel bronpagina's er nog over zijn. De lusvariabele in de voorbeelden dient alleen als teller van resterende iteraties; hij wordt nooit gebruikt om een pagina te selecteren, want om de eerder uitgelegde reden is de te selecteren pagina altijd 1

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++

PDF: Lusdiagram dat toont waarom elke doorgang SelectPage(1) moet aanroepen, omdat vastgelegde pagina's uit de boom worden verwijderd en de resterende pagina's opschuiven
CapturePage verwijdert elke verwerkte pagina zodat de wachtrij zichzelf hernoemt, waardoor pagina 1 de enige correcte selectorwaarde is