مقاله فنی

ممیزی فونت PDF/UA در Delphi: عرض‌ها، CharSet، CIDSet

یک گزارش veraPDF که می‌گوید عرض یک گلیف با برنامه فونت جاسازی‌شده هم‌خوانی ندارد، تقریباً هیچ نمی‌گوید: نه کدام گلیف و نه چرا. PDFlibPas این پرسش را این‌طور جواب می‌دهد که هر کد کاراکتر را از طریق cmap جاسازی‌شده به یک ایندکس گلیف حل می‌کند، متریک برنامه را به 1000 واحد به ازای هر em نرمال می‌کند و مقایسه را آن‌جا انجام می‌دهد

چرا عرض گلیف‌ها هم‌خوانی ندارند؟

زیرا دو عددی که مقایسه می‌شوند در دو دستگاه مختصات متفاوت زندگی می‌کنند و هیچ چیز در دیکشنری PDF تبدیل را به شما نمی‌گوید. یک دیکشنری فونت /Widths را در فضای گلیف می‌نویسد که PDF آن را یک‌هزارم em تعیین کرده است (ISO 32000-1 §9.2.4). جدول hmtx داخل برنامه TrueType جاسازی‌شده پیشرونده‌ها را در واحدهای طراحی فونت می‌نویسد و جدول head تعیین می‌کند چند تای آن‌ها یک em می‌سازد: 2048 برای اغلب فونت‌های TrueType، 1000 برای موارد مشتق از CFF و گاهی چیزی کاملاً متفاوت. مقادیر خام را مقایسه کنید و هر فونت 2048-upem در پیکره شما خراب به نظر می‌رسد. این همان تله‌ای است که ISO 14289-1 §7.21.5 سر هر کسی که بخواهد با خواندن فیلدهای دیکشنری عرض‌ها را ممیزی کند می‌گذارد

ممیزی عرض گلیف در PDFlibPas در Delphi: یک مدخل /Widths در فضای گلیف و یک پیشرونده hmtx در واحدهای طراحی فونت با مقیاس کردن متریک برنامه به 1000 واحد به ازای هر em پیش از هر مقایسه، به یک دستگاه مختصات مشترک می‌رسند
PDFlibPas هر پیشرونده hmtx را پیش از مقایسه با عرض دیکشنری به یک‌هزارم em مقیاس می‌کند و فقط موارد با فاصله بیش از یک واحد را گزارش می‌کند

PDFlibPas هنگام لود نرمال می‌کند. TPDFTrueTypeParser مقدار Advance * 1000 div unitsPerEm را در آرایه عرضش ذخیره می‌کند، پس Parser.GetWidth(GID) از قبل به همان هزارم‌های em که PDF استفاده می‌کند جواب می‌دهد و GetRawWidth هم وقتی واحدهای طراحی لازم دارید در دسترس می‌ماند. این‌جا نیمه سخت‌تر باقی می‌ماند: رسیدن از یک کد کاراکتر به یک ایندکس گلیف. برای یک فونت TrueType ساده مسیر به پرچم Symbolic در FontDescriptor بستگی دارد، بیت 3 از /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // فونت‌های Symbolic مستقیماً از طریق cmap برنامه آدرس می‌گیرند،
    // با قرارداد بایت پرارزش (3,0) به‌عنوان جایگزین
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // غیر Symbolic: کد -> نام گلیف از طریق encoding، نام -> Unicode
    // از طریق Adobe Glyph List، Unicode -> GID از طریق cmap برنامه
    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;

دو جزئیات در آن قطعه وزن دارند. رواداری یک واحد است نه صفر، چون نرمال‌سازی تقسیم صحیح است و یک فایل قانونی می‌تواند یک واحد خطا داشته باشد؛ دقیقاً همان عبارت «در حد یک هزارم em» که دیاگنوستیک 10036 گزارش می‌کند. و گارد GID < Parser.GlyphCount تزئین نیست. GetWidth برای فراخوان‌های رندر بخشنده نوشته شده: ایندکس خارج از بازه را به آخرین مدخل hmtx می‌بُرد و وقتی جدول غایب است به 750 فرو می‌افتد. بخشندگی برای رندر درست و برای ممیزی غلط است، پس ممیزی ایندکس را پیش از درخواست عرض رد می‌کند به‌جای اینکه به برش اعتماد کند

CIDFontType2 یک غیرمستقیم‌سازی دیگر اضافه می‌کند

