Technical Article

Предварителен преглед на печат и Device-Context извеждане в Delphi с PDFlibPas

Рендирането на PDF страница върху контекст на устройство (device context) на Windows за предварителен преглед на печат събира три координатни системи в един и същ ред код, които рядко съвпадат. PDF страницата се измерва в точки с начало в долния ляв ъгъл. Екранният DC се измерва в пиксели с начало в горния ляв ъгъл и коефициент на мащабиране по ваш избор. DC на принтера, който предварителен преглед трябва да прогнозира, измерва пикселите при разделителната способност на устройството, но поставя своето начало в ъгъла на областта за печат, а не в ъгъла на листа. Ако сгрешите някое от тези неща, предварителният преглед ще изглежда добре, докато отпечатаната страница ще излезе изместена, мащабирана или изрязана по някой от ръбовете. Обичайният симптом е формуляр с рамка, който се показва центриран при преглед, но се отпечатва с отрязани горни и леви линии, тъй като лазерният принтер не може да нанася мастило в най-външните няколко милиметра, а никой не е съобщил това на модула за предварителен преглед. losLab PDF Library (PDFlibPas) покрива целия този процес с повиквания за рендиране в контекст на устройство, конфигурационен слой за виртуален принтер и битмапи за преглед, генерирани от собствените метрики на принтера, което прави предварителния преглед точен по отношение на тези полета.

Геометрията на хартията не е геометрия на печатната област

Всеки целеви принтер се описва от два правоъгълника и отместването между тях е мястото, където възникват повечето грешки при предварителния преглед. Правоъгълникът на хартията е физическият лист. Правоъгълникът за печат е по-малката област, която печатащият механизъм може действително да достигне, ограничена от хардуерно поле, което се различава според модела на принтера и понякога според тавата за хартия. Печатащият слой на библиотеката измерва и двете. Базовият клас TPLPrinter предоставя свойствата PageWidth и PageHeight за печатаемата област, FullPageWidth и FullPageHeight за целия лист, както и PrintOffsetX с PrintOffsetY за разстоянието между техните начала, всички в пиксели на устройството при разделителната способност, отчетна от GetDPI. Една коректна функция за предварителен преглед мащабира същите тези стойности до разделителната способност на екрана, вместо да изчертава страницата в какъвто и да е правоъгълник, с който разполага контролата. Ако пропуснете тази стъпка, предварителният преглед мълчаливо ще приеме нулева граница, което е стойност, която никой реален принтер не използва.

Предварителен преглед на екрана чрез RenderPageToDC

За контрола за предварителен преглед на екрана RenderPageToDC(DPI, Page, DC) чертае страница от заредения документ директно върху произволен GDI контекст на устройство, независимо дали това е платно на TPaintBox, битмап в паметта или DC на метафайл. Аргументът DPI определя мащаба. Стойност 96 съответства приблизително на 100% изглед на класически дисплей, а удвояването й удвоява размера на рендиране.

procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);

begin

  // these three are sticky library state, not per-call parameters:

  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, той също се запазва след промяна, така че функция за експортиране, която го е увеличила, тихомълком преоразмерява всеки следващ предварителен преглед, докато нещо не го върне обратно.

Прегледите с прокрутка (scrolling) и частичните преначертавания имат специален вариант. RenderPageToDCClip приема спецификация за изрязване (clip) заедно с контекста на устройството, така че при инвалидиране на една област от прозореца се преначертава само тя, вместо да се растеризира отново цялата страница. При голямо мащабиране на страници с голям формат това е разликата между визуализатор, който следва плавно лентата за превъртане, и такъв, който закъснява и размазва изображението.

Задание за печат, съответстващо на предварителния преглед

Печатът се управлява чрез виртуален принтер. NewCustomPrinter клонира системен принтер в лична конфигурация на библиотеката, а SetupPrinter коригира този клонинг, без да засяга общосистемния DevMode: хартията се задава като настройка 1 (константа DMPAPER_*), а ориентацията като настройка 11. Предимството е пълна изолация. Дадена услуга може да отпечатва етикети в размер A4, докато принтерът по подразбиране на хоста остава на формат Letter, без да е необходимо възстановяване на настройките след това.

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);        // setting 1 = paper, DMPAPER_A4

    Pdf.SetupPrinter(Virt, 11, 1);       // setting 11 = orientation, 1 = portrait

    Opt := Pdf.PrintOptions(1, 1, 'Monthly Report');  // fit to paper, auto-rotate + center

    Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);

  finally

    Pdf.Free;

  end;

