기술 문서

순수 Pascal PDF 디코더의 JBIG2 커스텀 Huffman 테이블

PDFlibPas 3.539.22는 JBIG2 커스텀 Huffman 테이블을 네이티브로 디코딩합니다. PDFlibJBIG2.pas의 순수 Pascal 디코더가 Tables 세그먼트(type 53)를 파싱하고, ITU-T T.88 Annex B.3이 요구하는 대로 테이블 라인 순서로 정규 prefix 코드를 부여하며, 심볼 딕셔너리와 텍스트 영역에 대해 셀렉터 순서대로 커스텀 테이블 참조를 소비하고, 뒤따르는 바이트가 무엇이든 상관없이 선언된 세그먼트 길이로 모든 읽기를 제한합니다

이 작업을 하게 만든 파일은 겉보기에는 특별할 게 없었습니다. 스캔한 계약서인데, 훨씬 흔한 arithmetic coding이 아니라 Huffman 심볼 코딩으로 JBIG2 압축되어 있었고, 인코더가 표준 테이블 B.1~B.15 대신 자체 코드 테이블을 넣어 둔 파일이었습니다. 두 개의 독립적인 디코더가 refinement 픽셀에서 서로 다른 결과를 냈고, 당시의 PDFlibPas 디코더는 문서 세단기에 넣은 듯한 텍스트를 만들어 냈습니다. 글리프 조각이 몇 픽셀씩 밀리고, 모든 글자에서 한 열씩 빠져 있었습니다. 아무런 오류도 나지 않았습니다. 이게 몇 년씩 살아남는 버그의 모양입니다. 파일을 거부하는 디코더는 지원 티켓을 받지만, 살짝 잘못 그리는 디코더는 스캔 상태가 나빴다고 생각하는 고객을 얻기 때문입니다

JBIG2 Tables 세그먼트에는 실제로 무엇이 들어 있을까요?

Tables 세그먼트는 Huffman 테이블 하나를 압축해서 기술한 것입니다. flags 바이트 하나, 부호 있는 32비트 경계 두 개, 그리고 그 경계 사이 구간을 나누는 (prefix 길이, range 길이) 쌍의 나열로 이루어지며, 구조는 T.88 §7.4.13과 Annex B.2에 나와 있습니다. flags 바이트의 비트 0은 HTOOB으로, 그 테이블에 out-of-band 코드가 있는지를 나타냅니다. 비트 1~3에 1을 더한 값이 HTPS, 즉 각 prefix 길이를 쓰는 데 필요한 비트 수입니다. 비트 4~6에 1을 더한 값은 각 range 길이 필드의 폭인 HTRS입니다. 비트 7은 예약되어 있고, PDFlibPas는 이 비트가 서 있으면 이후 개정판이 무슨 뜻으로 쓰려 했는지 추측하지 않고 세그먼트를 거부합니다. 이어서 HTLOW와 HTHIGH가 부호 있는 32비트 정수로 따라오는데, 여기가 디코더가 처음 틀릴 수 있는 지점입니다. 부호 없는 값으로 읽으면 하한이 음수인 테이블, 즉 델타 부호화된 심볼 폭에서는 지극히 정상인 테이블이 40억에서 시작하는 것처럼 보입니다. 모든 필드는 리더를 건드리기 전에 요청한 비트 수가 세그먼트 데이터가 끝나는 비트 위치를 넘지 않는지 확인하는 로컬 ReadField 헬퍼를 거칩니다. 세그먼트를 넘겨 읽는 테이블은 다음 세그먼트 헤더를 prefix 길이로 삼켜 버리기 때문입니다

PDFlibPas의 JBIG2 커스텀 Huffman 디코딩 뒤에 있는 Tables 세그먼트 레이아웃: HTOOB, HTPS, HTRS를 담은 flags 바이트 하나와 거부되는 예약 비트, 부호 있는 HTLOW·HTHIGH 경계, prefix와 range 길이 쌍의 나열, 그리고 jbig2HuffmanLOW, 고정 32비트 상위 라인, 선택적 jbig2HuffmanOOB escape 라인입니다
세그먼트의 모든 필드를 경계 검사 헬퍼로 읽는데, 선언된 끝을 넘겨 읽는 테이블은 다음 세그먼트 헤더를 prefix 길이로 삼켜 버리기 때문입니다. 센티널 escape 라인은 내장 표준 테이블과 같은 규약을 따릅니다
// TCodeTableSegment.readSegment, PDFlibJBIG2.pas
EndBit := (Int64(decoder.reader.bytePointer) +
  segmentHeader.getSegmentDataLength) * 8;
