مقاله فنی

فونت‌ها و متن در PDF: چرا گلیف‌ها به جعبه تبدیل می‌شوند

یک PDF که در ماشین شما کامل به نظر می‌رسد و در ماشین دیگری به عنوان یک ردیف از جعبه‌های خالی رندر می‌شود، رایج‌ترین نقص فونت در نرم‌افزارهای سند است، و تقریباً هرگز به این معنی نیست که متن اشتباه است. کاراکترها سالم هستند، رمزگذاری (encoding) درست است، گلیف‌ها (glyphs) به سادگی در آنجا نیستند. چیزی که بین این دو ماشین تغییر کرد این است که سیستم عامل کدام فونت‌ها را نصب کرده بود، و شکاف بین یک فایل قابل حمل (portable) و یک فایل شکننده، تصمیمی است که هنگام نوشتن صفحه گرفته شده است: اینکه آیا فونت در داخل PDF سفر کرده است یا فرض شده که در طرف مقابل حضور دارد

درک اینکه چرا این اتفاق می‌افتد، و چرا یک خرابی جداگانه متنی با ظاهر قابل جستجو تولید می‌کند که به صورت چرت و پرت (gibberish) کپی می‌شود، به معنای نگاه کردن به نحوه ذخیره‌سازی متن در PDF است. این فرمت جملات را ذخیره نمی‌کند. کدهای گلیف (glyph codes) به علاوه یک برنامه فونت به علاوه جداولی که یکی را به دیگری نگاشت می‌کنند را ذخیره می‌سازد، و هر باگ رندرینگ یا استخراج در شکاف بین آن سه مورد زندگی می‌کند. آنچه در ادامه می‌آید گشتی در این ماشین‌آلات است، که بر اساس ISO 32000 پایه‌ریزی شده، و فراخوانی‌های Delphi که آن‌ها را در جایی که اهمیت دارند کنترل می‌کنند نیز آورده شده است

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

دایره لغات باعث اشتباه افراد می‌شود زیرا گفتار روزمره سه ایده متمایز را در کلمه "حرف" (letter) فرو می‌ریزد. یک کاراکتر (character) یک واحد انتزاعی از نوشتن است، یعنی ایده A بزرگ، که در یونیکد به صورت U+0041 شناسایی می‌شود. یک گلیف (glyph) یک شکل ترسیم‌شده است، یعنی طرح کلی منحنی-و-ساقه‌ای (curve-and-stem) که یک فونت خاص برای به تصویر کشیدن آن کاراکتر استفاده می‌کند. بین آن‌ها کد (code) قرار دارد: بایت یا بایت‌هایی در جریان محتوا که به نمایشگر می‌گویند کدام گلیف را در فونت فعلی نقاشی کند

نرم‌افزار PDF با کدها کار می‌کند. هنگامی که یک جریان محتوا یک رشته را نشان می‌دهد، آن بایت‌ها نمایه‌هایی (indices) به داخل فونت فعال هستند، نه یونیکد. رمزگذاری فونت تصمیم می‌گیرد که کد 65 به این معنی است که "گلیفی که در زیر 65 بایگانی شده است را رسم کن"، و هیچ چیز در آن عملیات نمی‌داند که نتیجه برای یک انسان شبیه یک A به نظر می‌رسد. این همان چیزی است که باعث می‌شود PDF در هر جایی که بتواند گلیف‌ها را پیدا کند به طور یکسان رندر شود، و این هم دلیلی است که چرا استخراج (extraction) یک مشکل جداگانه از نمایش است: رسم کردن فقط به کد-به-گلیف نیاز دارد، خواندن به کد-به-یونیکد نیاز دارد، و این‌ها دو جدول متفاوت هستند که می‌توانند به طور مستقل اختلاف نظر داشته باشند یا گم شوند

انواع فونت که شما در واقع با آن‌ها ملاقات خواهید کرد

استاندارد ISO 32000 چندین نوع دیکشنری فونت را تعریف می‌نماید، و در عمل سندی که شما دریافت یا تولید می‌کنید از یکی از این سه نوع استفاده می‌کند. دانستن اینکه به کدام یک نگاه می‌کنید، بیشتر مواردی را که ممکن است به خطا بروند توضیح می‌دهد

