Artykuł techniczny

Drukowanie wartości pól PDF z zachowaniem proporcji strony

Użytkownik wypełnia formularz PDF w przeglądarce w Delphi, wciska Print, a arkusz wychodzi z pustymi polami i stroną wielkości Letter rozciągniętą na A4. Obie wady siedzą w pętli druku: PDFium Component maluje wartości pól wyłącznie do bitmapy (albo wypieka je do spłaszczonej kopii), a każda strona chce jeden współczynnik skali zachowujący proporcje

Warstwa formularza to drugi przebieg, który potrzebuje bitmapy

Na Windows przeciążenie kontekstu urządzenia TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options) wręcza HDC drukarki wprost FPDF_RenderPage. To wołanie maluje zawartość strony, plus adnotacje znacznikowe, gdy ustawione jest reAnnotations. Interaktywna warstwa formularza nigdy nie dociera do DC — należy do środowiska wypełniania formularzy PDFium i jest rysowana przez FPDF_FFLDraw, którego jedynym celem jest FPDF_BITMAP. Wariantu HDC nie ma. Dwa przeciążenia bitmapowe TPdf.RenderPage odpalają FPDF_RenderPageBitmap i FPDF_FFLDraw na tych samych pikselach zawsze wtedy, gdy dokument został otwarty z FormFill ustawionym na True — domyślnie tak jest. Z rePrinting w opcjach PDFium Component zdejmuje podświetlenie pól przed przebiegiem formularza i przywraca domyślny kolor i alfa podświetlenia po nim, więc zabarwione ramki wokół pól edytowalnych zostają na ekranie. rePrinting mapuje się też na flagę FPDF_PRINTING silnika, która oznacza wyjście jako druk — przypadek, który ISO 32000-1 §12.5.3 (tabela 165) obejmuje bitem 3 flagi adnotacji: zgodny czytnik zostawia poza papierem adnotację z wyzerowanym bitem Print, jakiekolwiek by wyglądała na ekranie

Platforma rozstrzyga resztę. W silniku PDFium FPDF_RenderPage rysuje przez Windows GDI do kontekstu urządzenia i istnieje tylko w buildach Windows, podczas gdy FPDF_FFLDraw rysuje do bitmapy i niczego innego, na każdej platformie. Na Linuksie i pozostałych celach spoza Windows nie ma wołania GDI, więc przeciążenie HDC jest obsługiwane przez renderowanie bitmapy — z warstwą formularza włącznie — i rysowanie jej na canvas stojącym za handle. Wołanie kontekstu urządzenia niesie więc wartości pól poza Windows i pomija je na Windows. Trzymaj tę gałąź jawnie w kodzie współdzielonym między Delphi a Lazarus: gdy dokument ma środowisko formularza, a jego wartości należą na papier, proś o bitmapę na każdej platformie

Drukowanie pól formularza w Delphi z PDFium Component: TPdf.RenderPage na HDC drukarki woła FPDF_RenderPage bez warstwy formularza, podczas gdy przeciążenie TBitmap dokłada przebieg FPDF_FFLDraw przed StretchDIBits
Na Windows tylko przeciążenie bitmapowe RenderPage odpala przebieg formularza FPDF_FFLDraw, dlatego wpisane wartości pól docierają na papier tą drogą i znikają na drodze kontekstu urządzenia

Którą drogą ma iść zadanie druku?

Wybieraj po tym, co papier musi ponieść. Żywe wartości formularza wskazują drogę bitmapy, wyjście wektorowe drogę HDC, a spłaszczona kopia daje jedno i drugie w cenie zapisu i ponownego wczytania. SetPdfPrintMode działa na całą bibliotekę, więc zmieniaj go między zadaniami, nigdy między stronami

DrogaWartości formularza na papierzeWyjścieKoszt
RenderPage(DC, …)Nie, na WindowsWektorowe GDI; SetPdfPrintMode wybiera EMF (domyślnie) albo poziom PostScriptNajmniejszy spool, najszybciej
RenderPage do TBitmap, potem StretchDIBitsTak, łącznie z niezapisanymi edycjamiRaster w rozmiarze pikselowym, o który poprosiszSzerokość × wysokość × 4 bajty na stronę, plus kopia DIB
RenderPageBandedTakRaster, po jednym pasku narazJedna banda w pamięci zamiast całego arkusza
Spłaszczona kopia, potem RenderPage(DC, …)Tak, jako wypieczona zawartość stronyWektoroweZapis, ponowne wczytanie, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT)