Flags := ReadField(8);
if (Flags and $80) <> 0 then
  raise EJBIG2DecodeError.Create('reserved custom Huffman table flag');
PrefixBits := ((Flags shr 1) and 7) + 1;   // HTPS
RangeBits  := ((Flags shr 4) and 7) + 1;   // HTRS
LowValue   := Integer(ReadField(32));      // 부호 있는 HTLOW
HighValue  := Integer(ReadField(32));      // 부호 있는 HTHIGH
if LowValue >= HighValue then
  raise EJBIG2DecodeError.Create('invalid custom Huffman range bounds');
CurrentValue := LowValue;
while CurrentValue < HighValue do
begin
  PrefixLength := ReadField(PrefixBits);
  RangeLength  := ReadField(RangeBits);
  if RangeLength > 32 then
    raise EJBIG2DecodeError.Create('invalid custom Huffman range length');
  AddLine(CurrentValue, PrefixLength, RangeLength);
  Inc(CurrentValue, Int64(1) shl RangeLength);
end;
AddLine(LowValue - 1, ReadField(PrefixBits), jbig2HuffmanLOW);
AddLine(HighValue,   ReadField(PrefixBits), 32);
if (Flags and 1) <> 0 then
  AddLine(0, ReadField(PrefixBits), jbig2HuffmanOOB);

루프 뒤에 덧붙는 두 줄은 Annex B.2의 escape 라인입니다. 하위 range 라인은 HTLOW에서 1을 뺀 값에서 시작해 아래로 세고, 상위 range 라인은 HTHIGH에서 시작해 32비트 고정 범위를 가지며, 선택적인 OOB 라인은 값이 아예 없습니다. PDFlibPas는 이들을 센티널 range 길이 jbig2HuffmanLOW($FFFFFFFD)와 jbig2HuffmanOOB($FFFFFFFE)로 표시하는데, 내장 표준 테이블 열다섯 개가 쓰는 것과 같은 규약입니다. 그래서 디코딩 루프는 테이블이 규격에서 왔는지 파일에서 왔는지 신경 쓰지 않습니다

prefix 코드는 왜 테이블 라인 순서로 부여해야 할까요?

인코더가 코드를 아예 쓰지 않기 때문입니다. JBIG2 Tables 세그먼트는 prefix 길이만 담고, 양쪽이 Annex B.3의 정규 절차로 실제 비트 패턴을 복원합니다. 각 길이를 가진 라인이 몇 개인지 세고, 길이 1인 코드부터 부여한 다음 왼쪽으로 시프트하며 이어 가되, 같은 길이 안에서는 라인이 나타난 순서대로 코드를 배정합니다. 이 순서를 조금이라도 벗어나면 조용히 다른 테이블이 만들어집니다. 디코더는 알아채지 못합니다. 자기가 만들어 내는 모든 비트 패턴이 여전히 유효한 prefix 코드이긴 하고, 다만 인코더가 쓴 것과 다를 뿐이기 때문입니다. 그 결과로 나오는 것은 엉뚱한 심볼로 조립된, 그럴듯해 보이는 비트맵입니다

PDFlibPas JBIG2 디코더의 정규 prefix 코드 부여: 세그먼트에는 prefix 길이만 도착하고, Counts, Starts, Positions에 대한 안정 counting sort가 각 길이 안에서 선언 순서를 유지하며, 길이 1인 코드부터 배정하고 길이마다 코드를 왼쪽으로 시프트하며, 초과 구독은 Kraft 검사로 거부됩니다
인코더는 비트 패턴을 쓰지 않으므로 테이블 라인 순서를 조금이라도 벗어나면 다르지만 유효한 prefix 코드가 조용히 만들어지고 출력은 그럴듯해 보입니다. 길이 0인 라인은 미사용으로 빠지고 32비트보다 긴 prefix는 거부됩니다
// THuffmanDecoder.buildTable, PDFlibJBIG2.pas
FillChar(Counts, SizeOf(Counts), 0);
for I := 0 to length - 1 do
begin
  if table[I].prefixLen > 32 then
    raise EJBIG2DecodeError.Create(
      'Huffman prefixes longer than 32 bits are not supported');
  Inc(Counts[table[I].prefixLen]);
