Technical Article

Печат на PDF документи с PDFium VCL в Delphi

Координатите в PDF са в точки (points), а тези на принтера са в хардуерни единици на устройството (device units). Двете системи нямат нищо общо помежду си, докато не ги преобразувате съзнателно. Това разминаване е причината за повечето проблеми с лошо качество при печат в Delphi приложения: кодът изпраща правилния файл, но страницата излиза изрязана, разтегната или празна. PDFium VCL управлява отлично рендирането, а системната част на принтера е стандартен VCL. Двете се съчетават лесно с малко количество код, след като разберете очакванията на всяка страна.

Как работи веригата „рендиране преди печатâ€?/h2>

PDFium VCL не комуникира директно с принтери. Моделът на работа е следният: рендирате страницата в TBitmap с желаната от вас разделителна способност, след което прехвърляте този битмап върху платното на принтера чрез StretchDIBits. Методът TPdf.RenderPage връща битмап, собственост на извикващата програма, така че вие контролирате размерите в пиксели. Подайте [rePrinting] в списъка с опции и PDFium ще превключи процеса на рендиране към такъв, който изключва специфичните за екран ефекти (като LCD субпикселно хинтиране) и управлява правилно MediaBox на страницата за целите на печата. Ако пропуснете rePrinting, това, което изпращате към принтера, е екранно рендиране, което изглежда добре на монитор, но води до по-размити очертания при печат с висока DPI разделителна способност, тъй като решенията за хинтиране на 96 DPI екрани не са подходящи за печат при 300 или 600 DPI.

Свойството TPdf.Active е единствената проверка, която трябва да направите, преди да работите с което и да е свойство на страницата. Компонентът потиска мълчаливо грешките при зареждане: задаването на Active := True за повреден или защитен с парола файл не генерира изключение, а просто оставя Active на стойност False. Винаги правете тази проверка след присвояването. Четенето на PageCount или PageWidth за неактивен документ връща нула, което води до липса на действие без никакви съобщения за грешка, което е много трудно за диагностициране, след като задачата достигне до опашката за печат (spooler).

Минимален цикъл за печат

Най-простият работещ случай зарежда файл, отваря задача за печат, преминава през страниците и затваря задачата. Единственият деликатен детайл е, че Printer.NewPage не трябва да се извиква преди първата страница, откъдето идва и необходимостта от флага FirstPage. Прехвърлянето чрез StretchDIBits преминава през GetDIBSizes и GetDIB, за да извлече хардуерно независими битове от дескриптора на битмапа (bitmap handle), след което ги изрисува върху платното на принтера в пълния размер на страницата:

procedure PrintPdfFile(const FileName: string);
var
  Pdf: TPdf;
  I: Integer;
  Bitmap: TBitmap;
  InfoHeaderSize, ImageSize: DWORD;
  InfoHeader: PBitmapInfo;
  Image: Pointer;
  FirstPage: Boolean;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;  // load failed silently; bail out

    Printer.Title := Pdf.Title;
    Printer.BeginDoc;
    try
      FirstPage := True;
      for I := 1 to Pdf.PageCount do
      begin
        if FirstPage then
          FirstPage := False
        else
          Printer.NewPage;

        Pdf.PageNumber := I;

        // Render at printer resolution; rePrinting adjusts the render path
        Bitmap := Pdf.RenderPage(
          0, 0,
          Printer.PageWidth,
          Printer.PageHeight,
          ro0,
          [rePrinting]
        );
        try
          GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
          InfoHeader := AllocMem(InfoHeaderSize);
          try
            Image := AllocMem(ImageSize);
            try
              GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
              StretchDIBits(
                Printer.Canvas.Handle,
                0, 0, Printer.PageWidth, Printer.PageHeight,
                0, 0, Bitmap.Width, Bitmap.Height,
                Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
              );
            finally
              FreeMem(Image);
            end;
          finally
            FreeMem(InfoHeader);
          end;
        finally
          Bitmap.Free;
        end;
      end;
    finally
      Printer.EndDoc;
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Предаването на Printer.PageWidth и Printer.PageHeight как размери на битмапа означава, че рендирате в естествения размер на пикселите на принтера, което вече отчита DPI разделителната способност на устройството. Извикването на StretchDIBits след това картографира тези пиксели 1:1 върху страницата. Това осигурява възможно най-доброто качество без изрична DPI аритметика, но работи само когато PDF страницата и физическата хартия са с еднакъв размер. Когато те се различават, се нуждаете от изрично мащабиране.

Мащабиране, когато размерите на страницата и хартията се различават

