Technical Article

باگ EndDoc که زیرمجموعه‌سازی فونت را بی‌صدا غیرفعال می‌کرد

یک گزارش تولید کنید، یک فونت TrueType را جاسازی کنید، و خروجی در هر نمایشگری که امتحان کنید به درستی باز می‌شود. گلیف‌ها درست هستند، متن قابل انتخاب است، و فایل معتبر است. تنها نکته اشتباه، اندازه آن است. سندی که از چند ده نویسه لاتین استفاده کرده، کل فونت ۳۵۰ کیلوبایتی را به همراه دارد. سندی که یک پاراگراف زبان چینی را چاپ کرده، به جای بخش نیم مگابایتی مورد نیاز، یک فونت ۱۴ مگابایتی CJK را حمل می‌کند. هیچ خطایی رخ نداده، هیچ هشداری ثبت نشده و فایل اعتبارسنجی را با موفقیت پشت سر گذاشته است. این همان چیزی است که یک مرحله نهایی‌سازی نامرتب از بیرون به نظر می‌رسد: هیچ چیز با شکست مواجه نمی‌شود و تنها مدرک، عددی است که بسیار بزرگ است

باگی که این مشکل را ایجاد می‌کرد برای یک دوره انتشار در HotPDF وجود داشت و از آن زمان برطرف شده است. این موضوع ارزش نوشتن دارد نه به عنوان یک اعلامیه نقص، بلکه به عنوان یک درس، زیرا شکل این اشتباه عمومی است. هر موتور سندی دارای یک مرحله نهایی‌سازی است که اشیاء را درست قبل از نوشتن آن‌ها تغییر می‌دهد، و درستی آن مرحله کاملاً به ترتیب مراحل آن نسبت به سریال‌سازی بستگی دارد. اگر یک مرحله در سمت اشتباه نوشتن قرار گیرد، هیچ کاری انجام نمی‌دهد، به آرامی

زیرمجموعه‌سازی فونت قرار است چه کاری انجام دهد

یک فونت زیرمجموعه (Subset)، بخشی از یک فایل TrueType است که یک سند در واقع از آن استفاده می‌کند. بخش ۹.۹ استاندارد ISO 32000-1 شرح می‌دهد که چگونه یک برنامه فونت جاسازی‌شده در جریان ارجاع‌شده توسط توصیف‌گر فونت قرار می‌گیرد، و برای یک برنامه TrueType این جریان همان /FontFile2 با یک /Length1 است که تعداد بایت‌های فشرده‌نشده را نشان می‌دهد. زیرمجموعه‌سازی جداول glyf و loca را بازنویسی می‌کند تا فقط شامل گلیف‌های مورد ارجاع سند باشند، شناسه گلیف‌ها را مجدداً شماره‌گذاری می‌کند و نام /BaseFont را با یک برچسب شش حرفی مانند ABCDEF+ پیشوند می‌دهد تا فونت را به عنوان زیرمجموعه علامت‌گذاری کند، دقیقاً همان‌طور که مشخصات فنی نیاز دارد. یک فونت لاتین که به ده یا پانزده کیلوبایت زیرمجموعه می‌شود، تفاوت بین یک PDF سبک و سندی است که به خاطر یک عنوان، یک تایپ‌فیس کامل را ارسال می‌کند

نقطه‌ای که این اتفاق در آن رخ می‌دهد مهم است. زیرمجموعه‌سازی، تبدیلی نیست که شما روی بایت‌های موجود در دیسک اعمال کنید. این کار گراف شیء درون حافظه را ویرایش می‌کند: محتوای جریان /FontFile2 را کوچک می‌کند، /Length1 را اصلاح می‌کند و رشته /BaseFont را بازنویسی می‌کند. همه این‌ها باید زمانی که سریال‌ساز گراف را پیمایش کرده و بایت‌ها را صادر می‌کند، در جای خود باشند. اگر ویرایش‌ها پس از نوشتن بایت‌ها اعمال شوند، اشیایی را به‌روزرسانی می‌کنند که هیچ‌کس هرگز آن‌ها را نخواهد خواند

