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
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
| Droga | Wartości formularza na papierze | Wyjście | Koszt |
|---|---|---|---|
RenderPage(DC, …) | Nie, na Windows | Wektorowe GDI; SetPdfPrintMode wybiera EMF (domyślnie) albo poziom PostScript | Najmniejszy spool, najszybciej |
RenderPage do TBitmap, potem StretchDIBits | Tak, łącznie z niezapisanymi edycjami | Raster w rozmiarze pikselowym, o który poprosisz | Szerokość × wysokość × 4 bajty na stronę, plus kopia DIB |
RenderPageBanded | Tak | Raster, po jednym pasku naraz | Jedna banda w pamięci zamiast całego arkusza |
Spłaszczona kopia, potem RenderPage(DC, …) | Tak, jako wypieczona zawartość strony | Wektorowe | Zapis, 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
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
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/AppDefaultalbo brakującego wpisu. Standard chce go honorowanego nawet wtedy, gdy dialog druku nie zostaje pokazanyPrintPaperHandling(TPrintPaperHandling) niesie/DuplexjakophSimplex,phDuplexFlipShortEdge,phDuplexFlipLongEdgealbophUndefined.SetPdfPrintPaperHandlingDevModezapisuje to doDEVMODE.dmDuplexi ustawiaDM_DUPLEX; dlaphUndefinedzwraca False i zostawia rekord w spokoju, więc domyślna sterownika przeżywaPrintCopies(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