PDFlibPas فونت‌های ترکیبی را به همان شکل طی می‌کند، با /CIDToGIDMap که میان CID و گلیف قرار می‌گیرد. عرض‌ها در آرایه /W می‌آیند که ISO 32000-1 §9.7.4.3 دو شکل برایش تعیین کرده که آزادانه در یک آرایه در هم می‌آمیزند: یک CID شروع به‌همراه آرایه‌ای از عرض‌های متوالی، یا یک CID نخست، یک CID آخر و یک عرض تنها که روی کل بازه اعمال می‌شود. ممیزی هر دو را تجزیه می‌کند، سپس هر جفت به‌دست‌آمده را به همان مقایسه می‌سپارد و مجموع را زیر دیاگنوستیک 10037 گزارش می‌کند. گام نگاشت جایی است که فونت‌های ترکیبی تفاوت دارند و دلیل اهمیت دیاگنوستیک 10021 یعنی نبود نقشه، پیش از خواندن هر عرضی است — یک /CIDToGIDMap غایب یا بدشکل نه فقط §7.21.3.2 را نقض می‌کند بلکه پرسش عرض را بی‌جواب می‌کند

سه مسیر PDFlibPas از یک کد کاراکتر به یک ایندکس گلیف در Delphi: cmap برنامه برای فونت‌های TrueType Symbolic، یک انحراف با encoding و Adobe Glyph List برای غیر Symbolic‌ها، و یک گام CMap به‌علاوه /CIDToGIDMap برای CIDFontType2
مقایسه عرض تا وقتی کد کاراکتر به ایندکس گلیف حل نشود آغاز نمی‌شود و هر نوع فونت از مسیر متفاوتی به آن ایندکس می‌رسد
// /CIDToGIDMap یا نام /Identity است یا استریمی از ایندکس‌های گلیف 16 بیتی
// big-endian، یکی به ازای هر CID (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 یک‌مبنا هستند
  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;

ممیزی‌گر وقتی برنامه فونت رمزگشایی نمی‌شود چه باید بکند؟

هیچ نگویید. آزمون‌های کامل‌بودن /CharSet و /CIDSet که ISO 14289-1 §7.21.4.2 الزام می‌کند — دیاگنوستیک‌های 10038 و 10039 — جایی است که یک اعتبارسنج بیش‌ازحد سخت‌گیر به بار می‌شود، چون گزارش «CharSet شما ناقص است» برای خواننده از «رمزگشای Type 1 ما تسلیم شد» قابل تفکیک نیست. پس PDFlibPas نبود یک مدخل را فقط زمانی گزارش می‌کند که سه چیز با هم موفق شوند: برنامه فونت رمزگشایی شود، نگاشت کد به گلیف حل شود و خود مجموعه رمزگشایی شود. TPDFType1Decoder.LoadPFBFromString باید True برگرداند و یک شمارش charstring تولید کند پیش از اینکه نام هر گلیفی با رشته /CharSet آزموده شود؛ مسیر /CIDSet نیاز دارد استریم inflate شود و تعداد گلیف مثبت برگردد پیش از آزمودن حتی یک بیت. هر استثنایی در مسیر به «بدون یافته» فرو می‌ریزد نه به یک نقص

قاعده گزارش محافظه‌کارانه در ممیزی PDF/UA در PDFlibPas: نبود مدخل /CharSet یا /CIDSet فقط وقتی گزارش می‌شود که برنامه فونت رمزگشایی شود، نگاشت کد به گلیف حل شود و خود مجموعه رمزگشایی شود، و هر شکستی سکوت تولید می‌کند
پیش از صدور یافته نبود-مدخل سه موفقیت مستقل لازم است، پس رمزگشایی که تسلیم می‌شود به جای اتهام نادرست، یک منفی کاذب برای شما خرج می‌گذارد

این یک تمایل آگاهانه به منفی‌های کاذب است و ارزش دارد صریح گفته شود نه دفن شود. یک جدول CFF خراب، یک نوع Type 1 پشتیبانی‌نشده یا یک /CIDSet کوتاه‌تر از بازه گلیف، همه سکوت تولید می‌کنند نه دیاگنوستیک. استدلال این است که گزارش‌های ممیزی PDF/UA به نویسندگانی فوروارد می‌شود که ابزار را نساخته‌اند و یک اتهام نادرست بیشتر از یک یافته از دست‌رفته خرج دارد: نویسنده یک روز را صرف اثبات اینکه فایل منطبق واقعاً منطبق است می‌کند و اعتمادش به کل گزارش قطع می‌شود. پروتکل Matterhorn همین تفکیک را به شکل دیگری دارد وقتی آزمون‌هایی را که ماشین می‌تواند تصمیم بگیرد از آزمون‌هایی که انسان باید جدا می‌کند و نقطه بازرسی Fonts آن (31) جایگاه این‌هاست. اگر قرائت سخت‌گیرانه‌تر لازم دارید، PDFlibPas را به‌شکل گیت سریع و یک اعتبارسنج اختصاصی را به‌شکل نظر دوم اجرا کنید — همان جفت‌سازی‌ای که در پیش‌پرواز PDF/A و PDF/UA توضیح داده شده

محتوای صفحه یعنی /Contents یک لیست است نه یک استریم

