Технічна стаття

Попередній перегляд друку та виведення на контекст пристрою у Delphi з PDF Library for Delphi

Відображення сторінки PDF на контекст пристрою Windows для попереднього перегляду друку зводить три системи координат в один рядок коду, і вони рідко узгоджуються між собою. Сторінка PDF вимірюється в пунктах з початком координат у лівому нижньому куті. Екранний DC вимірюється в пікселях з початком у лівому верхньому куті та коефіцієнтом масштабування, який ви самі обираєте. DC принтера, той самий, який попередній перегляд має передбачити, вимірює пікселі в роздільній здатності пристрою, але розташовує свій початок координат у куті друкованої області, а не в куті аркуша. Помилившись хоч в одному з цих пунктів, ви отримаєте попередній перегляд, що виглядає нормально, тоді як надрукована сторінка виходить зсунутою, масштабованою або обрізаною по краю. Звичний симптом — бланк із рамкою, що в перегляді виглядає відцентрованим, а друкується з обрізаними верхньою та лівою лінійками, бо лазерний принтер не може покласти фарбу на кілька зовнішніх міліметрів, а перегляду про це ніхто не сказав. losLab PDF Library (PDF Library for Delphi) покриває весь цей шлях викликами рендерингу на контекст пристрою, шаром налаштування віртуального принтера та бітмапами попереднього перегляду, згенерованими за власними метриками принтера, — і саме це робить перегляд чесним щодо тих полів

Геометрія паперу — не геометрія друку

Будь-яку ціль друку описують два прямокутники, і саме зсув між ними — те місце, де живе більшість помилок попереднього перегляду. Прямокутник паперу — це фізичний аркуш. Прямокутник друку — менша область, якої механізм друку насправді може досягти, звужена на апаратне поле, що відрізняється для кожної моделі принтера, а іноді й для кожного лотка. Шар друку бібліотеки вимірює обидва. Клас TPLPrinter, що лежить в основі, надає PageWidth та PageHeight для області друку, FullPageWidth та FullPageHeight для повного аркуша, а також PrintOffsetX разом із PrintOffsetY для проміжку між їхніми початками координат — усе в пікселях пристрою на роздільній здатності, яку повідомляє GetDPI. Чесний попередній перегляд масштабує ці самі числа до роздільної здатності екрана, замість того щоб малювати сторінку в будь-якому прямокутнику, який випадково має елемент керування. Пропустіть цей крок — і перегляд мовчки припустить нульове поле, а це те єдине значення, яке не використовує жоден справжній принтер

Діаграма PDF Library for Delphi: повний паперовий аркуш проти меншого придатного для друку прямокутника, з PrintOffsetX і PrintOffsetY, що позначають апаратне поле між їхніми початками
Прямокутник паперу — це фізичний аркуш, тоді як прямокутник друку — те, чого здатен досягти рушій друку, і саме в розриві між їхніми початками координат живе більшість багів попереднього перегляду

Екранний перегляд через RenderPageToDC

Для елемента керування екранним попереднім переглядом RenderPageToDC(DPI, Page, DC) малює сторінку завантаженого документа прямо на будь-який контекст пристрою GDI, чи то полотно TPaintBox, позаекранний бітмап, чи DC метафайлу. Аргумент DPI встановлює масштаб. 96 приблизно відповідає перегляду 100% на класичному дисплеї, а подвоєння цього значення подвоює розмір відрендереного зображення

procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
  // ці три — липкий стан бібліотеки, а не параметри окремого виклику:
  FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
  FPdf.SetRenderDCErasePage(1);
  FPdf.SetRenderCropType(0);
  FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;

Пастка в тому, що шлях рендерингу на DC керується липким станом бібліотеки, а не параметрами окремого виклику. SetRenderDCOffset, SetRenderDCErasePage та SetRenderCropType — кожен із них зберігається, доки щось його не змінить, тож цикл побудови мініатюр, що запускається після того, як користувач підлаштував масштабований перегляд, успадковує будь-який зсув чи обрізання, залишені попереднім шляхом коду. Симптом — перегляд, що зсувається лише в конкретних послідовностях навігації, а це чи не найгірший з можливих типів помилок для відтворення. Встановити весь потрібний стан на початку обробника малювання, як вище, не коштує нічого і прибирає цілий клас проблем. Поруч ховається другий множник. Ефективна роздільна здатність виводу — це масштаб рендерингу, помножений на аргумент DPI, і хоча SetRenderScale за замовчуванням дорівнює 1.0, він так само зберігається після зміни, тож функція експорту, яка колись його підняла, тихцем перемасштабовує кожен наступний перегляд, доки щось не поверне його назад

