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

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

Размеры страницы PDF фиксируются в момент ее создания, поэтому нельзя просто масштабировать содержимое на месте так же, как размер изображения. Модель библиотеки, которая делает сжатие практичным, это захват и перерисовка: извлечение содержимого каждой страницы из документа в дескриптор, создание новой пустой страницы с исходным размером носителя, а затем отрисовка захваченного содержимого обратно в уменьшенном ограничивающем прямоугольнике. Окружающее пустое пространство становится полем. Например, при масштабе 70% на странице A4, 15% ширины приходится на каждую сторону и такая же доля сверху и снизу, что в точности соответствует арифметике полей, приведенной ниже

PDF: конвейер «захвати и перерисуй»: каждая страница PDF захватывается в дескриптор и отрисовывается назад на свежую пустую страницу, сохранённую в исходном размере носителя
Захват с перерисовкой поднимает содержимое каждой страницы в дескриптор и перерисовывает его уменьшенным на свежей странице исходных размеров

Как работает CapturePage

CapturePage принимает номер страницы, переводит содержимое этой страницы в объект захвата в памяти и удаляет страницу из дерева страниц документа. Это удаление является преднамеренным и является причиной того, что цикл всегда выбирает страницу 1 независимо от индекса итерации: как только страница 1 захвачена и удалена, то, что было страницей 2, становится новой странице 1 и так далее. Если вы будете увеличивать селектор страниц вместе со счетчиком цикла, вы будете пропускать каждую вторую страницу и получите половину ожидаемого результата

Дескриптор захвата, возвращаемый CapturePage, — не ссылка на страницу; это скорее снимок содержимого. Он остаётся действительным после отрисовки, так что один и тот же захват можно разместить более одного раза — до тех пор, пока вы не освободите его вызовом ReleaseCapturedPage или не исчезнет документ. DrawCapturedPage принимает этот дескриптор плюс прямоугольник назначения, заданный левым краем, верхним краем, шириной и высотой, — по умолчанию всё в пунктах. Библиотека масштабирует захваченное содержимое так, чтобы оно точно вписалось в этот прямоугольник, и сохраняет пропорции, только если ваш прямоугольник случайно совпал с исходными пропорциями. Для равномерного масштабирования прямоугольник должен иметь исходный размер, умноженный на коэффициент масштабирования, и быть отцентрирован на странице

Математика центрирования

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

PDF: при масштабе 70% прямоугольник назначения сидит по центру исходной страницы с рамкой в 15 процентов, вычисленной для каждой стороны
Масштабирование до 70% оставляет рамку в 15 процентов на сторону, и DrawCapturedPage получает этот центрированный прямоугольник в пунктах

Стоит отметить: SetOrigin(1) помещает начало координат в левый верхний угол, а не в левый нижний. Второе значение, которое вы передаёте в DrawCapturedPage, — это верхний край прямоугольника, отсчитанный от выбранного вами начала координат. Именно поэтому примеры сначала вызывают SetOrigin(1): с началом координат по умолчанию в левом нижнем углу тот же vertBorder поместил бы верхний край у самого низа страницы и вытолкнул бы большую часть содержимого за её пределы

Пример на C#

Следующий код прогоняет каждую страницу Pages.pdf через цикл захвата и перерисовки и записывает результат в newpages.pdf. PDFL — это экземпляр класса импорта PDFlibrary, поставляемого с DLL-редакцией и создаваемого как new PDFlibrary("PDFlibDLL64.dll"); ActiveX-редакция открывает те же методы через 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++)
    {
        // Всегда выбираем страницу 1: CapturePage удаляет страницу, поэтому страница 2
        // становится страницей 1 на следующей итерации.
        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 = ничего не захвачено, например у одностраничного источника: единственную страницу нельзя удалить
        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

Версия для Delphi использует TPDFlib напрямую, а не через класс импорта DLL, но последовательность вызовов идентична. Одно практическое отличие — защита выходного файла: 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);
      // 0 = ничего не захвачено, например у одностраничного источника: единственную страницу нельзя удалить
      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;

Что на самом деле контролирует коэффициент масштабирования

Значение 0.70 здесь означает, что отрисованное содержимое занимает 70% каждого измерения страницы, а не то, что файл составляет 70% от исходного размера в байтах. Размер файла после этой операции зависит от сложности исходного содержимого; страница с большими изображениями не сожмётся пропорционально, потому что пиксельные данные перерисовываются с тем же разрешением в меньшую площадь. Если цель — меньший файл, смотрите CompressContent для потоков содержимого страниц, вышедших несжатыми, и на повторное сжатие изображений (OptimizeImages в Delphi-редакции), а не на геометрическое масштабирование. LinearizeFile — инструмент другого рода: он переупорядочивает файл для постраничной загрузки (Fast Web View), что помогает просмотрщикам открывать большие файлы по сети, но не сжимает их

Цифра в 70% также не является жестким ограничением. Любое значение от 0.0 до 1.0 работает, а значения выше 1.0 увеличивают содержимое за пределы исходной границы страницы, которая обрезается по краю медиабокса, если вы также не увеличите размеры страницы. Документы смешанного размера обрабатываются естественно, потому что PageWidth и PageHeight запрашиваются для каждой страницы до расчета полей, поэтому документ, в котором нечетные страницы имеют формат A4, а четные — A3, будет производить правильно отцентрированный вывод на каждом размере страницы без какой-либо специальной обработки

Где что-то может пойти не так

На практике встречаются два режима отказа. Первый — это выходной файл, оставленный открытым в программе просмотра PDF с предыдущего запуска: SaveToFile завершится ошибкой или запишет ноль байт в зависимости от платформы, и новый вывод никогда не появится. Защита удаления файла в начале функции справляется с этим во время разработки, но в производственном конвейере безопаснее записывать во временный путь и переименовывать в случае успеха

Второй режим — несовпадение количества страниц. Поскольку CapturePage удаляет страницы из документа по мере обработки, количество, прочитанное из PageCount() до цикла, — правильная граница для итерации. Вызов PageCount() внутри цикла тоже не поможет: каждый проход удаляет одну страницу и добавляет одну, так что количество остаётся неизменным и ничего не говорит о том, сколько исходных страниц осталось. Переменная цикла в примерах служит лишь счётчиком оставшихся итераций; она никогда не используется для выбора страницы, потому что выбирать всегда нужно страницу 1 — по причине, объяснённой выше

PDF: диаграмма цикла, почему каждый проход должен вызывать SelectPage(1): захваченные страницы удаляются из дерева, а оставшиеся сдвигаются вперёд
CapturePage удаляет каждую обработанную страницу, поэтому очередь переименовывает сама себя, что делает страницу 1 единственно верным значением селектора

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