پرهزینه‌ترین اشتباه در ممیزی استریم محتوا، تلقی کردن /Contents به‌شکل یک استریم تنهاست. ISO 32000-1 §7.7.3.3 اجازه می‌دهد صفحه یک آرایه از استریم‌ها نگه دارد که الحاق آن‌ها، با فضای سفید بین بخش‌ها، برنامه صفحه است؛ تولیدکننده‌ها در نقاط دلخواه می‌شکنند و یک BT می‌تواند در یک عضو بنشیند و ET متناظرش در عضو بعدی. یک پردازنده محتوا حالت دارد — عمق تودرتویی marked-content، فونت انتخاب‌شده توسط آخرین 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‌ها واقعاً ساختار‌نیافته حساب می‌شوند؟

فقط آن‌هایی که صفحه واقعاً فراخوانی می‌کند، از یک نقطه فراخوان بیرون از marked content، که محتوای خودشان متن نشان می‌دهد. دیاگنوستیک 10040 §7.20 از ISO 14289-1 را با ثبت سه واقعیت مستقل به ازای هر شماره شیء اعمال می‌کند — متن دارد، فراخوانی شده، داخل marked content فراخوانی شده — و فقط اشتراک دو اول منهای سومی را گزارش می‌کند. هر یک از دو میان‌بُر به شکلی غلط است که خودتان منتشرش می‌کردید: پرچم زدن هر Form دارای متن در /Resources یک کتابخانه قالب را جریمه می‌کند که هیچ‌کس ازش نمی‌کشد، و پرچم زدن هر Form فراخوانی‌شده لوگوهای وکتور بدون متن را جریمه می‌کند که به تگ‌گذاری نیازی ندارند. نقطه فراخوانی با شماره شیء حل می‌شود نه با نام منبع، چون همان Form به‌طور روتین با نام‌های متفاوت در صفحات متفاوت دسترسی می‌گیرد. دیاگنوستیک همراه 10041 همان برنامه الحاق‌شده را برای §7.21.8 طی می‌کند، هر عملوند نمایش متن را از طریق فونت در محدوده حل می‌کند و کدهایی را می‌شمارد که به .notdef می‌افتند، که مستقل از حالت رندر متن ممنوع است — از جمله حالت نامرئی که پشت تصاویر اسکن‌شده استفاده می‌شود. نحوه پیچیدن Formهای باقی‌مانده یک پرسش درخت ساختار است که در مقاله ساختن ساختار PDF تگ‌شده پوشش داده شده

فونت‌هایی که اصلاً FontDescriptor ندارند

یک فونت جاسازی‌نشده ورودی قانونی این ممیزی است نه یک حالت خطا، و هر کمکی زیر بررسی جاسازی باید از آن جان سالم ببرد. وقتی PDFlibPas هیچ /FontDescriptor‌ای پیدا نمی‌کند یا توصیف‌گری بدون FontFile، FontFile2 یا FontFile3، دیاگنوستیک 10020 را ثبت می‌کند — یا 10022 وقتی نام یکی از Standard 14 است که §7.21.4 NOTE 5 آشکارا از معافیتش خودداری می‌کند — و بعد به بقیه فایل ادامه می‌دهد. این کل نقطه یک گزارش است: نویسنده همه یافته‌ها را در یک گذر می‌خواهد نه یک یافته به ازای هر اجرا. پس ارجاع توصیف‌گری که به کمک‌های عرض، cmap، CharSet و CIDSet داده می‌شود می‌تواند Nil باشد و هر کدام ورودی‌اش را آزمایش می‌کنند به‌جای اینکه فرض کنند بررسی قبلی ممیزی را قطع کرده. اگر راه‌حل جاسازی چیزهای جاافتاده است، سازوکارش در یادداشت جاسازی فونت‌های جاافتاده در یک PDF موجود آمده

اجرای ممیزی

یک فراخوان، روی فایلی که لزوماً خودتان تولیدش نکرده‌اید. TPDFlib.CheckFileCompliance یک انتخاب‌گر آزمون انطباق می‌گیرد — 2 برای PDF/UA-1 زیر ISO 14289-1:2014 — و یا صفر برمی‌گرداند یا یک هندل لیست رشته که مدخل‌هایش یک کد عددی، یک دونقطه و یک پیام خوانا هستند. یافته‌های فونت و استریم محتوا که این‌جا بحث شد بازه 10020 تا 10041 را اشغال می‌کنند، عددی جدا از کدهای 00xxx مربوط به PDF/A تا یک لاگ مختلط خوانا بماند. دادن 1 در Options روی اولین یافته کوتاه می‌آید، که در یک گیت بیلد می‌خواهید نه در یک ابزار نویسندگی. برای سندی که هنوز در حافظه باز است، 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/UA-1 را کنار مجموعه آزمون PDF/A، PDF/X و PDF/E حمل می‌کند