Pełny arkusz A4 przy 600 dpi to około 4960 × 7016 pikseli urządzenia, a przy 32 bitach na piksel wychodzi 139 197 440 bajtów na jedną stronę; skopiowanie tego do DIB dla StretchDIBits podwaja szczyt — ciężki ładunek dla procesu 32-bitowego. Dla przycisku Print przeglądarki droga bitmapy pozostaje właściwym domyślnym: użytkownik, który wpisuje coś w pole i wciska Print, oczekuje zobaczenia tej wartości na papierze, a banding trzyma pamięć w ryzach. Dokumenty opuszczające aplikację, jak archiwizowane wyciągi, są lepiej obsługiwane przez spłaszczoną kopię — wartości stają się zwykłą zawartością strony, a drukarka dostaje wyjście wektorowe

uses
  System.SysUtils, System.IOUtils, PDFium, FPdfUtils;  // FPdfUtils: FLAT_PRINT

function OpenFlattenedPrintCopy(Source: TPdf; out TempName: string): TPdf;
begin
  TempName := TPath.Combine(TPath.GetTempPath, TPath.GetRandomFileName + '.pdf');
  if not Source.SaveAs(TempName) then
    raise Exception.Create('Could not write the print copy');
  Result := TPdf.Create(nil);
  try
    Result.FileName := TempName;   // FormFill zostaje True (domyślnie)
    Result.Password := Source.Password;  // kopia zachowuje swoje szyfrowanie
    Result.Active := True;
    if not Result.Active then      // nieudane wczytanie zostawia TPdf nieaktywny
      raise Exception.Create('Could not reopen the print copy');
    Result.GenerateFormAppearances;          // przebuduj /AP z bieżącego /V
    if not Result.FlattenAllPages(FLAT_PRINT) then
      raise Exception.Create('Flattening failed');
  except
    Result.Free;
    TFile.Delete(TempName);
    raise;
  end;
end;

Droga spłaszczania ma regułę kolejności, która znaczy więcej niż flaga. FlattenAllPages wypieka strumień appearance każdego widgetu (ISO 32000-1 §12.5.5) w zawartość strony, a strumień appearance to cokolwiek ostatni producent zapisał; wartość pola w /V nie gra roli. Formularz wypełniony narzędziem, które nigdy nie przegenerowało swoich appearances, spłaszczy się do starego tekstu albo do niczego, więc GenerateFormAppearances biegnie pierwszy: przebudowuje appearances pól tekstowych z bieżących wartości i synchronizuje stany /AS checkboxów i radio, a potrzebuje, by FormFill było True w chwili otwierania dokumentu. Wszystko tego rób na kopii, bo spłaszczanie przepisuje strony tego TPdf, na którym biegnie, a otwarta przeglądarka chce mieć pola nadal edytowalne. Kopia idzie przez gałąź wektorową procedury druku poniżej z wyłączoną warstwą formularza; zwolnij ją i skasuj plik tymczasowy, gdy zadanie się skończy

Spłaszczona kopia druku w Delphi z PDFium Component: SaveAs do pliku tymczasowego, ponowne otwarcie z FormFill, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT), potem druk wektorowy przez RenderPage na kontekście urządzenia
Zawołanie GenerateFormAppearances przed FlattenAllPages sprawia, że wypiekane strumienie appearance pasują do bieżących wartości pól, zamiast do cokolwiek ostatni producent zostawił w /AP

Dopasowywanie każdej strony do obszaru drukowalnego

Skaluj raz, mniejszym z dwóch współczynników, i wycentruj wynik. Rozciąganie do Printer.PageWidth × Printer.PageHeight zniekształca każdą stronę o kształcie innym niż papier — stronę Letter (612 × 792 punktów) na A4 albo stronę landscape na arkuszu portrait. Strona drukarki to obszar drukowalny w pikselach urządzenia (HORZRES przez VERTRES), z marginesami sterownika już wykluczonymi. Po stronie PDF TPdf.PageWidth i PageHeight raportują stronę tak, jak jest wyświetlana: strona A4 portrait zapisana z /Rotate 90 wraca jako 842 × 595 punktów. ISO 32000-1 §7.7.3.3 (tabela 30) mówi, że strona ma być obrócona przez /Rotate przy wyświetlaniu albo druku, PDFium aplikuje to wewnątrz wołania renderu, a argument Rotation w RenderPage to tylko dodatkowy obrót, który przeglądarka dokłada, gdy użytkownik obraca widok. Przekaż tam własne PageRotation strony, a strona obróci się dwa razy. Zamieniaj szerokość z wysokością dla dopasowania tylko wtedy, gdy ten dodatkowy obrót wynosi 90 albo 270 stopni — ta sama para reguł, która rządzi dopasowaniem miniatur i zoomem przy obrocie; procedura poniżej rozszerza podstawową pętlę drukowania dokumentów PDF z PDFium Component o gałąź wartości formularza i drogę anulowania wyrzucającą zadanie

