Artykuł techniczny

Drukowanie dokumentów PDF za pomocą komponentu PDFium w Delphi

Współrzędne PDF są w punktach, współrzędne drukarki w jednostkach urządzenia, a te dwie rzeczy nie mają ze sobą nic wspólnego, dopóki celowo ich nie przekonwertujesz. To niedopasowanie jest źródłem większości problemów ze złym wydrukiem w aplikacjach Delphi: kod wysyła właściwy plik, ale strona wychodzi przycięta, rozciągnięta lub pusta. Komponent PDFium czysto obsługuje stronę renderowania; mechanizm drukarki to standardowe VCL. Te dwie części pasują do siebie za pomocą niewielkiej ilości kodu, gdy tylko zrozumiesz, czego oczekuje każda ze stron

Jak działa potok renderowania a następnie drukowania (render-then-print)

Komponent PDFium nie komunikuje się bezpośrednio z drukarkami. Wzorzec jest następujący: wyrenderuj stronę do TBitmap w pożądanej rozdzielczości, a następnie przenieś tę mapę bitową na płótno drukarki (canvas) za pomocą StretchDIBits. TPdf.RenderPage zwraca mapę bitową należącą do wywołującego, więc masz kontrolę nad wymiarami w pikselach. Przekaż [rePrinting] w zestawie opcji, a PDFium przełączy ścieżkę renderowania na taką, która pomija efekty dostępne tylko na ekranie, takie jak subpikselowe wygładzanie LCD, i poprawnie obsługuje MediaBox strony dla wydruku. Pominięcie rePrinting sprawi, że to, co wyślesz do drukarki, będzie renderowaniem ekranowym, które wygląda dobrze na monitorze, ale ma tendencję do tworzenia bardziej miękkiego wydruku na drukarkach o wysokim DPI, ponieważ decyzje o wygładzaniu podjęte dla ekranów o rozdzielczości 96 DPI nie pasują do drukowania przy 300 lub 600 DPI

TPdf.Active to jedyna brama do sprawdzenia przed dotknięciem jakiejkolwiek właściwości strony. Komponent bezgłośnie pochłania błędy ładowania: ustawienie Active := True na uszkodzonym lub chronionym hasłem pliku nie powoduje błędu (exception); po prostu pozostawia Active jako False. Zawsze sprawdzaj tę wartość po przypisaniu. Odczytywanie PageCount lub PageWidth na nieaktywnym dokumencie zwraca zero, co powoduje ciche operacje puste (no-ops), które są bardzo trudne do zdiagnozowania, gdy trafią do bufora wydruku

Minimalna pętla drukowania

Najprostszy działający przypadek ładuje plik, otwiera zadanie drukowania, iteruje strony i zamyka. Jedynym trudnym szczegółem jest to, że Printer.NewPage nie może być wywołane przed pierwszą stroną, stąd flaga FirstPage. Transfer StretchDIBits przechodzi przez GetDIBSizes i GetDIB, aby pobrać niezależne od urządzenia bity (device-independent bits) z uchwytu mapy bitowej, a następnie maluje je na płótnie drukarki na pełnym rozmiarze strony:

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;

Przekazanie Printer.PageWidth i Printer.PageHeight jako wymiarów mapy bitowej oznacza renderowanie w natywnym rozmiarze piksela drukarki, co już uwzględnia DPI urządzenia. Wywołanie StretchDIBits mapuje następnie te piksele w skali 1:1 na stronę. Daje to najlepszą osiągalną wierność bez wyraźnej arytmetyki DPI, ale działa tylko wtedy, gdy strona PDF i fizyczny papier mają przypadkiem ten sam rozmiar. Jeśli się różnią, potrzebujesz jawnego skalowania

Skalowanie, gdy rozmiary strony i papieru się różnią

Strona PDF w formacie A4 w orientacji pionowej nie dopasowuje się automatycznie do drukarki US Letter, a strona w orientacji poziomej przesłana do drukarki zorientowanej pionowo zostanie przycięta. Standardowym podejściem jest obliczenie jednolitego współczynnika skali ze stosunku pikseli drukarki do punktów PDF, a następnie zastosowanie go do obu wymiarów, aby zachować proporcje obrazu. Pdf.PageWidth i Pdf.PageHeight uwidaczniają bieżące wymiary strony w punktach, gdzie jeden punkt to 1/72 cala. Pomnożenie przez docelowe DPI i podzielenie przez 72 daje w wyniku piksele w tej rozdzielczości. Weź Min ze stosunków X i Y, aby uzyskać największą skalę, która nadal mieści się w obszarze zadruku:

