Technical Article

فونت‌های Stylistic Alternates در OpenType GSUB با Delphi خالص

یک طراح فونتی را با یک a تک‌داستانه برای سربرگ‌ها، یا یک صفر خط‌خورده برای جدول‌ها، یا مجموعه‌ای از حروف بزرگ تزئینی (swash capitals) برای یک جلد انتخاب می‌کند. آن گلیف‌ها از قبل در فونت وجود دارند. آن‌ها به سادگی پیش‌فرض نیستند. a پیش‌فرض از طریق جدول cmap از کاراکتر به یک گلیف نگاشت می‌شود و آلترنیت (alternate) با چند شناسه گلیف فاصله دارد که فقط از طریق یک قانون جایگزینی قابل دسترسی است. تولید آن آلترنیت در یک PDF به معنای خواندن قانون و صدور گلیف جایگزین در جریان محتوا است. این مقاله در مورد خواندن این قوانین، از نوع جایگزینی تکی (single-substitution)، در Object Pascal بدون هیچ کتابخانه بومی شکل‌دهی (shaping library) در زیر آن است.

محدوده کار عمداً محدود است. مجموعه‌ها و آلترنیت‌های استایلیستی جایگزینی‌های تک‌گلیف‌-ورودی و تک‌گلیف‌-خروجی هستند. آن‌ها بخشی از چیدمان OpenType هستند که می‌توانید با یک پیمایش جدول کوچک و قطعی حل کنید، که این امر آن‌ها را به گزینه مناسبی برای یک موتور Pascal تبدیل می‌کند که می‌خواهد از وابستگی‌های C آزاد بماند.

چرا Delphi خالص به جای HarfBuzz

کتابخانه HarfBuzz پاسخ واضحی برای «شکل‌دهی به این متن» است و برای شکل‌دهی کامل دوطرفه (bidirectional)، هندی یا عربی، پاسخ درستی است. اما این کتابخانه یک کتابخانه C است. متصل کردن آن به یک محصول Delphi یا C++Builder به معنای ارسال یک شیء بومی برای هر پلتفرم و معماری هدف، مطابقت با قرارداد فراخوانی آن، ردیابی سرعت انتشار آن و خواندن شرایط لایسنس آن در برابر لایسنس خودتان است. هیچ‌کدام از این‌ها به تنهایی سخت نیست. همه این‌ها اصطکاکی است که هرگز از بین نمی‌رود و زمانی که نیاز واقعی «دادن فرم ss01 این حرف به من» باشد، هیچ مزیتی به همراه ندارد.

جایگزینی تکی به موتور شکل‌دهی نیاز ندارد. این کار به یک پارسر برای چند فرمت زیرجدول GSUB و یک یا دو جستجوی دودویی نیاز دارد. نوشتن آن به زبان Pascal کل زنجیره ابزار را درون یک کامپایلر نگه می‌دارد. محدودیت واقعی این است که این رویکرد فقط جستجوهای جایگزینی گلیف را مدیریت می‌کند و نه چیز دیگری. این کار حل دوطرفه‌سازی (bidi) نیست، مرتب‌سازی مجدد هندی نیست و شکل‌دهی متنی خودکار نیست. جاهایی که به این موارد نیاز است، واقعاً نیاز است و یک پرس‌وجوی جایگزینی تکی جایگزین آن‌ها نخواهد شد.

سلسله‌مراتب GSUB، از بالا به پایین

سلسله‌مراتب GSUB، از بالا به پایین

جدول جایگزینی گلیف (Glyph Substitution table) به صورت زنجیره‌ای از ارجاعات غیرمستقیم سازماندهی شده است و یک پرس‌وجوی جایگزینی این زنجیره را از بالا می‌پیماید. در بالا ScriptList قرار دارد. یک تگ اسکریپت مانند latn یک ورودی را انتخاب می‌کند و تگ ویژه DFLT اسکریپت پیش‌فرضی است که وقتی هیچ اسکریپت خاص‌تری مطابقت ندارد اعمال می‌شود. ورودی اسکریپت به یک LangSys (سیستم زبان) اشاره می‌کند، با یک LangSys پیش‌فرض برای حالت متداول و موارد نام‌گذاری شده اختیاری برای زبان‌هایی که به رفتار متفاوتی نیاز دارند. ترکی مورد متداول است که در آن i نقطه‌دار و بی‌نقطه به مدیریت خاص خود نیاز دارند.

