مقاله فنی

جایگزین‌های سبکی جدول GSUB در OpenType در دلفی خالص

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

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

چرا دلفی خالص به جای HarfBuzz

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

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

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

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

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

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

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

جدول Coverage در دو فرمت ارائه می‌شود. فرمت 1 لیستی از شناسه‌های گلیف است که به ترتیب صعودی مرتب شده‌اند. شما یک گلیف را با یک جستجوی باینری پیدا می‌کنید، و موقعیت آن در لیست، ایندکس coverage آن است. فرمت 2 لیستی از رکوردهای بازه‌ای (range) است که هر کدام شامل یک گلیف شروع، یک گلیف پایان، و ایندکس coverage است که گلیف شروع به آن نگاشت می‌شود. گلیفی که در داخل یک بازه قرار دارد، ایندکس coverage خود را با جابجایی نسبت به شروع بازه به دست می‌آورد. هنگامی که گلیف‌های شرکت‌کننده پراکنده هستند، فرمت 1 فشرده است، و هنگامی که در مجموعه‌های متوالی قرار می‌گیرند، فرمت 2. هر دو مرتب شده‌اند، بنابراین هر دو در زمان لگاریتمی جستجو می‌شوند، و هر دو یا یک ایندکس coverage را برمی‌گردانند و یا به صورت واضح یک "شامل نمی‌شود" را که به موتور اجازه می‌دهد گلیف را به حال خود رها کند

جایگزینی تکی (Single Substitution)، دو فرمت

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

فرمت 2 یک آرایه صریح از شناسه‌های گلیف جایگزین را ذخیره می‌کند. ایندکس coverage از جدول Coverage، همان ایندکسِ داخل آن آرایه است، بنابراین گلیف در ایندکسِ coverage صفر تبدیل به اولین ورودی آرایه می‌شود، ایندکسِ coverage یک تبدیل به دومی می‌شود و الی آخر. هنگامی که جایگزین‌ها در یک آفست یکنواخت نیستند، فرمت 2 استفاده می‌شود، که مورد رایجی برای مجموعه‌های سبکیِ دست‌ساز است. در هر صورت این درخواست از سمت تماس‌گیرنده یکسان است. گلیف ورودی را بگیرید، آن را از Coverage عبور دهید، و اگر شامل آن می‌شود، اختلاف (delta) را اعمال کنید یا اسلات آرایه را بخوانید

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, []);

    // Default glyph for 'a' through the font's cmap.
    BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));

    // Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
    AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');

    // AltGID = BaseGID means the feature did not touch this glyph.
    if AltGID <> BaseGID then
      { emit AltGID in the content stream };
  finally
    Pdf.Free;
  end;
end;

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

برچسب‌های ویژگی‌های سبکی به چه معنا هستند

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

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

// Try a stylistic-set feature, then fall back to plain alternates.
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');
  // Still BaseGID if neither feature covers this glyph.
end;

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

پیش از اینکه هیچ جایگزینی‌ای اجرا شود، یک کاراکتر باید به یک گلیف تبدیل شود، و این وظیفه جدول cmap است. درخواستِ جایگزینی از یک شناسه گلیف شروع می‌شود، بنابراین مسیر همیشه از کاراکتر به گلیف از طریق cmap، و سپس از گلیف به جایگزین از طریق GSUB است. بخش جالب cmap بُرد آن است. یک زیرجدول فرمت 4، صفحه چندزبانه پایه (Basic Multilingual Plane) یا همان 65536 نقطه کد (code point) اول را پوشش می‌دهد، و این برای اکثر متون لاتین کافی است. این میزان برای نقاط کد از U+10000 به بالا، یعنی صفحات تکمیلی (supplementary planes)، که در آن الفبای-عددی ریاضی، بسیاری از نمادها و چندین اسکریپت زنده اکنون در آنجا قرار دارند، کافی نیست

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

var
  Pdf: THotPDF;
  Cp: Cardinal;
  GID, StyledGID: Word;
begin
  // A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
  Cp := $1D49C;
  GID := Pdf.GetUnicodeGlyphForCodepoint(Cp);  // format 12 lookup
  if GID <> 0 then
    StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
  else
    StyledGID := 0;  // font has no glyph for this code point
end;

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

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

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

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