PDF Library for Delphi(PDFlibPas)는 AddWebCaptureCommand를 통해 문서 catalog에 Web Capture 명령 메타데이터를 기록하며, ISO 32000-1 §14.10에 정의된 /SpiderInfo 딕셔너리를 채웁니다. 각 명령은 가져온 URL, 가져오기가 몇 단계 깊이까지 진행됐는지, 그리고 작은 플래그 집합을 기록합니다. 의도를 기록할 뿐 실제로는 아무것도 가져오지 않습니다
이 구분 때문에 이 API가 쉽게 오해받습니다. 지원 엔지니어가 벤더 온라인 문서로 조립한 PDF를 건네며 40페이지가 어디서 왔는지 물어본다고 해 봅시다. 생성 쪽이 /SpiderInfo를 채웠다면 답은 catalog 안에 있습니다. URL, 크롤링 깊이, 어쩌면 그 페이지를 만든 POST 본문까지요. 채우지 않았다면 콘텐츠 스트림을 아무리 읽어도 알 수 없습니다. 이런 출처 기록은 쓰기는 싸고, 구조적으로는 까다롭고, 거의 항상 생략됩니다
Catalog /SpiderInfo 딕셔너리는 실제로 무엇을 기록할까요?
/SpiderInfo는 문서 catalog에 매달리는 Web Capture 정보 딕셔너리로, PDF Library for Delphi가 신경 쓰는 두 가지만 담습니다. /V는 Web Capture 데이터베이스 버전 숫자로 라이브러리는 항상 1.0으로 쓰며, /C는 명령 딕셔너리의 배열입니다. 이 버전 번호는 PDF 버전이 아니라 Web Capture 데이터 모델 자체를 식별하며, ISO 32000-1 §14.10이 정의한 값은 1.0뿐입니다. 흥미로운 것은 전부 /C에 있습니다. 각 명령 딕셔너리는 /URL(요청된 주소), /L(그 주소에서 따라간 링크 단계 수), /F(탐색 플래그)를 담습니다. 일반 GET이 아닌 요청을 설명하는 선택 키 세 개도 있습니다. /P는 POST 데이터, /CT는 그 데이터의 콘텐츠 타입, /H는 추가 요청 헤더입니다. PDF Library for Delphi는 이 여섯 개를 전부 단일 record인 TPDFlibWebCaptureCommand에 매핑하는데, 필드는 URL, Levels, Flags, PostData, ContentType, Headers이고 두 Integer 카운터를 제외하면 모두 AnsiString입니다. /SpiderInfo가 있으면 문서의 최소 버전도 올라갑니다. AddWebCaptureCommand는 EnsureMinVersion('1.3', 'Catalog /SpiderInfo Web Capture command')를 호출하고, PDF Library for Delphi의 버전 감지 규칙도 이에 맞는 PDF 1.3 규칙을 등록해서 나중에 다시 읽은 문서도 같은 최소 버전을 보고합니다
Web Capture 명령은 왜 간접 객체여야 하나요?
사람들이 자주 걸려 넘어지는 구조 규칙이 하나 있습니다. 명령 딕셔너리는 /C에 들어가기 전에 반드시 간접 객체로 등록해야 합니다. ISO 32000-1 §14.10이 /C를 명령 딕셔너리의 배열이 아니라 명령 딕셔너리에 대한 간접 참조 배열로 규정하기 때문입니다. 직접 딕셔너리를 그 배열에 밀어 넣으면 유효한 PDF로 파싱은 되지만 Web Capture record로는 아무 의미도 없는 결과물이 됩니다. 뷰어나 아카이브 도구가 명령 목록을 훑다가 참조가 있어야 할 자리에서 inline 객체를 발견하고, 그 항목은 그냥 명령이 아닌 것으로 처리합니다. PDF Library for Delphi는 이 처리를 AddWebCaptureCommand 안에서 합니다. 딕셔너리를 만들고 OnNewIndObj에 넘겨 객체 번호와 세대 번호를 얻은 뒤, 그 쌍을 간접 참조로 감싸 배열에 추가합니다. OnNewIndObj가 양수 객체 번호를 만들어 내지 못하면 함수는 0을 반환하고 배열은 그대로 둡니다. 스펙이 간접 참조를 고집하는 이유는 단일 명령이 공유될 수 있기 때문입니다. 같은 fetch는 Web Capture 구조 안에서 여러 곳에서 참조될 수 있고, PDF에서 공유 객체는 정의상 간접 객체입니다
Delphi에서 Web Capture 명령은 어떻게 추가하나요?
record를 완전히 채운 뒤 AddWebCaptureCommand를 호출하면 새 명령의 1-based 인덱스가 돌아오고, 검증에서 거부되면 0이 돌아옵니다. 여기서는 완성도가 평소보다 더 중요합니다. Delphi는 로컬 record의 관리형 AnsiString 필드를 빈 값으로 초기화하지만 Integer 필드, 즉 Levels와 Flags는 그런 처리를 받지 않으므로, 채우는 걸 잊은 record는 스택에 남아 있던 값을 그대로 싣고 옵니다. AddWebCaptureCommand는 Levels가 최소 1, Flags가 음수가 아닐 것을 요구하며, 스택 쓰레기 값은 아예 거부되거나, 더 나쁘게는 그럴듯한 깊이로 조용히 받아들여집니다
var
Lib: TPDFlib;
Command: TPDFlibWebCaptureCommand;
Index: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('captured-site.pdf', '') = 1 then
begin
// 모든 필드를 할당합니다. Levels와 Flags는 일반 Integer라서
// 건너뛰면 스택 쓰레기 값을 지닙니다.
Command.URL := 'https://example.com/docs/index.html';
Command.Levels := 2;
Command.Flags := PDF_WEB_CAPTURE_SAME_SITE or PDF_WEB_CAPTURE_SAME_PATH;
Command.PostData := '';
Command.ContentType := '';
Command.Headers := '';
// 새 명령의 1-based 인덱스를 반환하며, 거부되면 0을 반환합니다.
Index := Lib.AddWebCaptureCommand(Command);
if Index = 0 then
raise Exception.Create('Web Capture command rejected');
Lib.SaveToFile('captured-site.pdf');
end;
finally
Lib.Free;
end;
end;
구조가 만들어지는 것도 첫 번째 성공 호출에서입니다. AddWebCaptureCommand는 내부 헬퍼 WebCaptureCommands(True)에 명령 배열을 요청하고, 그 헬퍼는 /V 항목을 가진 /SpiderInfo와 빈 /C 배열이 없을 때만 생성합니다. 발견한 것을 덮어쓰지 않는 데는 일부러 보수적입니다. catalog에 이미 값이 딕셔너리가 아닌 /SpiderInfo 키나 값이 배열이 아닌 /C 키가 있으면, 헬퍼는 기존 객체를 덮어쓰는 대신 nil을 반환합니다. 잘못 만들어진 상속 구조는 add를 실패시킬 뿐, PDF Library for Delphi가 이전 producer가 쓴 것을 파괴하게 만들지는 않습니다
플래그 화이트리스트: 정의된 비트 3개뿐
AddWebCaptureCommand는 Flags를 화이트리스트와 대조해 검증하며, PDF Library for Delphi가 정의한 세 상수 밖의 비트가 설정된 값은 거부합니다. PDF_WEB_CAPTURE_SAME_SITE = 1(같은 사이트에 머무는 링크만 따라감), PDF_WEB_CAPTURE_SAME_PATH = 2(같은 경로 아래의 링크만 따라감), PDF_WEB_CAPTURE_SUBMIT = 4(단순 가져오기가 아니라 폼 제출이었던 요청)입니다. 8이나 16, 혹은 실수로 들어온 -1을 넘기면 문서를 건드리지 않고 호출이 0을 반환합니다
이건 PDF 파서가 보통보다 엄격한데, 일부러 그렇습니다. /F는 자기 설명 구조가 없는 비트 필드라 정의되지 않은 비트는 손상된 비트와 구분이 안 되고, 나중에 값을 다시 읽는 소비자는 미래 확장과 오타를 구분할 방법이 없습니다. 쓰기 시점에 거부하면 모호함 자체가 파일에 들어가지 않습니다. 또 /P, /CT, /H는 대응하는 record 필드가 비어 있지 않을 때만 쓰이므로, 평범한 GET은 빈 문자열 placeholder 없이 /URL, /L, /F 세 키만 가진 명령 딕셔너리를 만듭니다. 본문이 없던 요청이라면 파일을 파싱하는 독자가 기대하는 바로 그 형태입니다
// 폼 제출: POST 본문과 그 콘텐츠 타입입니다.
Command.URL := 'https://example.com/search';
Command.Levels := 1;
Command.Flags := PDF_WEB_CAPTURE_SUBMIT; // 4
Command.PostData := 'q=iso32000&scope=all';
Command.ContentType := 'application/x-www-form-urlencoded';
Command.Headers := 'Accept-Language: en-US';
if Lib.AddWebCaptureCommand(Command) = 0 then
raise Exception.Create('submission command rejected');
// 같은 사이트 + 같은 경로는 합법적인 조합입니다. 1 or 2 = 3.
Command.Flags := PDF_WEB_CAPTURE_SAME_SITE or PDF_WEB_CAPTURE_SAME_PATH;
// 비트 3은 스펙에 정의되지 않아서 이 값은 파일에 도달하지 못합니다.
Command.Flags := 8;
if Lib.AddWebCaptureCommand(Command) <> 0 then
raise Exception.Create('undefined flag bit should have been refused');
명령 다시 읽기: 하나의 키, 두 가지 합법적 표현
GetWebCaptureCommand는 성공하면 1, 아니면 0을 반환하는데, 다른 producer가 쓴 파일을 읽는 쪽이라 writer보다 관대해야 합니다. 여기서 비대칭 두 가지가 중요합니다. 첫째, reader는 명령 항목이 간접 참조든 직접 딕셔너리든 받아들이고 필요하면 dereference합니다. PDF Library for Delphi는 스펙이 요구하는 간접 형태로만 쓰지만, 지저분한 파일 읽기를 거부해서 얻을 이득은 없습니다. 둘째, /P는 string이나 stream 어느 쪽이든 허용되는데, 이 규칙은 Web Capture를 훨씬 넘어 일반화됩니다. ISO 32000-1은 값이 임의로 커질 수 있을 때 단일 키가 string이나 stream 중 하나를 담게 하는 경우가 잦습니다. 여기서는 POST 본문이고, 같은 패턴은 메타데이터와 임베디드 페이로드에서도 나타납니다. 그래서 GetWebCaptureCommand 내부의 ReadString 헬퍼는 AllowStream 플래그를 받고, /P 읽기만 True를 넘깁니다. stream 값은 필터 체인을 거쳐 디코드되어 PostData 필드로 돌아오고, /URL, /CT, /H는 스펙이 stream 대안을 주지 않으므로 string 전용으로 남습니다. 직접 PDF 읽기 코드를 작성한다면 여기서 훔칠 만한 습관이 있습니다. 무조건 string이라고 가정하지 말고 어떤 키가 이중 타입인지 스펙을 확인하세요
var
Lib: TPDFlib;
Command: TPDFlibWebCaptureCommand;
I, Total, Removed: Integer;
begin
Total := Lib.GetWebCaptureCommandCount;
// 공개 인덱싱은 1-based이며 PDF Library for Delphi의 나머지와 일치합니다.
for I := 1 to Total do
if Lib.GetWebCaptureCommand(I, Command) = 1 then
Writeln(Format('%s levels=%d flags=%d post=%d bytes',
[Command.URL, Command.Levels, Command.Flags, Length(Command.PostData)]));
// /SpiderInfo 딕셔너리 전체를 제거하고 지워진 명령 수를
// 보고합니다. 지울 것이 없으면 0을 반환합니다.
Removed := Lib.ClearWebCaptureCommands;
Writeln(Format('cleared %d command(s)', [Removed]));
end;
ClearWebCaptureCommands는 명령 수를 센 뒤 /C만 비우는 대신 catalog에서 /SpiderInfo 키 전체를 제거합니다. 빈 명령 배열을 가진 /SpiderInfo를 남겨 두면 문서에 Web Capture 출처가 있고 아무것도 가져오지 않았다고 주장하는 셈인데, 출처가 아예 기록되지 않은 것과는 다른, 그리고 거짓된 주장입니다. 이 감각은 catalog 메타데이터 전반에 그대로 적용됩니다. 빈 껍데기 딕셔너리는 중립적인 잔여물이 아니라 하나의 주장입니다. 문서를 다시 쓰기 전에 이미 무엇을 주장하는지 감사한다면, 기존 PDF에서 outline과 annotation 액션을 다시 읽는 법의 introspection 패턴이 여기서 읽고-지우는 과정과 자연스럽게 어울립니다
메타데이터 기록은 Web Capture 구현이 아닙니다
범위에 대해 솔직해져야 합니다. AddWebCaptureCommand는 의도했거나 완료된 fetch의 기록을 저장할 뿐이며, 그게 전부입니다. PDF Library for Delphi는 HTTP 요청을 수행하지 않고, Levels 값이 묘사하는 링크를 따라가지 않으며, SAME_SITE나 SAME_PATH 플래그를 존중하지도 않습니다. 그 플래그들은 어떤 크롤러가 어떻게 행동했는지에 대한 문서일 뿐, 라이브러리가 실행하는 지시가 아닙니다. 여러분의 fetch 코드가 무엇이든 그건 전적으로 여러분 몫입니다
ISO 32000-1 §14.10의 나머지 두 조각도 이 API 밖에 있습니다. Web Capture는 콘텐츠 세트, 즉 개별 페이지 조각을 원래 소스 자료에 다시 묶어 주는 /Contents 구조와, 디지털 식별자를 그 콘텐츠 세트에 매핑하는 name tree를 정의합니다. PDF Library for Delphi는 둘 다 유지하지 않습니다. 여러분이 얻는 것은 명령 목록, 즉 "이 문서는 어디서 가져왔고 크롤링은 얼마나 깊이 진행됐나"에 대한 최상위 답변입니다. 출처 추적, 감사, 아카이브 입수 관점에서 보통 사람들이 실제로 물어보는 부분이 바로 이것입니다. URL이 감사용으로만 기록되는 게 아니라 독자가 클릭할 수 있어야 한다면 그건 별개의 작업입니다. 액션 쪽은 GoToR, GoToE, Launch 액션 만들기를, 열기/닫기 시 실행 동작은 문서 및 페이지 생명주기 액션 트리거를 참고하세요. 여기서 설명한 catalog /SpiderInfo 명령 메타데이터는 Delphi와 C++Builder용 네이티브 PDF 라이브러리인 PDF Library for Delphi의 일부입니다