نوع 1 (Type 1) فرمت اولیه طرح کلی PostScript ادوبی است که از منحنی‌های Bezier مکعبی ساخته شده است. چهارده فونت استاندارد که هر خواننده منطبقی باید تأمین کند، یعنی خانواده‌های Helvetica، Times، Courier، Symbol و ZapfDingbats از نوع Type 1 هستند، و یک دیکشنری فونت که نام یکی از آن‌ها را می‌برد می‌تواند به طور قانونی برنامه فونت را حذف کند. این تنها موردی است که در آن رها کردن یک فونت به صورت تعبیه‌نشده (unembedded) به جای شانس، از طریق مشخصات امن است. برای هر فونت دیگر از خانواده Type 1 برنامه باید تعبیه شود در غیر این صورت نمایشگر چیزی را جایگزین می‌سازد، که معمولاً یک فونت از نظر اندازه‌گیری مشابه اما از نظر بصری متفاوت است

تروتایپ (TrueType) از منحنی‌های درجه دو استفاده می‌نماید و از دنیای Apple و Microsoft آمده است. این چیزی است که بیشتر فونت‌های سیستم هستند، و چیزی است که شما اغلب آن را تعبیه خواهید کرد. یک فونت TrueType ساده در PDF به کدهای تک بایتی محدود می‌شود، بنابراین یکی از این فونت‌ها می‌تواند حداکثر به 256 گلیف در یک زمان بپردازد. این سقف، دلیل ساختاری است که CJK و سایر خطوط نوشتاری بزرگ نمی‌توانند روی یک فونت ساده سوار شوند

نوع 0 (Type 0)، فونت کامپوزیت یا CID-keyed، پاسخی به آن محدودیت است. این فونت از کدهای چند بایتی و یک CMap برای هدایت آن‌ها از طریق یک CIDFont فرزند (descendant) استفاده می‌کند، که طرح‌های کلی آن خودشان یا TrueType یا CFF/Type 1 هستند. این تنها نوع فونتی است که می‌تواند هزاران گلیف را حمل نماید، بنابراین هر PDF که زبان‌های چینی، ژاپنی، کره‌ای، یا یک ترکیب چندزبانه گسترده را نگه می‌دارد، چه نویسنده به آن فکر کرده باشد یا نه، در حال استفاده از Type 0 است. این معامله پیچیدگی است: قطعات متحرک بیشتر، که تعداد بیشتری از آن‌ها باید هم برای رندرینگ و هم برای استخراج درست باشند

One TrueType font rendered at 12, 18, 24, and 36 points in a PDF, showing that a single embedded outline scales to any size

یک جزئیات در پشت آن تصویر، اندازه فایل را هدایت می‌کند. یک فونت، کتابخانه‌ای از طرح‌های کلی (outlines) است، نه بیت‌مپ‌های با اندازه ثابت، بنابراین برنامه تعبیه‌شده یکسان در خدمت هر اندازه نقطه‌ای (point size) روی صفحه است. مقیاس‌بندی (Scaling) تبدیلی است که در زمان رسم اعمال می‌گردد، که به همین دلیل است که یک تیتر و متن بدنه آن یک فونت تعبیه‌شده را به اشتراک می‌گذارند و به همین دلیل است که هزینه تعبیه‌سازی به ازای هر فونت است، نه به ازای هر اندازه

تعبیه‌سازی (Embedding) تفاوت بین قابل حمل و شکننده است

تعبیه‌سازی یعنی برنامه فونت، یعنی داده‌های واقعی طرح کلی، به عنوان یک جریان در PDF نوشته می‌شود. یک خواننده روی ماشینی که هرگز نام فونت شما را نشنیده است، آن طرح‌های کلی را مستقیماً از فایل می‌خواند و گلیف‌های دقیق را رسم می‌نماید. از تعبیه‌سازی بگذرید و شما شرط می‌بندید که مقصد یک فونت با همان نام را دارد؛ وقتی این‌طور نباشد، نمایشگر به یک جایگزین برمی‌گردد. برای آن چهارده مورد استاندارد، آن جایگزینی تعریف‌شده و خوش‌خیم است. برای هر چیز دیگری، این موضوع از یک تقریب نزدیک در یک فونت (typeface) متفاوت تا نتیجه جعبه-خالی زمانی که هیچ جایگزینی اصلاً آن خط نوشتاری را پوشش نمی‌دهد، متغیر است

