مقاله فنی

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

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

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

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

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

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

نشانه خطا و اینکه چرا هیچ هشداری داده نمی‌شود

رفتار گزارش‌شده، وجود فونت‌های کامل در خروجی بدون هیچ پیغام خطایی بود. کاربری که یک فونت 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 §7.18.3) که هر صفحه‌ای که حاشیه‌نویسی (annotation) دارد باید در آن کلید /Tabs با مقدار /S معرفی شود، و روتین داخلی به نام EnsurePDFUATabsOnAnnotatedPages این کلید را در همان مرحله ثبت می‌کند. بررسی‌های خروجی هدف نیز در همانجا اجرا می‌شوند

همان خطای ترتیب که زیرمجموعه‌سازی را از کار می‌انداخت، همچنین کلید ترتیب برگه‌ها (tab-order) در PDF/UA روی صفحات حاشیه‌نویسی‌شده را از قلم انداخت، زیرا این مرحله در همان سمت اشتباه نوشتن قرار داشت. ابزارهای اعتبارسنجی مانند veraPDF و PAC نبود /Tabs /S را به عنوان یک نقض در بررسی 21-001 پروتکل Matterhorn گزارش می‌دهند. در نتیجه، تنها یک اشتباه در قرار دادن فراخوانی، نه تنها حجم فایل را افزایش داد؛ بلکه به‌طور خاموش الزام مطابقت با دسترسی‌پذیری را نیز مختل کرد، در حالی که در هیچ یک از این موارد خطایی نمایش داده نمی‌شد. این همان خطر مرحله نهایی‌سازی است: مراحل آن دارای پیش‌نیازهای مشترکی هستند و تنها یک اشتباه در ترتیب می‌تواند چندین مرحله را با هم از بین ببرد در حالی که همه فراخوانی‌ها همچنان موفقیت را گزارش می‌دهند

چگونه خطای خاموش تولید کشف می‌شود

باگی که استثنایی را ایجاد نمی‌کند، از طریق اجرای برنامه کشف نمی‌شود. این باگ با بررسی خروجی و مقایسه آن با آنچه که ورودی باید تولید می‌کرده کشف می‌شود. برای زیرمجموعه‌سازی فونت، بررسی‌ها کاملاً ملموس هستند. اندازه فایل خروجی را با یک انتظار تقریبی مقایسه کنید: سندی که فقط چند گلیف را پردازش کرده نباید به اندازه کل مجموعه فونت حجم داشته باشد. شیء فونت تعبیه‌شده را باز کرده و طول بایت‌های آن را بخوانید؛ یک /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 یک مقدار بولی است که الزامات PDF تگ‌دار و ترتیب برگه‌ها (tab-order) را روشن می‌کند

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 برای دلفی و C++Builder به همراه رابط‌های برنامه‌نویسی برای بارگیری، ویرایش، رمزگذاری و امضا که در مقالات دیگر این وبلاگ پوشش داده شده‌اند، در دسترس است