Для переглядачів із прокручуванням і частковим перемальовуванням є окремий варіант. RenderPageToDCClip приймає специфікацію обрізання разом із контекстом пристрою, тож інвалідація однієї смуги вікна перемальовує лише цю смугу, замість повторної растеризації всієї сторінки. При високому масштабі на сторінках великого формату це різниця між переглядачем, що встигає за смугою прокручування, і тим, що розмазується позаду неї

Завдання друку, що збігається з переглядом

Бік друку працює через віртуальний принтер. NewCustomPrinter клонує системний принтер у приватну для бібліотеки конфігурацію, а SetupPrinter налаштовує цей клон, не торкаючись загальносистемного DevMode: папір передається як налаштування 1 (константа DMPAPER_*), а орієнтація — як налаштування 11. Виграш — ізоляція. Сервіс може друкувати етикетки A4, поки типовий принтер хоста лишається на Letter, і після цього нічого не треба відновлювати

PDF Library for Delphi: потік від системного принтера за замовчуванням через NewCustomPrinter і SetupPrinter до ізольованого завдання друку, що ніколи не торкається загальносистемного DevMode
SetupPrinter перенацілює приватний для бібліотеки клон, тож служба може друкувати A4, тоді як типовий принтер хоста зберігає свій Letter DevMode недоторканим
var
  Pdf: TPDFlib;
  Virt: WideString;
  Opt: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Virt := Pdf.NewCustomPrinter(Pdf.GetDefaultPrinterName);
    Pdf.SetupPrinter(Virt, 1, 9);        // налаштування 1 = папір, DMPAPER_A4
    Pdf.SetupPrinter(Virt, 11, 1);       // налаштування 11 = орієнтація, 1 = книжкова
    Opt := Pdf.PrintOptions(1, 1, 'Monthly Report');  // підігнати під папір, автоповорот + центрування
    Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
  finally
    Pdf.Free;
  end;
end;

PrintOptions заслуговує на уважне прочитання. Вона повертає дескриптор опцій, який треба передати в PrintDocument або PrintPages; це не оточуючий стан. Побудувати опції й потім забути передати дескриптор — це збій, що стається тихо. Завдання друкується з типовими налаштуваннями, і ніхто цього не помічає, доки не очікувалася політика «підігнати під папір», а замість цього завелика сторінка вийшла обрізаною. Аргумент масштабування сторінки — саме те місце, де живе ця політика. Без масштабування зберігається точність розмірів, що важить для бланків, які потім вимірюють лінійкою. «Підігнати під папір» перемасштабовує все під аркуш. «Зменшувати великі сторінки» лишає звичайні сторінки без змін і втручається лише тоді, коли сторінка перевищує область друку, і це зазвичай правильне значення за замовчуванням для змішаного набору документів. Прапорець автоповороту й центрування обробляє альбомні сторінки без другого шляху коду

Застосунки, які вже керують TPrinter через діалоговий потік VCL, можуть передати його напряму. PrintDocumentToPrinterObject та PrintPagesToPrinterObject приймають налаштований екземпляр TPrinter, що лишає стандартний діалог друку поверхнею налаштування для користувача, поки бібліотека бере на себе рендеринг сторінок. Змішування цих двох підходів в одному шляху коду має тенденцію повертати той самий геометричний зсув, який уся інша робота тут мала на меті знищити, тож оберіть один із них. Шлях віртуального принтера пасує сервісам без нагляду; шлях TPrinter пасує інтерактивним застосункам

Вибірковий вивід працює так само. PrintPages приймає рядок діапазону, тож передавши ім'я віртуального принтера, '2-5,12' та дескриптор опцій, ви надрукуєте сторінки з 2 по 5 і сторінку 12 з непорушеним геометричним контрактом, і той самий синтаксис керує варіантами друку у файл. Ці файлові варіанти — практична відповідь для середовища без нагляду й без фізичного пристрою: регресійне тестування геометрії друку на сервері збірки, де взагалі немає черги драйверів. Рендерте той самий документ через ті самі опції у файловий артефакт на кожній збірці, і регресія геометрії перетворюється на diff замість скарги клієнта через три тижні