سیستم LangSys مجموعه‌ای از ایندکس‌های ویژگی (feature indices) را نام می‌برد. هر ایندکس به FeatureList اشاره می‌کند، جایی که یک رکورد ویژگی یک تگ چهاربایتی، از جمله ss01، و لیستی از ایندکس‌های جستجو (lookup indices) را حمل می‌کند. آن ایندکس‌ها در نهایت به LookupList اشاره می‌کنند، جایی که زیرجدول‌های جایگزینی واقعی زندگی می‌کنند. بنابراین حل ss01 به این معنی است: یافتن اسکریپت، یافتن LangSys آن، یافتن ویژگی که تگ آن ss01 است، جمع‌آوری جستجوهایی که نام می‌برد و اعمال آن‌ها. HotPDF به طور پیش‌فرض روی اسکریپت DFLT و LangSys پیش‌فرض قرار دارد که اکثر قریب به اتفاق طرح‌های متنی لاتین با آن ارائه می‌شوند، و روشی را برای بازنویسی تگ اسکریپت نشان می‌دهد زمانی که یک فونت ویژگی‌های خود را تحت یک اسکریپت خاص متصل می‌کند.

جدول‌های Coverage تصمیم می‌گیرند چه کسانی مشارکت کنند

هر زیرجدول جایگزینی با یک سوال شروع می‌شود: آیا این گلیف ورودی در این قانون شرکت می‌کند و اگر چنین است، در ایندکس‌گذاری خود قانون در کجا قرار می‌گیرد. این سوال توسط یک جدول Coverage پاسخ داده می‌شود و پاسخ یک ایندکس پوشش (coverage index) است، یک عدد ترتیبی کوچک که بقیه زیرجدول برای جستجوی اینکه گلیف به چه چیزی تبدیل می‌شود استفاده می‌کنند.

پوشش در دو فرمت ارائه می‌شود. فرمت ۱ لیستی از شناسه‌های گلیف است که به ترتیب صعودی مرتب شده‌اند. شما یک گلیف را با جستجوی دودویی پیدا می‌کنید و موقعیت آن در لیست، ایندکس پوشش آن است. فرمت ۲ لیستی از رکوردهای محدوده (range records) است، هر کدام یک گلیف شروع، یک گلیف پایان و ایندکس پوشش که گلیف شروع به آن نگاشت می‌شود. یک گلیف در یک محدوده، ایندکس پوشش خود را با افست از شروع محدوده دریافت می‌کند. فرمت ۱ زمانی فشرده است که گلیف‌های شرکت‌کننده پراکنده باشند و فرمت ۲ زمانی که در دوره‌های متوالی قرار دارند. هر دو مرتب شده‌اند، بنابراین هر دو در زمان لگاریتمی جستجو می‌شوند و هر دو یا یک ایندکس پوشش یا یک «not covered» تمیز برمی‌گردانند که به موتور اجازه می‌دهد گلیف را رها کند.

جایگزینی تکی، دو فرمت

جایگزینی تکی (Single Substitution) همان LookupType 1 است و یک گلیف را به دقیقاً یک جایگزین نگاشت می‌کند. این فرمت نیز دارای دو حالت است و تفاوت آن‌ها در بهینه‌سازی فضا. فرمت ۱ یک دلتای علامت‌دار واحد را ذخیره می‌کند. شناسه گلیف خروجی، شناسه گلیف ورودی به علاوه آن دلتا به پیمانه ۶۵۵۳۶ است. این روشی است که یک فونت جایگزینی را رمزگذاری می‌کند که در آن هر گلیف شرکت‌کننده در یک افست ثابت از آلترنیت خود قرار دارد، به عنوان مثال بلوکی از اعداد هم‌تراز که در فاصله ثابتی از اعداد قدیمی (oldstyle figures) منطبق قرار گرفته‌اند. جدول Coverage می‌گوید کدام گلیف‌ها واجد شرایط هستند و یک دلتا به همه آن‌ها خدمت می‌کند.

فرمت ۲ یک آرایه صریح از شناسه‌های گلیف جایگزین را ذخیره می‌کند. ایندکس پوشش از جدول Coverage، ایندکس ورودی به آن آرایه است، بنابراین گلیف در ایندکس پوشش ۰ به اولین ورودی آرایه تبدیل می‌شود، ایندکس پوشش ۱ به دومین ورودی و به همین ترتیب. فرمت ۲ زمانی استفاده می‌شود که آلترنیت‌ها در یک افست یکنواخت نباشند، که این حالت متداول برای مجموعه‌های استایلیستی دست‌ساز است. پرس‌وجو در هر صورت از سمت فراخوان‌کننده یکسان است. گلیف ورودی را بگیرید، آن را از طریق Coverage اجرا کنید و اگر پوشش داده شد، دلتا را اعمال کنید یا خانه آرایه را بخوانید.