end;
Active := 0;
Code := 0;
for Bits := 1 to 32 do
begin
  Starts[Bits]    := Active;
  Positions[Bits] := Active;
  Inc(Active, Counts[Bits]);
  if Code + UInt64(Counts[Bits]) > (UInt64(1) shl Bits) then
    raise EJBIG2DecodeError.Create('oversubscribed Huffman prefix codes');
  Code := (Code + UInt64(Counts[Bits])) shl 1;
end;
SetLength(Result, Active + 1);
for I := 0 to length - 1 do            // 안정 정렬: 원래 순서 유지
  if table[I].prefixLen > 0 then       // 각 prefix 길이 안에서
  begin
    Result[Positions[table[I].prefixLen]] := table[I];
    Inc(Positions[table[I].prefixLen]);
  end;
Code := 0;
for Bits := 1 to 32 do
begin
  for I := Starts[Bits] to Positions[Bits] - 1 do
  begin
    Result[I].prefix := Cardinal(Code);
    Inc(Code);
  end;
  Code := Code shl 1;
end;
Result[Active].rangeLen := jbig2HuffmanEOT;

THuffmanDecoder.buildTable이 비교 정렬이 아니라 counting sort인 데는 이유가 하나 있습니다. Counts, Starts, Positions에 대한 카운팅 패스는 구조적으로 안정 정렬이라, prefix 길이가 같은 라인이 선언된 순서 그대로 결과에 들어갑니다. Annex B.3이 코드를 배정하는 기준이 바로 그 순서입니다. prefix 길이가 0인 라인은 코드 배정 전에 빠집니다. B.3이 이들을 1비트 코드가 아니라 미사용으로 정의하기 때문입니다. 같은 루프 안에 가드가 두 개 있습니다. 초과 구독 검사는 그 깊이의 prefix 코드가 담을 수 있는 것보다 많은 코드를 길이가 주장하는 테이블을 잡아내는데, 이는 Kraft 부등식을 정수 비교로 표현한 것입니다. 이 검사가 없으면 악의적인 테이블이 두 라인에 동시에 맞는 코드를 만들어 내고, 디코더는 먼저 훑는 쪽을 골라 버립니다. 32비트 상한이 있는 이유는 prefix가 Cardinal이고 decodeInt의 매처가 비트를 하나에 모으기 때문입니다. T.88은 종이 위에서는 더 긴 prefix를 허용하지만 PDFlibPas는 이름을 붙여 거부하며, 실제 인코더가 그런 걸 내보내는 사례는 본 적이 없습니다. 값 연산에도 코드 연산만큼의 주의가 필요합니다. THuffmanTable.val은 Int64이고, 하위 range 라인은 HTLOW에서 1을 뺀 값에서 32비트 부호 없는 오프셋을 빼는 val - readBits(32)로 디코딩됩니다. Integer 중간값을 쓰면 그 뺄셈이 감싸 돌고, 감싸 돈 값이 그대로 심볼 폭으로 받아들여집니다. 64비트 경로는 실제 값을 계산하고, 부호 있는 32비트 범위에 맞는지 검사하고, 맞지 않으면 예외를 냅니다. 조용한 손상이 명시적인 거부로 바뀌는 지점입니다

3.539.22 이전에는 커스텀 테이블이 왜 한 번도 발동하지 않았을까요?

