PDFlibPas는 libtiff 바인딩 대신 직접 작성한 Object Pascal 파서로 TIFF를 디코드하며, 3.534.1 버전은 이 파서가 입력을 거부하는 지점을 정확히 조였습니다. BigTIFF 매직 43은 이제 이름을 붙여 거부하고, TileOffsets와 TileByteCounts는 태그 파싱 단계에서 거부하며, 모든 버퍼는 256 MiB 디코드 상한 안에서 Int64 산술로 크기를 잡습니다
이번 수정이 막은 결함은 실험실에서는 절대 나타나지 않습니다. 3년간 조용히 돌아온 스캔 게이트웨이에서, 고객이 지리 공간 아카이브나 병리 슬라이드 의료 영상을 통과시키는 순간 나타납니다. 파일에는 정상적인 TIFF 헤더가 있습니다. 파싱도 됩니다. 그 결과로 나오는 것은 줄무늬 노이즈 한 페이지이거나 서비스를 끌어내리는 수 기가바이트 할당이고, 그 어느 단계에서도 입력이 잘못됐다고 선언하는 이는 없습니다. 공학적으로 방어할 가치가 있는 것이 바로 이 실패 형태입니다. 크래시가 아니라, 당당하게 전달되는 오답입니다
II나 MM만으로 클래식 TIFF라고 확신할 수 없는 이유
바이트 순서 마커는 두 계열이 공유하기 때문입니다. 클래식 TIFF와 BigTIFF는 모두 II나 MM으로 시작하고, 실제로 둘을 가르는 필드는 바로 뒤의 16비트 매직입니다. TIFF 6.0 명세가 정의하는 클래식 TIFF는 42, 64비트 오프셋을 쓰는 BigTIFF는 43입니다. FValidTIFF := PopWord = 42로 쓴 로더는 클래식 TIFF에 대해서는 틀리지 않지만, 본질적으로 다른 두 거부를 하나의 조용한 불리언으로 뭉개버립니다. 그래서 BigTIFF가 이름만 바꾼 잘린 JPEG과 구분되지 않습니다. PDFlibPas는 이제 사례를 분리하고 각각을 TPDFTIFF.LastError에 기록합니다. 4바이트 미만의 헤더, 잘못된 바이트 순서 마커, 매직 43, 그 밖의 매직 값이 모두 별개의 텍스트를 냅니다. 라이브러리는 여전히 BigTIFF를 디코드하지 않으며, 그렇다고 분명히 말하는 것 자체가 요점입니다. 호출자는 “이 파일은 TIFF가 아니다”와 “이 파일은 TIFF이지만 내장 디코더가 구현하지 않는 64비트 오프셋 레이아웃을 쓴다”의 차이를 받습니다. 이는 답장 한 번으로 끝나는 지원 티켓과 일주일의 추측으로 번지는 티켓의 차이입니다
var
Tiff: TPDFTIFF;
Page: Integer;
begin
Tiff := TPDFTIFF.Create;
try
Tiff.LoadFromFile('inbox\scan-0417.tif');
if not Tiff.ValidTIFF then
raise Exception.Create('TIFF rejected: ' + Tiff.LastError);
if Tiff.PageCount < 1 then
raise Exception.Create('TIFF carries no decodable page');
for Page := 1 to Tiff.PageCount do
Writeln(Format('page %d: %dx%d, %d spp',
[Page,
Tiff.PageInfo[Page].Width,
Tiff.PageInfo[Page].Height,
Tiff.PageInfo[Page].SamplesPerPixel]));
finally
Tiff.Free;
end;
end;
타일은 다른 오프셋 배열이 아니라 다른 기하 구조다
PDFlibPas는 픽셀 데이터를 만지기 전, 태그를 파싱하는 동안 타일 TIFF를 거부합니다. 버그를 부르는 지름길은 눈에 보입니다. 태그 324(TileOffsets)와 태그 325(TileByteCounts)는 파일 오프셋과 바이트 수의 배열로, 구조적으로 스트립 배열과 동일합니다. 그래서 기존 스트립 필드만 여기에 연결하면 두 줄이면 끝나고 깨끗이 컴파일됩니다. 그러나 그것은 틀렸습니다. 타일은 가장자리가 패딩된 2차원 격자를 이루고, 타일마다 자체 행 스트라이드를 가지며, RowsPerStrip 의미는 전혀 없습니다. TIFF 6.0의 타일 이미지 섹션이 이를 명시합니다. 타일 페이로드를 스트립 디코더에 밀어 넣으면 시끄럽게 실패하지 않습니다. SimpleExtract와 CompDecode는 잘못된 스트라이드로 데이터를 걸어가며, 크기는 맞고 픽셀은 틀린 이미지를 내놓습니다. 예전 코드는 여기에 TTIFFPage에 StripsAreTiles, ColumnsPerTile, RowsPerTile을 유지하는 문제까지 겹쳤습니다. 타일 조립기가 없는 디코더가 기록한 타일 기하였습니다. 3.534.1에서는 태그 324와 325의 핸들러가 타일 오류를 내고 IFD를 즉시 포기하므로, 거부가 “tiled”라는 단어를 실어 나르고 몇 주 뒤 렌더링 불만으로 표면화하지 않습니다
차원 하나의 클램프는 메모리 예산이 아니다
너비와 높이를 각각 65,535로 제한하는 것은 필요하지만 턱없이 부족합니다. 할당량을 결정하는 값은 곱이기 때문입니다. RowsPerStrip * Width * SamplesPerPixel는 어느 한쪽이 자기 한계에 닿기도 전에 32비트 산술을 넘칠 수 있고, 넘치지 않더라도 그 어떤 서비스도 시도해서는 안 되는 할당을 가리킬 수 있습니다. PDFlibPas는 행 바이트를 Int64로 계산하고 세 상한을 함께 적용합니다. 차원당 65,535, 색상 구성 요소 32개, 디코드 바이트 256 MiB
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// TPDFTIFF.ValidatePageForDecode 내부
BitsPerPixel := Int64(P.BitsPerSample) * P.SamplesPerPixel;
RowBytes := (Int64(P.Width) * BitsPerPixel + 7) div 8;
if (RowBytes < 1) or
(RowBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(Int64(P.Height) > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES div RowBytes) then
Exit(False);
DecodedBytes := RowBytes * P.RowsPerStrip;
if (DecodedBytes < 1) or
(DecodedBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(DecodedBytes > MaxInt) then
Exit(False);
여기에는 상수 자체보다 중요한 세부 사항이 세 가지 있습니다. 높이 검사는 곱셈이 아니라 나눗셈으로 쓰여, 과도한 곱이 아예 만들어지지 않습니다. 1 미만이거나 이미지 높이를 초과하는 RowsPerStrip은 먼저 높이로 정규화되는데, 이는 TIFF 6.0이 이미 암시하는 단일 스트립 해석이며 적대적 태그가 스트립 버퍼를 부풀리는 것을 막습니다. 그리고 이 루틴은 공유됩니다. ValidatePageForDecode는 태그 파싱 끝에서 한 번, SimpleExtract와 CompDecode 두 진입점에서 다시 실행되므로, 디코더로 곧장 향하는 코드도 예산을 우회할 수 없습니다. 이는 PDFlibPas가 신뢰할 수 없는 PDF 객체 그래프를 파싱할 때 따르는 것과 같은 규칙입니다. 세 개 문 중 하나에서만 집행되는 제한은 제한이 아니기 때문입니다
PageInfo를 읽기 전에 호출자가 무엇을 확인해야 하는가
ValidTIFF를 먼저 확인하고, 다음으로 PageCount를 확인한 뒤에야 PageInfo를 인덱싱합니다. 거부된 파일은 PageCount를 0으로 남길 수 있고, GetPageInfo는 범위를 벗어난 인덱스에 초기화되지 않은 TTIFFPage 레코드를 돌려주므로, 오류를 보고하러 가는 길에 해상도나 샘플 수를 읽는 오류 경로는 결국 노이즈를 읽습니다. 3.534.1은 라이브러리 내부의 두 호출자를 모두 고쳤습니다. 이미지 가져오기 경로는 유효한 분기 안에서만 XRes와 YRes를 읽고, TPDFlib.GetImagePageCount는 0이 아닌 페이지 수를 그대로 믿는 대신 ValidTIFF를 요구합니다. 또한 AddImageFromFile의 Options 인수는 다중 페이지 TIFF에서 1 기반 페이지 번호이므로, GetImagePageCount는 루프가 끝난 뒤가 아니라 시작되기 전에 신뢰할 수 있어야 합니다. 페이지 0은 이제 조기 반환의 사고가 아니라 “여기에는 디코드할 것이 없다”는 실제 답입니다. 이것이 가장 중요한 순간은 양면 스캔 배치를 콜레이트하고 인터리브할 때입니다. 조용히 잘못 디코드된 한 장이 엉뚱한 위치에 끼어들 수 있으니까
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // 잘못된 헤더, BigTIFF, 타일 레이아웃 또는 예산 초과
Pdf.NewDocument;
for I := 1 to Pages do
begin
Pdf.NewPage;
ImageID := Pdf.AddImageFromFile('inbox\scan-0417.tif', I);
if ImageID > 0 then
begin
Pdf.SelectImage(ImageID);
Pdf.DrawImage(0, 0, 595, 842);
end;
end;
Pdf.SaveToFile('scan-0417.pdf');
finally
Pdf.Free;
end;
end;
디코더를 만들 것인가, libtiff를 링크할 것인가
PDFlibPas는 내장 디코더를 유지하며, 결정 요인은 작성 주체가 아니라 플랫폼 도달 범위입니다. 약 1,873줄의 Object Pascal은 컴파일러가 가는 곳이라면 어디서든 컴파일됩니다. Win32, Win64, macOS, iOS, Android, 그리고 Linux의 FPC까지. libtiff 4.7.1은 34개의 tif_*.c 번역 단위에 흩어진 약 30,000줄의 C이고, 현재 존재하는 사전 빌드 오브젝트 파일은 Windows만 커버합니다. 이를 채택하면 완전한 TIFF 커버리지를, C 툴체인을 돌릴 수 있는 기기만 남는 지원 플랫폼 목록과 아직 아무도 검증하지 않은 링커 패스 한 번과 맞바꾸는 셈입니다
그 대가도 포장 없이 말할 가치가 있습니다. 내장 디코더는 스캔 문서 작업이 실제로 만들어내는 것을 처리합니다. CCITT Group 3 1차원과 2차원, Group 4, LZW, Deflate, PackBits, TIFF 내장 JPEG이며, WhiteIsZero, BlackIsZero, RGB, 팔레트, CMYK 포토메트릭에 Predictor 1과 2까지입니다. 이 페이로드들은 ISO 32000-1 §7.4.4와 §7.4.6의 PDF 필터와 나란히 놓이며, 그래서 TIFF 프런트엔드가 스캔 파이프라인에서 큰 비중을 차지합니다. 처리하지 못하는 것은 BigTIFF, 타일, 부동소수점 Predictor 3, PixarLog와 SGILog, 구식 JPEG 압축 6, 서브 IFD 피라미드입니다. 3.534.1부터 이 모든 것은 엉뚱한 이미지가 아니라 이름 붙은 거부가 되었고, 라이브러리는 libtiff 결정을 다시 열 트리거 목록을 문서로 유지합니다:
- 고객이 BigTIFF 파일을 보고하고 변환 단계가 아니라 기본 지원을 필요로 할 때
- 고객이 의료, GIS 또는 산업 소스의 타일 TIFF를 보고하고 제자리에서 디코드해야 할 때
- 고객이 부동소수점 Predictor 3 TIFF를 보고할 때
- 공개된 취약점이 내장 CCITT 또는 LZW 디코드 경로에 걸릴 때
- 크로스 플랫폼 논거가 힘을 잃을 때. macOS, iOS, Android 지원이 중단되거나, 재사용 가능한 libtiff 통합이 이미 macOS와 Linux를 커버하는 경우입니다
마이그레이션 자체는 가설이 아니라 범위가 정해진 작업입니다. USE_LIBTIFF 조건부 컴파일이 TPDFTIFF 공개 표면을 그대로 유지하고, LoadFromStream을 스트림 콜백과 함께 TIFFClientOpen으로 우회시키며, Pascal 파서는 비 Windows 폴백으로 남습니다. 그 트리거 중 하나가 실제로 작동하기 전까지는, 디코더 두 개와 두 배의 테스트 매트릭스를 유지하는 일이 고객이 체감할 어떤 이득도 사지 않습니다. 탈출로를 문서로 적어 둔 채 비용을 미루는 것은 그것을 무시하는 것과 다릅니다
스캔 문서 파이프라인이 서게 되는 지점
TPDFTIFF를 변환기가 아니라 게이트로 다루십시오. 파일을 로드하고 ValidTIFF를 읽고, false일 때마다 LastError를 그대로 기록하십시오. 그 문자열이야말로 현장 보고에서 진단으로 가는 가장 짧은 경로입니다. 게이트를 통과하지 못한 파일은 상류에서 변환하면 여전히 구제됩니다. 오늘날 BigTIFF와 타일 소스의 실용적 답이 그것입니다. TIFF 바깥의 입력에는 PDFlibPas가 별도 경로를 풉니다. AVIF, HEIF, JPEG XL 이미지 입력 경로입니다. 어떤 디코더가 어떤 포맷을 담당하는지는 저절로 정해지게 두지 않고 명시적으로 남습니다
이 모든 것은 평범한 이미지 API 뒤에 있으므로, 문서 파이프라인은 애초에 확인했어야 할 페이지 수 확인 외에는 호출 코드 한 줄도 바꾸지 않고 더 단단해진 경계를 얻습니다. 델파이나 C++Builder용 네이티브 TIFF to PDF 경로를 저울질하고 있다면, 전체 구성 요소와 이미지 처리는 PDF Library for Delphi 페이지에 문서화되어 있습니다