Пользователь заполняет PDF-форму в Delphi-viewer, жмёт Print — и лист выходит из принтера со всеми полями пустыми и страницей формата Letter, растянутой до полного A4. Обе болячки сидят в печатном цикле: PDFium Component рисует значения полей только в битмап (или запекает их во флэттен-копию), а каждой странице нужен один коэффициент масштаба с сохранением пропорций
Слой форм — второй проход, которому нужен битмап
На Windows перегрузка для контекста устройства TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options) отдаёт HDC принтера прямо в FPDF_RenderPage. Тот вызов рисует содержимое страницы плюс разметочные аннотации, когда взведён reAnnotations. Интерактивный слой форм до DC не доходит — он принадлежит form-fill-окружению PDFium и рисуется FPDF_FFLDraw, у которого единственная цель — FPDF_BITMAP. HDC-варианта нет. Два битмапных перегруза TPdf.RenderPage гонят FPDF_RenderPageBitmap и FPDF_FFLDraw по одним и тем же пикселям, когда документ открыт с FormFill в True — это дефолт. С rePrinting в опциях PDFium Component убирает подсветку полей перед форм-проходом и возвращает дефолтные цвет и альфу подсветки после, так что тонированные рамки вокруг редактируемых полей остаются на экране. rePrinting также отображается на флаг движка FPDF_PRINTING, помечающий вывод как печать, — случай, который ISO 32000-1 §12.5.3 (Table 165) разбирает битом 3 флагов аннотации: соответствующий стандарту читатель не выносит на бумагу аннотацию со сброшенным битом Print, как бы она ни выглядела на экране
Остальное решает платформа. В движке PDFium FPDF_RenderPage рисует через Windows GDI в контекст устройства и существует только в Windows-сборках, тогда как FPDF_FFLDraw рисует исключительно в битмап и на всех платформах. На Linux и прочих не-Windows целях GDI-вызова нет, поэтому HDC-перегруз обслуживается рендером битмапа — с включённым слоем форм — и отрисовкой его на канву за дескриптором. Вызов через контекст устройства, стало быть, увозит значения полей с Windows и оставляет их за бортом на Windows. Держите ветку явной в коде, общем для Delphi и Lazarus: когда у документа есть форм-окружение и его значения должны попасть на бумагу, запрашивайте битмап на любой платформе
Каким маршрутом идти печатному заданию?
Выбирайте по тому, что должно оказаться на бумаге. Живые значения форм указывают на битмапный путь, векторный вывод — на путь HDC, а флэттен-копия даёт и то и другое ценой сохранения и перезагрузки. SetPdfPrintMode действует на всю библиотеку, поэтому меняйте его между заданиями и никогда между страницами
| Маршрут | Значения форм на бумаге | Вывод | Цена |
|---|---|---|---|
RenderPage(DC, …) | Нет, на Windows | Векторный GDI; SetPdfPrintMode выбирает EMF (дефолт) или уровень PostScript | Минимальный спул, быстрее всех |
RenderPage в TBitmap, затем StretchDIBits | Да, включая несохранённые правки | Растр в запрошенном пиксельном размере | Width × Height × 4 байта на страницу плюс копия DIB |
RenderPageBanded | Да | Растр, по одной полосе за раз | Одна полоса в памяти вместо целого листа |
Флэттен-копия, затем RenderPage(DC, …) | Да, как запечённый контент страницы | Вектор | Сохранить, перезагрузить, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT) |
Полный лист A4 при 600 dpi — это примерно 4960 × 7016 пикселей устройства, а при 32 битах на пиксель — 139 197 440 байт на одну страницу; копирование в DIB ради StretchDIBits удваивает пик, тяжёлая ноша для 32-битного процесса. Для кнопки Print просмотрщика битмапный путь всё равно правильный дефолт: пользователь, набравший текст в поле и нажавший Print, ждёт это значение на бумаге, а полосы держат память в узде. Документам, покидающим приложение, — архивным выпискам и тому подобному — лучше подходит флэттен-копия: значения становятся обычным контентом страницы, а принтер получает векторный вывод
uses
System.SysUtils, System.IOUtils, PDFium, FPdfUtils; // FPdfUtils: там FLAT_PRINT
function OpenFlattenedPrintCopy(Source: TPdf; out TempName: string): TPdf;
begin
TempName := TPath.Combine(TPath.GetTempPath, TPath.GetRandomFileName + '.pdf');
if not Source.SaveAs(TempName) then
raise Exception.Create('Could not write the print copy');
Result := TPdf.Create(nil);
try
Result.FileName := TempName; // FormFill остаётся True (дефолт)
Result.Password := Source.Password; // копия сохраняет шифрование
Result.Active := True;
if not Result.Active then // неудавшаяся загрузка оставляет TPdf неактивным
raise Exception.Create('Could not reopen the print copy');
Result.GenerateFormAppearances; // пересобрать /AP из текущих /V
if not Result.FlattenAllPages(FLAT_PRINT) then
raise Exception.Create('Flattening failed');
except
Result.Free;
TFile.Delete(TempName);
raise;
end;
end;
У флэттен-маршрута есть правило порядка, которое важнее флага. FlattenAllPages запекает поток внешнего вида каждого виджета (ISO 32000-1 §12.5.5) в контент страницы, а поток внешнего вида — это что угодно из написанного последним производителем; значение поля в /V роли не играет. Форма, заполненная инструментом, который так и не перегенерировал свои внешние виды, флэттенится в старый текст или в пустоту, поэтому первым бежит GenerateFormAppearances: он пересобирает внешние виды текстовых полей из текущих значений и синхронизирует состояния /AS чекбоксов и радиокнопок, а для работы ему нужно, чтобы при открытии документа FormFill был True. Делайте всё это на копии: флэттенинг переписывает страницы того TPdf, на котором запущен, а открытому viewer'у нужны редактируемые поля. Копия идёт через векторную ветку печатной процедуры ниже со слоем форм выключенным; освободите её и удалите временный файл, когда задание закончится
Вписываем каждую страницу в печатную область
Масштабируйте один раз — по меньшему из двух коэффициентов — и центрируйте результат. Растягивание в Printer.PageWidth × Printer.PageHeight искажает каждую страницу, чья форма отличается от бумаги: Letter (612 × 792 пунктов) на A4 или альбомная страница на портретном листе. Со стороны принтера это печатная область в пикселях устройства (HORZRES на VERTRES), уже без полей драйвера. Со стороны PDF TPdf.PageWidth и PageHeight уже отчитывают страницу так, как она показывается: портретный A4, сохранённый с /Rotate 90, возвращается как 842 × 595 пунктов. ISO 32000-1 §7.7.3.3 (Table 30) велит поворачивать страницу по /Rotate при показе и печати, PDFium применяет его внутри вызова рендера, а аргумент Rotation у RenderPage — только дополнительный поворот, который viewer добавляет, когда пользователь вращает вид. Передадите туда собственный PageRotation страницы — и страница повернётся дважды. Меняйте ширину с высотой местами для вписывания только когда этот дополнительный поворот — 90 или 270 градусов, та же пара правил, что правит вписыванием миниатюр и зумом при повороте; процедура ниже расширяет базовый цикл печати PDF-документов с PDFium Component веткой значений форм и путём отмены, выбрасывающим задание
uses
Winapi.Windows, System.SysUtils, Vcl.Graphics, Vcl.Forms, Vcl.Printers, PDFium;
procedure FitRect(PageW, PageH: Double; Swap: Boolean;
AreaW, AreaH: Integer; out X, Y, W, H: Integer);
var
T, Scale: Double;
begin
if Swap then
begin
T := PageW; PageW := PageH; PageH := T;
end;
if (PageW <= 0) or (PageH <= 0) then
begin
X := 0; Y := 0; W := AreaW; H := AreaH;
Exit;
end;
Scale := AreaW / PageW;
if AreaH / PageH < Scale then
Scale := AreaH / PageH;
W := Round(PageW * Scale);
H := Round(PageH * Scale);
X := (AreaW - W) div 2;
Y := (AreaH - H) div 2;
end;
procedure BlitBitmap(DC: HDC; Bitmap: TBitmap; X, Y: Integer);
var
InfoSize, ImageSize: DWORD;
Info: PBitmapInfo;
Bits: Pointer;
begin
GetDIBSizes(Bitmap.Handle, InfoSize, ImageSize);
Info := AllocMem(InfoSize);
try
Bits := AllocMem(ImageSize);
try
GetDIB(Bitmap.Handle, 0, Info^, Bits^);
StretchDIBits(DC, X, Y, Bitmap.Width, Bitmap.Height, 0, 0,
Bitmap.Width, Bitmap.Height, Bits, Info^, DIB_RGB_COLORS, SRCCOPY);
finally
FreeMem(Bits);
end;
finally
FreeMem(Info);
end;
end;
function PrintPages(Pdf: TPdf; FromPage, ToPage: Integer; FormLayer: Boolean;
ExtraRotation: TRotation; var Cancel: Boolean): Boolean;
const
Opts: TRenderOptions = [reAnnotations, rePrinting];
var
Page, X, Y, W, H: Integer;
Bitmap: TBitmap;
begin
Result := False;
Printer.BeginDoc;
try
for Page := FromPage to ToPage do
begin
if Page > FromPage then
Printer.NewPage;
Pdf.PageNumber := Page;
FitRect(Pdf.PageWidth, Pdf.PageHeight, ExtraRotation in [ro90, ro270],
Printer.PageWidth, Printer.PageHeight, X, Y, W, H);
if FormLayer then
begin
Bitmap := Pdf.RenderPage(0, 0, W, H, ExtraRotation, Opts);
if Bitmap <> nil then // nil при вырожденном размере страницы
try
BlitBitmap(Printer.Canvas.Handle, Bitmap, X, Y);
finally
Bitmap.Free;
end;
end
else
Pdf.RenderPage(Printer.Canvas.Handle, X, Y, W, H, ExtraRotation, Opts);
Application.ProcessMessages; // даёт кнопке Cancel взвести флаг
if Cancel then
Break;
end;
Result := not Cancel;
finally
if Result then
Printer.EndDoc // отдаёт задание спулеру
else
Printer.Abort; // отмена или исключение: не печатается ничего
end;
end;
Viewer вызывает это с Pdf.FormFill в роли FormLayer; флэттен-копия передаёт False. EndDoc — коммит, а не шаг уборки. После BeginDoc Printer.EndDoc отдаёт спулеру всё отрендеренное к этому моменту как валидное задание, поэтому безусловный EndDoc в finally превращает отменённую печать в полупечатную. Процедура решает в finally по флагу успеха: EndDoc после последней страницы, Abort после отмены или исключения. Выход из цикла требует той же аккуратности: Delphi и Free Pascal оба вычисляют конечное значение for-цикла один раз, на входе. Обнуление ToPage или счётчика копий из обработчика Cancel не остановит уже бегущий цикл; проверяйте флаг и делайте Break, а с вложенными циклами копий и раскладок выходите из каждого уровня. И поскольку Application.ProcessMessages позволяет сработать и горячим клавишам меню, гасите Open, Save и Print, пока задание бежит
Держим растровый путь в пределах памяти
TPdf.RenderPageBanded рендерит страницу горизонтальными полосами выбранной вами высоты и передаёт каждую полосу в callback. Каждая полоса — срез полноразмерной страницы, ничего не перемасштабируется, и несёт те же значения полей, что и полностраничный битмап. Полосами по 512 строк страница шириной 4960 пикселей требует около 10 МБ на полосу вместо 139 МБ на лист — примерно одна четырнадцатая памяти. TPdfBandCallback — указатель на метод, получающий индекс полосы, её верхнее смещение в пикселях страницы и битмап и возвращающий True для продолжения; RenderPageBanded возвращает число полос, 0 при неудаче
type
TBandPrinter = class
private
FDC: HDC;
FLeft, FTop: Integer;
public
constructor Create(DC: HDC; Left, Top: Integer);
function PrintBand(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean;
end;
constructor TBandPrinter.Create(DC: HDC; Left, Top: Integer);
begin
inherited Create;
FDC := DC;
FLeft := Left;
FTop := Top;
end;
function TBandPrinter.PrintBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean;
begin
BlitBitmap(FDC, Bitmap, FLeft, FTop + BandTopY);
Result := True; // False останавливает оставшиеся полосы
end;
// вызывается из цикла страниц вместо ветки полностраничного битмапа
procedure PrintPageBanded(Pdf: TPdf; Page, X, Y, W, H: Integer;
ExtraRotation: TRotation);
var
Bands: TBandPrinter;
begin
Bands := TBandPrinter.Create(Printer.Canvas.Handle, X, Y);
try
if Pdf.RenderPageBanded(W, H, 512, Bands.PrintBand, ExtraRotation,
[reAnnotations, rePrinting]) = 0 then
raise Exception.CreateFmt('Page %d could not be rendered', [Page]);
finally
Bands.Free;
end;
end;
Что хозяину делать с ViewerPreferences и битами разрешений?
Читайте их и действуйте по ним в собственном коде; PDFium о них сообщает и рисует страницу независимо. Производитель может сложить печатные подсказки в словарь /ViewerPreferences каталога (ISO 32000-1 §12.2, Table 150), и TPdf выставляет наружу те, что формируют диалог печати:
PrintScaling(Boolean) равен False, когда файл говорит/PrintScaling /None, что значит печатать в натуральную величину, и True при/AppDefaultили отсутствующей записи. Стандарт хочет, чтобы это уважалось, даже если диалог печати не показываетсяPrintPaperHandling(TPrintPaperHandling) несёт/DuplexкакphSimplex,phDuplexFlipShortEdge,phDuplexFlipLongEdgeилиphUndefined.SetPdfPrintPaperHandlingDevModeзаписывает его вDEVMODE.dmDuplexи ставитDM_DUPLEX; дляphUndefinedвозвращает False и оставляет запись в покое, так что дефолт драйвера выживаетPrintCopies(Integer) — это/NumCopies, число копий для диалога
Биты разрешений следуют тому же правилу. Для стандартного security handler ISO 32000-1 §7.6.3.2 (Table 22) назначает бит 3 /P печати, а для обработчиков revision 3 и старше бит 12 — печати в полной точности; при взведённом бите 3 и сброшенном 12 печать ограничена низкоуровневым представлением внешнего вида, возможно, с деградацией. TPdf.GrantedPermissions декодирует маску в permPrint и permPrintHighQuality, а HasPermission проверяет одно значение; движок рисует что попросите, так что принуждение — работа хозяина, обычно неактивная кнопка Print. Когда разрешён только permPrint, чтение, согласующееся со стандартом, — битмапный путь при умеренном разрешении, а не векторный или PostScript-вывод, который позволяет полная выдача. Биты описывают документ, как он открыт: файл, отпертый владельцеским паролем, отчитывает полный доступ, а UserHasPermission отвечает только за биты пользовательского уровня
uses
Winapi.Windows, System.SysUtils, Vcl.Dialogs, Vcl.Printers, PDFium;
procedure PreparePrintJob(Pdf: TPdf; Dialog: TPrintDialog;
out ActualSize, HighQuality: Boolean);
var
Device, Driver, Port: array[0..MAX_PATH] of Char;
DevModeHandle: THandle;
DevMode: PDeviceMode;
begin
if not Pdf.HasPermission(permPrint) then
raise Exception.Create('This document does not permit printing');
HighQuality := Pdf.HasPermission(permPrintHighQuality);
ActualSize := not Pdf.PrintScaling; // /PrintScaling /None
Printer.GetPrinter(Device, Driver, Port, DevModeHandle);
if DevModeHandle <> 0 then
begin
DevMode := GlobalLock(DevModeHandle);
try
SetPdfPrintPaperHandlingDevMode(DevMode^, Pdf.PrintPaperHandling);
finally
GlobalUnlock(DevModeHandle);
end;
Printer.SetPrinter(Device, Driver, Port, DevModeHandle); // диалог стартует от него
end;
Dialog.MinPage := 1;
Dialog.MaxPage := Pdf.PageCount;
Dialog.Options := Dialog.Options + [poPageNums];
if Pdf.PrintCopies > 1 then
Dialog.Copies := Pdf.PrintCopies;
end;
// используется вместо FitRect при ActualSize = True
procedure ActualSizeRect(Pdf: TPdf; out X, Y, W, H: Integer);
begin
// 72 пункта на дюйм со стороны PDF, пиксели устройства на дюйм у принтера
W := Round(Pdf.PageWidth / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSX));
H := Round(Pdf.PageHeight / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSY));
X := (Printer.PageWidth - W) div 2; // минус, когда страница крупнее
Y := (Printer.PageHeight - H) div 2; // печатной области: края обрезаются
end;
Will-print и did-print действия на настоящей границе печати
Документные печатные скрипты исполняются, когда их исполняет хозяин. Словарь дополнительных действий каталога (ISO 32000-1 §12.6.3, Table 197) определяет WP — JavaScript-действие перед печатью — и DP, исполняемое после. PDFium Component выставляет оба через ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) и dlaDidPrint и держит их вне вызовов рендера: миниатюры, снимки и конвертации тоже рендерят, и ни одна из них не должна выглядеть для документного скрипта как пользовательская печать. Порядок — ваша забота. Will-print скрипт, штампующий дату или заполняющий поле «напечатано кем», меняет значения форм, поэтому гоняйте его до рендера первой страницы, а на флэттен-маршруте — до сохранения копии; did-print стреляйте только после того, как EndDoc закоммитил задание. Вызов возвращает False с сообщением, когда форм-окружение отсутствует или его отказывает LifecycleActionPolicy; для недоверенных файлов lapDisabled выключает документные действия. Более широкая картина скриптинга — в запуске AcroForm JavaScript с событиями хоста
На Windows задание, обязанное нести значения форм, живёт на битмапной ветке: RenderPage в TBitmap при обычных разрешениях, RenderPageBanded при 600 dpi и выше. Перегруз через контекст устройства у PDFium Component придержите для флэттен-копий и документов без полей, где его векторный вывод — лучший результат