HotPDF, natywny komponent PDF dla Delphi, dekoduje obrazy CCITTFaxDecode z pełnym wsparciem rozszerzenia trybu nieskompresowanego T.4/T.6 i odzyskiwaniem uszkodzonych wierszy sterowanym przez DamagedRowsBeforeError. Tryb nieskompresowany jest wchodzony wyłącznie przez kod rozszerzenia kończący się na 111, a uszkodzone wiersze są naprawiane tylko dla danych Group 3 z ustawionym EndOfLine. Wszystko inne kończy się czysto, zamiast produkować skośną stronę
Obrazy kodowane faxem to długi ogon dekodowania obrazów PDF. Większość strumieni CCITT, jakie spotkasz, wyszedł ze sterownika skanera albo potoku captury dokumentów, który emituje wyłącznie zwykłe Group 4, i dekoder obsługujący ten przypadek przejdzie każdy test, jaki mu podrzucisz. Potem przychodzi archiwum z piętnastoletniego serwera faxowego, strona renderuje się jako ukośny szum od wiersza 400 w dół, i odkrywasz, że te dwie części rekomendacji ITU-T, których nikt nie implementuje, to dokładnie te dwie części, których ten plik używa. Ten artykuł przechodzi przez te dwie części tak, jak obchodzi je HotPDF, i przez miejsca, w których dekoder może się mylić, wciąż wyglądając poprawnie
Które parametry CCITTFaxDecode faktycznie zmieniają strumień bitów?
Cztery wpisy w słowniku DecodeParms zmieniają to, jak dekoder kadruje strumień bitów: K, EndOfLine, EncodedByteAlign i DamagedRowsBeforeError. ISO 32000-1 §7.4.6 (tabela 11) je definiuje, a pomylenie któregokolwiek przesuwa każdy wiersz po pierwszej niezgodności. BlackIs1, Columns, Rows i EndOfBlock też mają znaczenie, ale dotyczą polaryzacji, geometrii i miejsca zatrzymania dekodowania, a nie sposobu znajdowania granicy wiersza
Kwybiera schemat kodowania: wartość ujemna znaczy czysto dwuwymiarowe Group 4 (T.6), zero znaczy jednowymiarowy Modified Huffman (Group 3 1D), a dodatnia znaczy mieszane Group 3 2D, gdzie bit znacznikowy po każdym EOL mówi, czy następny wiersz jest 1D, czy 2DEndOfLinemówi, że każdy wiersz Group 3 jest poprzedzony kodem EOL, jedenastoma zerami zakończonymi jedynkąEncodedByteAlignmówi, że każdy zakodowany wiersz startuje na granicy bajtu, więc dekoder musi wyrzucić bity wyściółki, zanim go przeczytaDamagedRowsBeforeErrorto, ile uszkodzonych wierszy dekoder może tolerować, zanim się podda, z domyślną wartością 0
HotPDF czyta wszystkie te wartości ze słownika obrazu, gdy wywołasz THotPDF.ExtractLoadedImage albo THotPDF.DecodeLoadedImageStream. Jedna świadoma dewiacja jest warta poznania: gdy brakuje Columns, HotPDF cofa się do Width obrazu, a nie do specyfikacyjnego domyślnego 1728, a gdy brakuje Rows, używa Height. Producent pomijający Columns na skanie A4 o 2480 pikselach się myli, ale to zdarza się na tyle często, że zaufanie geometrii obrazu daje użyteczną stronę tam, gdzie literalny domyślny dałby śmieci
Dlaczego 000000 nie może być traktowane jako koniec bloku?
W wierszach dwuwymiarowych sześć zer wiodących jest niejednoznacznych, i dekoder czytający je jako początek EOFB odczyta poprawny wpis trybu nieskompresowanego jako koniec obrazu. Group 4 nie ma EOL między wierszami; jedyne kody bogate w zera to EOFB, czyli dwa EOL jeden za drugim, oraz kody rozszerzeń z ITU-T T.4 i T.6. Oba zaczynają się od 000000, a siódmy bit to miejsce, w którym się rozchodzą
Dekoder CCITT w HotPDF rozstrzyga prefiks bit po bicie. Siódmy bit równy 1 znaczy, że następne trzy bity to kod rozszerzenia, pełny wzorzec to 0000001xxx. Tylko xxx = 111 wchodzi w tryb nieskompresowany; każda inna wartość rozszerzenia jest traktowana jako zniekształcona, bo filtr faxowy PDF nie ma zastosowania dla pozostałych. Siódmy bit równy 0 może tylko kontynuować EOL, więc dekoder wymaga wtedy czterech dalszych zer i zamykającej jedynki, jedenaście zer i jedynka łącznie, zanim zgłosi EOFB. Cokolwiek krótszego lub dłuższego to błąd, nie pobłażliwy koniec obrazu
Wersja pobłażliwa kusi, bo sprawia, że cała klasa zepsutych plików wygląda dobrze: dekodowanie po prostu kończy się wcześniej i dostajesz skrócony obraz bez wyjątku. To też dokładnie ten błąd, który ucina legalne faksy na pierwszym odcinku danych nieskompresowanych, co na zeskanowanym formularzu bywa zwykle gęstym podpisem albo pieczątką. HotPDF wybiera ścisłe czytanie, więc krótki obraz znaczy, że enkoder naprawdę tam skończył
Jak tryb nieskompresowany koduje piksele?
Tryb nieskompresowany zastępuje kodowanie długości serii literalnymi wzorcami pikseli, gdzie każde słowo kodowe niesie najwyżej pięć pikseli, dopóki kod wyjścia nie przełączy z powrotem. Wiersze jednowymiarowe wchodzą w niego 12-bitowym kodem 000000001111; wiersze dwuwymiarowe używają 10-bitowego rozszerzenia 0000001111 opisanego wyżej. Enkodery przełączają się na niego w regionach półtonowych, gdzie długości serii jednego lub dwóch pikseli kosztowałyby więcej niż same piksele
Wewnątrz trybu słowa kodowe liczy się po zerach wiodących. Zero do czterech zer zakończonych jedynką znaczy tyle białych pikseli i potem jeden czarny. Pięć zer zakończonych jedynką znaczy pięć białych pikseli i nic poza tym. Sześć do dziesięciu zer zakończonych jedynką to kody wyjścia: zera ponad sześć emitują do czterech dalszych białych pikseli, a pojedynczy bit po kodzie wyjścia, bit znacznikowy, ustawia kolor następnej serii, gdy zwykłe kodowanie serii wraca
{ Słowa kodowe trybu bez kompresji (ITU-T T.4 / T.6)
W = piksel biały, B = piksel czarny, T = bit znacznika
1 B
01 W B
001 W W B
0001 W W W B
00001 W W W W B
000001 W W W W W
0000001T wyjście, potem wznowienie serii kolorem T
00000001T W, wyjście
000000001T W W, wyjście
0000000001T W W W, wyjście
00000000001T W W W W, wyjście
więcej niż dziesięć zer: uszkodzony }
Bit znacznikowy to subtelna część. Kodowanie serii jest stanowe: wiersz 1D naprzemiennie prowadzi serie białe i czarne, a wiersz 2D śledzi kolor bieżącego elementu zmiany a0. Po kodzie wyjścia dekoder musi wznowić kolorem, który nazywa znacznik, a on może, ale nie musi, zgadzać się z ostatnim literalnym pikselem. Dekoder HotPDF zapisuje zmianę koloru w bieżącej pozycji, gdy znacznik różni się od śledzonego koloru, więc następna seria startuje z właściwego koloru. Zignoruj znacznik, a reszta wiersza wyjdzie z odwróconym kolorem, a w trybie 2D każdy kolejny wiersz odziedziczy szkodę przez swoją linię odniesienia
Co odzyskuje DamagedRowsBeforeError, a czego nie?
DamagedRowsBeforeError pozwala HotPDF podmienić uszkodzony wiersz i kontynuować, ale tylko gdy K jest 0 lub dodatnie, a EndOfLine jest true. Te dwa warunki nie są arbitralne. Odzyskiwanie potrzebuje sposobu, by znaleźć, gdzie zaczyna się następny wiersz, gdy bity przestają mieć sens, a jedynym pewnym punktem orientacyjnym w strumieniu CCITT jest kod EOL. Dane Group 4 nie mają EOL, a dane Group 3 bez obowiązkowych EOL nie dają gwarancji, że którykolwiek istnieje, więc w obu przypadkach uszkodzony wiersz kończy dekodowanie bez względu na wartość parametru
Gdy wiersz pada, dekoder HotPDF skanuje w przód do następnego EOL, serii co najmniej jedenastu zer zakończonej jedynką, i podstawia wiersz zastępczy. Pierwszy uszkodzony wiersz po dobrym jest podmieniany kopią poprzedniego wiersza, co na zeskanowanej stronie tekstowej bywa na tyle blisko, że niewidoczne. Uszkodzony wiersz następujący bezpośrednio po innym uszkodzonym jest podmieniany wierszem całkiem białym, bo kopiowanie kopii rozmazałoby linię tekstu w wysoki czarny pasek. Podstawienie staje się potem linią odniesienia dla następnego wiersza 2D, co trzyma strumień trybu mieszanego dekodowalnym po naprawie
Dwie granice łatwo przeoczyć. Budżet jest kumulatywny w skali całego obrazu, nie na przebieg złych wierszy, więc DamagedRowsBeforeError 3 toleruje trzy uszkodzone wiersze łącznie, a czwarty wywraca dekodowanie. I nieudane dekodowanie w HotPDF to wszystko albo nic: THotPDF.ExtractLoadedImage zwraca nil, a nie bitmapę z poszarpanym dołem. THPDFLoadedImageInfo.Decodable raportuje, że filtr jest wspierany, nie że ten konkretny strumień jest nienaruszony, więc zawsze sprawdzaj bitmapę pod kątem nil
Dlaczego skan musi się zatrzymać na początku EOL
Skan resynchronizacji zostawia czytnik na pierwszym zerze EOL, nie za nim. Dekoder ma jedną pętlę kadrowania, która konsumuje EOL, sprawdza podwójny EOL kończący blok i czyta bit znacznikowy trybu mieszanego. Jeśli skan odzyskiwania połknie samo EOL, ta pętla zobaczy wiersz bez EOL, też uzna go za uszkodzony i spali drugą jednostkę budżetu, przy czym wyrzuci dobry wiersz. Gorzej, pominięty bit znacznikowy oznacza, że wiersz 2D zostanie zdekodowany jako 1D albo odwrotnie. Ta sama reguła działa, gdy dekoder serii 1D trafi w nieoczekiwany EOL w środku wiersza: cofa się do miejsca, gdzie zaczął się kod serii, więc znacznik wciąż tam jest dla skanu odzyskiwania
Inwentaryzacja obrazów CCITT we wczytanym PDF
Zanim zaufasz partii zarchiwizowanych faksów, wypisz, które obrazy używają CCITTFaxDecode i które nie chcą się dekodować. THotPDF.GetLoadedImageInfo daje geometrię i nazwę filtra bez dekodowania czegokolwiek, a THotPDF.ExtractLoadedImage mówi, czy strumień przeżył. Szeroki obraz tego, jak dyspozytor obrazów trasuje każdy filtr, przynosi przewodnik po wyodrębnianiu wczytanych obrazów PDF i ich filtrach dekodujących
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('fax-archive.pdf', '') <= 0 then
Exit;
for I := 0 to Pdf.GetLoadedImageCount - 1 do
begin
if not Pdf.GetLoadedImageInfo(I, Info) then
Continue;
if Pos('CCITT', UpperCase(String(Info.Filter))) = 0 then
Continue;
Bmp := Pdf.ExtractLoadedImage(I); // nil, gdy strumień jest zbyt uszkodzony
try
if Bmp = nil then
Writeln(Format('obj %d: %dx%d CCITT failed to decode',
[Info.ObjectNumber, Info.Width, Info.Height]))
else
Bmp.SaveToFile(Format('fax_%d.bmp', [I]));
finally
Bmp.Free; // bitmapa należy do wołającego
end;
end;
finally
Pdf.Free;
end;
end;
Przy porażkach zachowuj dowody. THotPDF.ExtractLoadedImageToStream w domyślnym trybie lismEncoded zapisuje przechowane bajty strumienia bez dotknięcia, czego chcesz, żeby podać drugiemu dekoderowi, owinąć w kontener TIFF do oglądania albo załączyć do zgłoszenia błędu. Reenkodowanie uszkodzonego faksu tylko zatuszuje, gdzie była szkoda
procedure SaveRawFax(Pdf: THotPDF; ImageIndex: Integer; const FileName: string);
var
Fs: TFileStream;
begin
Fs := TFileStream.Create(FileName, fmCreate);
try
if not Pdf.ExtractLoadedImageToStream(ImageIndex, Fs, lismEncoded) then
raise Exception.CreateFmt('Image %d has no stream data', [ImageIndex]);
finally
Fs.Free;
end;
end;
Jeśli sam przechodzisz strumienie obrazów przez THotPDF.CollectLoadedImageStreams, podawaj każdy THPDFStreamObject do THotPDF.DecodeLoadedImageStream i dostaniesz tę samą ścieżkę CCITT, te same parametry i te same reguły odzyskiwania. Zwolnij zwróconą listę, ale nie obiekty strumieni w niej, które pozostają własnością dokumentu
Czy podnosić DamagedRowsBeforeError na plikach, którymi nie sterujesz?
Nie, nie przez edycję PDF, i zwykle wcale. Parametr należy do producenta, który jest jedyną stroną wiedzącą, czy jego kanał transmisji gubi bity. Plik, który deklaruje DamagedRowsBeforeError 0, a potem zawiera uszkodzony wiersz, mówi ci, że kopia archiwalna jest uszkodzona, a uczciwym wynikiem jest porażka dekodowania, którą można zalogować. Po cichu łatane wiersze w umowie albo dokumentacji medycznej zamieniają wykrywalną wadę w wyglądająco wiarygodny obraz z brakującą linią, co jest gorszym wynikiem dla każdego, kto później będzie na nim polegał
Bezpieczniejszy wzorzec dla niezaufanych archiwów to trzymać dekodowanie ścisłym, zanotować, które numery obiektów padły, i skierować je do człowieka albo osobnego dekodera. Jeśli archiwum jest duże i pochodzi z wielu źródeł, wypuszczenie dekodowania obrazów poza proces ogranicza to, co wrogi strumień może zrobić aplikacji gospodarza; artykuł o izolowaniu kodeków obrazów przy przetwarzaniu niezaufanych PDF-ów omawia taki układ, a przewodnik po kompresji JBIG2 porównuje drugi filtr dwutonowy, jaki znajdziesz w tych samych skanowanych archiwach
FAQ
Czy HotPDF wspiera tryb nieskompresowany CCITT w obrazach Group 4?
Tak. HotPDF rozpoznaje 10-bitowe rozszerzenie 0000001111 w wierszach dwuwymiarowych i 12-bitowy kod wejścia 000000001111 w wierszach jednowymiarowych, dekoduje literalne słowa kodowe i honoruje bit znacznikowy wyjścia, gdy kodowanie serii wznawia bieg
Dlaczego DamagedRowsBeforeError nie działa na moim faxie Group 4?
Odzyskiwanie wymaga znaczników EOL do resynchronizacji. HotPDF stosuje DamagedRowsBeforeError tylko, gdy K jest 0 lub dodatnie, a EndOfLine jest true; dane Group 4 (ujemne K) nie mają EOL, więc każdy uszkodzony wiersz kończy dekodowanie
Co zwraca ExtractLoadedImage dla uszkodzonego obrazu CCITT?
THotPDF.ExtractLoadedImage zwraca nil, gdy szkoda przekracza to, na co pozwala DamagedRowsBeforeError, albo w ogóle nie da się jej odzyskać. Nigdy nie zwraca częściowej bitmapy, więc testuj pod nil i używaj ExtractLoadedImageToStream, żeby zachować surowe bajty
HotPDF obsługuje obrazy CCITT, JBIG2, JPEG, JPEG 2000 i Flate we wczytanych PDF-ach ze zwykłego kodu Delphi i C++Builder, bez zewnętrznej DLL dla ścieżki faxowej. Szczegóły i edycje są na stronie komponentu HotPDF Delphi PDF