PDFlibPas(PDF Library for Delphi)는 파일을 쓰기 전에 모든 오브젝트를 PDF 버전 규칙 테이블과 대조하고, 그동안 그 PDF 버전 preflight가 평범한 CAD 측정 딕셔너리를 지리공간용으로 착각했습니다. 한 페이지짜리 CAD 도면은 잘 불러와졌는데, 그러고 나서 SaveToFile이 0을 반환하고 LastErrorCode 602와 함께 1.7 ExtensionLevel 3을 요구했습니다. 수정된 규칙은 rectilinear /Measure 딕셔너리(/Subtype /RL)를 평범한 PDF 1.6으로 취급하고, extension 게이트는 진짜 지리공간 마커를 위해 남겨 둡니다
파일은 코퍼스 입수로 들어왔습니다. 한 페이지, optional-content 그룹 하나, rectilinear 측정 뷰포트 둘, 그러니까 건축 CAD 패키지가 도면에서 거리를 읽을 수 있게 뷰어를 위해 기록해 두는 종류의 출력입니다. 특이할 게 하나도 없었고, 바로 그래서 그 거부가 문제였습니다. 유효한 파일을 막는 preflight는 느린 preflight보다 나쁩니다. 호출자가 문서에 존재하지 않는 기능을 가리키는 권위 있어 보이는 진단을 받기 때문입니다. 수정은 두 부분으로 이루어졌습니다. 한 규칙 뒤의 스펙 해석, 그리고 그 규칙이 보던 수준에서는 두 딕셔너리 타입을 구분할 수 없다는 깨달음입니다
PDFlibPas의 저장 시점 버전 preflight는 어떻게 동작할까?
저장 게이트인 PrepareAndCheckSaveVersion은 모든 간접 오브젝트를 PDFFeatureRules와 대조하고, 매칭되면서 대상이 허용하는 것보다 더 많은 것을 요구하는 첫 규칙에서 실패합니다. 대상은 문서 버전(또는 LockSaveVersion이 고정한 버전)에 /Extensions /ADBE 아래 선언된 Adobe extension level을 더한 것입니다. 각 TPDFFeatureRule 레코드는 MinVersion, MinExtensionLevel, fmkDictKey나 fmkDictSubtype 같은 MatchKind, Match 문자열, 사람이 읽을 수 있는 Feature 이름, 선택적 콜백을 담습니다. AddRule은 평범한 버전 규칙을 등록하고, AddExtensionRule은 MinVersion을 항상 17로 고정한 뒤 extension level을 얹으므로, extension 규칙은 PDF 1.7에 올바른 /Extensions 엔트리가 더해져야만 충족됩니다. 게이트가 걸리면 필요한 버전과 기능 이름이 호출자를 위해 보관되고, GetInformation 키 311, 312, 313이 그것을 노출합니다
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
raise Exception.Create('load failed');
if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 311: 필요한 버전, 312: 이를 발동시킨 기능,
// 313: 저장 대상이 잠긴 버전 (잠기지 않았으면 '')
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
평범한 CAD 도면이 오류 602로 실패한 이유는?
규칙 테이블에는 AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil)이 들어 있었는데, 이것은 /Measure 키만 달고 있으면 어떤 딕셔너리든 발화했고, 모든 측정 뷰포트는 그 키를 갖고 있습니다. 페이지의 /VP 배열이 뷰포트 딕셔너리들을 담고, 각 뷰포트는 /Measure로 자기 측정 딕셔너리를 가리키는데, 키 존재 매칭은 그 측정 딕셔너리가 실제로 무엇인지 들여다보지 않고 거기서 멈췄습니다. 로드 시점 기능 스캔은 문서 버전을 1.7로 올릴 수 있었지만 입력 파일을 대신해 /Extensions 선언을 쓰는 일은 결코 없으므로, 저장 게이트는 extension level 0의 PDF 1.7을 보고 1.7 ExtensionLevel 3을 보고했습니다. extension 선언을 지어내지 않는 이 거부는 의도된 것입니다. 라이브러리는 잘못된 규칙을 덮으려고 입력 파일을 조용히 승격하지 않습니다
스펙은 rectilinear 케이스에 대해 명확합니다. Measure 딕셔너리는 PDF 1.6에서 도입됐고, ISO 32000-1 §12.9는 /Subtype의 기본값을 RL로 둡니다. 스케일 비율, X와 Y 숫자 형식, 거리와 면적이라는 자체 엔트리 세트로 기술되는 rectilinear 좌표계입니다. 지리공간 측정은 PDF 1.7 위에 얹힌 Adobe Extension Level 3의 나중 추가물로, /Subtype /GEO로 식별되며 지리적 점 배열과 좌표계 딕셔너리, 표시 단위를 담습니다. Delphi에서 GeoPDF 뷰포트, GPTS와 LPTS 배열 읽기에서 훑어본 구조들입니다. 두 딕셔너리는 같은 /Measure 키에 매달리므로, 키에서 멈추는 어떤 규칙도 둘 모두에게 맞을 수 없습니다. 구별 정보는 한 단계 아래, 측정 딕셔너리 자체에 있습니다
수정된 규칙 세트가 여전히 강제하는 것은?
수정은 무조건적 키 규칙을 삭제하고 진짜 버전 요구 사항을 기술하는 게이트들은 남깁니다. /VP나 /UserUnit을 실은 페이지는 여전히 CB_PagePDF16Entries를 통해 PDF 1.6이 필요하고, /PtData 키는 여전히 extension level 3이 필요하며, CB_GeospatialDictionary는 도달한 키가 아니라 콘텐츠로 측정 딕셔너리의 지리공간 여부를 판정합니다
// 제거됨: /Measure 키를 가진 모든 딕셔너리가 지리공간으로 간주됐음
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);
AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);
function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
Dict: TPDFDictionary;
begin
Result := False;
if not (Obj is TPDFDictionary) then
Exit;
Dict := TPDFDictionary(Obj);
Result := (Dict.StringValue('Subtype') = 'GEO') or
(Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
(Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
(Dict.FindIndexByKeyName('PDU') >= 0);
end;
공유 Delphi와 FPC 회귀 테스트가 그 경계를 양쪽에서 고정합니다. 측정 딕셔너리가 /Subtype을 생략한 뷰포트와 /RL을 명시한 뷰포트 둘 다 PDF 1.6에서 통과하고, 같은 페이지는 PDF 1.5에서 여전히 거부되며, 기능 감지는 이제 그것에 대해 extension을 보고하지 않습니다. /GPTS 배열을 추가하면 판정이 다시 1.7 ExtensionLevel 3으로 뒤집히는데, extension level을 선언하면 통과하고, 벌거벗은 /Subtype /GEO 딕셔너리는 선언 없이는 거부됩니다. 콜백은 설계상 보수적입니다. rectilinear 딕셔너리가 길 잃은 /GCS나 /PDU 키를 함께 갖고 있으면 지리공간으로 취급하는데, 그 키들은 RL 모델에서 아무 의미가 없기 때문입니다
LockSaveVersion은 이 변경이 호출자에게 드러나는 지점입니다. TPDFlib.LockSaveVersion은 '1.0'부터 '1.7'까지 받고 그 외에는 0을 반환하며, 문서 버전을 고정하고 작성 측 호출이 조용히 올리지 못하게 막지만, 저장 게이트는 여전히 잠긴 값을 기준으로 돌아갑니다. 수정된 규칙으로는 1.6에 잠긴 CAD 파일이 말끔히 저장됩니다. 1.6에 잠긴 진짜 GeoPDF는 여전히 602를 받는데, 이게 정답이고, SetMeasureDictCoordinateSystem 같은 지리공간 저작 호출은 API로 그 콘텐츠를 만들 때 extension level 3을 스스로 선언합니다
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 1.6 위의 진짜 콘텐츠, 예컨대 GEO 측정 딕셔너리
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;
버전 규칙 스캔은 왜 필요 이상으로 느렸을까?
스캔은 검사하기 전에 모든 TPDFFeatureRule을 지역 레코드로 복사했고, 레코드가 AnsiString 필드 두 개를 담고 있으므로 복사할 때마다 참조 카운트 두 개를 조정하고 이전 값을 해제했습니다. preflight는 모든 오브젝트 트리의 모든 노드를, 스칼라까지 포함해 방문하므로 그 비용은 오브젝트 수 곱하기 규칙 수가 됐고, 대상 버전에 적용조차 되지 않는 규칙들도 먼저 복사된 뒤 건너뛰어졌습니다. PDFFeatureRules는 유닛 초기화 때 한 번 채워지고 읽기 전용으로 취급되므로, v3.539.17은 테이블 엔트리를 MatchSingleRule과 RuleExceedsTarget에 곧장 넘깁니다. 이 함수들의 const Rule 파라미터는 문자열을 건드리지 않고 참조만 받습니다
// 이전: 규칙마다, 방문한 오브젝트마다 관리형 레코드 복사
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
Continue;
// 이후: const 파라미터가 불변 테이블 엔트리를 제자리에서 읽음
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
FeatureName := PDFFeatureRules[X].Feature;
Result := False;
Exit;
end;
측정된 효과는 좁고 그렇게 인용되어야 합니다. 벤치마크는 20,000개의 숫자형 오브젝트 배열을 PDF 1.4 대상과 라운드당 열 번 대조합니다. -O2로 빌드한 FPC Win64에서 다섯 라운드의 중앙값은 0.711초에서 0.203초로 떨어졌고, 두 빌드의 순서를 뒤집어 돌리면 0.459초 대 0.150초였습니다. 규칙 매칭 경로만으로는 대략 3배의 이득입니다. 실제 저장은 지연 기능 감지와 오브젝트 디코딩, 직렬화 비용도 지불하므로 그 비율이 전체 저장 시간으로 이어지지는 않습니다. 규칙 순서와 콜백, 버전 임계값, 첫 실패 진단은 바뀌지 않았고, 저장 사이에 규칙을 캐시하거나 건너뛰어 얻은 이득도 아닙니다
불러온 PDF가 버전 preflight에 실패하면 무엇을 확인해야 할까?
버전을 만지기 전에 키 311과 312를 읽으세요. 기능 이름이 지리공간 딕셔너리를 가리키는데 파일은 rectilinear 측정만 그리고 있다면 그게 바로 이 오탐이고, 최신 빌드는 파일을 그대로 저장합니다. 기능이 진짜라면 extension을 선언하거나 콘텐츠를 정직하게 담는 버전으로 잠그세요. 게이트를 입막으려고 버전만 올리면, 출하하는 것을 하류 소비자가 읽을 수 있는지라는 질문이 가려집니다. 유계이고 증거에 기반한 검사라는 같은 원칙이 엔지니어링 문서를 위한 PDF/E-1 author-mode preflight를 움직입니다. CAD 도면이 버전 숫자가 아니라 컨포먼스 표준과 만나는 자리입니다
버전 컴플라이언스 검사와 측정·지리공간 딕셔너리, 저장 버전 잠금은 모두 Delphi, C++Builder, Lazarus 개발자를 위한 PDFlibPas 툴킷, PDF Library for Delphi의 일부입니다