var
  Pdf: THotPDF;
  BaseGID, AltGID: Word;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
    Pdf.SetFont('My Stylistic Face', 12, []);

    BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));
    AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');

    if AltGID <> BaseGID then
      { emit AltGID in the content stream };
  finally
    Pdf.Free;
  end;
end;

قراردادی که ارزش توجه دارد، عبور مستقیم (pass-through) است. GetSingleSubstituteGlyph شناسه گلیف ورودی را در هر بار عدم تطابق بدون تغییر برمی‌گرداند: بدون فونت، بدون جدول GSUB، بدون ویژگی مطابقت یافته، بدون تطابق پوشش. این بدان معنی است که فراخوانی به صورت بدون قید و شرط ایمن است. شما آلترنیت را می‌خواهید و اگر وجود نداشته باشد، دقیقاً همان چیزی را که وارد کرده‌اید پس می‌گیرید، بنابراین کد فراخوانی هرگز نیاز به مدیریت حالت خاص برای فونتی که فاقد این ویژگی است ندارد.

معنی تگ‌های ویژگی استایلیستی

تگ ویژگی کل واژگان مربوط به آلترنیتی است که درخواست می‌کنید و تگ‌های مرتبط با کار استایلیستی لیست کوتاهی هستند. جفت برجسته salt، آلترنیت‌های استایلیستی، دسترسی همه‌جانبه به فرم‌های آلترنیت یک گلیف است، و ss01 تا ss20، بیست مجموعه استایلیستی شماره‌گذاری شده است که یک فونت می‌تواند تعریف کند، که هر کدام یک بسته نام‌گذاری شده از جایگزینی‌ها هستند که طراح با هم گروه‌بندی می‌کند. به عنوان مثال، یک فونت ممکن است یک a تک‌داستانه و یک R با پایه مستقیم را تحت ss03 قرار دهد، بنابراین فعال کردن آن مجموعه، استایل هر دو را تغییر می‌دهد.

در اطراف آن‌ها چندین تگ جایگزینی تکی دیگر قرار دارند. aalt دسترسی به همه آلترنیت‌ها (access-all-alternates)، یعنی اجتماع هر آلترنیتی است که یک گلیف دارد، که معمولاً به عنوان یک ویژگی پالت گلیف ارائه می‌شود. titl حروف بزرگ عنوان را که برای اندازه‌های بزرگ بریده شده‌اند انتخاب می‌کند. subs و sups به جای پیش‌فرض‌های کوچک‌شده، ارقام واقعی زیرنویس (subscript) و بالانویس (superscript) را جایگزین می‌کنند. ordn فرم‌های ترتیبی را تولید می‌کند، مانند حروف برجسته در 1st و 2nd. frac کسرها را می‌سازد، اگرچه کسرهای قطری کامل همچنین به لیگاتور و منطق متنی تکیه می‌کنند که فراتر از جایگزینی تکی ساده است. برای موارد تک‌گلیف، مکانیزم مشابه ss01 است: تگ را به پرس‌وجوی جایگزینی پاس دهید و گلیف آلترنیت را بخوانید.

function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
  const PreferredTag: AnsiString): Word;
begin
  Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
  if Result = BaseGID then
    Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
end;

فرمت cmap 12 و صفحات کمکی

قبل از اینکه هر جایگزینی بتواند اجرا شود، یک کاراکتر باید به یک گلیف تبدیل شود و این کار جدول cmap است. پرس‌وجوی جایگزینی از یک شناسه گلیف شروع می‌شود، بنابراین مسیر همیشه کاراکتر به گلیف از طریق cmap و سپس گلیف به آلترنیت از طریق GSUB است. بخش جالب cmap دامنه دسترسی آن است. یک زیرجدول فرمت ۴، صفحه چندزبانه اصلی (Basic Multilingual Plane)، یعنی ۶۵۵۳۶ نقطه کد اول را پوشش می‌دهد و این برای اکثر متون لاتین کافی است. اما برای نقاط کد از U+10000 به بالا، یعنی صفحات کمکی (supplementary planes)، که در آن حروف و اعداد ریاضی، بسیاری از نمادها و چندین اسکریپت زنده اکنون زندگی می‌کنند، کافی نیست.

