Техническа статия

Отпечатване на PDF документи с PDFium Component в Delphi

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

Как работи тръбопроводът рендирай-след-това-отпечатай

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

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

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

Най-простият работещ случай зарежда файл, отваря задача за печат, итерира страниците и затваря. Единственият сложен детайл е, че Printer.NewPage не трябва да се извиква преди първата страница, оттук и флагът FirstPage. Прехвърлянето със StretchDIBits преминава през GetDIBSizes и GetDIB, за да изтегли независими от устройството (device-independent) битове от манипулатора (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;  // зареждането се провали тихо; излезте

    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;

        // Рендиране с разделителната способност на принтера; rePrinting настройва пътя на рендиране
        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 картографира (maps) тези пиксели 1:1 върху страницата. Това ви дава най-добрата постижима вярност (fidelity) без никаква изрична DPI аритметика, но работи само когато PDF страницата и физическата хартия случайно са с еднакъв размер. Когато те се различават, имате нужда от изрично мащабиране (scaling)

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

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

// Побиране на PDF страница в площта за печат със запазване на съотношението
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // целева резолюция на рендиране
  Pdf.PageNumber := PageIndex;

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

  // Ограничаване до 1.0 само за свиване до побиране (shrink-to-fit) (без увеличение)
  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]);
  // ... прехвърляне със StretchDIBits както по-горе
end;

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

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

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

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

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

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

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

Двойката AllocMem / FreeMem вътре в блока за прехвърляне следва същото правило. GetDIBSizes ви казва от колко памет се нуждаят DIB хедърът и данните за пикселите; вие алокирате, запълвате, рисувате и освобождавате всичко в обхвата на една страница. Позволяването на който и да е блок да изтече (leak) ще доведе до това, че задачата за печат ще изчерпи купчината на процеса (process heap) при документи, по-дълги от няколко десетки страници

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

API-то за рендиране и документи, показано тук, е част от PDFium Component за Delphi и C++Builder