PDFlibPas는 임베디드 파일을 문서 전체가 아니라 특정 한 페이지에 붙입니다. 페이로드 자체는 문서의 EmbeddedFiles 이름 트리에 등록한 채로 두고, /AF 배열을 페이지 딕셔너리에 기록하는 방식입니다. 이 분할이 바로 ISO 32000-2 §14.13이 기술하는 구조이며, 문서 수준 첨부로는 답할 수 없는 질문, 즉 이 데이터가 어느 페이지에 속하는지를 리더가 답할 수 있게 해 주는 것도 이 분할 덕분입니다
쓰임새는 범용 첨부보다 훨씬 구체적입니다. 각 페이지가 자기 차트 뒤에 있는 원본 측정 시계열을 갖고 있는 설문 보고서. 모든 페이지가 자기 텍스트 레이어를 만들어 낸 OCR 결과를 쥐고 있는 스캔 배치. 각 도면이 렌더링된 CAD 추출물을 함께 실은 도면 세트. 어느 쪽이든 문서 수준 첨부 목록이라면 페이지 번호를 파일 이름에 새긴 파일 무더기가 되는데, 그것은 구조가 아니라 관습일 뿐입니다
하나의 페이로드, 두 곳에서 참조
중요한 구조적 포인트는 페이지 수준 연관이 무엇인가의 사본을 하나 더 만들지 않는다는 것입니다. 파일은 한 번만 임베디드되고 문서 수준 첨부와 정확히 같은 방식, 같은 파일 스펙 장치를 써서 EmbeddedFiles 이름 트리에 등록됩니다. 달라지는 것은 참조와 그 관계 키가 기록되는 위치, 즉 문서 카탈로그가 아니라 페이지 딕셔너리입니다
귀결은 두 가지입니다. 첫째, 문서 수준 첨부만 아는 리더도 페이로드를 찾습니다. 그런 리더가 보는 이름 트리 안에 페이로드가 있으니까요. 둘째, 페이지 연관을 지우면 지워지는 것은 바인딩이지 파일이 아닙니다. ClearPageAssociatedFiles는 페이지를 연관 파일에서 떼어 내되 페이로드는 이름 트리를 통해 여전히 도달 가능한 상태로 둡니다. 보수적인 동작입니다. 연관을 지우는 연산이 문서의 다른 부분이 참조할 수도 있는 데이터를 조용히 파괴해서는 안 되죠
이 함수에는 알아 둘 가치가 있는, 의도적으로 좁게 설정된 성공 조건이 하나 있습니다. 페이지가 실제로 /AF 키를 갖고 있었을 때만 성공을 보고합니다. 애초에 연관이 없던 페이지는 후한 확인 대신 실패를 돌려주므로, 호출자가 no-op을 완료된 정리로 착각할 일이 없습니다
var
Lib: TPDFlib;
Idx, I: Integer;
begin
Lib := TPDFlib.Create(nil);
try
Lib.LoadFromFile('survey-report.pdf');
// 3페이지 차트를 만든 측정 시계열을 첨부합니다
Idx := Lib.AddPageAssociatedFileFromFile(3,
'series-03.csv', // 디스크상의 파일
'measurements.csv', // PDF 안에서의 표시 이름
'text/csv', // MIME 타입
'Raw measurement series for figure 3',
'Data'); // AFRelationship, ISO 32000-2 14.13
if Idx < 0 then
raise Exception.Create('page association refused');
for I := 0 to Lib.GetPageAssociatedFileCount(3) - 1 do
Writeln('page 3 associated file, embedded index ',
Lib.GetPageAssociatedFileEmbeddedIndex(3, I));
Lib.SaveToFile('survey-report-with-data.pdf');
finally
Lib.Free;
end;
end;
관계 문자열은 실무에서 자유 텍스트가 아닙니다. ISO 32000-2는 Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema, Unspecified라는 어휘 집합을 정의하고 소비자들은 이를 기준으로 동작합니다. 차트 뒤의 숫자에는 Data, 페이지가 생성된 원본 문서에는 Source, 동등한 표현에는 Alternative. 지금 파이프라인 어디도 그것을 읽지 않더라도 어휘 집합 안에서 고르세요. 체인의 다음 도구가 읽을지도 모르니까요
같은 조회가 양방향 모두 FollowRef가 필요한 이유는?
참조 따라가기는 서로 다른 두 질문에 답하고, 코드는 자신이 둘 중 무엇을 묻는지 알아야 하기 때문입니다. 간접 참조를 따라가는 키 조회는 참조가 가리키는 오브젝트를 돌려줍니다. 따라가지 않는 조회는 참조 자체를 돌려줍니다. 둘 다 올바른데, 잘못된 쪽을 쓰면 에러가 아니라 조용한 오동작이 나옵니다
연관 파일을 읽는 것이 첫 번째 방향의 예입니다. 파일 스펙의 /EF와 /F 키 뒤에 있는 임베디드 스트림의 오브젝트 번호를 얻으려면 조회는 따라가면 안 됩니다. 따라가면 참조가 스트림 오브젝트로 해석되어 버려 오브젝트 번호가 사라지기 때문입니다. 규칙은 일반화됩니다. 오브젝트 내용이 아니라 오브젝트 신원이 필요한 코드 경로는 모두 raw 참조를 받아야 합니다
옵셔널 콘텐츠는 반대 방향을 보여 주며, 찾는 데 비용이 더 들었습니다. 옵셔널 콘텐츠 프로퍼티 딕셔너리는 카탈로그에 간접 오브젝트로 기록되므로, 따라가지 않고 읽어 돌아오는 것은 딕셔너리가 아니라 참조입니다. 그 값에 타입 체크를 하면 실패하고, 자연스러운 폴백 분기, 즉 설정이 없으면 하나 만들라는 분기가 돌아 이미 존재하던 설정을 덮어써 버립니다. 예외는 커녕 아무 소리도 없습니다. 옵셔널 콘텐츠 그룹과 레이어에서 설명한 레이어들은 그저 기본 가시성 상태를 잃을 뿐입니다
교훈은 두 사례를 넘어 일반화됩니다. 조회가 참조와 오브젝트 중 무엇이든 돌려줄 수 있다면, 벌거벗은 타입 체크는 에러 처리가 아닙니다. 결국 잘못된 이유로 선택될 분기일 뿐입니다. 각 호출 지점이 무엇을 필요로 하는지 명시적으로 결정하고, 카탈로그 딕셔너리를 protected 접근자로 들추기보다 옵셔널 콘텐츠 카운트 프로퍼티처럼 질문에 직접 답하는 공개 API를 선호하세요
// 문서 수준 첨부와 페이지 수준 연관은 공존합니다. 임베디드 파일은
// 문서 수준에서도 associated로 표시될 수 있습니다
if Lib.IsEmbeddedFileAssociated(0) = 0 then
Lib.SetEmbeddedFileAssociated(0, 1, 'Supplement');
Writeln('document associated files: ', Lib.GetAssociatedFileCount);
Writeln('page 3 associated files : ',
Lib.GetPageAssociatedFileCount(3));
// 지우기는 페이지 바인딩만 떼어냅니다. 페이로드는 이름 트리에 그대로
if Lib.ClearPageAssociatedFiles(3) > 0 then
Writeln('page 3 associations removed, payloads still reachable');
컨포먼스 모드가 첨부에 하는 일
아카이브 프로파일은 무엇을 임베드할 수 있는지 제한하고, 그 제한은 저장 시점이 아니라 진입점에서 강제됩니다. PDF/A-1은 임베디드 파일을 전면 금지하고, PDF/A-2는 PDF/A 문서만 임베드를 허용하며, PDF/A-3은 임의 파일 타입까지 임베드를 연 프로파일입니다. 하이브리드 인보이스 포맷이 정확히 이 위에서 만들어지는 이유죠
PDFlibPas는 활성 컨포먼스 모드가 허용하지 않으면 호출 시점에 첨부를 거부합니다. 수백 개 연산 뒤 출력 중에가 아니라요. 에러를 처리하기 가장 값싼 위치를 고른 의도적 선택입니다. 호출 지점의 거부는 추가하던 파일 이름을 알려 주지만, 저장 시점의 거부는 문서 하나를 지적할 뿐 마흔 개 첨부 중 무엇이 범인인지는 여러분이 알아내야 합니다
전자 인보이스에서 연관 파일이 그렇게 자주 등장하는 이유이기도 합니다. 하이브리드 인보이스는 사람이 읽는 PDF에 기계가 읽는 XML 페이로드를 붙이고 올바른 관계로 표시한 것이며, 컨테이너 프로파일과 관계 키 모두 관습이 아니라 스펙의 일부입니다. 이 구성은 Factur-X와 ZUGFeRD 하이브리드 인보이스 만들기에서, 메타데이터 쪽은 PDF/A-3 XMP 익스텐션 스키마에서 다룹니다
연관을 문서 단위가 아니라 페이지 단위로 해야 하는 때는?
소비자가 데이터가 어느 페이지에 속하는지 알아야 할 때, 그리고 그럴 때뿐입니다. 문서 수준 첨부가 더 단순하고 뷰어 지원도 넓으며, 페이로드가 문서 전체를 기술하는 경우, 인보이스 XML, 서명 매니페스트, 소스 아카이브라면 그것으로 충분합니다. 페이로드가 실제로 페이지 범위이고 페이지 신원이 그 의미의 일부일 때 페이지 수준 연관에 손을 뻗으세요
지원이 실무적 제약입니다. 페이지 수준 연관 파일은 PDF 2.0 구성물이고 뷰어 지원은 문서 수준 첨부보다 얇습니다. 그래도 페이로드는 어느 쪽이든 이름 트리에 있기 때문에 페이지의 /AF를 무시하는 뷰어도 첨부 목록에는 파일을 보여 주며, 열화는 우아합니다. 하지만 페이지 바인딩이 유용한 메타데이터가 아니라 소비자에게 본질이라면, 가정하지 말고 실제 타깃 리더를 검증하세요
페이지 수준 연관 파일, 문서 수준 첨부, 그리고 둘을 통괄하는 아카이브 프로파일 게이트는 PDFlibPas Delphi PDF 라이브러리에 들어 있습니다. 들어오는 쪽에서 오래된 파일을 수리하는 일도 병행한다면, 메타데이터 수리와 함께 PDF/A로 변환의 메타데이터와 컨포먼스 작업이 이 첨부 경로들 중 무엇을 애초에 쓸 수 있는지를 결정합니다