델파이 PDF 라이브러리의 범위 확인 오류는 일관된 입력 패턴을 따르지 않기 때문에 원인을 파악하기 어렵다는 평판을 얻고 있습니다. 동일한 문서가 한 컴퓨터에서는 오류를 생성하지만 다른 컴퓨터에서는 생성하지 않으며, 동일한 코드 경로가 3페이지 파일에서는 예외를 발생시키지만 12페이지 파일에서는 깨끗하게 실행됩니다. 이러한 불일치는 거의 항상 단일 근본 원인으로 추적됩니다. 즉, PDF 페이지 객체는 파일 순서대로 저장되지 않습니다. 라이브러리가 카탈로그에 선언된 페이지 트리를 탐색하는 대신 객체를 순차적으로 스캔하여 내부 페이지 배열을 구축하는 경우, 유효한 범위가 호출자가 예상하는 것과 일치하지 않는 인덱스를 구성하게 되며 범위 확인은 가장 최악의 순간에 그 불일치를 포착합니다
델파이에서 범위 확인이 작동하는 방식
{$R+} 컴파일러 지시문이 활성화된 상태(디버그 구성의 기본값)에서 델파이 RTL은 런타임에 모든 배열 인덱스, 문자열 첨자 및 열거형 할당을 유효성 검사합니다. 범위를 벗어난 액세스는 조용히 인접한 메모리를 읽는 대신 ERangeError를 발생시킵니다. 그 동작은 가치가 있습니다: 데이터 구조를 손상시켜 수백 줄 뒤에 실패하게 놔두는 대신 잠재적인 버그를 일찍 드러냅니다. 실망스러운 부분은 예외가 인덱스가 잘못 계산된 지점이 아니라 액세스 사이트에서 발생한다는 것입니다. 호출 스택이 PDF 유닛의 깊게 중첩된 메서드를 가리킬 때 실제 실수는 일반적으로 여러 프레임 뒤에 있습니다
복합 부울 조건은 이 문제를 더 악화시킵니다. 델파이는 단락(short-circuit) 의미론을 사용하여 and 식을 왼쪽에서 오른쪽으로 평가하지만, 단락은 왼쪽이 False일 때만 평가를 건너뜁니다. 다음과 같은 식은:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
안전해 보이지만 FDocStarted가 True이고 DestIndex가 음수가 아닌 경우에만 범위를 벗어난 인덱스로부터 보호합니다. DestIndex < Length(PageArr) 확인은 DestIndex가 음수일 때 아무 작업도 수행하지 않습니다. 부호 있는 산술 연산에서 음의 정수를 음이 아닌 길이와 비교하면 True가 반환되고 후속 배열 액세스는 여전히 범위 오류를 발생시키기 때문입니다. 범위 확인을 가장 바깥쪽 위치로 이동하는 것이 올바른 수정 사항입니다:
if (DestIndex >= 0) and (DestIndex < Length(PageArr)) then
begin
if FDocStarted and (PageArr[DestIndex].PageObj <> nil) then
Result := PageArr[DestIndex].PageObj
else
Result := nil;
end
else
raise ERangeError.CreateFmt(
'Page index %d is out of range (0..%d)',
[DestIndex, Length(PageArr) - 1]);
이것은 기계적인 수정입니다. 충돌을 멈춥니다. 애초에 DestIndex가 유효한 범위를 벗어난 값을 받은 이유는 설명하지 않습니다
진짜 원인: 객체 순서 대 페이지 순서
ISO 32000-1 §7.7.3은 페이지 트리를 Kids 배열이 표시 순서대로 페이지 객체를 나열하는 Pages 노드의 트리로 정의합니다. 파일은 작성자가 선택한 오프셋에 이러한 객체를 저장합니다. 객체 번호 20은 바이트 스트림에서 객체 번호 3보다 물리적으로 선행할 수 있습니다. Kids 체인을 따르는 대신 상호 참조 테이블을 객체 번호 순서로 반복하여 페이지 목록을 작성하는 라이브러리는 사용자가 예상하는 것과 다른 시퀀스를 생성합니다. 생성기가 우연히 페이지를 순서대로 작성한 문서에서는 모든 것이 잘 작동합니다. 그렇지 않은 문서에서는 라이브러리의 페이지 번호 매기기와 호출자의 페이지 번호 매기기 사이의 불일치로 인해 PageArr을 벗어나는 인덱스가 생성됩니다
올바른 접근 방식은 카탈로그에서 시작하여 /Pages 간접 참조를 확인하고 Kids 배열을 재귀적으로 탐색하는 것입니다. 중간 Pages 노드가 없는 평면 문서의 경우 탐색은 간단합니다:
procedure BuildPageIndexFromTree(
const KidsArray: THPDFArray;
var PageArr: TPageObjArray);
var
i, Idx: Integer;
Child: THPDFObject;
ChildType: string;
begin
for i := 0 to KidsArray.Count - 1 do
begin
Child := KidsArray.GetIndirectObject(i);
if Child = nil then
Continue;
ChildType := Child.GetNameValue('/Type');
if ChildType = 'Page' then
begin
Idx := Length(PageArr);
SetLength(PageArr, Idx + 1);
PageArr[Idx].PageObj := Child;
end
else if ChildType = 'Pages' then
begin
// 중간 노드: Kids로 재귀
BuildPageIndexFromTree(Child.GetArray('/Kids'), PageArr);
end;
end;
end;
이 실행이 끝난 후 PageArr[0]은 해당 객체가 바이트 스트림의 어디에 위치하든 관계없이 뷰어가 표시할 첫 번째 페이지입니다. 표시 순서를 가정하는 호출자가 전달한 인덱스가 이제 올바르게 매핑되고 범위 오류가 중지됩니다
하드코딩된 임시방편(workarounds)은 문제를 악화시킵니다
근본 원인이 밝혀지지 않은 코드베이스에서는 경험적 패치를 찾는 것이 일반적입니다: 총 개수가 3과 같으면 첫 번째와 마지막 페이지를 바꿉니다, 특정 생성기의 문서에 대해 인덱스를 회전시킵니다, 첫 번째 객체 번호가 임계값을 초과할 때 오프셋을 적용합니다. 이러한 각 패치는 작성될 때 수중에 있었던 테스트 파일 세트와 정확히 일치합니다. 다른 PDF 소스를 추가하면 해당 패치 중 하나가 잘못된 시간에 실행되어 이중으로 잘못된 인덱스를 생성합니다: 순서가 잘못된 배열에서 계산되었기 때문에 잘못되었고, 적용할 수 없는 매핑이 그 위에 적용되었기 때문에 다시 잘못되었습니다. 범위 검사기가 다운스트림 어딘가에서 이를 포착하고 스택 트레이스는 유용한 곳을 가리키지 않습니다
유일한 생산적인 방법은 모든 경험적 매핑을 제거하고 페이지 배열 구성을 적절한 트리 탐색으로 대체하는 것입니다. 인덱스가 구조적으로 정확해지면 패치가 필요 없으며 범위 검사기는 방해물이 아니라 자산이 됩니다
이 패턴을 나타내는 라이브러리를 유지 관리하는 경우 일시적으로 릴리스(Release) 빌드에서 범위 확인을 활성화하고 Word, LaTeX, 스캐너 펌웨어, PDF-to-PDF 분할 유틸리티에서 생성한 문서 등 다양한 PDF 말뭉치에 대해 실행해 보세요. 예외를 트리거하는 파일은 페이지 객체 순서가 코드가 가정하는 탐색 순서와 다른 파일입니다. 각각은 별개의 버그가 아니라 데이터 포인트입니다
델파이 PDF 라이브러리를 호출하는 새 코드의 경우, 실용적인 조언은 라이브러리의 페이지 수를 권위 있는 것으로 취급하고 0..PageCount - 1 내에 있는지 먼저 확인하지 않고 외부 데이터의 산술에서 파생된 인덱스를 절대 전달하지 않는 것입니다. HotPDF 컴포넌트는 BeginDoc 이후 또는 문서를 로드한 후 THotPDF.PageCount를 통해 결정된 페이지 수를 노출합니다. 이 값은 항상 페이지 트리 탐색을 반영하며 임의의 인덱스 산술 연산에 대한 상한으로 사용하기에 안전합니다