Dopasowanie strony przy druku w Delphi z PDFium Component: strona Letter rozciągnięta do Printer.PageWidth i PageHeight kontra jedna skala zachowująca proporcje, wycentrowana w obszarze drukowalnym, z /Rotate już wpisanym w TPdf.PageWidth
Skalowanie mniejszym z dwóch współczynników trzyma każdą stronę niezniekształconą i wycentrowaną, a szerokość z wysokością zamieniają się tylko przy dodatkowym obrocie ro90 albo ro270
uses
  Winapi.Windows, System.SysUtils, Vcl.Graphics, Vcl.Forms, Vcl.Printers, PDFium;

procedure FitRect(PageW, PageH: Double; Swap: Boolean;
  AreaW, AreaH: Integer; out X, Y, W, H: Integer);
var
  T, Scale: Double;
begin
  if Swap then
  begin
    T := PageW; PageW := PageH; PageH := T;
  end;
  if (PageW <= 0) or (PageH <= 0) then
  begin
    X := 0; Y := 0; W := AreaW; H := AreaH;
    Exit;
  end;
  Scale := AreaW / PageW;
  if AreaH / PageH < Scale then
    Scale := AreaH / PageH;
  W := Round(PageW * Scale);
  H := Round(PageH * Scale);
  X := (AreaW - W) div 2;
  Y := (AreaH - H) div 2;
end;

procedure BlitBitmap(DC: HDC; Bitmap: TBitmap; X, Y: Integer);
var
  InfoSize, ImageSize: DWORD;
  Info: PBitmapInfo;
  Bits: Pointer;
begin
  GetDIBSizes(Bitmap.Handle, InfoSize, ImageSize);
  Info := AllocMem(InfoSize);
  try
    Bits := AllocMem(ImageSize);
    try
      GetDIB(Bitmap.Handle, 0, Info^, Bits^);
      StretchDIBits(DC, X, Y, Bitmap.Width, Bitmap.Height, 0, 0,
        Bitmap.Width, Bitmap.Height, Bits, Info^, DIB_RGB_COLORS, SRCCOPY);
    finally
      FreeMem(Bits);
    end;
  finally
    FreeMem(Info);
  end;
end;

function PrintPages(Pdf: TPdf; FromPage, ToPage: Integer; FormLayer: Boolean;
  ExtraRotation: TRotation; var Cancel: Boolean): Boolean;
const
  Opts: TRenderOptions = [reAnnotations, rePrinting];
var
  Page, X, Y, W, H: Integer;
  Bitmap: TBitmap;
begin
  Result := False;
  Printer.BeginDoc;
  try
    for Page := FromPage to ToPage do
    begin
      if Page > FromPage then
        Printer.NewPage;
      Pdf.PageNumber := Page;
      FitRect(Pdf.PageWidth, Pdf.PageHeight, ExtraRotation in [ro90, ro270],
        Printer.PageWidth, Printer.PageHeight, X, Y, W, H);
      if FormLayer then
      begin
        Bitmap := Pdf.RenderPage(0, 0, W, H, ExtraRotation, Opts);
        if Bitmap <> nil then          // nil przy zdegenerowanym rozmiarze strony
        try
          BlitBitmap(Printer.Canvas.Handle, Bitmap, X, Y);
        finally
          Bitmap.Free;
        end;
      end
      else
        Pdf.RenderPage(Printer.Canvas.Handle, X, Y, W, H, ExtraRotation, Opts);
      Application.ProcessMessages;     // pozwala przyciskowi Cancel ustawić flagę
      if Cancel then
        Break;
    end;
    Result := not Cancel;
  finally
    if Result then
      Printer.EndDoc                   // oddaje zadanie do spoolera
    else
      Printer.Abort;                   // anulowanie albo wyjątek: nic się nie drukuje
  end;
end;

