Brama faksowa nie chce Twojego 24-bitowego renderu strony. Nie chce go też pipeline archiwalny przechowujący milion faktur wyglądających jak skany ani front-end OCR, który sprowadza wszystko do czerni i bieli jeszcze zanim zacznie szukać znaków. Wszystkie trzy przypadki chcą tego samego: czystej 1-bitowej bitmapy, jeden bit na piksel, gdzie każda kropka jest albo tuszem, albo papierem. Daj im pełnokolorowy BMP, a i tak wyrzucą 23 bity na piksel, zwykle z gorszym przebiegiem ditheringu, niż zrobiłbyś to sam. Ciekawsze pytanie brzmi, gdzie powinien zajść ten down-conversion, a odpowiedź w PDFlibPas mówi coś pożytecznego o tym, jak rozszerzać renderer, którego wolałbyś nie przepisywać
PDFlibPas to natywna biblioteka PDF w Object Pascal dla Delphi i C++Builder. Jej rdzeń renderujący rasteruje stronę do bitmapy i potrafi wypuszczać BMP, PNG, JPEG, WMF oraz kilka innych formatów. Do niedawna nie potrafił jednak zwrócić prawdziwej bitmapy monochromatycznej ani renderować tylko fragmentu strony. Obie rzeczy trafiły do wersji v3.83.0 i obie zostały zbudowane jako cienkie warstwy wygody nad istniejącym rendererem, a nie jako zmiany w samym rasterizerze. To ograniczenie stanowi całą historię
Dlaczego down-conversion robić po renderowaniu, a nie wewnątrz renderera
Oczywistym sposobem stworzenia obrazu 1-bitowego wydaje się powiedzenie rasterizerowi, żeby rysował w 1 bit. To jest też droga, która psuje wszystko inne. Wewnętrzna bitmapa renderera jest tworzona z twardo wpisanym PixelFormat := pf24bit w konstruktorze PDFlibRenderer, a ta 24-bitowa powierzchnia jest współdzielona przez każdą ścieżkę renderowania: eksport PNG, podgląd device context, wyjście JPEG, wszystko. Przestaw ją na pf1bit u źródła i nie dodasz nowej funkcji monochromatycznej, tylko pogorszysz wierność kolorów dla każdego wywołującego w całej bibliotece oraz zapiszesz się na debugowanie tuzina dalszych regresji
Dlatego RenderPageToMonochromeFile idzie dokładnie przeciwną drogą. Funkcja renderuje stronę normalnie do tymczasowego 24-bitowego BMP i dopiero potem sprowadza wynik do 1 bitu jako etap post-processingu. Renderer pozostaje nietknięty. Zachowanie monochromatyczne żyje w całości wewnątrz metody wygody, co oznacza, że nie może wpływać na nikogo, kto jej nie wywołuje. To taki kompromis, który warto nazwać wprost: post-process płaci jedną dodatkową alokacją bitmapy i jednym plikiem tymczasowym, a w zamian utrzymuje rdzeń niosący cały ciężar całkowicie poza zakresem zmian. Dla funkcji obsługującej przypadki brzegowe związane z faksem i archiwizacją to właściwa strona bilansu
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
// 200 DPI is the classic Group 4 fax resolution; page index is 1-based
Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
finally
Pdf.Free;
end;
end;
Jak naprawdę zachodzi redukcja do 1 bitu
Down-conversion opiera się na GDI, a nie na ręcznie pisanej pętli progowania, i ten wybór ma znaczenie dla jakości wyniku. Wewnątrz metody tymczasowa bitmapa 24-bitowa jest ładowana do TBitmap, druga TBitmap jest tworzona z PixelFormat := pf1bit o tych samych wymiarach, a piksele przechodzą jednym blitem:
// inside RenderPageToMonochromeFile, after loading the 24-bit ColorBmp
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE tells GDI to dither the 24-bit source down to 1-bit
SetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);
StretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,
ColorBmp.Canvas.Handle, 0, 0, ColorBmp.Width, ColorBmp.Height, SRCCOPY);
MonoBmp.SaveToFile('out.bmp');
Sztuczka polega na użyciu SetStretchBltMode z HALFTONE. Mimo że źródło i cel mają ten sam rozmiar i faktyczne skalowanie nie zachodzi, stretch mode nadal steruje tym, jak GDI mapuje kolory do palety 1-bitowej. HALFTONE wymusza halftone dithering, zamieniając szare obszary i wygładzone krawędzie tekstu na wzory czarno-białych kropek zamiast twardego obcięcia do najbliższego z dwóch kolorów. Pomiń to wywołanie trybu albo użyj domyślnego BLACKONWHITE, a zawartość w skali szarości spłaszczy się do blokowych kształtów po prostym progowaniu. Dla dokumentów skanowanych i wyjścia pod preprocessing OCR wynik z ditheringiem jest prawie zawsze tym, czego chcesz
Jeden detal jest nienegocjowalny i łatwo go zepsuć: tymczasowy render musi być BMP. RenderPageToMonochromeFile wywołuje ogólny renderer z kodem opcji 0, czyli BMP. Argument opcji w RenderPageToFile jest małym integer enum i wartości nie są tu zamienne: 0 to BMP, 1 to JPEG, 2 to WMF, 3 to EMF, 5 to PNG i tak dalej. Down-converter wykonuje potem TBitmap.LoadFromStream na pliku tymczasowym. Jeśli przekażesz WMF, ustawiając 2, to takie ładowanie rzuci "Bitmap image is not valid", ponieważ Windows Metafile jest wektorowym strumieniem rekordów, a nie DIB. Monochromatyczny down-conversion jest operacją rastrową od początku do końca, więc pośredni format także musi być rastrowy
Renderowanie tylko podregionu strony
Druga metoda, RenderPageRegionToFile, renderuje tylko prostokąt strony zamiast całości. Zastosowania są znajome, jeśli kiedykolwiek budowałeś viewer dokumentów: wycięcie bloku podpisu z umowy, wygenerowanie kafelka dla powiększonej mapy dużego rysunku albo wyciągnięcie jednego ostemplowanego regionu do miniatury bez płacenia kosztem rasteryzacji całej strony przy wysokim DPI. Sygnatura jest prosta:
// Clip is "Left,Top,Width,Height" in PDF points (72 pt = 1 inch)
// Here: a 2.5in x 1in box, one inch in from the top-left of the page
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');
String clip to cztery double oddzielone przecinkami w punktach PDF, parsowane ręcznie wewnątrz metody, żeby ominąć problemy locale i DelimitedText. Na podstawie szerokości i wysokości metoda oblicza rozmiar bitmapy wyjściowej jako Round(Width * DPI / 72) na Round(Height * DPI / 72), alokuje w pamięci bitmapę pf24bit dokładnie tego rozmiaru i renderuje do jej device context przez RenderPageToDCClip. Wynikowy plik zawiera wyłącznie wycięty prostokąt, rozmiarowany do regionu, a nie do całej strony
Parametr clip, który nic nie robił
To miejsce okazało się ostrzejsze, niż wygląda. RenderPageToDCClip od dawna niosło parametr Clip i było to kłamstwo. Wywołanie przyjmowało argument, przekazywało go dalej do TPDFPageTree.RenderPageToDC, a tamta implementacja kompletnie go ignorowała, nigdy nie przekazując go do renderera. Mogłeś podać dowolny prostokąt i i tak dostać z powrotem całą stronę. Każdy, kto podłączył RenderPageToDCClip oczekując crop, w rzeczywistości dostawał render całej strony i, zależnie od własnego układu, mógł tego nawet nie zauważyć
Wersja v3.83.0 podłączyła ten przewód. RenderPageToDC teraz parsuje ten sam prostokąt "Left,Top,Width,Height" w punktach i stosuje go jako prawdziwy region clip GDI na docelowym device context zanim renderer zacznie rysować. Konwersja z punktów na piksele urządzenia to zwykły współczynnik skali DPI / 72, zastosowany do wszystkich czterech krawędzi. Sekwencja wokół renderu to standardowy taniec save/clip/restore:
// inside TPDFPageTree.RenderPageToDC, when Clip is non-empty
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
Round(ClipLeft * ScaleFactor),
Round(ClipTop * ScaleFactor),
Round((ClipLeft + ClipWidth) * ScaleFactor),
Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer draws the page here ...
// in the finally block:
RestoreDC(TargetDC, -1);
Para SaveDC / RestoreDC(-1) utrzymuje bezpieczeństwo wielokrotnego wywoływania: region clip jest odkładany na stos stanu DC, strona jest rysowana, a oryginalny clip wraca niezależnie od sposobu wyjścia z renderu. RestoreDC(TargetDC, -1) przywraca ostatnio zapisany stan, co jest standardowym idiomem dla zbalansowanego save/restore. Pomiń restore, a wywołujący, który użyje tego samego DC do późniejszego renderu całej strony, odkryje tajemnicze przycięcie do ostatniego regionu. Naprawienie martwego parametru naprawiło też RenderPageRegionToFile przy okazji, bo ta nowa metoda przechodzi dokładnie tą samą ścieżką
Jedna rzecz dotycząca zachowania jest warta zapamiętania: clip przycina, a nie skaluje. Strona nadal jest rasteryzowana przy żądanym DPI, w swojej normalnej pozycji, a region clip po prostu odrzuca wszystko poza prostokątem. Nie powiększasz regionu po to, by wypełnił wyjście; wycinasz okno z pełnorozdzielczego renderu. Jeśli chcesz powiększyć region, podnieś DPI. Współrzędne prostokąta są interpretowane w device space po skali points-to-pixels, mierzone od lewego górnego rogu renderowanej powierzchni, więc planuj swoje Left oraz Top od góry strony w dół. Jeśli chcesz głębiej zobaczyć, jak PDFlibPas steruje device context przy wyjściu ekranowym, tekst uzupełniający o print preview i wyjściu device context przechodzi przez tę samą hydraulikę DC od strony wyświetlania
Uczciwa granica: 1-bit BMP, a nie G4 TIFF
Łatwo byłoby sprzedać to jako „wyjście gotowe na faks”, więc tutaj granica zostanie postawiona wprost. RenderPageToMonochromeFile produkuje pf1bit BMP. Nie produkuje CCITT Group 4 TIFF, czyli formatu zwykle oczekiwanego przez prawdziwy workflow faksowy albo archiwum TIFF. Powód jest konkretny, a nie wynikający z przeoczenia: jednostka CCITT w PDFlibPas potrafi obecnie dekodować strumienie G4, ale nie ma G4 encodera. Bez encodera nie ma gdzie zapisać skompresowanych przebiegów monochromatycznych, więc ścieżka monochromatyczna kończy się na nieskompresowanym 1-bitowym DIB
W praktyce to nadal jest użyteczne. 1-bitowy BMP ma poprawny format pikseli, jest gotowy i zditherowany, a większość toolchainów faksowych, archiwalnych albo OCR chętnie go przyjmie albo sama przekonwertuje do G4 w jednym dalszym kroku. Ale jeśli Twoim literalnym wymaganiem jest Group 4 TIFF wychodzący prosto z biblioteki, to jeszcze nie ten przypadek i trzeba zaplanować własny etap kompresji. Wiedza o tym, gdzie funkcja się kończy, jest równie ważna jak wiedza o tym, co robi
Obie metody są celowo niewielkie i właśnie to jest lekcja projektowa, którą warto wynieść z tej strony: convenience API siedzące nad rendererem może dodawać realne możliwości, wyjście monochromatyczne i przycinanie regionów, bez grzebania w rasterizerze i destabilizowania każdego innego wywołującego. Gdy naprawdę musisz wybierać między silnikami renderowania dla bazowej rasteryzacji, przegląd wielosilnikowego renderowania PDF w Delphi omawia te kompromisy szczegółowo. Aby zobaczyć pełną powierzchnię renderowania i resztę API, zajrzyj na stronę PDFlibPas Delphi PDF Library, której strona produktu daje pełny obraz