// 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;

Renderowanie z Dpi = 300 odpowiada większości drukarek biurowych. Przy 600 DPI, mapa bitowa pojedynczej strony A4 zajmuje około 34 megapiksele, co oznacza około 100 MB jako 32-bitowa mapa bitowa; przyrost jakości dla zwykłych dokumentów tekstowych jest minimalny, a koszt pamięci na stronę jest znaczny. Zachowaj 600 DPI dla drukarni lub ciężkich rysunków technicznych wektorowych, gdzie ma to rzeczywiste znaczenie

Flaga reAnnotations w drugim bloku kodu jest niezależna od rePrinting. Dołącz ją, gdy użytkownik oczekuje, że na papierze pojawią się pieczątki, zakreślenia i pola komentarzy. Pomiń ją w przypadku danych wyjściowych zawierających tylko treść (content-only). Obie flagi można dowolnie łączyć

Obracanie stron

PDFium przechowuje rotację strony w pliku PDF jako wpis /Rotate, dostępny przez Pdf.PageRotation, który zwraca wartość TRotation (ro0, ro90, ro180, ro270). System współrzędnych drukarki odwraca obroty o 90 i 270 stopni w stosunku do ekranu. Jeśli przekażesz surową wartość PageRotation bezpośrednio do RenderPage bez żadnego dopasowania, poziome strony osadzone w pionowym dokumencie zostaną wydrukowane do góry nogami na większości sterowników drukarek systemu Windows. Rozwiązaniem jest prosta zamiana przed wywołaniem renderowania: zmapuj ro90 na ro270 i ro270 z powrotem na ro90, pozostawiając ro0 i ro180 bez zmian

Przed wdrożeniem zweryfikuj to zachowanie na konkretnej drukarce docelowej. Działanie sterowników w kwestii obracania nie jest jednolite u wszystkich dostawców, a niektóre sterowniki stosują własną korektę obrotu na poziomie GDI. Jeśli widzisz podwójne obracanie, usuń zamianę; jeśli nie widzisz żadnej korekty, dodaj ją. Dokument o mieszanej orientacji, na przemian z pionowymi i poziomymi stronami, jest najszybszym sposobem na wychwycenie dowolnego z błędów podczas testów

Zarządzanie pamięcią podczas długiego zadania drukowania

Każde wywołanie RenderPage alokuje nowy TBitmap, którego właścicielem jest wywołujący i którego należy zwolnić. W powyższej pętli blok try/finally Bitmap.Free obsługuje to poprawnie dla jednej strony na raz. Nie kumuluj map bitowych między stronami: renderowanie w rozdzielczości 300 DPI 200-stronicowego dokumentu zużyłoby gigabajty zanim pierwsza strona trafiłaby do bufora wydruku. Zwolnij każdą mapę bitową przed przejściem do następnej strony

Para AllocMem / FreeMem wewnątrz bloku transferu podlega tej samej regule. GetDIBSizes informuje, ile pamięci potrzebuje nagłówek DIB i dane pikseli; alokujesz, wypełniasz, malujesz i zwalniasz wszystko w zakresie jednej strony. Jeśli któryś z bloków spowoduje wyciek (leak), doprowadzi to do wyczerpania sterty procesu przez zadanie drukowania w przypadku dokumentów dłuższych niż kilkadziesiąt stron

Jeśli musisz uruchamiać zadania drukowania w wątku w tle, zachowaj TPdf i wszystkie wywołania drukarki VCL w tym samym wątku. Sam TPdf nie jest bezpieczny dla wątków (thread-safe) pomiędzy instancjami współdzielącymi globalny stan biblioteki DLL PDFium; najbezpieczniejszym modelem jest jeden TPdf na wątek, a każdy z nich ładuje swoją własną kopię pliku

Pokazane tutaj API renderowania i dokumentów jest częścią komponentu PDFium Component dla Delphi i C++Builder