두 결함이 서로를 가려 주고 있었습니다. 첫 번째는 한 줄짜리 setter 버그였습니다. TTextRegionHuffmanFlags.setFlags가 자신이 저장할 필드와 같은 이름으로 인자를 받아서, Self.flagsAsInt := flagsAsInt가 초기화되지 않은 필드를 자기 자신에 대입했고, 모든 셀렉터가 0으로 읽혔습니다. 그래서 커스텀 테이블을 요청하는 텍스트 영역이 대신 표준 테이블 F, H, K를 거쳐 갔습니다. 두 번째 결함 때문에 첫 번째만 고쳤다면 심볼은 여전히 손상됐을 것입니다. Huffman 심볼 딕셔너리가 심볼을 비압축 collective bitmap으로 저장할 때 각 행의 마지막 바이트는 부분 바이트인데, 예전 복사 루프는 유효 비트 수를 담은 padding을 가장 낮은 유효 비트의 위치로 취급했습니다. 폭 63픽셀인 행은 마지막 바이트에서 7비트가 아니라 1비트만 복사했습니다. 수정된 루프는 for bitPointer := 7 downto ((8 - padding) and 7)로 돌고, 7비트와 9비트 폭의 합성 픽스처가 바이트 경계 양쪽을 고정합니다. 셀렉터가 제대로 읽히면 테이블은 규격이 나열한 순서대로 배분됩니다. 텍스트 영역은 T.88 §7.4.3.1.2가 FS, DS, DT, RDW, RDH, RDX, RDY, RSIZE로 정하고, 심볼 딕셔너리는 §7.4.2.1.1이 DH, DW, BMSIZE, AGGINST로 정합니다. 2비트 셀렉터는 각각 표준 테이블 0 또는 1, 표준 테이블이 두 개뿐인 필드에서는 예약인 2, 커스텀인 3을 뜻하며, 커스텀을 고를 때마다 참조된 세그먼트 가운데 다음 Tables 세그먼트를 참조 순서대로 소비합니다. NextCustomHuffmanTable이 정확히 그 순회를 하고, 영역이 참조한 테이블 수가 셀렉터가 요구하는 수보다 적으면 missing custom Huffman table reference를 냅니다. 같은 수정에 한 줄이 더 있습니다. 입력 심볼과 새 심볼을 합쳐 하나뿐인 Huffman 심볼 딕셔너리는 log2 공식으로 심볼 코드 길이가 0으로 계산되지만, 이 포맷의 Huffman 변형은 모든 심볼 ID를 최소 1비트로 씁니다. 그래서 TSymbolDictionarySegment의 if sdHuffman and (symbolCodeLength = 0) then symbolCodeLength := 1이 refinement와 aggregate 경로가 심볼 ID마다 0비트를 읽는 것을 막습니다

세그먼트 경계가 보장하는 것은 무엇일까요?

PDFlibPas는 모든 헤더의 세그먼트 데이터 길이를 양방향이 지켜야 하는 계약으로 취급합니다. 세그먼트는 선언된 끝을 넘겨 읽을 수 없고, 짧게 끝나서 다음 헤더를 예측할 수 없는 위치에 남겨 둘 수도 없습니다. 이 계약에서 따라 나오는 규칙들은 하나하나 작습니다. 비트 31이 서 있는 데이터 길이는 T.88 §7.2.7의 unknown-length 마커이고, handleSegmentDataLength는 이를 음수로 매핑합니다. readSegments는 종료자를 찾아 앞으로 훑는 대신 그 음수를 곧바로 거부합니다. 참조되는 모든 세그먼트 번호는 현재 세그먼트 번호보다 작아야 하고 이미 존재해야 하므로, 앞을 가리키거나 끊어진 참조는 어떤 영역이 그것을 해석하려 들기 전에 실패합니다. END_OF_PAGE와 END_OF_FILE은 데이터 0바이트를 선언해야 합니다. Profiles 세그먼트(type 52)는 32비트 카운트와 그 개수만큼의 32비트 식별자를 담고 픽셀은 전혀 없으므로, 선언된 길이에 대해 4 더하기 4 곱하기 카운트로 검사하고 건너뛰며, 뒤 세그먼트가 번호로 참조할 수 있도록 세그먼트 목록에만 남겨 둡니다. 알 수 없는 프로파일 식별자는 알 수 없는 인코딩이 아니며, 그렇게 취급하면 멀쩡히 디코딩되는 파일까지 거부하게 됩니다

// TJBIG2StreamDecoder.readSegments, PDFlibJBIG2.pas
DataLength := segmentHeader.getSegmentDataLength;
if DataLength < 0 then
  raise EJBIG2DecodeError.Create(Context +
    'unknown or oversized segment length is not supported');
if DataLength > Length(reader.Data) - reader.bytePointer then
  raise EJBIG2DecodeError.Create(Context + 'truncated segment data');
DataEnd := reader.bytePointer + DataLength;
for I := 0 to noOfReferredToSegments - 1 do
  if (referredToSegments[I] >= segmentHeader.getSegmentNumber) or
     (findSegment(referredToSegments[I]) = nil) then
    raise EJBIG2DecodeError.Create(Context + 'invalid segment reference');
// ... 이 타입의 세그먼트 객체를 생성합니다 ...
reader.SegmentEnd := DataEnd;
segment.readSegment;
if reader.bytePointer > DataEnd then
  raise EJBIG2DecodeError.Create(Context +
    'decoded data exceeds declared segment length');
