Техническа статия

Мащабиране на PDF страници до 70% с losLab PDF Library

Размерите на PDF страницата са фиксирани в момента на нейното създаване, така че не можете просто да мащабирате съдържанието на място по начина, по който бихте преоразмерили изображение. Моделът на библиотеката, който прави смаляването практично, е улавяне-и-пречертаване (capture-and-redraw): извадете съдържанието на всяка страница от документа в манипулатор (handle), създайте нова празна страница с оригиналния размер на носителя, след което начертайте уловеното съдържание обратно в намалена ограничаваща кутия (bounding box). Околните празни пространства стават марж. При 70% мащаб на страница A4, например, 15% от ширината се пада от всяка страна и същата част отгоре и отдолу, което е точно това, което произвежда аритметиката за границите по-долу

Как работи CapturePage

CapturePage взема номер на страница, премества съдържанието на тази страница в обект за улавяне в паметта и премахва страницата от дървото на страниците на документа. Това премахване е умишлено и е причината цикълът винаги да избира страница 1, независимо от индекса на итерацията: след като страница 1 бъде уловена и изтрита, това, което е било страница 2, става новата страница 1 и така нататък. Ако увеличавате селектора на страници заедно с брояча на цикъла, ще пропускате всяка втора страница и ще завършите с половината от очаквания резултат

Манипулаторът за улавяне (capture handle), върнат от CapturePage, не е референция към страница; той е по-скоро като моментна снимка на съдържанието. Той остава валиден, докато не извикате DrawCapturedPage или не го освободите изрично. DrawCapturedPage взема този манипулатор плюс целеви правоъгълник, зададен като ляво отместване, долно отместване, ширина и височина, всички в точки. Библиотеката мащабира уловеното съдържание, за да пасне точно на този правоъгълник, като запазва съотношението на страните само ако вашият правоъгълник случайно съвпада с оригиналните пропорции. За равномерно мащабиране искате правоъгълникът да бъде с оригиналния размер, умножен по коефициента на мащабиране, центриран на страницата

Математиката за центриране

С 70% коефициент на мащабиране останалите 30% от всяко измерение се разделят поравно между двете страни. Така хоризонталният отстъп е pageWidth * (1.0 - 0.70) / 2, което е 15% от ширината, а вертикалният отстъп следва същата формула, използвайки височината на страницата. Целевият правоъгълник за DrawCapturedPage след това започва от (horizBorder, vertBorder) и обхваща pageWidth - 2 * horizBorder на pageHeight - 2 * vertBorder. Тази аритметика не е специфична за библиотеката; това е просто геометрията на симетричното вместване на по-малък правоъгълник в по-голям

Едно нещо си струва да се отбележи: SetOrigin(1) поставя началото на координатите горе-вляво, а не долу-вляво. Стойностите на границите, които предавате на DrawCapturedPage, се измерват от това начало, което сте задали, така че ако превключите режимите на началото между зареждането и чертането, центрирането ще бъде неправилно

Пример на C#

Следващият код обработва всяка страница от Pages.pdf чрез цикъла на улавяне-и-пречертаване и записва резултата в newpages.pdf. PDFL е ActiveX/COM обект обвивка (wrapper object), добавен към проекта от 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

Версията на Delphi използва TPDFlib директно, а не чрез COM слоя, но последователността на извикванията е идентична. Една практическа разлика е защитата на изходния файл: FileExists плюс DeleteFile вместо File.Delete, защото SaveToFile ще се провали, ако целта е заключена от предишно изпълнение, което все още е отворено в програма за преглед

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;

Какво всъщност контролира коефициентът на мащабиране

Стойността 0.70 тук означава, че изобразеното съдържание заема 70% от всяко измерение на страницата, а не че файлът е 70% от първоначалния си размер в байтове. Размерът на файла след тази операция зависи от сложността на оригиналното съдържание; страница с големи изображения няма да се свие пропорционално, защото пикселните данни се пречертават при същата разделителна способност в по-малка област. Ако целта е компресия на ниво байтове, правилният подход е LinearizeFile или повторно запазване с компресия на потока (stream compression), а не геометрично мащабиране

Стойността от 70% също не е твърдо ограничение. Всяка стойност между 0.0 и 1.0 работи, а стойности над 1.0 уголемяват съдържанието извън оригиналните граници на страницата, което го отрязва на ръба на медийната кутия (media box), освен ако не увеличите и размерите на страницата. Документите със смесени размери се обработват естествено, защото PageWidth и PageHeight се заявяват за всяка страница преди изчисляването на границите, така че документ, в който нечетните страници са A4, а четните страници са A3, ще произведе правилно центриран резултат на всеки размер страница без никакви специални случаи (special casing)

Къде нещата могат да се объркат

В практиката се появяват два режима на повреда. Първият е изходен файл, оставен отворен в PDF програма за преглед от предишно изпълнение: SaveToFile ще се провали или ще запише нула байта в зависимост от платформата, и новият резултат никога не се записва. Защитата с изтриване на файл в горната част на функцията се справя с това за разработка, но в производствен конвейер (production pipeline) записването във временен път и преименуването при успех е по-безопасно

Вторият е несъответствие в броя на страниците. Тъй като CapturePage премахва страници от документа, докато ги обработва, броят, който четете от PageCount() преди цикъла, е правилната граница за итериране. Извикването на PageCount() вътре в цикъла би върнало намаляващо число при всяко преминаване и би излязло преждевременно, оставяйки последните страници необработени. Променливата на цикъла в примерите служи само като брояч на оставащите итерации; тя никога не се използва за избор на страница, защото страницата за избор винаги е 1 поради причината, обяснена по-рано

Извикванията за манипулиране на страници, показани тук, включително CapturePage, DrawCapturedPage и SetPageDimensions, са част от losLab PDF Library за Delphi, C#, VB.NET и C++