건축 CAD 패키지가 내보낸 평면도는 보통 첫 페이지에 /VP 배열과 뷰포트 둘을 갖고, 각 뷰포트는 /Measure 엔트리를 달고 있어 뷰어가 도면에서 미터를 바로 읽을 수 있습니다. GeoPDF 지도 도판도 같은 /VP 배열과 같은 /Measure 키를 갖습니다. 평면도는 평범한 PDF 1.6이고 지도 도판은 PDF 1.7에 Adobe Extension Level 3를 요합니다. 키 이름에서 멈추는 버전 검사는 둘 모두에 확장을 요구하고, 평면도는 자기가 품은 적 없는 지리공간 콘텐츠에 대한 메시지와 함께 저장 시점에 거부됩니다
하나의 /Measure 키, 두 개의 다른 딕셔너리
키 이름은 뷰포트가 측정 가능하다는 것만 말해 주고, 어떻게 측정하는지는 그 뒤의 딕셔너리만 말해 줍니다. Measure 딕셔너리는 PDF 1.6에서 왔고, ISO 32000-1 §12.9는 rectilinear 종류를 정의합니다. /R의 스케일 비율 문자열, /X와 /Y의 숫자 형식 배열, 그리고 거리(/D)와 면적(/A)의 추가 배열입니다. 지리공간 측정은 나중에, PDF 1.7 위에 얹힌 Adobe Extension Level 3의 일부로 왔습니다. /Subtype /GEO로 표시되며 /GCS의 지리적 좌표계, 선택적 /DCS의 표시 좌표계, /GPTS와 /LPTS의 짝지은 점 배열, /PDU의 선호 표시 단위를 갖습니다. 이 구조들은 Delphi에서 GeoPDF 뷰포트, GPTS와 LPTS 배열 읽기에서 훑어 봅니다
사람을 넘어뜨리는 세부 사항은 기본값입니다. §12.9는 /Subtype의 기본값을 RL로 두고, 수많은 CAD 익스포터가 이를 활용해 그 엔트리를 생략합니다. 빠진 subtype을 "알 수 없음, 최악을 가정"으로 읽는 검사기는 그런 도면을 전부 지리공간 문서로 만듭니다. /Measure 키에 매칭하는 검사기는 subtype 질문 자체에 도달하지도 못합니다. 키를 들고 있는 뷰포트 딕셔너리가 답을 주는 측정 딕셔너리보다 한 단계 위이기 때문입니다
PDF Library for Delphi의 저장 preflight가 판정에 이르는 방식
PDF Library for Delphi(PDFlibPas)는 SaveToFile 안에서 출력이 쓰이기 전에 버전 preflight를 돌립니다. 모든 간접 객체를 방문해 중첩된 딕셔너리와 배열로 내려가며, 각 노드를 기능 규칙 테이블과 대조합니다. 규칙마다 최소 PDF 버전을 이름으로 갖고, extension 기능에는 Adobe extension level을 갖습니다. extension 규칙은 PDF 1.7과 카탈로그의 맞는 /Extensions /ADBE 엔트리가 함께여야만 충족됩니다. 검사 대상은 문서 버전, 또는 LockSaveVersion으로 고정한 버전에 카탈로그가 선언한 extension level을 더한 것입니다. 대상에 매칭하고 초과하는 첫 규칙이 저장을 끝냅니다. SaveToFile은 0을 반환하고 LastErrorCode는 602, 즉 PDFLIB_ERROR_VERSION_COMPLIANCE의 값이 됩니다
오류 코드 하나로는 무엇을 고쳐야 하는지 알 수 없습니다. 세 개의 GetInformation 키가 나머지를 갖고 있습니다. 311은 문제의 기능이 요구하는 버전(예: 1.7 ExtensionLevel 3), 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;
측정 콘텐츠에 대해 테이블은 세 규칙을 갖고, 맨 /Measure 키에는 의도적으로 규칙을 두지 않습니다. /VP나 /UserUnit을 갖는 페이지는 PDF 1.6이 필요하고, /PtData 딕셔너리는 extension level 3이 필요하며, 측정 딕셔너리는 /Subtype /GEO라고 하거나 /GCS, /DCS, /GPTS, /LPTS, /PDU 중 하나라도 담고 있으면 extension level 3이 필요합니다. 그래서 /Subtype을 생략한 측정 딕셔너리와 /RL이라고 적은 측정 딕셔너리는 둘 다 1.6에서 통과하고, 같은 페이지라도 1.5에서는 /VP 때문에 거부됩니다. /GPTS 배열을 더하면 답은 1.7 ExtensionLevel 3이 됩니다
콘텐츠 검사는 의도적으로 보수적입니다. 길 잃은 /GCS나 /PDU를 함께 담은 rectilinear 딕셔너리는 지리공간으로 취급됩니다. 그 파일을 통과시키는 것보다 거부하는 편이 낫습니다. 그 키들은 rectilinear 모델에서 아무 의미가 없으므로, 쓴 쪽은 지리공간 도구거나 파일을 손상시킨 무언가이고, GEO를 아는 뷰어는 그것을 해석하려 들 수도 있습니다. 로더는 반대 방향으로도 똑같이 조심합니다. 기능 스캔은 문서 버전을 1.7로 올릴 수 있지만 입력 파일을 대신해 /Extensions 선언을 쓰는 일은 결코 없습니다. 확장 선언은 콘텐츠에 대한 주장이고, 그것을 할 수 있는 것은 당신의 코드나 저작 호출뿐입니다
저장하기 전에 도면의 뷰포트 검사하기
312가 지리공간 기능을 가리키는데 파일이 평범한 도면임을 확신한다면, 측정 딕셔너리를 직접 들여다보세요. 공개된 뷰포트 호출만으로 충분합니다. GetMeasureDictCoordinateSystem은 /Subtype을 읽어 RL이면 1, GEO면 2, 엔트리가 없으면 0을 돌려주는데, §12.9에 따르면 0도 rectilinear를 뜻합니다. 점 배열 개수는 지리참조 데이터가 있는지 알려 줍니다
// 로드된 문서의 모든 측정 딕셔너리를 보고합니다.
// 여기의 모든 호출은 읽기만 합니다. 파일에 키를 추가하는 것은 하나도 없습니다.
procedure ReportMeasureDicts(Pdf: TPDFlib);
var
Page, VP, VPID, MD, Kind: Integer;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.SelectPage(Page);
for VP := 1 to Pdf.GetPageViewPortCount do
begin
VPID := Pdf.GetPageViewPortID(VP);
MD := Pdf.GetViewPortMeasureDict(VPID);
if MD = 0 then
Continue;
Kind := Pdf.GetMeasureDictCoordinateSystem(MD); // 0 = 생략됨(RL), 1 = RL, 2 = GEO
Writeln(Format('page %d viewport "%s": subtype %d, GPTS %d, LPTS %d',
[Page, string(Pdf.GetViewPortName(VPID)), Kind,
Pdf.GetMeasureDictGPTSCount(MD), Pdf.GetMeasureDictLPTSCount(MD)]));
end;
end;
end;
그 루프에는 두 호출이 일부러 빠져 있습니다. GetMeasureDictGCSDict와 GetMeasureDictDCSDict입니다. 이 둘은 저작을 위해 만들어진 get-or-create 접근자입니다. /GCS 엔트리가 없는 측정 딕셔너리에 GetMeasureDictGCSDict는 빈 엔트리를 추가하고 문서에 extension level 3을 선언합니다. rectilinear 평면도를 탐지 용도로 호출하면 딕셔너리가 이제 실제로 /GCS를 담게 되고, 1.6으로 잠긴 이어지는 저장은 방금 진단이 쓴 콘텐츠 때문에, 올바르게도, 거부됩니다. 좌표계가 존재하는지 알고 싶다면 subtype과 점 배열 개수가 부작용 없이 그 답을 줍니다
출력을 PDF 1.6으로 잠그기
고정된 버전은 소비자가 상한을 갖고 있을 때 출력을 정직하게 유지합니다. TPDFlib.LockSaveVersion은 '1.0'부터 '1.7'까지의 문자열을 받고 그 외에는 0을 반환합니다. 문서 버전을 고정하고 작성 쪽 호출이 제멋대로 올리지 못하게 하지만, 저장 preflight는 여전히 잠긴 값을 기준으로 돕니다. 1.6으로 잠긴 CAD 도면은 깨끗하게 저장됩니다. 1.6으로 잠긴 진짜 GeoPDF는 602를 받는데, 그것이 원하는 답입니다. 다른 선택지는 헤더의 약속이 콘텐츠의 요구보다 작은 파일입니다
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
try
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
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))]);
finally
Pdf.UnlockSaveVersion;
end;
버전을 만지기 전에 311과 312를 읽으세요. 기능이 진짜 지리공간 콘텐츠라면 정직한 선택은 두 가지입니다. 문서를 1.7에 확장 선언과 함께 잠금 해제로 두거나, 1.6 전용 소비자에게 보낼 사본에서 지리공간 데이터를 빼는 것입니다. 오류를 없애려고 잠금만 올리는 것은 피해야 할 선택입니다. 그것은 진짜 질문, 즉 하류 리더가 출하하는 것을 다룰 수 있는지를 가려 버립니다. API로 지리공간 콘텐츠를 만들 때는 SetMeasureDictCoordinateSystem과 SetMeasureDictGPTSCount 같은 SetMeasureDict* 설정자가 스스로 extension level 3을 선언하므로, 저작된 파일은 이미 라벨이 붙은 채 preflight에 도착합니다. 유계이고 증거에 기반한 검사라는 같은 습관은 엔지니어링 문서를 위한 PDF/E-1 author-mode preflight에서도 나타납니다. 그곳에서는 CAD 도면이 버전 숫자가 아니라 컨포먼스 표준과 만납니다
규칙 스캔이 테이블을 제자리에서 읽는 이유
preflight의 비용은 대략 객체 노드 수 곱하기 규칙 수이고, 노드 수에는 모든 배열 안의 모든 숫자와 이름이 포함됩니다. 규칙당 오버헤드가 보이는 것보다 중요해지는 이유입니다. 테이블의 각 규칙은 매칭 문자열과 기능 이름, 두 개의 AnsiString 필드를 담은 레코드입니다. 그런 레코드를 지역 변수로 복사하면 Delphi나 FPC는 참조 카운트를 두 번 올리고 지역 변수가 갖고 있던 것을 해제합니다. 규칙마다 노드마다 한 번씩이라면, 대상 버전에 적용되지도 않고 바로 다음 줄에서 버려질 규칙들에 대해서도 문자열 부기 비용을 지불하는 셈입니다
규칙 테이블은 유닛 초기화 때 한 번 채워지고 그 뒤로는 수정되지 않으므로, 스캔은 테이블 엔트리를 const 레코드 매개변수로 매칭 코드에 넘기며 살아 있는 자리에서 읽습니다. const 레코드 매개변수는 참조로 넘어가므로, 규칙을 검사하는 것이 문자열 필드를 하나도 건드리지 않고 참조 카운트도 제자리에 머뭅니다. 효과는 규칙 매칭 경로에 집중됩니다. 실제 저장은 객체 디코딩과 지연 기능 감지, 출력 직렬화도 돌리고 그 단계들은 규칙을 넘기는 방식의 영향 밖입니다. 규칙 순서와 임계값, 311과 312의 첫 실패 진단은 이에 의존하지 않고, 저장 사이에 캐시되는 규칙 결과도 없으므로, 편집해서 두 번 저장한 문서는 두 번 모두 전체 검사를 받습니다
같은 패턴은 문자열, 인터페이스, 동적 배열을 담은 레코드 테이블을 도는 루프가 있는 자기 Delphi 코드 어디에서든 점검할 가치가 있습니다:
type
TMyRule = record
Name: string; // managed 필드: 복사는 참조 카운트 작업 비용
Threshold: Integer;
end;
// 읽는 이의 자기 코드, 패턴으로 제시
function Applies(const Rule: TMyRule; Value: Integer): Boolean;
begin
Result := Value >= Rule.Threshold; // const: 복사 없음, 참조 카운트 왕복 없음
end;
// 핫 루프에서는 지역 레코드에 먼저 대입하지 말고
// 테이블 엔트리를 직접 넘기세요
for I := 0 to High(Rules) do
if Applies(Rules[I], V) then
Inc(Hits);
측정 뷰포트와 지리공간 딕셔너리, 버전 잠금, 저장 시점 컴플라이언스 검사는 모두 Delphi, C++Builder, Lazarus 프로젝트를 위한 PDF Library for Delphi에 함께 실려 있습니다