با HotPDF کنترل از طریق یک ویژگی (property) منفرد انجام می‌شود که قبل از باز شدن سند تنظیم می‌گردد. FontEmbedding به کتابخانه می‌گوید که فونت‌هایی را که با آن‌ها رسم می‌کند در فایل بسته‌بندی نماید:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Compression := cmFlateDecode;
    Pdf.FontEmbedding := True;          // outlines travel inside the file
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Calibri', [], 11);
    Pdf.CurrentPage.TextOut(72, 760, 0, 'This renders the same on a machine without Calibri.');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

این ترتیب‌بندی تزئینی نیست. متد BeginDoc جایی است که HotPDF ساختار سند را متعهد (commit) می‌شود، بنابراین FontEmbedding باید قبل از آن فراخوانی true باشد. آن را بعداً اختصاص دهید و هیچ خطایی وجود ندارد، هیچ هشداری نیست، فقط فایلی است که بی‌سروصدا بدون فونت‌های خود بیرون رفته است. این بدترین نوع باگ است: این باگ از هر آزمایشی روی ماشین توسعه‌دهنده، جایی که اتفاقاً فونت نصب شده است، عبور می‌کند و تنها در ماشین یک مشتری که فونت در آن نیست ظاهر می‌شود

تعبیه‌سازی همچنین جایی است که صدور مجوز (licensing) با مهندسی روبرو می‌شود. یک برنامه فونت دارای پرچم‌هایی (flags) است که توضیح می‌دهند آیا ممکن است آزادانه تعبیه شود، فقط برای پیش‌نمایش باشد، یا اصلاً امکان‌پذیر نباشد. رعایت آن پرچم‌ها مسئولیت شماست، نه مسئولیت رندرکننده (renderer)، و "این کار کرد" به معنای "این مجاز بود" نیست

زیرمجموعه‌سازی (Subsetting): فقط گلیف‌هایی را که استفاده کردید تعبیه کنید

تعبیه‌سازی کامل (Full embedding) کل برنامه فونت را در فایل می‌نویسد. یک فونت بزرگ CJK TrueType می‌تواند به چندین مگابایت برسد، و تعبیه کل آن برای نشان دادن دوازده کاراکتر، اتلافی است که در یک سند چند صفحه‌ای انباشته می‌شود. زیرمجموعه‌سازی این مشکل را با نوشتن تنها گلیف‌هایی که سند به آن‌ها ارجاع می‌دهد حل می‌کند، سپس نام فونت را با یک برچسب شش حرفی و یک علامت مثبت، به شکل ABCDEF+Calibri در لیست فونت هر PDF زیرمجموعه‌شده تغییر می‌دهد، بنابراین خواننده هرگز فونت جزئی را با یک فونت کامل سیستم با همان نام اشتباه نمی‌گیرد

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

فونت‌های یونیکد و مشکل جعبه CJK

وقتی متن، لاتین ساده نیست، مسیر فونت ساده به پایان می‌رسد، و راه‌حل این است که یک فونت با قابلیت یونیکد را به صراحت ثبت کنید و اجازه دهید HotPDF یک فونت Type 0 از آن بسازد. RegisterUnicodeTTF یک فایل TrueType را با مسیر بارگیری می‌کند؛ پس از آن نام ثبت‌شده در SetFont مانند هر نام دیگری قابل استفاده است:

Pdf.FontEmbedding := True;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansCJKsc-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('NotoSansCJKsc-Regular', [], 14);
Pdf.CurrentPage.TextOut(72, 720, 0, '你好,世界 こんにちは 안녕하세요');
Pdf.EndDoc;

دو چیز این موضوع را می‌سازد یا می‌شکند. فونت باید خطوط نوشتاری موجود در رشته را پوشش دهد: یک TrueType فقط-لاتین (Latin-only) گلیف‌های چینی را به وجود نخواهد آورد چون شما از آن خواسته‌اید، و نتیجه دوباره جعبه‌های خالی است، این بار به این دلیل که گلیف واقعاً در آن فونت وجود ندارد. و تعبیه‌سازی باید روشن بماند، زیرا یک فونت Type 0 مونتاژ شده از یک TTF ثبت‌شده، برای خواننده‌ای که نمی‌تواند طرح‌های کلی را پیدا کند بی‌معنی است. برای محتوای ترکیبی، انتخاب بادوام یک فونت با پوشش گسترده است که خانواده‌های Noto و Arial Unicode MS پاسخ‌های معمولی هستند که به صورت تعبیه‌شده و زیرمجموعه‌شده استفاده می‌شوند

