veraPDF 보고서가 글리프 폭이 임베드된 폰트 프로그램과 불일치한다고 말해줘도, 어느 글리프인지 왜 그런지는 거의 알려주지 않습니다. PDFlibPas는 각 문자 코드를 임베드된 cmap으로 글리프 인덱스까지 해석하고, 프로그램 메트릭을 em당 1000 단위로 정규화한 뒤 그 지점에서 비교함으로써 그 질문에 답합니다
글리프 폭은 왜 불일치하는가
비교되는 두 숫자가 서로 다른 좌표계에 살고 있고, PDF 사전 어디도 그 변환을 알려주지 않기 때문입니다. 폰트 사전은 /Widths를 글리프 공간으로 쓰는데, PDF는 이를 em의 천분의 일로 고정합니다(ISO 32000-1 §9.2.4). 임베드된 TrueType 프로그램 안의 hmtx 테이블은 폰트 디자인 단위로 어드밴스를 기록하고, head 테이블이 그중 몇 개가 em 하나인지 결정합니다. 대부분의 TrueType 페이스는 2048, CFF 계열은 1000, 가끔 완전히 다른 값입니다. 원시 값을 비교하면 코퍼스의 모든 2048-upem 폰트가 깨진 것처럼 보입니다. 사전 필드를 읽어 폭을 감사하려는 사람에게 ISO 14289-1 §7.21.5가 놓은 함정이 바로 그것입니다
PDFlibPas는 로드 시점에 정규화합니다. TPDFTrueTypeParser는 폭 배열에 Advance * 1000 div unitsPerEm을 저장하므로, Parser.GetWidth(GID)는 이미 PDF가 쓰는 천분의 일 em 단위로 답하고, 디자인 단위가 필요할 때는 GetRawWidth가 그대로 남습니다. 그래도 더 어려운 절반이 남습니다. 문자 코드에서 글리프 인덱스로 가는 길입니다. 단순 TrueType 폰트의 경로는 FontDescriptor의 Symbolic 플래그, /Flags의 비트 3에 달려 있습니다
Parser := TPDFTrueTypeParser.Create;
try
Parser.LoadFromString(FontProgram);
if Symbolic then
begin
// 심볼릭 페이스는 프로그램 cmap을 통해 직접 주소되고,
// (3,0) 하이바이트 규약이 폴백이다
GID := Parser.GetGlyphIndex(Code);
if GID = 0 then
GID := Parser.GetGlyphIndex($F000 + Code);
end
else
begin
// 비심볼릭: 인코딩으로 code -> 글리프 이름, Adobe Glyph List로
// 이름 -> Unicode, 프로그램 cmap으로 Unicode -> GID
UnicodeValue := GetGlyphUnicode(EncodingNames[Code and $FF]);
if UnicodeValue = 0 then
Continue;
GID := Parser.GetGlyphIndex(UnicodeValue);
end;
if (GID > 0) and (GID < Parser.GlyphCount) then
if Abs(PDFWidth - Parser.GetWidth(GID)) > 1 then
Inc(MismatchCount);
finally
Parser.Free;
end;
이 조각에는 무게를 지닌 세부가 둘 있습니다. 허용 오차는 0이 아니라 1입니다. 정규화가 정수 나눗셈이고 정상적으로 생산된 파일도 한 단위 어긋날 수 있으며, 진단 10036이 보고하는 “em의 천분의 일 이내” 문구가 정확히 그것입니다. 그리고 GID < Parser.GlyphCount 가드는 장식이 아닙니다. GetWidth는 렌더링 호출자를 위해 관대하게 쓰였습니다. 범위 밖 인덱스를 hmtx의 마지막 항목으로 클램프하고 테이블이 없으면 750으로 폴백합니다. 관대함은 렌더링에는 맞고 감사에는 틀립니다. 그래서 감사는 클램프를 믿는 대신 폭을 물어보기 전에 인덱스를 거부합니다
CIDFontType2는 간접 한 층을 더 얹는다
PDFlibPas는 복합 폰트를 같은 방식으로 걸어가되, CID와 글리프 사이에 /CIDToGIDMap을 끼워 넣습니다. 폭은 /W 배열로 도착하는데, ISO 32000-1 §9.7.4.3이 한 배열 안에서 자유롭게 섞이는 두 형태를 줍니다. 시작 CID 뒤에 연속 폭의 배열이 오거나, 첫 CID, 마지막 CID, 그 구간에 적용되는 단일 폭이 오는 형태입니다. 감사는 둘 다 파싱한 뒤 결과 쌍 전부를 같은 비교로 넘기고, 총계를 진단 10037 아래 보고합니다. 매핑 단계가 복합 폰트가 다른 지점이며, 그래서 어떤 폭을 읽기 전에도 누락 맵 진단 10021이 중요합니다 — 없거나 형식이 잘못된 /CIDToGIDMap은 §7.21.3.2를 위반할 뿐 아니라 폭 질문 자체를 답 없게 만듭니다
// /CIDToGIDMap은 /Identity 이름이거나 CID마다 하나의 빅엔디안 16비트
// 글리프 인덱스를 담은 스트림이다 (ISO 32000-1 섹션 9.7.4.2)
Obj := DerefIndRef(FDoc, CIDFont.FindValueByKeyName('CIDToGIDMap'));
if (Obj is TPDFName) and (TPDFName(Obj).Name = 'Identity') then
begin
GID := CID;
Result := True;
end
else if Obj is TPDFStream then
begin
Data := TPDFStream(Obj).GetDecodedStream;
P := CID * 2 + 1; // Pascal 문자열은 1 기반이다
if (P >= 1) and (P + 1 <= Length(Data)) then
begin
GID := (Integer(Byte(Data[P])) shl 8) or Integer(Byte(Data[P + 1]));
Result := True;
end;
end;
폰트 프로그램이 디코드되지 않을 때 감사자는 무엇을 해야 하는가
아무 말도 하지 않습니다. ISO 14289-1 §7.21.4.2가 요구하는 /CharSet과 /CIDSet 완전성 검사 — 진단 10038과 10039 — 는 과열된 검증자가 책무로 변하는 지점입니다. 보고서를 읽는 사람에게 “CharSet이 불완전하다”는 통보는 “우리의 Type 1 디코더가 포기했다”와 구분되지 않기 때문입니다. 따라서 PDFlibPas는 세 가지가 모두 성공할 때만 누락 항목을 보고합니다. 폰트 프로그램이 디코드되고, 코드-글리프 매핑이 해석되고, 셋 자체가 디코드되는 경우입니다. 어떤 글리프 이름이든 /CharSet 문자열과 대조하기 전에 TPDFType1Decoder.LoadPFBFromString이 True를 반환하고 charstring 수를 내놓아야 하고, /CIDSet 경로는 비트 하나를 검사하기 전에 스트림이 인플레이트되고 글리프 수가 양수로 돌아와야 합니다. 도중의 어떤 예외든 결함이 아니라 “발견 없음”으로 붕괴됩니다
이는 의도된 거짓 음성 쪽 편향이며, 묻어두기보다 분명히 말할 가치가 있습니다. 손상된 CFF 테이블, 지원되지 않는 Type 1 변형, 글리프 범위보다 짧은 /CIDSet이 모두 진단 대신 침묵을 냅니다. 근거는 이렇습니다. PDF/UA 감사는 도구를 만들지 않은 작성자에게 전달되고, 거짓 고발은 놓친 발견보다 비용이 큽니다. 작성자는 준수 파일이 준수임을 증명하는 하루를 태우고 보고서 전체를 불신하게 됩니다. Matterhorn Protocol은 기계가 판정할 수 있는 검사와 사람이 해야 하는 검사를 갈라놓으며 같은 구분을 다른 형태로 만들고, 이것들이 사는 곳이 Fonts 체크포인트(31)입니다. 더 엄격한 판독이 필요하면 PDFlibPas를 빠른 게이트로 돌리고 전용 검증자를 제2의 의견으로 붙이십시오 — 이 조합은 PDF/A와 PDF/UA 프리플라이트 워크스루에서 기술한 것과 같습니다
페이지 /Contents는 스트림이 아니라 목록이다
콘텐츠 스트림 감사에서 가장 비싼 실수 하나는 /Contents를 하나의 스트림으로 다루는 것입니다. ISO 32000-1 §7.7.3.3은 페이지가 스트림 배열을 담을 수 있게 하며, 부분 사이의 공백을 포함한 연결이 페이지 프로그램입니다. 생산자는 임의 지점에서 나누고, BT가 한 멤버에 앉고 짝 ET가 다음 멤버에 앉을 수 있습니다. 콘텐츠 프로세서는 상태를 유지합니다 — 마크드 콘텐츠 중첩 깊이, 마지막 Tf가 고른 폰트, 텍스트 객체 플래그 — 그리고 Process는 진입 시 그 상태를 리셋합니다. 배열 멤버마다 한 번 호출하면 첫 멤버 뒤의 모든 스트림이 현재 폰트 없이 시작하므로, 완벽하게 태그된 텍스트가 태그 없고 폰트 없는 노이즈로 읽힙니다. PDFlibPas는 먼저 연결하고 한 번 처리합니다
function ContentObjectData(FDoc: TSmartPDFDocument; Obj: TPDFObject): AnsiString;
var
I: Integer;
begin
Result := '';
Obj := DerefIndRef(FDoc, Obj);
if Obj is TPDFStream then
Result := TPDFStream(Obj).GetDecodedStream
else if Obj is TPDFArray then
for I := 0 to TPDFArray(Obj).Count - 1 do
Result := Result + ContentObjectData(FDoc, TPDFArray(Obj).Item[I]) + #10;
end;
// 연결 전체에 Process 한 번. 멤버마다 한 번씩이 아니다
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));
어떤 Form XObject가 실제로 비구조로 친다는가
페이지가 실제로 호출하는 것들, 마크드 콘텐츠 바깥의 호출 지점에서, 자기 콘텐츠가 텍스트를 보이는 것들만입니다. 진단 10040은 ISO 14289-1 §7.20을 객체 번호마다 세 개의 독립적 사실 — 텍스트가 있다, 호출되었다, 마크드 콘텐츠 안에서 호출되었다 — 로 기록하고 앞의 둘의 교집합에서 셋째를 뺀 것만 보고함으로써 집행합니다. 두 지름길은 각각 출시하고 싶어지는 방식으로 틀립니다. /Resources의 텍스트를 지닌 모든 Form에 깃발을 꽂으면 아무도 그리지 않는 템플릿 라이브러리를 처벌하고, 호출된 모든 Form에 깃발을 꽂으면 텍스트가 없고 태그가 필요 없는 벡터 로고를 처벌합니다. 호출 지점은 리소스 이름이 아니라 객체 번호로 해석됩니다. 같은 Form이 페이지마다 다른 이름으로 닿는 일이 상례이기 때문입니다. 짝 진단 10041은 §7.21.8을 위해 같은 연결 프로그램을 걸어가며, 범위 안의 폰트로 각 텍스트 표시 피연산자를 해석하고 .notdef에 착지하는 코드를 셉니다. 텍스트 렌더링 모드와 무관하게 금지됩니다 — 스캔 이미지 뒤에서 쓰이는 보이지 않는 모드를 포함해. 살아남은 Form을 어떻게 감싸야 하는지는 구조 트리 질문이며, 태그된 PDF 구조 구축 문서가 다룹니다
FontDescriptor가 아예 없는 폰트
임베드되지 않은 폰트는 이 감사의 정당한 입력이지 오류 상태가 아니며, 임베드 검사 아래의 모든 헬퍼가 그것을 견뎌야 합니다. PDFlibPas가 /FontDescriptor를 찾지 못하거나, FontFile, FontFile2, FontFile3가 없는 디스크립터를 만나면 진단 10020을 기록합니다 — 이름이 Standard 14 중 하나면 10022인데, §7.21.4 NOTE 5가 그 면제를 뚜렷이 거부합니다 — 그리고 파일의 나머지를 계속 걷습니다. 보고서의 전부가 바로 그것입니다. 작성자는 실행마다 발견 하나가 아니라 한 번에 모든 발견을 원합니다. 그래서 폭, cmap, CharSet, CIDSet 헬퍼에 넘겨지는 디스크립터 참조는 Nil일 수 있고, 각자는 이전 검사가 감사를 중단했다고 가정하는 대신 진입 시 그것을 검사합니다. 해결이 누락된 것을 임베드하는 것이라면 절차는 기존 PDF에 누락된 폰트 임베드하기 노트에 있습니다
감사 실행하기
호출 한 번, 꼭 여러분이 만든 파일이 아닌 파일에 대해. TPDFlib.CheckFileCompliance는 컴플라이언스 테스트 선택자 — ISO 14289-1:2014의 PDF/UA-1이면 2 — 를 받아 0이나 문자열 목록 핸들을 반환하는데, 그 항목은 숫자 코드, 콜론, 읽을 수 있는 메시지입니다. 여기서 다룬 폰트와 콘텐츠 스트림 발견은 그 범위에서 10020부터 10041을 차지하며, 00xxx PDF/A 코드와는 수치적으로 갈라져 혼합 로그도 읽힙니다. Options에 1을 넘기면 첫 발견에서 단락되는데, 저작 도구보다 빌드 게이트에서 원하는 동작입니다. 메모리에 열려 있는 문서에는 GetPDFUADiagnostics가 디스크 왕복 없이 동등한 검사를 돌립니다
var
Issues, Count, I: Integer;
begin
// ComplianceTest = 2는 PDF/UA-1 선택. Options = 0은 모든 발견 보고
Issues := PDF.CheckFileCompliance('delivery.pdf', '', 2, 0);
if Issues = 0 then
WriteLn('delivery.pdf: PDF/UA-1 conformant')
else
begin
Count := PDF.GetStringListCount(Issues);
for I := 1 to Count do
WriteLn(' ', PDF.GetStringListItem(Issues, I)); // 예: 10037 CIDFontType2 ...
end;
end;
이 모든 것은 기계에 외부 검증자 바이너리를 요구하지 않습니다. 매 빌드마다 도는 검사와 누군가 기억할 때 도는 검사의 차이가 그것입니다. 여기서 기술한 컴플라이언스와 진단 API는 표준 PDFlibPas Delphi PDF Library에 배송되며, 그 제품 페이지가 PDF/A, PDF/X, PDF/E 테스트 스위트와 나란히 PDF/UA-1의 전체 진단 코드 표를 실어 나릅니다