Przeglądarka woła to z Pdf.FormFill jako FormLayer; spłaszczona kopia przekazuje False. EndDoc to commit, a nie krok porządkowy. Po BeginDoc Printer.EndDoc wręcza spoolerowi wszystko, co zostało narysowane, jako poprawne zadanie, więc bezwarunkowe EndDoc w finally zamienia anulowany druk w półwydrukowany. Procedura rozstrzyga w finally na podstawie flagi sukcesu: EndDoc po ostatniej stronie, Abort po anulowaniu albo wyjątku. Wyjście z pętli potrzebuje podobnej staranności, bo Delphi i Free Pascal oba oceniają wartość końcową pętli for raz, przy wejściu. Ustawienie ToPage albo liczby kopii na zero z handlera Cancel nie zatrzyma pętli już biegnącej; testuj flagę i Break, a przy zagnieżdżonych pętlach kopii i składania, wychodź z każdego poziomu. A skoro Application.ProcessMessages pozwala odpalać też skróty menu, wyłącz Open, Save i Print na czas zadania

Trzymanie drogi rastrowej w granicach pamięci

TPdf.RenderPageBanded renderuje stronę w poziomych pasach o wysokości, jaką wybierzesz, i przekazuje każdy pas do callbacku. Każdy pas to plaster strony w pełnym rozmiarze — nic nie jest przeliczane — i niesie te same wartości pól co bitmapa całej strony. Przy bandach po 512 wierszy strona o szerokości 4960 pikseli potrzebuje około 10 MB na band zamiast 139 MB na arkusz. Z grubsza jedna czternasta pamięci. TPdfBandCallback to wskaźnik metody, który dostaje indeks bandy, górny offset bandy w pikselach strony i bitmapę, i zwraca True, by kontynuować; RenderPageBanded zwraca liczbę band, 0 przy niepowodzeniu

type
  TBandPrinter = class
  private
    FDC: HDC;
    FLeft, FTop: Integer;
  public
    constructor Create(DC: HDC; Left, Top: Integer);
    function PrintBand(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean;
  end;

constructor TBandPrinter.Create(DC: HDC; Left, Top: Integer);
begin
  inherited Create;
  FDC := DC;
  FLeft := Left;
  FTop := Top;
end;

function TBandPrinter.PrintBand(BandIndex, BandTopY: Integer;
  Bitmap: TBitmap): Boolean;
begin
  BlitBitmap(FDC, Bitmap, FLeft, FTop + BandTopY);
  Result := True;                      // False zatrzymuje pozostałe bandy
end;

// wołane z pętli stron w miejsce gałęzi bitmapy całej strony
procedure PrintPageBanded(Pdf: TPdf; Page, X, Y, W, H: Integer;
  ExtraRotation: TRotation);
var
  Bands: TBandPrinter;
begin
  Bands := TBandPrinter.Create(Printer.Canvas.Handle, X, Y);
  try
    if Pdf.RenderPageBanded(W, H, 512, Bands.PrintBand, ExtraRotation,
      [reAnnotations, rePrinting]) = 0 then
      raise Exception.CreateFmt('Page %d could not be rendered', [Page]);
  finally
    Bands.Free;
  end;
end;

Co gospodarz ma zrobić z ViewerPreferences i bitami uprawnień?

Czytaj je i działaj po nich we własnym kodzie; PDFium je raportuje i renderuje stronę niezależnie od nich. Producent może zapisać podpowiedzi druku w słowniku /ViewerPreferences katalogu (ISO 32000-1 §12.2, tabela 150), a TPdf wystawia te, które kształtują dialog druku:

  • PrintScaling (Boolean) jest False, gdy plik mówi /PrintScaling /None, czyli druk w rozmiarze rzeczywistym, i True dla /AppDefault albo brakującego wpisu. Standard chce go honorowanego nawet wtedy, gdy dialog druku nie zostaje pokazany
  • PrintPaperHandling (TPrintPaperHandling) niesie /Duplex jako phSimplex, phDuplexFlipShortEdge, phDuplexFlipLongEdge albo phUndefined. SetPdfPrintPaperHandlingDevMode zapisuje to do DEVMODE.dmDuplex i ustawia DM_DUPLEX; dla phUndefined zwraca False i zostawia rekord w spokoju, więc domyślna sterownika przeżywa
  • PrintCopies (Integer) to /NumCopies, liczba kopii do wstawienia w dialog

Bity uprawnień podążają za tą samą regułą. Dla standardowego handlera bezpieczeństwa ISO 32000-1 §7.6.3.2 (tabela 22) przydziela bit 3 /P drukowi, a dla handlerów rewizji 3 i późniejszych bit 12 drukowi w pełnej wierności; przy ustawionym bicie 3 i wyzerowanym bicie 12 druk jest ograniczony do niskopoziomowej reprezentacji appearance, być może z pogorszeniem. TPdf.GrantedPermissions dekoduje maskę w permPrint i permPrintHighQuality, a HasPermission testuje pojedynczą wartość; silnik narysuje, o cokolwiek poprosisz, więc egzekwowanie to robota gospodarza — zwykle wyłączony przycisk Print. Gdy przyznane jest wyłącznie permPrint, czytanie zgodne ze standardem to droga bitmapy w skromnej rozdzielczości zamiast wyjścia wektorowego albo PostScript, na jakie pozwala pełne przyznanie. Bity opisują dokument taki, jak został otwarty: plik odblokowany hasłem owner raportuje pełny dostęp, a UserHasPermission odpowiada za bity poziomu użytkownika

uses
  Winapi.Windows, System.SysUtils, Vcl.Dialogs, Vcl.Printers, PDFium;

procedure PreparePrintJob(Pdf: TPdf; Dialog: TPrintDialog;
  out ActualSize, HighQuality: Boolean);
var
  Device, Driver, Port: array[0..MAX_PATH] of Char;
  DevModeHandle: THandle;
  DevMode: PDeviceMode;
begin
  if not Pdf.HasPermission(permPrint) then
    raise Exception.Create('This document does not permit printing');
  HighQuality := Pdf.HasPermission(permPrintHighQuality);
  ActualSize := not Pdf.PrintScaling;            // /PrintScaling /None

  Printer.GetPrinter(Device, Driver, Port, DevModeHandle);
  if DevModeHandle <> 0 then
  begin
    DevMode := GlobalLock(DevModeHandle);
    try
      SetPdfPrintPaperHandlingDevMode(DevMode^, Pdf.PrintPaperHandling);
    finally
      GlobalUnlock(DevModeHandle);
    end;
    Printer.SetPrinter(Device, Driver, Port, DevModeHandle);  // dialog startuje stąd
  end;

  Dialog.MinPage := 1;
  Dialog.MaxPage := Pdf.PageCount;
  Dialog.Options := Dialog.Options + [poPageNums];
  if Pdf.PrintCopies > 1 then
    Dialog.Copies := Pdf.PrintCopies;
end;

// używane zamiast FitRect, gdy ActualSize jest True
procedure ActualSizeRect(Pdf: TPdf; out X, Y, W, H: Integer);
begin
  // 72 punkty na cal po stronie PDF, piksele urządzenia na cal po stronie drukarki
  W := Round(Pdf.PageWidth / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSX));
  H := Round(Pdf.PageHeight / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSY));
  X := (Printer.PageWidth - W) div 2;  // ujemne, gdy strona jest większa
  Y := (Printer.PageHeight - H) div 2; // od obszaru drukowalnego: krawędzie ucinane