علامت مشکل، و اینکه چرا هیچ خطایی صادر نشد

رفتار گزارش‌شده، وجود فونت‌های کامل در خروجی بدون هیچ‌گونه عیب‌یابی بود. کاربری که یک فونت Unicode TrueType را ثبت کرده و یک سند معمولی تولید کرده بود، متوجه شد که شیء فونت جاسازی‌شده هم‌اندازه فایل منبع .ttf است و نام /BaseFont هیچ پیشوند شش حرفی زیرمجموعه را حمل نمی‌کند. خروجی هرگز بین اجراهایی که از ده گلیف استفاده می‌کردند و اجراهایی که از ده هزار گلیف استفاده می‌کردند، کوچک‌تر نشد

عدم وجود هرگونه خطا، بخشی است که این کلاس از باگ‌ها را پرهزینه می‌کند. یک روال زیرمجموعه‌سازی که در زمان نادرستی اجرا می‌شود، همچنان به کار خود ادامه می‌دهد. این روال میزان استفاده از نقاط کد انباشته‌شده را پیمایش می‌کند، یک زیرمجموعه کاملاً صحیح می‌سازد و آن را روی گراف شیء در حافظه اعمال می‌کند. در داخل، کار انجام شده و فراخوانی بدون مشکل بازمی‌گرداند. تنها مشکل این است که گراف شیء ویرایش‌شده دیگر چیزی نیست که نوشته می‌شود، زیرا نویسنده قبلاً کار خود را تمام کرده است. از دیدگاه فراخوان‌کننده، سند بدون هیچ حادثه‌ای تولید و ذخیره شده است، که این دقیقاً برداشتی است که یک شکست بی‌صدا ایجاد می‌کند

علت اصلی، ترتیب نهایی‌سازی بود

در HotPDF کار بستن سند در داخل EndDoc رخ می‌دهد. مرحله زیرمجموعه‌سازی یک روال داخلی به نام BuildAndApplyUnicodeFontSubset است. این روال مجموعه نقاط کد استفاده‌شده برای هر سند را می‌خواند که در یک بیت‌مپ نگهداری می‌شود و مسیر صادرکننده متن هنگام نمایش گلیف‌ها آن را پر می‌کند، هر نقطه کد استفاده‌شده را از طریق جدول نقاط کد به گلیف کش‌شده به یک شناسه گلیف واقعی نگاشت می‌کند و برنامه فونت را حول آن بسته بازنویسی می‌کند. هنگامی که یک فونت Unicode TrueType ثبت می‌شود، مسیر صادرکننده برای هر کاراکتری که رسم می‌کند یک ثبت در مجموعه نقاط کد استفاده‌شده تنظیم می‌کند، بنابراین تا زمانی که سند بسته می‌شود، موتور دقیقاً می‌داند که زیرمجموعه باید کدام گلیف‌ها را نگه دارد

نقص این بود که BuildAndApplyUnicodeFontSubset پس از اینکه SaveToStream یا SaveToFile سند را سریال‌سازی کرده بودند فراخوانی می‌شد. ویرایش‌های زیرمجموعه‌ساز در /FontFile2، مقدار اصلاح‌شده /Length1 و پیشوند شش حرفی /BaseFont همگی بر روی یک گراف شیء محاسبه می‌شدند که قبلاً به بایت تبدیل شده بود. راه حل، تغییر ترتیب در یک خط بود: انتقال فراخوانی زیرمجموعه به قبل از سریال‌سازی، تا نویسنده فونت زیرمجموعه‌سازی‌شده را صادر کند نه فونت اصلی را. توالی اصلاح‌شده ابتدا زیرمجموعه‌ساز را اجرا می‌کند و پس از آن سریال‌سازی را انجام می‌دهد

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
    Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
    Pdf.EndDoc;                 // subsetting runs here, before the write
    Pdf.SaveToFile('Report.pdf');
  finally
    Pdf.Free;
  end;
end;