فرمت ۱۲ زیرجدولی است که کل محدوده U+0000 تا U+10FFFF را پوشش می‌دهد. این یک لیست مرتب شده از گروه‌ها است، هر گروه یک نقطه کد شروع، یک نقطه کد پایان و یک شناسه گلیف شروع، به طوری که یک دوره متوالی از نقاط کد به یک دوره متوالی از گلیف‌ها نگاشت می‌شود. HotPDF نقاط کد را با یک استراتژی ترکیبی حل می‌کند که با نحوه شکل‌دهی داده‌ها مطابقت دارد. نقاط کد در BMP از یک آرایه مستقیم که با نقطه کد ایندکس‌گذاری شده سرویس‌دهی می‌شوند، یک جستجوی واحد بدون سرچ. نقاط کد در صفحات کمکی از یک جدول پراکنده (sparse table) مرتب شده بر اساس نقطه کد سرویس‌دهی می‌شوند و با جستجوی دودویی سرچ می‌شوند. نتیجه این است که GetUnicodeGlyphForCodepoint یک Cardinal کامل می‌گیرد و در کل محدوده به درستی پاسخ می‌دهد و شناسه گلیف ۰، یعنی گلیف .notdef را برای هر نقطه کدی که فونت نگاشت نمی‌کند، برمی‌گرداند.

var
  Pdf: THotPDF;
  Cp: Cardinal;
  GID, StyledGID: Word;
begin
  Cp := $1D49C;
  GID := Pdf.GetUnicodeGlyphForCodepoint(Cp);  // format 12 lookup
  if GID <> 0 then
    StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
  else
    StyledGID := 0;
end;

جایی که این پرس‌وجوها متوقف می‌شوند

APIهای جایگزینی تکی به یک شکل از سوال پاسخ می‌دهند و ارزش دارد که بدانیم به چه چیزی پاسخ نمی‌دهند. LookupType 1 یکی از هشت نوع جایگزینی است. این پرس‌وجو جایگزینی چندگانه LookupType 2 را که در آن یک گلیف به چند گلیف تبدیل می‌شود، مدیریت نمی‌کند، و همچنین جایگزینی لیگاتور LookupType 4 را که در آن چندین گلیف به یکی تبدیل می‌شوند، مدیریت نمی‌کند. این روش انواع متنی و متنی زنجیره‌ای (contextual and chaining-contextual)، یعنی LookupTypes 5 و 6 را که تنها زمانی که یک گلیف در همسایگی خاصی ظاهر می‌شود اجرا می‌شوند، و همچنین انواع افزونه و زنجیره‌ای معکوس را مدیریت نمی‌کند. یک کسر قطری، یک پیوند دواناگری، یا یک آبشار آغازین-میانی-پایانی عربی یک مشکل توالی (sequence problem) است و یک جستجوی جایگزینی تکی به ازای هر گلیف نمی‌تواند آن را بیان کند.

همچنین شکل‌دهی خودکار را انجام نمی‌دهد. هیچ‌چیز در اینجا بخشی از متن را بررسی نمی‌کند، تصمیم نمی‌گیرد که کدام ویژگی‌ها را روشن کند و آن‌ها را به ترتیبی که اسکریپت نیاز دارد اعمال کند. فراخوان‌کننده تگ ویژگی را انتخاب می‌کند و آن را گلیف به گلیف اعمال می‌نماید. این دقیقاً ابزار مناسبی برای مجموعه‌ها و آلترنیت‌های استایلیستی است که به صورت انتخابی (opt-in) و محلی هستند، و دقیقاً ابزار اشتباهی برای اسکریپتی است که به مرتب‌سازی مجدد نیاز دارد. تیز نگه داشتن مرز همان چیزی است که به مسیر جایگزینی اجازه می‌دهد کوچک و قابل پیش‌بینی بماند.

برای مواردی که به کار در سطح توالی نیاز دارند، داستان اسکریپت‌های پیچیده در مقاله ما در مورد شکل‌دهی متن با اسکریپت پیچیده در Delphi دنبال می‌شود. اگر جایگزینی‌های شما بخشی از یک کار گزارش‌دهی بزرگ‌تر است که تصاویر و فونت‌های دیگر را نیز در صفحه قرار می‌دهد، راهنمای خروجی گزارش با فونت‌ها و تصاویر نحوه قرارگیری این قطعات در کنار هم را پوشش می‌دهد. همه این‌ها بر روی یک موتور اجرا می‌شوند، یعنی کامپوننت HotPDF برای Delphi و C++Builder، که پرس‌وجوهای جایگزینی GSUB را در کنار APIهای تعبیه‌سازی فونت، زیرمجموعه‌سازی و متن که در بخش‌های دیگر این وبلاگ پوشش داده شده‌اند، حمل می‌کند.