end;

Odpalanie akcji will-print i did-print na prawdziwej granicy druku

Skrypty druku poziomu dokumentu odpalają się wtedy, gdy odpala je gospodarz. Słownik additional-actions katalogu (ISO 32000-1 §12.6.3, tabela 197) definiuje WP, akcję JavaScript wykonywaną przed drukiem, i DP, wykonywaną po. PDFium Component wystawia oba przez ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) i dlaDidPrint, i trzyma je z dala od wołań renderu: miniatury, snapse i konwersje też renderują, a żadna z nich nie powinna wyglądać jak druk użytkownika dla skryptu dokumentu. Kolejność masz do dopięcia. Skrypt will-print stemplujący datę albo wypełniający pole "printed by" zmienia wartości formularza, więc odpal go, zanim pierwsza strona zostanie narysowana, a na drodze spłaszczania — zanim kopia zostanie zapisana; did-print pal dopiero po tym, jak EndDoc zacommituje zadanie. Wołanie zwraca False z komunikatem, gdy środowisko formularza nie istnieje albo LifecycleActionPolicy mu odmawia; dla plików niezaufanych lapDisabled wyłącza akcje dokumentu. Szerszy obraz skryptowania jest w uruchamianiu AcroForm JavaScript z eventami gospodarza

Na Windows zadanie, które musi ponieść wartości formularza, należy na gałąź bitmapy: RenderPage do TBitmap w zwykłych rozdzielczościach, RenderPageBanded przy 600 dpi i więcej. Zostaw przeciążenie kontekstu urządzenia PDFium Component dla spłaszczonych kopii i dokumentów bez pól, gdzie jego wyjście wektorowe jest lepszym wynikiem