Teknisk artikkel

Skalering av PDF-sider til 70 % med losLab PDF-biblioteket

PDF-sidedimensjoner er fastsatt på det tidspunktet en side opprettes, så du kan ikke bare omskalere innhold på stedet slik du kanskje ville endret størrelse på et bilde. Bibliotekmodellen som gjør krymping praktisk, er capture-and-redraw (fang-og-tegn-på-nytt): løft hver sides innhold ut av dokumentet og inn i et håndtak (handle), opprett en ny blank side med den opprinnelige mediestørrelsen (media size), og tegn deretter det fangede innholdet tilbake i en redusert omsluttende boks (bounding box). Det omkringliggende hvite rommet blir margen. På 70 % skala på en A4-side, for eksempel, havner 15 % av bredden på hver side, og samme andel øverst og nederst, som er nøyaktig hva grense-aritmetikken (border arithmetic) nedenfor produserer

Hvordan CapturePage fungerer

CapturePage tar et sidenummer, forfremmer (promotes) den sidens innhold til et minnebasert fangst-objekt (capture object), og fjerner siden fra dokumentets sidetre. Denne fjerningen er bevisst, og er grunnen til at løkken alltid velger side 1 uavhengig av iterasjonsindeksen: når side 1 er fanget og slettet, blir det som var side 2 den nye side 1, og så videre. Hvis du inkrementerer side-velgeren (page selector) sammen med løkketelleren (loop counter), vil du hoppe over annenhver side og ende opp med halvparten av forventet utdata

Fangst-håndtaket returnert av CapturePage er ikke en sidereferanse; det er mer som et innholdsøyeblikksbilde (content snapshot). Det forblir gyldig til du kaller DrawCapturedPage eller frigjør det eksplisitt. DrawCapturedPage tar det håndtaket pluss et destinasjonsrektangel gitt som venstre forskyvning (offset), bunn-forskyvning, bredde og høyde, alt i punkter (points). Biblioteket skalerer det fangede innholdet til å passe nøyaktig i det rektangelet, og bevarer sideforholdet (aspect ratio) kun hvis rektangelet ditt tilfeldigvis matcher de originale proporsjonene. For uniform skalering ønsker du at rektangelet skal være den opprinnelige størrelsen multiplisert med skaleringsfaktoren, sentrert på siden

Senteringsmatematikken

Med en 70 % skaleringsfaktor blir de resterende 30 % av hver dimensjon delt likt mellom de to sidene. Så den horisontale innrykkelsen er pageWidth * (1.0 - 0.70) / 2, som er 15 % av bredden, og den vertikale innrykkelsen følger den samme formelen med sidehøyden. Destinasjonsrektangelet for DrawCapturedPage starter da på (horizBorder, vertBorder) og spenner over pageWidth - 2 * horizBorder ganger pageHeight - 2 * vertBorder. Den aritmetikken er ikke bibliotekspesifikk; det er bare geometrien for å plassere et mindre rektangel symmetrisk inne i et større

En ting det er verdt å merke seg: SetOrigin(1) plasserer koordinatopprinnelsen (coordinate origin) øverst til venstre i stedet for nederst til venstre. Grenseverdiene (border values) du sender til DrawCapturedPage måles fra hvilken enn opprinnelse du har satt, så hvis du bytter opprinnelsesmoduser mellom innlasting og tegning, vil sentreringen bli feil

C#-eksempel

Følgende kode behandler hver side av Pages.pdf gjennom fang-og-tegn-på-nytt-syklusen og skriver resultatet til newpages.pdf. PDFL er ActiveX/COM-wrapper-objektet lagt til i prosjektet fra 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-eksempel

Delphi-versjonen bruker TPDFlib direkte i stedet for gjennom COM-laget, men kallesekvensen er identisk. En praktisk forskjell er beskyttelsen av utdatafilen: FileExists pluss DeleteFile i stedet for File.Delete, fordi SaveToFile vil feile hvis destinasjonen er låst av en tidligere kjøring som fortsatt er åpen i en seer (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;

Hva skaleringsfaktoren faktisk kontrollerer

Verdien 0.70 her betyr at det gjengitte innholdet opptar 70 % av hver sidedimensjon, ikke at filen er 70 % av dens opprinnelige bytestørrelse. Filstørrelse etter denne operasjonen avhenger av kompleksiteten til det originale innholdet; en side med store bilder vil ikke krympe proporsjonalt fordi pikseldataene tegnes på nytt med samme oppløsning i et mindre område. Hvis komprimering på bytenivå er målet, er den rette tilnærmingen LinearizeFile eller lagring på nytt med strøm-komprimering (stream compression), ikke geometrisk skalering

Tallet 70 % er heller ikke en hard grense. Enhver verdi mellom 0.0 og 1.0 fungerer, og verdier over 1.0 forstørrer innholdet forbi den opprinnelige sidegrensen, noe som klippes av ved mediebokskanten (media box edge) med mindre du også øker sidedimensjonene. Dokumenter med blandede størrelser håndteres naturlig, fordi PageWidth og PageHeight spørres per side før grenseberegningen, så et dokument der oddetallssider er A4 og partallssider er A3 vil produsere riktig sentrert utdata på hver sidestørrelse uten noen spesiell håndtering

Hvor ting kan gå galt

To feilmoduser dukker opp i praksis. Den første er en utdatafil etterlatt åpen i en PDF-leser fra en tidligere kjøring: SaveToFile vil feile eller skrive null byte avhengig av plattformen, og det nye utdataet lander aldri. Filslettings-beskyttelsen (file-delete guard) i toppen av funksjonen håndterer det for utvikling, men i en produksjonspipeline er det tryggere å skrive til en midlertidig filbane og gi den nytt navn ved suksess

Den andre er avvik i sideantall (page count mismatch). Fordi CapturePage fjerner sider fra dokumentet etter hvert som det behandler dem, er antallet du leser fra PageCount() før løkken den riktige grensen å iterere mot. Å kalle PageCount() inne i løkken ville returnere et synkende tall på hver gjennomkjøring og avslutte tidlig, og etterlate de siste sidene ubehandlet. Løkkevariabelen i eksemplene tjener kun som en gjenværende-iterasjoner-teller; den brukes aldri til å velge en side, fordi siden som skal velges alltid er 1, av årsaken forklart tidligere

Side-manipuleringskallene (The page manipulation calls) vist her, inkludert CapturePage, DrawCapturedPage, og SetPageDimensions, er en del av losLab PDF Library for Delphi, C#, VB.NET, og C++