مقاله فنی

رندر کردن فونت‌های تعبیه‌شده PDF در دلفی با HotPDF

کامپوننت HotPDF فونت‌های تعبیه‌شده PDF را در دلفی بدون نیاز به نصب هیچ چیز در سیستم رندر می‌کند: خط لوله رندر گلیف‌های تعبیه‌شده در HPDFGlyphRender.pas برنامه‌های فونت ذخیره شده در خود فایل PDF را تجزیه می‌کند — خطوط کلی TrueType glyf از FontFile2، استرینگ‌های کاراکتر CFF Type 2 از FontFile3، و استریم‌های محتوای گلیف Type 3 — و آنها را به عنوان مسیرهای برداری پر شده GDI بازپخش می‌کند. این مقاله بررسی عمیق دقت فونت در پشت صحنه رندر صفحات PDF به TBitmap با استفاده از HotPDF است: آن بخش رندرکننده را به طور کلی پوشش می‌دهد، و این بخش به نحوه دریافت اشکال دقیق متن در آن صفحات می‌پردازد

وجود جعبه‌ها، فضاهای خالی یا کاراکترهای با خطای جزئی در خروجی رندر شده PDF، تقریباً همیشه به این معنی است که رندرکننده به جای استفاده از فونت تعبیه‌شده در فایل، فونتی را از سیستم‌عامل درخواست کرده است. شکایت همیشه به یک شکل مطرح می‌شود: سند در سیستمی که آن را تولید کرده عالی به نظر می‌رسد، سپس مشتری آن را در یک سرور تمیز یا یک دسکتاپ قفل‌شده باز می‌کند و فاکتور ژاپنی مربع‌های خالی (tofu) را نشان می‌دهد، یا یک فونت جایگزینِ مشابه هر شکست خط (line break) را جابجا می‌کند. فونت‌ها هرگز در آن سیستم نبوده‌اند — فقط در داخل فایل PDF قرار دارند — و رندرکننده‌ای که فقط به جایگزینی فونت سیستم اکتفا می‌کند، نمی‌تواند آنها را ببیند. فونت‌های زیرمجموعه (Subset) این وضعیت را بدتر می‌کنند: یک زیرمجموعه ممکن است چهل گلیف را تحت کدهای کاراکتری که به طور خصوصی به آن فایل اختصاص داده شده حمل کند، تخصیصی که هیچ فونت نصب‌شده‌ای آن را به اشتراک نمی‌گذارد

استاندارد ISO 32000-1 §9.9 سه حامل را برای یک برنامه فونت تعبیه‌شده در توصیف‌گر فونت تعریف می‌کند: FontFile حامل یک برنامه اصلی Type 1 است، FontFile2 یک برنامه TrueType را نگه می‌دارد، و FontFile3 حامل یک CFF خالص (Type1C یا CIDFontType0C) یا یک پوشش‌دهنده OpenType است. نوع چهارم، یعنی فونت Type 3 در استاندارد ISO 32000-1 §9.6.5، اصلاً هیچ داده باینری را تعبیه نمی‌کند — هر گلیف یک استریم کوچک از محتوای PDF است که در محل اجرا می‌شود. این سه حامل در ریاضیات خطوط کلی تفاوت دارند (B-splineهای درجه دوم در برابر استرینگ‌های کاراکتر مکعبی در برابر اپراتورهای صفحه اختیاری)، بنابراین یک رندرکننده دقیق به یک مفسر جداگانه برای هر کدام، به علاوه یک لایه کدگذاری نیاز دارد که کدهای کاراکتر را قبل از لمس هر خط کلی، به شاخص گلیف صحیح تبدیل کند

چگونه HotPDF خطوط کلی TrueType glyf را به مسیرهای GDI تبدیل می‌کند؟