end;

Функцията PrintOptions заслужава подробно разглеждане. Тя връща дескриптор на опциите (options handle), който трябва да подадете на PrintDocument или PrintPages; това не е глобално състояние. Генерирането на опциите и последващото пропускане на дескриптора води до мълчалив отказ. Заданието се отпечатва с настройките по подразбиране и никой не забелязва проблема, докато не се изиска напасване към хартията, а извънредно голяма страница излезе изрязана. Аргументът за мащабиране на страницата е мястото, където се дефинира това поведение. Липсата на мащабиране запазва точните размери, което е важно за формуляри, чиито размери се измерват с линийка. Опцията Fit-to-paper преоразмерява всичко спрямо листа. Shrink-large-pages не променя нормалните страници и се намесва само когато дадена страница надхвърля печатаемата област, което обикновено е правилното поведение по подразбиране за смесен набор от документи. Флагът auto-rotate-and-center навигира хоризонталните страници без нужда от допълнителен код.

Приложенията, които вече управляват TPrinter чрез стандартния диалогов прозорец на VCL, могат да го предадат директно. PrintDocumentToPrinterObject и PrintPagesToPrinterObject приемат конфигурирания екземпляр на TPrinter, което запазва стандартния диалогов прозорец за печат като потребителски интерфейс за конфигуриране, докато библиотеката се грижи за рендирането на страниците. Смесването на двата подхода в един и същ код обикновено възстановява неточностите в геометрията, които се опитваме да избегнем, така че изберете само единия. Използването на виртуален принтер е подходящо за автоматизирани услуги без потребителски интерфейс; подходът с TPrinter е предназначен за интерактивни приложения.

Селективното извеждане работи по същия начин. PrintPages приема низ с диапазон от страници, така че подаването на името на виртуалния принтер, диапазона '2-5,12' и дескриптора на опциите отпечатва страници от 2 до 5 и страница 12 с правилна геометрия. Същият синтаксис се използва и за вариантите за печат във файл. Вариантите за печат във файл са практичното решение за автоматизирани среди без физически устройства: тестване за регресия на геометрията на печат на сървър за компилация, който няма инсталирани драйвери. Рендирането на един и същ документ с едни и същи опции във файлов артефакт при всяко компилиране превръща регресиите в обикновена разлика (diff) в кодовия архив, вместо в оплакване от клиент три седмици по-късно.

Битмапи за предварителен преглед със собствени метрики на принтера

Предварителен преглед, рендиран при 96 DPI спрямо предполагаем размер на страницата, дава отговор на грешен въпрос. Той показва как изглежда страницата, а не какво ще пренесе конкретният принтер върху тази хартия. 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 пиксела осигурява достатъчно ясно изображение за диалогов прозорец и поддържа минимални изисквания към паметта дори за инженерни чертежи с голям размер, при които пълно рендиране с разделителната способност на принтера от 600 DPI би заело гигабайти памет.

Запазване на избора на принтер от потребителя

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

Изборът на тава допълва възможностите за съхранение на настройките. GetPrinterBins съобщава източниците на хартия, предоставени от драйвера, което е важно за бланки, при които първата страница се взема от тавата с бланки, а останалите от обикновена хартия. Това е поведение, което потребителите очакват приложението да запомни, и отпечатването върху грешен тип хартия се възприема като грешка, дори ако всеки байт от PDF файла е напълно коректен.

Използване на една и съща библиотека за преглед и печат

Последното важно решение определя точността на извеждане. Изборът на машина за рендиране се прилага както за екрана, така и за принтера, поради което съществува изкушението да се използва бърза библиотека за предварителен преглед и прецизна за печат. Избягвайте това. Изпълнението на предварителния преглед и на заданието за печат чрез различни машини възстановява разликите в точността, които предварителният преглед се опитва да премахне, и това се вижда ясно върху хартията. Разликите между вградената библиотека, Cairo и PDFium са разгледани в рендиране на PDF с няколко машини в Delphi; изберете една и я използвайте и за двете цели.

Документи, които са твърде големи, за да бъдат заредени лесно в паметта преди печат, могат да бъдат отворени чрез пътя за директен достъп, описан в сливане, разделяне и директен достъп до големи PDF файлове, който рендира страници в контекст на устройство директно от файлов дескриптор, без да изгражда дървото на документа. Пълната справочна информация за API за печат е достъпна на продуктовата страница на losLab PDF Library за Delphi.