네이티브 Delphi PDF 컴포넌트인 HotPDF는 CCITTFaxDecode 이미지를 디코드하며 T.4/T.6 무압축 모드 확장과 DamagedRowsBeforeError가 주도하는 손상 행 복구를 완전히 지원합니다. 무압축 모드는 111로 끝나는 확장 코드로만 진입하고, 손상된 행은 EndOfLine이 설정된 Group 3 데이터에서만 수리됩니다. 그 외의 경우는 비틀린 페이지를 만들어 내는 대신 깔끔하게 실패합니다
팩스 인코딩 이미지는 PDF 이미지 디코딩의 롱테일입니다. 마주치는 CCITT 스트림 대부분은 순수한 Group 4만 내보내는 스캐너 드라이버나 문서 캡처 파이프라인에서 나왔고, 흔한 경우만 다루는 디코더는 던지는 모든 테스트를 통과합니다. 그러다 15년 된 팩스 서버의 아카이브가 도착하고, 한 페이지가 400행부터 대각선 노이즈로 렌더링되고, 아무도 구현하지 않는 ITU-T 권고의 두 부분이 바로 그 파일이 쓰는 두 부분임을 발견하게 됩니다. 이 글은 HotPDF가 이들을 어떻게 다루는지, 그리고 디코더가 올바로 보이면서도 잘못될 수 있는 지점들을 짚어 갑니다
실제로 비트 스트림을 바꾸는 CCITTFaxDecode 파라미터는 무엇인가요?
DecodeParms 딕셔너리의 네 항목이 디코더가 비트 스트림을 프레이밍하는 방식을 바꿉니다: K, EndOfLine, EncodedByteAlign, DamagedRowsBeforeError. ISO 32000-1 §7.4.6(Table 11)이 이들을 정의하며, 하나라도 잘못 잡으면 첫 불일치 이후 모든 행이 어긋납니다. BlackIs1, Columns, Rows, EndOfBlock도 중요하지만, 이들은 행 경계를 찾는 방식이 아니라 극성, 기하, 디코딩이 멈추는 지점에 영향을 줍니다
K는 코딩 방식을 고릅니다: 음수는 순수 2차원 Group 4(T.6), 0은 1차원 Modified Huffman(Group 3 1D), 양수는 혼합 Group 3 2D를 뜻하며, 이 경우 각 EOL 뒤의 태그 비트가 다음 행이 1D인지 2D인지를 말해 줍니다EndOfLine은 모든 Group 3 행이 EOL 코드, 즉 1이 뒤따르는 11개의 0 비트로 시작한다는 뜻입니다EncodedByteAlign은 인코딩된 각 행이 바이트 경계에서 시작한다는 뜻이므로, 디코더는 읽기 전에 패딩 비트를 버려야 합니다DamagedRowsBeforeError는 디코더가 포기하기 전까지 용납할 수 있는 손상 행의 수이며 기본값은 0입니다
THotPDF.ExtractLoadedImage나 THotPDF.DecodeLoadedImageStream을 호출하면 HotPDF는 이 모든 값을 이미지 딕셔너리에서 읽습니다. 의도된 예외 하나는 알아 둘 가치가 있습니다: Columns가 없으면 HotPDF는 사양 기본값인 1728 대신 이미지의 Width로 폴백하고, Rows가 없으면 Height를 씁니다. 2480픽셀 A4 스캔에서 Columns를 빠뜨린 생산자는 잘못했지만, 이미지 기하를 신뢰하는 쪽이 쓸 만한 페이지를 주고 문자 그대로의 기본값은 쓰레기를 주는 일이 흔할 정도로 자주 있습니다
000000을 블록의 끝으로 취급하면 안 되는 이유
2차원 행에서는 앞의 6개 0 비트가 애매하며, 이를 EOFB의 시작으로 읽는 디코더는 유효한 무압축 모드 진입을 이미지의 끝으로 잘못 읽습니다. Group 4는 행 사이에 EOL이 없습니다; 0이 많은 코드는 두 EOL이 연달아 놓인 EOFB와 ITU-T T.4 및 T.6의 확장 코드뿐입니다. 둘 다 000000으로 시작하며, 일곱 번째 비트에서 갈라집니다
HotPDF CCITT 디코더는 이 접두어를 비트 단위로 해석합니다. 일곱 번째 비트가 1이면 다음 세 비트가 확장 코드이고 전체 패턴은 0000001xxx입니다. xxx = 111만이 무압축 모드로 진입하며, 다른 확장 값은 PDF 팩스 필터가 쓸 일이 없으므로 형식 불량으로 취급됩니다. 일곱 번째 비트가 0이면 EOL의 연속일 수밖에 없으므로, 디코더는 EOFB를 보고하기 전에 0 네 비트와 마무리 1, 즉 총 열한 개의 0과 하나의 1을 더 요구합니다. 그보다 짧든 길든 에러이지 관대한 이미지 끝이 아닙니다
관대한 버전은 유혹적입니다. 깨진 파일 한 부류 전체가 멀쩡해 보이게 하니까요: 디코딩이 그저 일찍 멈추고 예외 없이 짧은 이미지를 받습니다. 하지만 바로 그것이 정당한 팩스를 무압축 데이터 구간 첫 지점에서 잘라 버리는 버그이기도 합니다. 스캔된 양식에서 그 지점은 보통 짙은 서명이나 도장입니다. HotPDF는 엄격한 해석을 택하므로, 짧은 이미지는 인코더가 정말로 거기서 끝냈다는 뜻입니다
무압축 모드는 픽셀을 어떻게 인코딩하나요?
무압축 모드는 런렝스 부호화를 문자 그대로의 픽셀 패턴으로 대체합니다. 각 코드 워드는 최대 다섯 픽셀을 담고, 탈출 코드가 돌아갈 때까지 계속됩니다. 1차원 행은 12비트 코드 000000001111로 진입하고; 2차원 행은 앞서 말한 10비트 확장 0000001111을 씁니다. 인코더는 한두 픽셀짜리 런이 픽셀 자체보다 비용이 더 드는 하프톤 비슷한 영역에서 이 모드로 갈아탑니다
모드 안에서 코드 워드는 앞의 0 개수로 세어집니다. 1 앞에 0이 0개에서 4개면 그만큼의 흰 픽셀 뒤에 검은 픽셀 하나를 뜻합니다. 1 앞에 0이 다섯 개면 흰 픽셀 다섯 개 그뿐임을 뜻합니다. 1 앞에 0이 6개에서 10개면 탈출 코드입니다: 6을 넘는 0들은 흰 픽셀을 최대 네 개 더 내보내고, 탈출 코드 뒤의 단일 비트, 즉 태그 비트는 일반 런 부호화가 재개될 때 다음 런의 색을 정합니다
{ 무압축 모드 코드 워드 (ITU-T T.4 / T.6)
W = 흰 픽셀, B = 검은 픽셀, T = 태그 비트
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 종료, 이후 색 T로 런 재개
00000001T W, 종료
000000001T W W, 종료
0000000001T W W W, 종료
00000000001T W W W W, 종료
0이 열 개 초과: 형식 불량 }
태그 비트가 미묘한 부분입니다. 런 부호화는 상태를 가집니다: 1D 행은 흰 런과 검은 런을 번갈고, 2D 행은 현재 변화 요소 a0의 색을 추적합니다. 탈출 코드 뒤에는 태그가 가리키는 색으로 재개해야 하며, 그 색은 마지막 문자 픽셀과 일치할 수도, 아닐 수도 있습니다. HotPDF 디코더는 태그가 추적하던 색과 다르면 현재 위치에 색 변화를 기록해서 다음 런이 올바른 색에서 시작하게 합니다. 태그를 무시하면 행의 나머지가 색 반전으로 나오고, 2D 모드에서는 그 손상이 참조 선을 통해 이후 모든 행으로 이어집니다
DamagedRowsBeforeError는 무엇을 복구하고 무엇을 복구하지 않나요?
DamagedRowsBeforeError는 HotPDF가 손상된 행을 대체하고 계속 진행하게 해 주지만, K가 0 또는 양수이고 EndOfLine이 true일 때만 그렇습니다. 이 두 조건은 마음대로 정한 게 아닙니다. 복구는 비트가 의미를 잃은 뒤 다음 행이 어디서 시작하는지 찾는 방법을 필요로 하는데, CCITT 스트림에서 유일하게 믿을 만한 랜드마크가 EOL 코드입니다. Group 4 데이터에는 EOL이 없고, 필수 EOL이 없는 Group 3 데이터는 존재를 보장하지 못하므로, 두 경우 모두 파라미터 값과 무관하게 손상된 행이 디코딩을 끝냅니다
행이 실패하면 HotPDF 디코더는 다음 EOL, 즉 1이 뒤따르는 최소 열한 개의 0 비트 런을 앞으로 스캔해서 대체 행으로 바꿔 넣습니다. 정상 행 다음의 첫 손상 행은 이전 행의 복사본으로 대체되는데, 스캔된 텍스트 페이지에서는 보통 눈에 띄지 않을 만큼 비슷합니다. 손상 행 바로 다음에 또 손상 행이 온 경우에는 대신 전부 흰 행으로 대체합니다. 복사본의 복사본은 텍스트 한 줄을 검은 기둥으로 번지게 만들기 때문입니다. 대체 행은 이어서 다음 2D 행의 참조 선이 되며, 이 덕분에 수리 후에도 혼합 모드 스트림이 계속 디코딩 가능합니다
놓치기 쉬운 경계가 둘 있습니다. 예산은 나쁜 행 묶음마다가 아니라 이미지 전체에 걸쳐 누적됩니다. DamagedRowsBeforeError 3은 총 세 개의 손상 행을 용납하고 네 번째에서 디코드가 실패한다는 뜻입니다. 그리고 HotPDF에서 실패한 디코드는 전부 아니면 전무입니다: THotPDF.ExtractLoadedImage는 밑동이 떨어진 비트맵 대신 nil을 반환합니다. THPDFLoadedImageInfo.Decodable은 이 특정 스트림이 온전하다는 게 아니라 필터가 지원된다는 것만 알려 주므로, 비트맵이 nil인지 항상 확인하세요
스캔이 EOL 시작 지점에서 멈춰야 하는 이유
재동기화 스캔은 리더를 EOL의 첫 0 비트에, 그 뒤가 아니라 그 자리에 남겨 둡니다. 디코더에는 EOL을 소비하고, 블록을 끝내는 이중 EOL을 확인하고, 혼합 모드 태그 비트를 읽는 프레이밍 루프가 하나 있습니다. 복구 스캔이 EOL 자체를 삼켜 버리면 그 루프는 EOL 없는 행을 보고, 그것도 손상으로 취급하고, 정상 행을 버리면서 예산을 한 단계 더 태웁니다. 더 나쁜 것은, 건너뛴 태그 비트 때문에 2D 행이 1D로 디코딩되거나 그 반대가 된다는 점입니다. 1D 런 디코더가 행 중간에 뜻밖의 EOL을 만났을 때도 같은 규칙이 적용됩니다: 런 코드가 시작된 자리로 되감으므로, 표식은 복구 스캔을 위해 그 자리에 남습니다
로드된 PDF의 CCITT 이미지 목록 조사하기
아카이브된 팩스 묶음을 믿기 전에, 어느 이미지가 CCITTFaxDecode를 쓰고 어느 것이 디코드에 실패하는지 목록을 뽑아 두세요. THotPDF.GetLoadedImageInfo는 아무것도 디코드하지 않고 기하와 필터 이름을 알려 주고, THotPDF.ExtractLoadedImage는 스트림이 살아남았는지를 알려 줍니다. 이미지 디스패처가 각 필터를 어떻게 라우팅하는지 더 넓은 그림은 로드된 PDF 이미지와 디코드 필터 추출하기 가이드를 참고하세요
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
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; // 비트맵의 소유권은 호출자에게
end;
end;
finally
Pdf.Free;
end;
end;
실패 건에 대해서는 증거를 보관하세요. 기본 lismEncoded 모드의 THotPDF.ExtractLoadedImageToStream은 저장된 스트림 바이트를 그대로 기록합니다. 두 번째 디코더에 넘기거나, 검토를 위해 TIFF 컨테이너에 싸거나, 버그 리포트에 첨부할 때 필요한 것이 바로 그것입니다. 손상된 팩스를 재인코딩하면 손상 위치만 가려질 뿐입니다
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;
THotPDF.CollectLoadedImageStreams로 이미지 스트림을 직접 훑는다면 각 THPDFStreamObject를 THotPDF.DecodeLoadedImageStream에 넘기세요. 같은 CCITT 경로, 파라미터, 복구 규칙이 적용됩니다. 반환된 리스트는 해제하되 그 안의 스트림 오브젝트는 해제하지 마세요. 소유권은 문서에 남아 있습니다
내가 통제하지 않는 파일에서 DamagedRowsBeforeError를 올려야 할까요?
아니요, PDF를 수정해서라도, 그리고 보통은 아예 하지 말아야 합니다. 이 파라미터의 주인은 생산자이며, 전송 채널이 비트를 떨어뜨리는지 아는 유일한 존재도 생산자입니다. DamagedRowsBeforeError 0을 선언한 파일에 손상 행이 들어 있다면 아카이브 사본이 깨졌다고 말하는 것이고, 정직한 결과는 로그로 남길 수 있는 디코드 실패입니다. 계약서나 진료 기록의 행을 조용히 덧대면 감지 가능한 결함이 한 줄 빠진 그럴듯해 보이는 이미지로 변하는데, 나중에 그것을 의지하는 사람에게는 더 나쁜 결과입니다
신뢰할 수 없는 아카이브에는 더 안전한 패턴이 있습니다. 디코드를 엄격하게 유지하고, 실패한 오브젝트 번호를 기록하고, 그것을 사람이나 별도의 디코더로 넘기는 것입니다. 아카이브가 크고 여러 출처에서 온다면 이미지 디코딩을 프로세스 밖으로 분리하는 것이 적대적 스트림이 호스트 애플리케이션에 할 수 있는 일을 제한합니다; 신뢰할 수 없는 PDF 처리 시 이미지 코덱 격리하기 글이 그 셋업을 다루고, JBIG2 바이레벨 압축 가이드는 같은 스캔 아카이브에서 만나게 될 다른 바이레벨 필터를 비교합니다
FAQ
HotPDF는 Group 4 이미지의 CCITT 무압축 모드를 지원하나요?
예. HotPDF는 2차원 행의 10비트 0000001111 확장과 1차원 행의 12비트 000000001111 진입 코드를 인식하고, 문자 코드 워드를 디코드하며, 런 부호화가 재개될 때 탈출 태그 비트를 존중합니다
Group 4 팩스에 DamagedRowsBeforeError가 효과가 없는 이유는 무엇인가요?
복구에는 재동기화를 위한 EOL 마커가 필요합니다. HotPDF는 K가 0 또는 양수이고 EndOfLine이 true일 때만 DamagedRowsBeforeError를 적용합니다; Group 4 데이터(K 음수)에는 EOL이 없으므로 손상된 행이 있으면 디코드가 끝납니다
손상된 CCITT 이미지에 대해 ExtractLoadedImage는 무엇을 반환하나요?
THotPDF.ExtractLoadedImage는 손상이 DamagedRowsBeforeError가 허용하는 수준을 넘거나 아예 복구할 수 없을 때 nil을 반환합니다. 부분 비트맵을 반환하는 일은 없으므로, nil 검사를 하고 난 바이트를 남기려면 ExtractLoadedImageToStream을 쓰세요
HotPDF는 일반 Delphi와 C++Builder 코드에서 로드된 PDF의 CCITT, JBIG2, JPEG, JPEG 2000, Flate 이미지를 처리하며, 팩스 경로에 외부 DLL이 필요 없습니다. 세부 사항과 에디션은 HotPDF Delphi PDF 컴포넌트 페이지에 있습니다