با اصلاح ترتیب، هیچ تغییری در کد فراخوان‌کننده ایجاد نمی‌شود. پس از ثبت فونت Unicode TrueType، زیرمجموعه‌سازی به طور پیش‌فرض فعال است. شما فونت را ثبت می‌کنید، سند را شروع می‌کنید، رسم می‌کنید و آن را به پایان می‌رسانید، و زیرمجموعه از گلیف‌هایی که قبل از خروج بایت‌ها از حافظه استفاده کرده‌اید ساخته می‌شود

چرا یک مرحله اشتباه، خود یک دسته کامل از مشکلات است

دلیل اینکه این موضوع ارزش یک درس را دارد و نه فقط یک پاورقی، این است که EndDoc لیستی از مراحل پایانی را صادر می‌کند و تک‌تک آن‌ها نسبت به موقعیت خود نسبت به نوشتن حساس هستند. زیرمجموعه‌سازی فونت یکی از آن‌هاست. خروجی PDF/A به یک جریان /CIDSet نیاز دارد که دقیقاً شناسه‌های گلیف موجود در زیرمجموعه را شمارش کند، محدودیتی که استاندارد ISO 19005 اعمال می‌کند تا یک اعتبارسنج بتواند مطابقت برنامه جاسازی‌شده را با آنچه توصیف‌گر فونت ادعا می‌کند تأیید کند؛ این جریان در همان پنجره نهایی‌سازی صادر می‌شود و به ساخته شدن اولیه زیرمجموعه بستگی دارد. استاندارد PDF/UA-1 طبق بخش ۷.۱۸.۳ استاندارد ISO 14289-1 الزامی می‌کند که هر صفحه‌ای که دارای یادداشت (Annotation) است، کلید /Tabs را با مقدار /S تعریف کند، و یک روال داخلی به نام EnsurePDFUATabsOnAnnotatedPages این کلید را در همان مرحله ثبت می‌کند. بررسی‌های خروجی هدف (Output-intent) نیز در آنجا اجرا می‌شوند

همان خطای ترتیبی که زیرمجموعه‌سازی را غیرفعال می‌کرد، کلید ترتیب تب PDF/UA را نیز در صفحات دارای یادداشت حذف کرد، زیرا آن مرحله در همان سمت اشتباه نوشتن قرار داشت. ابزارهای veraPDF و PAC نبود /Tabs /S را به عنوان نقض پروتکل Matterhorn در بخش ۲۱-۰۰۱ گزارش می‌دهند. بنابراین، یک فراخوانی اشتباه نه تنها اندازه فایل را افزایش داد، بلکه به طور هم‌زمان یک نیاز انطباق دسترسی‌پذیری را بدون هیچ خطایی نقض کرد. این خطر یک مرحله نهایی‌سازی است: مراحل آن در یک پیش‌شرط مشترک هستند و یک اشتباه ترتیبی منفرد می‌تواند چندین مورد از آن‌ها را به طور هم‌زمان از کار بیندازد در حالی که هر فراخوانی همچنان موفقیت‌آمیز بازمی‌گرداند

چگونه یک خرابی صدور بی‌صدا واقعاً شناسایی می‌شود

باگی که هیچ استثنایی (Exception) ایجاد نمی‌کند، با اجرای برنامه شناسایی نمی‌شود. این باگ با بازرسی خروجی و مقایسه آن با آنچه ورودی باید تولید می‌کرد، شناسایی می‌شود. برای زیرمجموعه‌سازی فونت، بررسی‌ها ملموس هستند. اندازه فایل خروجی را با یک انتظار تقریبی مقایسه کنید: سندی که به چند گلیف محدود دست زده است نباید به اندازه یک تایپ‌فیس کامل باشد. شیء فونت جاسازی‌شده را باز کرده و طول بایت آن را بخوانید؛ یک /FontFile2 زیرمجموعه‌سازی‌شده برای یک فونت لاتین، کسری کوچک از فایل منبع است. نام /BaseFont را بخوانید و مطمئن شوید که پیشوند شش حرفی وجود دارد، زیرا نبود آن سیگنال مستقیمی است که نشان می‌دهد هیچ زیرمجموعه‌سازی اعمال نشده است