Бітмапи попереднього перегляду за власними метриками принтера

Перегляд, відрендерений на 96 DPI проти якогось припущеного розміру сторінки, відповідає не на те питання. Він показує, як виглядає сторінка, а не те, що саме цей принтер покладе на цей папір. GetPrintPreviewBitmapToString закриває цей розрив, будуючи перегляд із того самого кастомного принтера й того самого дескриптора опцій, що й майбутнє завдання, тож розмір паперу, орієнтація, політика масштабування, поворот і апаратний зсув — усе це живить бітмап. Те, що повертається, — це саме те, що покаже аркуш

PDF Library for Delphi: протиставлення екранного прев'ю з припущеним розміром сторінки, що спотворює поля, та бітмапу, вірного принтеру, побудованого з власних дескрипторів принтера й опцій завдання
GetPrintPreviewBitmapToString малює попередній перегляд із того самого хендла власного принтера та опцій, що й фінальне завдання, тож бітовий образ показує поля й обертання, які аркуш реально отримає
procedure ShowPrinterTruePreview(Pdf: TPDFlib; const Virt: WideString; Opt: Integer);
var
  Data: AnsiString;
  Strm: TMemoryStream;
  Bmp: TBitmap;
begin
  Data := Pdf.GetPrintPreviewBitmapToString(Virt, 1, Opt, 1200, 0);
  Strm := TMemoryStream.Create;
  try
    Strm.WriteBuffer(PAnsiChar(Data)^, Length(Data));
    Strm.Position := 0;
    Bmp := TBitmap.Create;
    try
      Bmp.LoadFromStream(Strm);
      PreviewImage.Picture.Assign(Bmp);
    finally
      Bmp.Free;
    end;
  finally
    Strm.Free;
  end;
end;

Аргумент MaxDimension обмежує довшу сторону бітмапа. 1200 пікселів лишається чітким для діалогу попереднього перегляду й тримає пам'ять помірною навіть для інженерних креслень формату E, де рендер на повній роздільній здатності принтера в 600 DPI розрісся б до гігабайтів

Запам'ятовування вибору принтера користувача

Діалоги друку, що забувають свої налаштування між сеансами, самі собою породжують звернення в підтримку. Пара DevMode, GetPrinterDevModeToString та SetPrinterDevModeFromString, серіалізує повну конфігурацію драйвера принтера в непрозорий рядок, який можна сховати в налаштуваннях користувача й відновити наступного сеансу, включно зі специфічними для драйвера опціями, які жоден загальний API навіть не намагається моделювати. Зберігайте принтер за іменем із GetPrinterNames, ніколи за індексом у списку. Порядок індексів змінюється щоразу, коли принтер додають чи видаляють, тож збережений індекс тихцем вкаже на не той пристрій, щойно список зсунеться. GetDefaultPrinterName покриває запасний варіант, коли запам'ятований пристрій зник повністю

Вибір лотка завершує історію зі збереженням стану. GetPrinterBins повідомляє джерела паперу, які надає драйвер, і це важить для робочих процесів із фірмовими бланками, де перша сторінка бере папір із лотка з бланком, а решта — зі звичайного паперу. Це та політика, яку користувачі очікують від застосунку запам'ятати поряд з усім іншим, і завдання друку, що потрапляє не на той папір, читається як помилка, навіть якщо кожен байт PDF був правильним

Тримайте один рушій для перегляду й друку

Одне останнє рішення тихо керує точністю відтворення. Вибір рушія рендерингу застосовується і до екрана, і до принтера як цілей, тож виникає спокуса переглядати швидким рушієм, а друкувати точним. Опирайтеся цій спокусі. Проведення перегляду й завдання через різні рушії заново вносить саме той зсув точності, який чесний перед принтером перегляд мав на меті прибрати, причому робить це так, що проявляється лише на папері. Компроміси між вбудованим рушієм, Cairo та PDFium зважено в статті багаторушійний рендеринг PDF у Delphi; оберіть один і використовуйте його з обох боків

Документи, які надто великі, щоб комфортно завантажити їх перед друком, можна відкрити через шлях прямого доступу, описаний у статті злиття, розділення й прямий доступ до великих PDF, що рендерить сторінки на контекст пристрою прямо з дескриптора файлу, не будуючи дерево документа. Повний довідник API друку — на сторінці продукту losLab PDF Library for Delphi