زبان‌های راست-به-چپ (Right-to-left) و خطوط نوشتاری پیچیده یک لایه شکل‌دهی (shaping) را روی پوشش اضافه می‌کنند. HotPDF برای عربی و عبری RtLTextOut را در معرض دید قرار می‌دهد که تغییر ترتیب جهتی را مدیریت می‌کند، بنابراین شما ترتیب منطقی را ارسال کرده و اجازه می‌دهید کتابخانه آن را چیدمان کند. درست کردن زبان عربی به معنای پوشش (coverage) به علاوه شکل‌دهی (shaping) به علاوه جهت (direction) است، یعنی سه چیز جداگانه، و وجود یک جعبه در آنجا می‌تواند به این معنی باشد که هر یک از آن‌ها با شکست مواجه شده است

جدول ToUnicode: جایی که کپی-پیست زندگی می‌کند

همه موارد بالا به ترسیم مربوط می‌شد. استخراج (Extraction) تصویر آینه‌ای است و به دلایل خاص خود با شکست مواجه می‌شود. یک نمایشگر، صفحه‌ای را با استفاده از نگاشت کد-به-گلیفِ فونت رندر می‌کند، اما وقتی کاربری متنی را انتخاب کرده و آن را کپی می‌نماید، نمایشگر باید همان کدها را دوباره به یونیکد تبدیل کند. آن نگاشت معکوس، CMap به نام ToUnicode است، یعنی یک جریان اختیاری که به فونت متصل شده است

هنگامی که این جدول وجود داشته باشد و درست باشد، متن کپی‌شده به عنوان کاراکترهای مناسب بیرون می‌آید. هنگامی که وجود نداشته باشد یا اشتباه باشد، یا فونت با کدهای گلیف سفارشی زیرمجموعه شده باشد و هیچ ToUnicode نوشته نشده باشد، صفحه کامل به نظر می‌رسد و کلیپ‌بورد پر از چرت و پرت (garbage) می‌گردد: کدهای گلیف به گونه‌ای خوانده می‌شوند که گویی یونیکد هستند، که برای یک زیرمجموعه با رمزگذاری سفارشی این‌طور نیستند. به همین دلیل است که یک سند اسکن‌شده با لایه متنی OCR می‌تواند قابل جستجو باشد در حالی که یک PDF متولدشده به صورت دیجیتالی از یک تولیدکننده بی‌دقت این‌گونه نیست. رندرینگ و استخراج از جداول متفاوتی استفاده می‌کنند، بنابراین یک فایل می‌تواند یکی را راضی نماید و در دیگری شکست بخورد. اگر استخراج برای خروجی شما مهم است، با یک نقشه ToUnicode صحیح به عنوان یک الزام برخورد کنید، و به جای اعتماد به اینکه آن در فایل وجود دارد، آن را با کپی کردن متن از یک نمونه تأیید نمایید

نحوه تشخیص سریع یک باگ فونت

حالت خرابی به شما می‌گوید کجا را نگاه کنید. جعبه‌های خالی در ماشین دیگر تقریباً همیشه به معنای فونتی است که تعبیه نشده است، بنابراین ابتدا تعبیه‌سازی و دوم پوشش گلیف را بررسی کنید. جعبه‌هایی که حتی در ماشین خودتان ظاهر می‌شوند به پوشش (coverage) اشاره می‌کنند: فونت، آن خط نوشتاری را شامل نمی‌شود، بدون توجه به تعبیه‌سازی. متنی که به درستی رندر می‌شود اما به صورت بی‌معنی کپی می‌گردد، یک مشکل ToUnicode است، نه یک مشکل رندرینگ، و ور رفتن با فونت‌ها یا تعبیه‌سازی آن را برطرف نخواهد کرد زیرا ترسیم هرگز خراب نبوده است. برای خواندن یک فایل تمام‌شده، آن را در Acrobat باز کنید و به بخش Document Properties، Fonts نگاه کنید: یک ورودی سالم نوع را نشان می‌دهد، می‌گوید Embedded یا Embedded Subset، و نام رمزگذاری را ذکر می‌کند. فونتی که باید تعبیه شود و نشده است، قبل از اینکه مشتری متوجه شود، خود را در آنجا اعلام می‌نماید

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

فراخوانی‌های SetFont، FontEmbedding، و RegisterUnicodeTTF که در اینجا نشان داده‌اند بخشی از ابزار HotPDF Component برای Delphi و C++Builder هستند