PDF страница във формат A4 Portrait (портрет) не съответства автоматично на принтер, зареден с хартия US Letter, а хоризонтална страница (Landscape), подадена към принтер с портретна ориентация, ще бъде изрязана. Стандартният подход е да се изчисли унифициран коефициент на мащабиране въз основа на съотношението между пикселите на принтера и PDF точките, след което той да се приложи към двата размера, запазвайки пропорцията (aspect ratio). Свойствата Pdf.PageWidth и Pdf.PageHeight показват текущите размери на страницата в точки (points), където една точка е 1/72 от инча. Умножаването по целевото DPI и разделянето на 72 ги превръща в пиксели за съответната разделителна способност. Използвайте по-малката стойност (Min) от съотношенията по X и Y, за да получите най-големия мащаб, който все още се вписва в печатната област:

// Fit PDF page to printable area, preserving aspect ratio
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // target render resolution
  Pdf.PageNumber := PageIndex;

  ScaleX := Printer.PageWidth  / (Pdf.PageWidth  * Dpi / 72);
  ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
  Scale  := Min(ScaleX, ScaleY);

  // Clamp to 1.0 for shrink-to-fit only (no enlargement)
  if Scale > 1.0 then Scale := 1.0;

  DestWidth  := Round(Pdf.PageWidth  * Dpi / 72 * Scale);
  DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);

  Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
    [rePrinting, reAnnotations]);
  // ... transfer with StretchDIBits as above
end;

Рендирането при Dpi = 300 е подходящо за повечето офис принтери. При 600 DPI битмапът за единична страница формат A4 достига приблизително 34 мегапиксела, което е около 100 MB как 32-битов битмап; подобрението в качеството за обикновени текстови документи е минимално, а консумацията на памет на страница е значителна. Запазете 600 DPI за печатници или за чертежи с много векторни детайли, където разликата наистина има значение.

Флагът reAnnotations във втория кодов блок е независим от rePrinting. Включете го, когато потребителят очаква печати, маркирания и полета за коментари да се появят на хартия. Пропуснете го за съдържание без анотации. Двата флага могат да се комбинират свободно.

Завъртане на страницата

PDFium съхранява завъртането на страницата в PDF документа като запис /Rotate, достъпен чрез Pdf.PageRotation, който връща стойност от тип TRotation (ro0, ro90, ro180, ro270). Координатната система на принтера обръща завъртанията на 90 и 270 градуса спрямо екрана. Ако подадете оригиналната стойност на PageRotation директно към RenderPage без никаква корекция, хоризонталните страници в портретен документ ще се отпечатат наобратно на повечето драйвери за принтери под Windows. Решението е обикновена размяна преди извикването за рендиране: съпоставете ro90 към ro270 и ro270 обратно към ro90, оставяйки ro0 и ro180 непроменени.

Тествайте това поведение на вашия конкретен целеви принтер преди пускането на софтуера. Поведението на драйверите по отношение на завъртането не е еднакво при различните производители, като някои драйвери прилагат собствена корекция за завъртане на GDI ниво. Ако забележите двойно завъртане, премахнете размяната на стойностите; ако не виждате никаква корекция, я добавете. Документ със смесена ориентация (сменящи се портретни и хоризонтални страници) е най-бързият начин да откриете тези проблеми по време на тестове.

Управление на паметта при големи задачи за печат

Всяко извикване на RenderPage заделя нов обект TBitmap, който е собственост на извикващата програма и трябва да бъде освободен от нея. В горния цикъл блокът try/finally Bitmap.Free управлява това правилно за всяка отделна страница. Не натрупвайте битмапи на различни страници в паметта: рендирането на 200-страничен документ при 300 DPI би консумирало гигабайти памет още преди първата страница да е достигнала до опашката за печат. Освобождавайте всеки битмап, преди да преминете към следващата страница.

Двойката функции AllocMem / FreeMem вътре в блока за прехвърляне следва същото правило. Функцията GetDIBSizes ви казва колко памет изискват DIB заглавието и пикселните данни. Вие ги заделяте, попълвате, изрисувате и освобождавате в рамките на обработката на една страница. Изтичането на памет от който и да е от блоковете ще доведе до изчерпване на системната памет при документи с дължина над няколко десетки страници.

Ако трябва да изпълнявате задачи за печат във фонова нишка (background thread), дръжте TPdf и всички VCL извиквания за печат в същата нишка. Самият компонент TPdf не е нишково безопасен между екземпляри, които споделят глобалното състояние на PDFium DLL библиотеката. Най-сигурният модел е по един екземпляр на TPdf за всяка нишка, като всяка зарежда свое собствено копие на файла.

Представеният тук API за рендиране и обработка на документи е част от PDFium VCL Component за Delphi и C++Builder.