Một báo cáo veraPDF nói rằng độ rộng glyph mâu thuẫn với chương trình font nhúng chẳng nói cho bạn điều gì về glyph nào hoặc vì sao. PDFlibPas trả lời câu hỏi đó bằng cách phân giải từng mã ký tự qua cmap nhúng ra chỉ số glyph, chuẩn hóa số liệu của chương trình về 1000 đơn vị mỗi em, rồi so sánh tại đó
Vì sao độ rộng glyph mâu thuẫn nhau?
Vì hai con số đang so sánh sống trong hai hệ tọa độ khác nhau, và không có gì trong dictionary PDF nói cho bạn phép chuyển đổi. Dictionary font ghi /Widths trong không gian glyph, mà PDF cố định bằng một phần nghìn của em (ISO 32000-1 §9.2.4). Bảng hmtx bên trong chương trình TrueType nhúng ghi các advance theo đơn vị thiết kế font, và bảng head quyết định bao nhiêu đơn vị như vậy tạo thành một em: 2048 với phần lớn mặt chữ TrueType, 1000 với các mặt phái sinh từ CFF, thi thoảng là một thứ hoàn toàn khác. So sánh giá trị thô và mọi font 2048-upem trong kho dữ liệu của bạn trông đều hỏng. Đó là cái bẫy ISO 14289-1 §7.21.5 giăng cho bất kỳ ai định kiểm toán độ rộng bằng cách đọc các trường dictionary
PDFlibPas chuẩn hóa ngay khi nạp. TPDFTrueTypeParser lưu Advance * 1000 div unitsPerEm trong mảng width của nó, nên Parser.GetWidth(GID) đã trả lời theo nghìn phần em đúng như PDF dùng, còn GetRawWidth vẫn sẵn dùng khi bạn cần lại đơn vị thiết kế. Vẫn còn nửa khó hơn: đi từ một mã ký tự đến một chỉ số glyph. Với font TrueType đơn giản, lộ trình phụ thuộc vào cờ Symbolic trong FontDescriptor, bit 3 của /Flags
Parser := TPDFTrueTypeParser.Create;
try
Parser.LoadFromString(FontProgram);
if Symbolic then
begin
// Mặt Symbolic được địa chỉ hóa trực tiếp qua cmap của chương trình,
// với quy ước byte cao (3,0) làm phương án dự phòng
GID := Parser.GetGlyphIndex(Code);
if GID = 0 then
GID := Parser.GetGlyphIndex($F000 + Code);
end
else
begin
// Không Symbolic: mã -> tên glyph qua encoding, tên -> Unicode
// qua Adobe Glyph List, Unicode -> GID qua cmap của chương trình
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;
Hai chi tiết trong đoạn code đó mang sức nặng. Dung sai là một đơn vị, không phải 0, vì phép chuẩn hóa là chia số nguyên và một tệp được tạo ra chính đáng có thể lệch đúng một đơn vị; đó chính xác là cách diễn đạt "trong phạm vi một phần nghìn em" mà diagnostic 10036 báo. Và guard GID < Parser.GlyphCount không phải đồ trang trí. GetWidth được viết để dễ dãi với các caller render, kẹp chỉ số ngoài phạm vi về entry cuối của hmtx và quay về 750 khi bảng vắng mặt. Dễ dãi đúng cho render và sai cho kiểm toán, nên cuộc kiểm toán từ chối chỉ số trước khi xin độ rộng thay vì tin vào phép kẹp
CIDFontType2 thêm một lớp gián tiếp nữa
PDFlibPas đi qua các font composite theo cùng cách, với /CIDToGIDMap chen giữa CID và glyph. Độ rộng đến trong mảng /W, mà ISO 32000-1 §9.7.4.3 cho phép hai hình dạng đan xen tự do trong một mảng: một CID khởi đầu theo sau bởi một mảng các độ rộng liên tiếp, hoặc một CID đầu, một CID cuối, và một độ rộng duy nhất áp dụng cho cả đoạn. Cuộc kiểm toán parse cả hai, rồi đưa từng cặp kết quả vào cùng một phép so sánh, và báo tổng số dưới diagnostic 10037. Bước mapping là chỗ font composite khác nhau, và đó là lý do diagnostic 10021 về thiếu map quan trọng trước khi bạn đọc bất kỳ độ rộng nào — một /CIDToGIDMap vắng mặt hay dị dạng không chỉ vi phạm §7.21.3.2, nó làm câu hỏi độ rộng trở nên không thể trả lời
// /CIDToGIDMap là name /Identity hoặc một stream các chỉ số glyph 16-bit
// big-endian, mỗi CID một chỉ số (ISO 32000-1 mục 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; // chuỗi Pascal đánh chỉ số từ 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;
Người kiểm toán nên làm gì khi chương trình font không giải mã được?
Im lặng. Các phép kiểm tra tính đầy đủ /CharSet và /CIDSet mà ISO 14289-1 §7.21.4.2 yêu cầu — diagnostic 10038 và 10039 — là nơi một validator vội vàng biến thành gánh nặng, vì một báo cáo "CharSet của bạn không đầy đủ" với người đọc không thể phân biệt được với "decoder Type 1 của chúng tôi đã bỏ cuộc". Vì thế PDFlibPas chỉ báo cáo một entry còn thiếu khi ba điều đều thành công: chương trình font giải mã được, mapping mã sang glyph phân giải được, và tập hợp đó tự nó giải mã được. TPDFType1Decoder.LoadPFBFromString phải trả về True và cho ra một số lượng charstring trước khi bất kỳ tên glyph nào được đối chiếu với chuỗi /CharSet; đường /CIDSet cần stream inflate được và số glyph trả về dương trước khi một bit duy nhất được kiểm tra. Bất kỳ exception nào trên đường đi đều gộp về "không phát hiện", không phải một khiếm khuyết
Đó là một thiên lệch có chủ ý về phía âm tính giả, và xứng đáng được nói thẳng thay vì vùi đi. Một bảng CFF hỏng, một biến thể Type 1 không được hỗ trợ, hay một /CIDSet ngắn hơn phạm vi glyph đều cho ra im lặng thay vì một diagnostic. Lý do là các cuộc kiểm toán PDF/UA thường bị chuyển tiếp tới những tác giả không xây dựng công cụ, và một lời buộc tội sai tốn kém hơn một phát hiện bị bỏ sót: tác giả đốt một ngày để chứng minh một tệp tuân thủ là tuân thủ, rồi ngừng tin cả báo cáo. Matterhorn Protocol vạch cùng một ranh giới dưới hình thức khác khi tách các phép kiểm tra máy quyết định được khỏi các phép kiểm tra con người phải làm, và checkpoint Fonts (31) của nó chính là nơi các phép này sống. Nếu bạn cần cách đọc nghiêm ngặt hơn, hãy chạy PDFlibPas làm cổng nhanh và một validator chuyên dụng làm ý kiến thứ hai — cặp bài trùng đó cũng là thứ được mô tả trong bài hướng dẫn preflight PDF/A và PDF/UA
/Contents của trang là một danh sách, không phải một stream
Sai lầm đắt giá nhất trong kiểm toán content stream là coi /Contents như một stream duy nhất. ISO 32000-1 §7.7.3.3 cho phép một trang giữ một mảng các stream mà phần ghép nối của chúng, với khoảng trắng giữa các phần, chính là chương trình trang; các producer cắt tại những điểm tùy ý, và một BT có thể nằm ở phần tử này còn ET cặp đôi của nó nằm ở phần tử kế. Một bộ xử lý nội dung giữ trạng thái — độ sâu lồng marked-content, font được Tf cuối chọn, cờ text-object — và Process đặt lại trạng thái đó khi vào. Gọi nó một lần cho mỗi phần tử mảng và mọi stream sau stream đầu đều khởi đầu không có font hiện hành, nên văn bản vốn được tag rất chuẩn đọc thành nhiễu không tag, không font. PDFlibPas ghép nối trước rồi xử lý một lần
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;
// Một lời gọi Process trên toàn bộ phần ghép, không bao giờ một lời gọi mỗi phần tử
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));
Form XObjects nào thực sự tính là phi cấu trúc?
Chỉ những Form mà trang thực sự gọi, từ một call site ngoài marked content, và nội dung của nó có hiển thị văn bản. Diagnostic 10040 thi hành ISO 14289-1 §7.20 bằng cách ghi ba sự thật độc lập cho mỗi số đối tượng — có văn bản, được gọi, được gọi bên trong marked content — và chỉ báo cáo giao của hai điều đầu trừ điều thứ ba. Mỗi con tắt trong hai con tắt đều sai theo cách bạn sẽ ship: gắn cờ mọi Form chứa văn bản trong /Resources trừng phạt một thư viện template không ai vẽ ra, và gắn cờ mọi Form được gọi trừng phạt các logo vector không chứa văn bản và không cần tag. Call site được phân giải theo số đối tượng chứ không theo tên tài nguyên, vì cùng một Form thường được chạm tới qua những tên khác nhau trên các trang khác nhau. Diagnostic 10041 đi kèm đi qua cùng chương trình đã ghép cho §7.21.8, phân giải từng toán tử hiển thị văn bản qua font trong phạm vi và đếm các mã rơi vào .notdef, điều bị cấm bất kể chế độ render văn bản — kể cả chế độ vô hình dùng phía sau ảnh quét. Những Form sống sót nên được bọc thế nào là câu hỏi của cây cấu trúc, được nói trong bài về xây dựng cấu trúc PDF có tag
Font không hề có FontDescriptor
Một font không nhúng là dữ liệu vào hợp lệ của cuộc kiểm toán này, không phải trạng thái lỗi, và mọi helper phía dưới phép kiểm tra nhúng đều phải sống sót qua nó. Khi PDFlibPas không tìm thấy /FontDescriptor, hoặc một descriptor không có FontFile, FontFile2, hay FontFile3, nó ghi diagnostic 10020 — hoặc 10022 khi tên nằm trong nhóm Standard 14, mà §7.21.4 NOTE 5 thẳng thắn từ chối miễn trừ — rồi tiếp tục đi hết phần còn lại của tệp. Đó là ý nghĩa trọn vẹn của một báo cáo: tác giả muốn mọi phát hiện trong một lượt, chứ không phải một phát hiện mỗi lần chạy. Vì thế, tham chiếu descriptor đưa cho các helper width, cmap, CharSet và CIDSet có thể là Nil, và mỗi helper tự kiểm tra điều đó khi vào thay vì giả định một phép kiểm tra trước đó đã dừng cuộc kiểm toán. Nếu cách sửa là nhúng những gì còn thiếu, cơ chế nằm trong ghi chú về nhúng font còn thiếu vào một tệp PDF hiện có
Chạy cuộc kiểm toán
Một lời gọi, trên một tệp bạn chưa chắc từng tạo ra. TPDFlib.CheckFileCompliance nhận một bộ chọn bài kiểm tra tuân thủ — 2 cho PDF/UA-1 theo ISO 14289-1:2014 — và trả về hoặc 0 hoặc một handle danh sách chuỗi mà các entry là một mã số, một dấu hai chấm, và một thông điệp đọc được. Các phát hiện về font và content stream bàn ở đây chiếm 10020 đến 10041 trong khoảng đó, được giữ tách biệt về số với các mã 00xxx của PDF/A để một log lẫn vẫn đọc được. Truyền 1 vào Options sẽ cắt ngắn ngay tại phát hiện đầu tiên, điều bạn muốn ở một cổng build chứ không phải trong một công cụ soạn thảo. Với tài liệu còn mở trong bộ nhớ, GetPDFUADiagnostics chạy cuộc kiểm tra tương đương mà không cần qua đĩa
var
Issues, Count, I: Integer;
begin
// ComplianceTest = 2 chọn PDF/UA-1; Options = 0 báo mọi phát hiện
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)); // ví dụ 10037 CIDFontType2 ...
end;
end;
Không điều nào trong số này cần một binary validator bên ngoài trên máy, và đó là khác biệt giữa một phép kiểm tra chạy ở mọi build và một phép kiểm tra chạy khi ai đó nhớ ra. Các API tuân thủ và diagnostic mô tả ở đây đi kèm PDFlibPas Delphi PDF Library chuẩn, với trang sản phẩm mang bảng mã diagnostic đầy đủ cho PDF/UA-1 cạnh các bộ kiểm tra PDF/A, PDF/X và PDF/E