Размеры страницы PDF фиксируются в момент ее создания, поэтому нельзя просто масштабировать содержимое на месте так же, как размер изображения. Модель библиотеки, которая делает сжатие практичным, это захват и перерисовка: извлечение содержимого каждой страницы из документа в дескриптор, создание новой пустой страницы с исходным размером носителя, а затем отрисовка захваченного содержимого обратно в уменьшенном ограничивающем прямоугольнике. Окружающее пустое пространство становится полем. Например, при масштабе 70% на странице A4, 15% ширины приходится на каждую сторону и такая же доля сверху и снизу, что в точности соответствует арифметике полей, приведенной ниже
Как работает CapturePage
CapturePage принимает номер страницы, переводит содержимое этой страницы в объект захвата в памяти и удаляет страницу из дерева страниц документа. Это удаление является преднамеренным и является причиной того, что цикл всегда выбирает страницу 1 независимо от индекса итерации: как только страница 1 захвачена и удалена, то, что было страницей 2, становится новой странице 1 и так далее. Если вы будете увеличивать селектор страниц вместе со счетчиком цикла, вы будете пропускать каждую вторую страницу и получите половину ожидаемого результата
Дескриптор захвата, возвращаемый 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, добавленный в проект из 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 или повторное сохранение со сжатием потока, а не геометрическое масштабирование
Цифра в 70% также не является жестким ограничением. Любое значение от 0.0 до 1.0 работает, а значения выше 1.0 увеличивают содержимое за пределы исходной границы страницы, которая обрезается по краю медиабокса, если вы также не увеличите размеры страницы. Документы смешанного размера обрабатываются естественно, потому что PageWidth и PageHeight запрашиваются для каждой страницы до расчета полей, поэтому документ, в котором нечетные страницы имеют формат A4, а четные — A3, будет производить правильно отцентрированный вывод на каждом размере страницы без какой-либо специальной обработки
Где что-то может пойти не так
На практике встречаются два режима отказа. Первый — это выходной файл, оставленный открытым в программе просмотра PDF с предыдущего запуска: SaveToFile завершится ошибкой или запишет ноль байт в зависимости от платформы, и новый вывод никогда не появится. Защита удаления файла в начале функции справляется с этим во время разработки, но в производственном конвейере безопаснее записывать во временный путь и переименовывать в случае успеха
Второй — это несовпадение количества страниц. Поскольку CapturePage удаляет страницы из документа по мере их обработки, счетчик, который вы читаете из PageCount() перед циклом, является правильной границей для итерации. Вызов PageCount() внутри цикла будет возвращать уменьшающееся число при каждом проходе и досрочно завершит цикл, оставив последние страницы необработанными. Переменная цикла в примерах служит только в качестве счетчика оставшихся итераций; она никогда не используется для выбора страницы, потому что страница для выбора всегда 1 по причине, объясненной ранее
Показанные здесь вызовы для управления страницами, включая CapturePage, DrawCapturedPage и SetPageDimensions, являются частью библиотеки losLab PDF для Delphi, C#, VB.NET и C++