if reader.bytePointer < DataEnd then
begin
  reader.bytePointer := DataEnd;   // MMR은 EOFB를 읽지 않고 남길 수 있습니다
  reader.bitPointer := 7;
end;

그 루프의 꼬리 부분이, 예전 버전 디코더가 MMR로 코딩된 영역에서 틀렸던 자리입니다. MMR 디코더는 마지막 행의 마지막 픽셀이 나오면 끝났다는 것을 압니다. T.88 §6.2.5.7이 데이터 끝에 두는 EOFB 종료자를 아직 소비하지 않았는데 그 시점이 올 수 있습니다. 예전 코드는 리더가 이미 다음 헤더에 있다고 가정했기 때문에, 남은 종료자 바이트가 세그먼트 번호로 파싱되고 몇 바이트 뒤에서 엉뚱한 오류와 함께 스트림이 실패했습니다. 이제는 선언된 끝이 이깁니다. 그 끝을 넘겨 읽으면 오류이고, 짧게 끝나는 것은 정상이며, 리더는 DataEnd로 옮겨지고 비트 포인터는 초기화되어 다음 헤더를 파일이 말한 위치에서 읽습니다. 같은 원칙이 PDFlibPas가 신뢰할 수 없는 PDF 구조를 파싱하는 모든 곳에 나타납니다. 선언된 길이가 경계이고, 디코더는 더 친절한 경계를 찾아 나서지 않습니다

Huffman refinement는 비트맵 크기를 어디서 읽을까요?

arithmetic 디코더가 시작하기 전에, 그리고 Huffman 모드에만 있는 필드에서 읽습니다. 텍스트 영역 인스턴스가 refinement를 가지고 있고(RI가 0이 아님) SBHUFF가 설정되어 있으면, T.88 §6.4.11에 따라 디코더는 선택된 테이블로 RDW, RDH, RDX, RDY를 읽고, RSIZE 테이블로 BMSIZE를 읽고, 바이트 경계로 정렬한 다음에야 정확히 BMSIZE 바이트에 대해 일반 refinement 디코딩을 실행합니다. arithmetic 모드 텍스트 영역에는 그런 필드가 없으므로, 두 모드가 코드 경로 하나를 공유하는 디코더는 이 필드를 건너뛰고 arithmetic 디코더를 두 바이트 이상 일찍 시작해 모든 심볼을 쓰레기와 비교하며 refinement하게 됩니다. §6.5.8.2.2에 나오는 REFAGG와 단일 refinement 인스턴스를 쓰는 심볼 딕셔너리 경로도 같은 BMSIZE 필드를 가지며 결과도 같습니다. PDFlibPas에서 이 크기의 상한은 전체 스트림의 끝이 아니라 readSegments가 설정한 현재 세그먼트의 끝인 TStreamReader.SegmentEnd입니다. 다음 세그먼트의 바이트를 빌려야만 만족되는 BMSIZE는 이미 잘못된 값이고, 이를 스트림 길이로 검증하면 arithmetic 디코더가 다음 헤더까지 읽어 들어가기 때문입니다. 하한 2바이트는 arithmetic 디코더가 항상 소비하는 첫 바이트 쌍을 반영한 것이고, refinement가 끝나면 리더는 arithmetic 디코더가 얼마나 앞서 읽었든 상관없이 RefinementEnd로 건너뜁니다. 그 디코더의 최종 위치는 다음 Huffman 코딩 필드의 위치가 아니기 때문입니다

PDFlibPas JBIG2 디코더의 Huffman 모드 refinement 경계: RDW, RDH, RDX, RDY가 자기 테이블에서 디코딩되고 BMSIZE가 RSIZE 테이블에서 디코딩되어 바이트 정렬된 뒤, arithmetic 디코더가 RefinementEnd와 SegmentEnd 사이에 담긴 정확히 BMSIZE 바이트를 refinement하며, 2바이트 미만이거나 세그먼트 경계를 넘는 크기는 거부합니다
2바이트라는 하한은 arithmetic 디코더가 항상 소비하는 첫 바이트 쌍을 반영하고, 상한은 전체 스트림이 아니라 현재 세그먼트이며, refinement 뒤에 리더는 앞서 읽은 양과 무관하게 RefinementEnd로 건너뜁니다
// TJBIG2Bitmap 텍스트 영역 디코딩, Huffman refinement 경로
RefinementSize := huffmanDecoder.decodeInt(huffmanRSizeTable).intResult;
huffmanDecoder.consumeRemainingBits;
if (RefinementSize < 2) or
   (RefinementSize > huffmanDecoder.reader.SegmentEnd -
                     huffmanDecoder.reader.bytePointer) then
  raise EJBIG2DecodeError.Create('invalid refinement bitmap size');
