Technický článek

Škálování stránek PDF na 70 % pomocí losLab PDF Library

Rozměry stránky PDF jsou fixně stanoveny v okamžiku jejího vytvoření, takže nemůžete jednoduše přeškálovat obsah na místě tak, jak byste změnili velikost obrázku. Model knihovny, díky kterému je zmenšování praktické, je zachycení-a-překreslení (capture-and-redraw): vyjměte obsah každé stránky z dokumentu do identifikátoru (handle), vytvořte novou prázdnou stránku s původní velikostí média a poté nakreslete zachycený obsah zpět ve zmenšeném ohraničujícím rámečku (bounding box). Okolní bílé místo se stane okrajem. Například při zmenšení na 70 % na stránce A4 připadne 15 % šířky na každou stranu a stejný zlomek nahoru a dolů, což je přesně to, co produkuje aritmetika okrajů níže

Jak funguje CapturePage

CapturePage převezme číslo stránky, povýší obsah této stránky do objektu zachycení v paměti a odstraní stránku ze stromu stránek dokumentu. Toto odstranění je záměrné a je důvodem, proč smyčka vždy vybírá stránku 1 bez ohledu na index iterace: jakmile je stránka 1 zachycena a odstraněna, to, co bylo stránkou 2, se stane novou stránkou 1, a tak dále. Pokud zvýšíte selektor stránek spolu s počítadlem smyčky, přeskočíte každou druhou stránku a skončíte s polovinou očekávaného výstupu

Identifikátor (handle) zachycení vrácený funkcí CapturePage není odkazem na stránku; je to spíše snímek obsahu. Zůstává platný, dokud nezavoláte DrawCapturedPage nebo jej explicitně neuvolníte. DrawCapturedPage přebírá tento identifikátor a cílový obdélník zadaný jako levý offset, spodní offset, šířka a výška, vše v bodech. Knihovna škáluje zachycený obsah tak, aby se přesně vešel do tohoto obdélníku, a zachovává poměr stran, pouze pokud se váš obdélník shoduje s původními proporcemi. Pro rovnoměrné škálování potřebujete obdélník o původní velikosti vynásobený faktorem změny měřítka a vycentrovaný na stránce

Matematika centrování

S faktorem měřítka 70 % se zbývajících 30 % každého rozměru rozdělí rovným dílem mezi obě strany. Horizontální odsazení je tedy pageWidth * (1.0 - 0.70) / 2, což je 15 % šířky, a vertikální odsazení se řídí stejným vzorcem s použitím výšky stránky. Cílový obdélník pro DrawCapturedPage pak začíná na (horizBorder, vertBorder) a pokrývá pageWidth - 2 * horizBorder krát pageHeight - 2 * vertBorder. Tato aritmetika není specifická pro knihovnu; je to prostě geometrie symetrického umístění menšího obdélníku dovnitř většího

Jedna věc, která stojí za zmínku: SetOrigin(1) umístí počátek souřadnic do levého horního rohu spíše než do levého dolního rohu. Hodnoty okrajů, které předáte DrawCapturedPage, se měří od počátku, který nastavíte, takže pokud mezi načítáním a kreslením přepnete režimy počátku, centrování bude posunuté

Příklad v C#

Následující kód zpracuje každou stránku Pages.pdf cyklem zachycení a překreslení a zapíše výsledek do newpages.pdf. PDFL je objekt wrapperu ActiveX/COM přidaný do projektu z 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++)
    {
        // Vždy vybírejte stránku 1: CapturePage odstraní stránku, takže stránka 2
        // se stane stránkou 1 v další iteraci.
        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");
}

Příklad v Delphi

Verze Delphi používá TPDFlib přímo spíše než přes vrstvu COM, ale sekvence volání je identická. Jedním z praktických rozdílů je ochrana výstupního souboru: FileExists plus DeleteFile namísto File.Delete, protože SaveToFile selže, pokud je cíl uzamčen předchozím spuštěním, které je stále otevřené v prohlížeči

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;

Co vlastně faktor škálování řídí

Hodnota 0,70 zde znamená, že vykreslený obsah zabírá 70 % každého rozměru stránky, nikoli že soubor má 70 % své původní velikosti v bajtech. Velikost souboru po této operaci závisí na složitosti původního obsahu; stránka s velkými obrázky se proporcionálně nezmenší, protože data pixelů se překreslí ve stejném rozlišení do menší oblasti. Pokud je cílem komprese na úrovni bajtů, správným přístupem je LinearizeFile nebo opětovné uložení s kompresí proudů, nikoli geometrické škálování

Hodnota 70 % také není tvrdým limitem. Jakákoli hodnota mezi 0,0 a 1,0 funguje, a hodnoty nad 1,0 zvětší obsah za původní hranici stránky, což dojde k oříznutí na okraji rámečku média (media box), pokud nezvětšíte také rozměry stránky. S dokumenty o smíšených velikostech se zachází přirozeně, protože hodnoty PageWidth a PageHeight se dotazují (query) pro každou stránku před výpočtem okrajů, takže dokument, ve kterém jsou liché stránky A4 a sudé stránky A3, vytvoří správně vycentrovaný výstup na každé velikosti stránky bez jakéhokoli speciálního ošetřování případů

Kde se věci mohou pokazit

V praxi se objevují dva režimy selhání. Prvním je výstupní soubor ponechaný otevřený v prohlížeči PDF z předchozího spuštění: SaveToFile selže nebo zapíše nula bajtů v závislosti na platformě a nový výstup se nikdy neuloží. Pojistka odstranění souboru na začátku funkce to řeší pro vývoj, ale v produkčním řetězci je bezpečnější zápis do dočasné cesty a po úspěšném dokončení jeho přejmenování

Druhým je neshoda v počtu stránek. Vzhledem k tomu, že CapturePage během zpracování odstraňuje stránky z dokumentu, je počet, který načtete z PageCount() před cyklem, správnou hranicí, podle které se má iterovat. Volání PageCount() uvnitř smyčky by vrátilo klesající číslo při každém průchodu a skončilo by předčasně a ponechalo by poslední stránky nezpracované. Proměnná cyklu v příkladech slouží pouze jako počítadlo zbývajících iterací; nikdy se nepoužívá k výběru stránky, protože stránka, která se má vybrat, je vždy 1 z dříve vysvětleného důvodu

Zde uvedená volání pro manipulaci se stránkami, včetně CapturePage, DrawCapturedPage a SetPageDimensions, jsou součástí losLab PDF Library pro Delphi, C#, VB.NET a C++