کلاس THPDFEmbeddedTTF در HPDFGlyphRender.pas جدول loca را برای مکان‌یابی هر رکورد گلیف می‌خواند، خطوط کلی glyf را نقطه به نقطه پیمایش می‌کند و یک مسیر GDI صادر می‌کند. دو قرارداد TrueType نیاز به مدیریت صریح دارند. اول اینکه نقاط متوالی خارج از منحنی (off-curve)، نشان‌دهنده یک نقطه روی منحنی در نقطه میانی خود هستند، و خطی که تمام نقاط آن خارج از منحنی هستند از نقطه میانی آخرین و اولین نقاط خود شروع می‌شود — در صورت نادیده گرفتن هر یک از این قوانین، گلیف‌های گرد دارای لبه‌های صاف می‌شوند یا از بین می‌روند. دوم اینکه منحنی‌های TrueType از نوع بزیه درجه دوم (quadratic Béziers) هستند در حالی که متد PolyBezierTo در GDI منحنی‌های مکعبی (cubics) را می‌پذیرد، بنابراین هر بخش درجه دوم به طور دقیق افزایش درجه می‌یابد و به بخش‌های خطی صاف تبدیل نمی‌شود

افزایش درجه بدون اتلاف است: منحنی مکعبی، همان منحنی یکسان را ترسیم می‌کند، بنابراین خط کلی رندر شده با آنچه یک بیننده استاندارد از همان جدول ترسیم می‌کند، در هر زومی مطابقت دارد. کار باقی‌مانده، موقعیت‌دهی است. هر گلیف در واحدهای فونت (معمولاً یک شبکه ۱۰۰۰ یا ۲۰۴۸ واحد در هر em) نوشته می‌شود و رندرکننده ماتریس مقیاس، ماتریس متن و ماتریس تبدیل فعلی را قبل از پر شدن مسیر، در یک تبدیل گلیف به دستگاه ترکیب می‌کند. ترتیب در اینجا بیش از آنچه به نظر می‌رسد اهمیت دارد: ترکیب معکوس این سه ماتریس باعث می‌شود هر گلیف به سمت مبدأ فشرده شود — صفحه‌ای با ظاهر خراب که باگ واقعی آن یک خط جبر ماتریسی است

// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0),  C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve

مفسر استرینگ کاراکتر Type 2 چگونه با فونت‌های CFF برخورد می‌کند

کلاس THPDFEmbeddedCFF برای برنامه‌های FontFile3 یک مفسر واقعی استرینگ کاراکتر Type 2 را فراهم می‌کند: این مفسر ساختارهای CFF INDEX، دیکشنری‌های Top DICT و Private DICT را تجزیه می‌کند، سپس هر استرینگ کاراکتر را اجرا کرده و بخش‌های مسیر را مستقیماً به GDI صادر می‌کند. ابتدا یک پوشش‌دهنده OpenType (کانتینر OTTO) حذف می‌شود تا به جدول CFF خالص برسد؛ استریم‌های بدون پوشش CIDFontType0C و Type1C مستقیماً مصرف می‌شوند. استرینگ‌های کاراکتر یک زبان پشته‌ای فشرده هستند و سه مورد از قراردادهای آن تعیین می‌کنند که آیا مفسر با استریم بایت هماهنگ می‌ماند یا خیر. پیشوند عرض اختیاری به این معنی است که اولین عملگر پاک‌کننده پشته ممکن است یک عملوند پیشرو اضافی را حمل کند. عملگر hintmask نشان‌دهنده یک vstemhm در زمانی است که عملوندها هنوز روی پشته هستند، و تعداد بایت‌های ماسک که باید نادیده گرفته شوند به تعداد stemهای انباشته شده بستگی دارد — اگر تعداد را یک بار اشتباه محاسبه کنید، هر آپ‌کد بعدی اشتباه خوانده می‌شود. و فراخوانی‌های زیرروال قبل از جستجو، یک بایاس (انحراف) به شاخص خود (۱۰۷، ۱۱۳۱ یا ۳۲۷۶۸ بسته به تعداد زیرروال‌ها) اضافه می‌کنند، بنابراین یک فراخوانی بدون بایاس به کل روی زیرروال اشتباهی می‌نشیند