var
  Pdf: THotPDF;
  Output: TMemoryStream;
begin
  Output := TMemoryStream.Create;
  try
    Pdf := THotPDF.Create(nil);
    try
      Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
      Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
      Pdf.EndDoc;
      Pdf.SaveToStream(Output);
    finally
      Pdf.Free;
    end;
    // A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
    if Output.Size > 100 * 1024 then
      raise Exception.Create('Font subset did not shrink the output');
  finally
    Output.Free;
  end;
end;

برای خروجی PDF/A، این بررسی دقیق‌تر هم هست، زیرا یک اعتبارسنج کار را برای شما انجام می‌دهد. سطح انطباق را تنظیم کرده و نتیجه را از طریق veraPDF اجرا کنید: فقدان /CIDSet یا زیرمجموعه‌ای که با توصیف‌گر مطابقت ندارد، به جای اینکه منتظر بماند تا شما با چشم متوجه شوید، به عنوان یک بند ناموفق گزارش می‌شود. سوئیچ‌های انطباق که این کار نهایی‌سازی را هدایت می‌کنند، ویژگی‌هایی در سند هستند. ویژگی PDFACompliance یک رشته مانند '2B' برای PDF/A-2 سطح B می‌گیرد و PDFUACompliance یک مقدار Boolean است که الزامات PDF تگ‌شده و ترتیب تب‌ها را فعال می‌کند

Pdf := THotPDF.Create(nil);
try
  Pdf.PDFACompliance := '2B';     // PDF/A-2 Level B, drives /CIDSet emission
  Pdf.PDFUACompliance := True;    // stamps /Tabs /S on annotated pages
  Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
  Pdf.BeginDoc;
  Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
  Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
  Pdf.EndDoc;
  Pdf.SaveToFile('Report_PDFA.pdf');
finally
  Pdf.Free;
end;

درس مهندسی

دو قانون از این موضوع حاصل می‌شود. اول اینکه هر مرحله نهایی‌سازی که اشیاء را تغییر می‌دهد باید قبل از سریال‌سازی آن اشیاء اجرا شود، و مرحله پایانی یک موتور سند باید به عنوان یک خط لوله مرتب خوانده شود که در آن سریال‌سازی آخرین اقدام است، نه اقدامی در میان چندین اقدام دیگر. دومین مورد، همان چیزی است که در اینجا بیشترین زمان را هدر داد: برای یک مرحله صدور، عدم وجود خطا دلیلی بر موفقیت نیست. روالی که زیرمجموعه درستی را می‌سازد و آن را روی گراف اشتباهی که قبلاً نوشته شده اعمال می‌کند، هیچ مشکلی گزارش نمی‌دهد، زیرا از دیدگاه خودش همه چیز درست بوده است. تأیید اعتبار باید به محصول نهایی نگاه کند، نه به کد بازگشتی. اندازه خروجی را بررسی کنید، طول بایت فونت جاسازی‌شده و پیشوند /BaseFont آن را بخوانید و اجازه دهید veraPDF خروجی PDF/A را قضاوت کند، جایی که نبود /CIDSet یک نقص بی‌صدا را به یک شکست مشخص تبدیل می‌کند

سمت تولیدکننده مدیریت فونت، نحوه ثبت و جاسازی فونت‌ها برای خروجی گزارش، در مقاله ما در مورد فونت‌ها و تصاویر در خروجی گزارش پوشش داده شده است. سمت اعتبارسنجی، که در آن این مراحل نهایی‌سازی در برابر استانداردها بررسی می‌شوند، در راهنمای اعتبارسنجی PDF/A و PDF/UA شرح داده شده است. هر دو با کارهای مربوط به زیرمجموعه‌سازی و انطباق که در اینجا توضیح داده شد جفت می‌شوند، که به عنوان بخشی از کامپوننت HotPDF برای Delphi و C++Builder همراه با APIهای بارگذاری، ویرایش، رمزگذاری و امضا که در بخش‌های دیگر این وبلاگ پوشش داده شده‌اند، عرضه می‌شود