RefinementEnd := huffmanDecoder.reader.bytePointer + RefinementSize;
arithmeticDecoder.start;
// ... readGenericRefinementRegion ...
if huffmanDecoder.reader.bytePointer > RefinementEnd then
  raise EJBIG2DecodeError.Create('refinement data exceeds declared size');
huffmanDecoder.reader.bytePointer := RefinementEnd;
huffmanDecoder.reader.bitPointer := 7;

검증한 것과 여전히 거부하는 것

이 일을 시작하게 한 샘플, 즉 커스텀 테이블과 Huffman refinement를 쓴 500x473 픽셀 JBIG2 이미지는 이제 독립적인 디코더와 비교해 다른 픽셀이 0인 비트맵으로 디코딩되고, 합성 7비트·9비트 collective bitmap 픽스처도 양쪽에서 기대한 행을 만들어 냅니다. 원래 샘플에서 서로 다른 결과를 내던 두 독립 디코더는 지금도 서로 다릅니다. PDFlibPas는 그중 하나와 일치하고, 정직하게 말하면 네이티브 출력이 하나의 독립 구현 및 읽은 그대로의 규격과 일치한다는 것이지 세상 모든 디코더가 일치한다는 것은 아닙니다. 스위트의 잘못된 입력 쪽은 다음을 다룹니다:

  • 예약된 flag 비트 또는 예약된 셀렉터 값
  • 라인 중간에서 잘린 테이블
  • 초과 구독된 prefix 길이와 32비트보다 긴 prefix
  • 셀렉터가 참조한 것보다 많은 커스텀 테이블을 요구하는 영역
  • 디코딩 실패 후 낡은 출력이 호출자가 결과로 착각하도록 남겨지지 않고 지워지는지 확인

세 가지 제한은 의도적으로 남겨 둔 것입니다. 모든 세그먼트 헤더가 세그먼트 데이터보다 앞서는 random-access 스트림 구성은 파일 헤더 flags를 읽는 즉시 JBIG2 random-access organisation is not supported를 냅니다. 검증에 쓸 대표 샘플이 없고, 절반만 구현된 경로는 이름 있는 거부보다 나쁘기 때문입니다. 커스텀 테이블은 65,536라인과 32비트 prefix로 상한이 정해져 있습니다. 그리고 공개 디코딩 진입점 TPLJBIG2Decoder.LoadFromByteArray는 페이지 연관 번호 0을 찾는 대신, 스트림 순서상 첫 페이지 비트맵을 getPageAsJBIG2Bitmap(0)로 반환합니다. 즉 마주치는 첫 페이지 정보 세그먼트를 씁니다. 임베드된 PDF 스트림은 한 장뿐인 페이지에 1번을 붙이는 일이 흔하고, 연관 번호로 0번 페이지를 요청하면 아무것도 찾지 못하기 때문입니다. 오류 문구는 TPLJBIG2Decoder.LastError에 들어갑니다. 결함의 세그먼트 번호, 타입, 바이트 오프셋을 담는 내부 디코더 진단이며, 라이브러리 수준의 TPDFlib.LastErrorCode와는 다른 것입니다. 여기까지는 인코딩 쪽과 무관합니다. 인코딩은 JBIG2 인코더 백엔드와 링크 방식 노트에서 다룹니다. 읽기 경로는 남의 인코더가 내보내기로 한 것을 그대로 받아들여야 하고, 규칙은 나머지 이미지 스택과 공유합니다. 내장 TIFF 디코더와 그 BigTIFF·타일 레이아웃 거부도 마찬가지입니다. 이름을 붙여 거부하고, 선언된 경계를 넘어 바이트를 빌리지 않고, 감싸 돈 중간값이 유효한 답으로 통과하지 못할 만큼 넓게 연산합니다. Delphi나 C++Builder용 네이티브 JBIG2 읽기 경로를 검토 중이라면, 디코더와 나머지 이미지 처리에 대한 문서는 PDF Library for Delphi 페이지에 있습니다