فونت CFF با کلید CID یک سطح از غیرمستقیم بودن را اضافه می‌کند که پیاده‌سازی‌های ساده را به اشتباه می‌اندازد: کد کاراکتر یک CID را انتخاب می‌کند، اما شاخص استرینگ کاراکتر یک GID است و مجموعه کاراکتر فونت GID را به CID نگاشت می‌کند — بنابراین رندرکننده قبل از ترسیم، نگاشت معکوس CID به GID را می‌سازد و دیکشنری Private DICT را برای هر گلیف از طریق FDSelect برای فونت‌هایی که دارای چندین دیکشنری هستند انتخاب می‌کند. برنامه‌های Type1C با کلید نام، که حامل معمولی برای فونت‌های ساده Type 1 هستند، کدهای تک بایتی را از طریق کدگذاری داخلی برنامه CFF یا از طریق مکانیزم کدگذاری سطح PDF که در ادامه توضیح داده می‌شود، حل می‌کنند. یک هشدار واقعی: مفسر عملگرهای hint را برای هماهنگ نگه داشتن استریم می‌خواند اما هینت‌گذاری را اجرا نمی‌کند، محدودیتی که در انتها مورد بحث قرار گرفته است

فونت Type 3 چیست و چگونه رسم می‌شود؟

یک گلیف Type 3 اصلاً یک خط کلی نیست — استاندارد ISO 32000-1 §9.6.5 آن را به عنوان یک استریم محتوا تعریف می‌کند، بنابراین HotPDF آن را با هل دادن وضعیت گرافیکی، ترکیب ماتریس فونت، اندازه فونت و ماتریس متن روی CTM و اجرای پروسه گلیف از طریق همان مفسر عملگر که صفحات را رسم می‌کند، در حالی که منابع /Resources خود فونت در محدوده است، رندر می‌کند. دو جزئیات مشخصات برای درستی کار اهمیت دارد. مقادیر /Widths در Type 3 به جای فضای متن ۱/۱۰۰۰ که هر نوع فونت دیگری استفاده می‌کند، در فضای گلیف بیان می‌شوند، بنابراین پیشروی‌ها باید از /FontMatrix عبور کنند — در غیر این صورت فونت‌های بارکد با ماتریس ۰.۰۱ با یک مرتبه بزرگی اشتباه حرکت می‌کنند. و یک پروسه گلیف که با عملگر d1 باز می‌شود، دو تعهد را ایجاد می‌کند که رندرکننده آنها را اعمال می‌کند: رنگ‌آمیزی به جعبه محدودکننده (bounding box) اعلام‌شده محدود می‌شود، و طبق استاندارد ISO 32000-1 §9.6.5.2 گلیف عملگرهای رنگ خود را نادیده می‌گیرد و با رنگ پرکننده فعلی فراخوان‌کننده رسم می‌شود، بنابراین عملگرهای rg، g، k و جفت‌های استروک آنها در داخل پروسه برای مدت زمان آن گلیف سرکوب می‌شوند. نادیده گرفتن قانون رنگ باعث می‌شود یک فونت بارکد d1 که توسط صفحه با رنگ آبی مهر شده است، مشکی بیرون بیاید؛ نادیده گرفتن کادر محدودکننده باعث می‌شود یک گلیف بدفرم خارج از سلول خود رسم شود

چگونه کدهای کاراکتر به شناسه‌های گلیف تبدیل می‌شوند

مفسرهای خطوط کلی تنها نیمی از کار هستند، زیرا بایت‌ها در یک رشته متنی PDF کدهای کاراکتر هستند و نه شاخص‌های گلیف، و استاندارد ISO 32000-1 دو بخش فرعی را به این نگاشت اختصاص داده است. برای فونت‌های ساده، بخش ۹.۶.۶ یک اولویت سخت‌گیرانه را تجویز می‌کند: یک آرایه /Differences کدگذاری پایه (WinAnsiEncoding، MacRomanEncoding یا StandardEncoding) را بازنویسی می‌کند، که خود نقشه برنامه فونت را بازنویسی می‌کند. HotPDF آن زنجیره را به یک جدول کد به GID با ۲۵۶ ورودی حل می‌کند و نام‌های گلیف را به سه مسیر به شاخص‌های گلیف ترجمه می‌کند: تطابق دقیق مجموعه کاراکتر در داخل برنامه CFF، نام‌های عددی gNN/glyphNN که به عنوان شاخص‌های واقعی در نظر گرفته می‌شوند، و ترجمه نام به یونیکد لیست گلیف ادوبی و به دنبال آن جستجوی cmap برای برنامه‌های TrueType. برای فونت‌های ترکیبی، بخش ۹.۷ مدیریت را به CIDToGIDMap واگذار می‌کند: مورد معمول /Identity است، اما ورودی ممکن است استریمی از جفت‌های big-endian باشد که توسط CID شاخص‌گذاری شده‌اند — و خروجی یونیکد خود HotPDF دقیقاً از همان شکل استریم برای زیرمجموعه‌های فشرده استفاده می‌کند، بنابراین مسیر استریم یک گوشه عجیب و غریب نیست

// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // out of range maps to .notdef

هنگام دستیابی به جدول cmap در TrueType، HotPDF یک زنجیره جایگزین را طی می‌کند: ابتدا زیرجدول‌های یونیکد ویندوز (فرمت ۴، سپس فرمت ۱۲ برای صفحات تکمیلی)، سپس زیرجدول نماد (۳۰۰) با کنوانسیون منطقه استفاده خصوصی F000 آن که به بایت پایین آینه شده است — دلیلی که یک فونت نماد مانند Wingdings به کدهای ساده ASCII پاسخ می‌دهد — سپس فرمت‌های قدیمی ۶ و ۰. زیرجدول‌های فرمت ۲ به طور تعمدی تفسیر نمی‌شوند: آنها صفحه‌های کد چند بایتی قدیمی مانند Shift-JIS و Big5 را نگاشت می‌کنند و نه یونیکد را، و فونت‌های مدرن CJK در هر صورت همیشه یک زیرجدول فرمت ۴ یا فرمت ۱۲ را حمل می‌کنند. هر کدی که از هیچ‌یک از این مسیرها عبور نکند، برای آن تک گلیف به رسم GDI بازمی‌گردند، بنابراین یک کاراکتر غیرقابل نگاشت تنها یک گلیف را خراب می‌کند و نه کل متن را

آنچه مسیر تعبیه‌شده انجام نمی‌دهد

مرزها ارزش بیان ساده را دارند. هینت‌گذاری (Hinting) اجرا نمی‌شود — خطوط کلی همانطور که نوشته شده‌اند پر می‌شوند، که در DPI ۱۵۰ و بالاتر از خروجی هینت‌گذاری‌شده غیرقابل تشخیص است اما در اندازه‌های بسیار کوچک می‌تواند به اندازه یک پیکسل با یک رسترایزر هینت‌گذاری‌شده تفاوت داشته باشد. برنامه‌های اصلی Type 1 در FontFile (استرینگ‌های کاراکتر رمزگذاری‌شده با eexec) تفسیر نمی‌شوند و محورهای فونت متغیر OpenType اعمال نمی‌شوند؛ هر دو مورد، مانند یک برنامه فونت آسیب‌دیده یا جدول glyf بدون هیچ cmap قابل استفاده‌ای، به جای شکست در رندر صفحه، به رسم فونت سیستم بازمی‌گردند. همین رویکردِ اولویت-دقت به جاهای دیگر رندرکننده نیز تسری می‌یابد — الگوهای سایه‌زنی محوری و شعاعی همان رفتاری را دریافت می‌کنند که شایسته گرادیان‌ها است — و بخش تولید داستان ظرافت فونت خود را در نحوه ترتیب‌دهی زیرمجموعه‌های فونت توسط EndDoc دارد

استفاده از این خط لوله اصلاً نیازی به کد خاص فونت ندارد – هر مکانیزم بالا به طور خودکار در داخل فراخوانی رندر صفحه فعال می‌شود

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Embedded TrueType, CFF, and Type 3 fonts render from the
      // file itself — nothing needs to be installed on this machine
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

نتیجه عملی همان چیزی است که صندوق ورودی پشتیبانی شما به آن اهمیت می‌دهد: یک فایل PDF که فونت‌های خود را حمل می‌کند، با همان فونت‌ها در یک سرور بیلد، در یک کانتینر ویندوز یا در دسکتاپ مشتری که هرگز آن تایپ‌فیس را ندیده است رندر می‌شود. خط لوله رندر گلیف‌های تعبیه‌شده به عنوان بخشی از HotPDF Component برای دلفی و C++Builder ارائه می‌شود — یک کتابخانه بومی VCL که ایجاد، ویرایش، استخراج متن و رندر صفحات PDF را بدون هیچ وابستگی به DLLهای خارجی پوشش می‌دهد