مقاله فنی

شکل‌دهی متنی افزونه‌ای: Uniscribe و HarfBuzz در Delphi

شکل‌دهی متن در مؤلفه PDFium از یک آبجکت قابل‌نصب می‌گذرد. ConfigureTextShaper شکل‌دهی‌ای را نصب می‌کند که هر نقطه ورود شکل‌دهی از طریقش مسیریابی می‌شود، و هر چه آنجا بود جایگزین و آزاد می‌کند؛ ActiveTextShaper نصب‌شده را برمی‌گرداند و پیش‌فرض پلتفرم را در اولین استفاده می‌سازد؛ ActiveTextShaperName گزارش می‌دهد کدام بک‌اند زنده است؛ ClearTextShaper نصب را می‌اندازد و اجازه می‌دهد پیش‌فرض دوباره ساخته شود. روی Windows پیش‌فرض TPdfUniscribeTextShaper است. زیر Free Pascal TPdfHarfBuzzTextShaper هست، که libharfbuzz را در زمان اجرا متصل می‌کند تا کتابخانه غایب یک وضعیت گزارش‌شده باشد نه یک شکست بارگذاری

معماری شکل‌دهی متنی افزونه‌ای در مؤلفه دلفی PDFium: ConfigureTextShaper و ActiveTextShaper و ClearTextShaper یک بک‌اند نصب‌شده را مدیریت می‌کنند، Uniscribe روی ویندوز و یک HarfBuzz متصل در زمان اجرا زیر Free Pascal
هر فراخوانی شکل‌دهی از آبجکت شکل‌دهنده نصب‌شده واحد می‌گذرد، با یک پیش‌فرض پلتفرمی روی هر هدف

یک اینترفیس، دو بک‌اند که کار را کاملاً متفاوت تقسیم می‌کنند. فهمیدن همان عدم‌تقارن است که مسیر پرتابل را از تولید متنی که درست شکل گرفته و غلط جا افتاده بازمی‌دارد

چرا بک‌اند ویندوز یک کلاس است و پرتابل سه تکه؟

چون Uniscribe چهار API است که وانمود می‌کنند یکی‌اند. ScriptItemize یک رشته را بر اساس خط نوشتاری بخش‌بندی و سطوح دوسویه را حل می‌کند؛ ScriptShape نویسه‌ها را به glyphها نگاشت می‌کند؛ ScriptPlace پیشروی‌ها و آفست‌ها را محاسبه می‌کند؛ ScriptLayout ران‌های حاصل را به ترتیب بصری می‌گذارد. پس بک‌اندی که روی آن ساخته شده چیزی برای افزودن ندارد، که دلیل این است که شکل‌دهنده Windows یک کلاس واحد با یک متد است

HarfBuzz دو میانی را پوشش می‌دهد. رانی که جهت و خط نوشتاری‌اش را فراخواننده از قبل تصمیم گرفته شکل می‌دهد و جا می‌اندازد، و درباره اینکه یک بند چگونه به ران‌ها می‌شکند یا ران‌ها به چه ترتیبی ظاهر می‌شوند نظری ندارد. پس بک‌اند پرتابل بقیه را فراهم می‌کند: الگوریتم دوسویه سطوح تعبیه را حل می‌کند، توابع یونیکد مربوط به HarfBuzz متن را بر اساس خط نوشتاری بخش‌بندی می‌کنند، و ران‌ها در ترتیب بصری‌ای که قاعده L2 از UAX #9 تولید می‌کند چیده می‌شوند. نیمه دوسویه به‌قدر کافی اساسی است که یونیت خودش باشد، که در مقاله سطوح تعبیه UAX #9 توصیف شده است

مقایسه خط لوله شکل‌دهی برای متن PDF: Uniscribe چهار عمل ScriptItemize و ScriptShape و ScriptPlace و ScriptLayout را درون یک کلاس تأمین می‌کند، در حالی که HarfBuzz فقط شکل‌دهی و جای‌گذاری را دور مراحل خودِ UAX #9 مؤلفه پوشش می‌دهد
Uniscribe هر چهار مرحله را پوشش می‌دهد؛ مسیر پرتابل باید بخش‌بندی و ترتیب بصری را خودش فراهم کند

شکل‌دهنده فونت‌ها را حل نمی‌کند، و آن عمدی است

Uniscribe باینری فونت را از یک device context مربوط به GDI می‌خواند. معادل پرتابلی برای آن نیست، و اختراع یکی درون یک یونیت شکل‌دهی یعنی تصمیم‌گرفتن، از طرف هر برنامه، اینکه فونت‌ها از fontconfig می‌آیند، از CoreText، از یک پوشه فونت برنامه، یا از یک پایگاه داده. پس بک‌اند HarfBuzz یک resolver می‌گیرد: callbackای که نام یک فونت را به بایت‌های TrueType یا OpenType نگاشت می‌کند. برگرداندن False درخواست شکل‌دهی را همان‌طور مردود می‌کند که یک فونت GDI ناخوانا روی Windows مردودش می‌کند

uses
  FPdfTextShaping
{$IFDEF FPC}
  , FPdfTextShapingHb
{$ENDIF}
  ;

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // خط‌مشی شما: fontconfig، CoreText، یک پوشه فونت برنامه، یک پایگاه داده
  Result := FLookup.TryGetValue(LowerCase(FontName), Path);
  if Result then
    FontData := TFile.ReadAllBytes(Path);
end;

procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
  // مالکیت به یونیت می‌گذرد؛ یک‌بار هنگام راه‌اندازی فراخوانی کن،
  // پیش از آنکه چیزی متن را شکل بدهد
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // روی دلفی پیش‌فرض پلتفرم (Uniscribe) در صورت تقاضا ساخته می‌شود،
  // پس اصلاً نیازی به نصب نیست
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

بیرون نگه‌داشتن کشف فونت از شکل‌دهنده سود دومی دارد که در سرورها سر باز می‌کند: همان فرایند می‌تواند با یک مجموعه فونت تعبیه‌شده شکل بدهد که ربطی به آنچه روی دستگاه نصب است ندارد، که همان چیزی است که وقتی خروجی باید میان میزبان‌ها بایت‌به‌بایت بازتولیدپذیر باشد می‌خواهید. مؤلفه همچنین یک ارائه‌دهنده فونت سیستم میزبان برای مواردی که واقعاً فونت‌های نصب‌شده را می‌خواهید آشکار می‌کند، که در مقاله ارائه‌دهنده فونت سیستم پوشش داده شده است

رکورد نتیجه بی‌طرف-به-بک‌اند است، و خوشه‌ها دلیلش‌اند

هر دو بک‌اند همان TPdfShapedText را پر می‌کنند: متن مبدأ، نام فونت، اندازه، بایت‌های فونت، یک آرایه از ران‌ها، پهنای کل، تعداد glyph و تعداد نویسه منطقی. هر TPdfShapedRun بازه‌اش در متن مبدأ، جایگاه بصری X، پهنایش، سطح دوسویه‌اش و یک پرچم راست‌به‌چپ، به‌علاوه glyphهایش را حمل می‌کند. هر TPdfShapedGlyph یک شناسه glyph، یک پیشروی، آفست‌های X و Y، و خوشه‌ای را که به آن تعلق دارد به‌صورت یک شروع و یک طول در متن مبدأ حمل می‌کند

همان فیلدهای خوشه هستند که رکورد را قابل استفاده می‌کنند نه صرفاً اطلاعاتی. شکل‌دهی نگاشت یک‌به‌یک نیست: یک هجای دیوانگری از چهار نویسه یک glyph می‌شود، یک لیگاچر عربی دوتا را یکی می‌کند، و یک نویسه منفرد می‌تواند چندین اعراب تولید کند. بدون بازه‌های خوشه نمی‌توانید نویسه‌گردان بگذارید، کلیک را hit-test کنید، یا یک انتخاب را برجسته کنید، چون نمی‌توانید بگویید یک glyph به کدام نویسه‌ها تعلق دارد. با آن‌ها، حساب محلی است و همان کد برای هر دو بک‌اند کار می‌کند

بازه‌های خوشه glyph در TPdfShapedText: یک glyph هجای دیوانگری از چهار نویسه، یک لیگاچر عربی از دو نویسه، و یک پایه به‌علاوه اعراب از یک نویسه، که هر یک از طریق ClusterStart و ClusterLength به عقب نگاشت می‌شود
بازه‌های خوشه هر glyph را به نویسه‌های مبدأش نگاشت برمی‌گردانند تا نویسه‌گردان‌ها، hit testها و انتخاب‌ها کار کنند
var
  Shaped: TPdfShapedText;
  R, G: Integer;
begin
  if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
    for R := 0 to High(Shaped.Runs) do
    begin
      // ران‌ها از قبل به ترتیب بصری می‌رسند با VisualX پرشده
      X := Shaped.Runs[R].VisualX;
      for G := 0 to High(Shaped.Runs[R].Glyphs) do
      begin
        EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
          X + Shaped.Runs[R].Glyphs[G].OffsetX,
          Shaped.Runs[R].Glyphs[G].OffsetY);
        X := X + Shaped.Runs[R].Glyphs[G].Advance;
      end;
    end;
end;

بودجه‌ها به رکورد گزینه‌ها تعلق دارند

TPdfTextShapingOptions یک جهت به‌علاوه سه سقف حمل می‌کند: حداکثر نویسه‌ها، حداکثر glyphها و حداکثر ران‌ها، با تابع کلاس Default که مقادیر منطقی پر می‌کند. سقف‌ها پارانویا درباره ورودی بدشکل نیستند؛ حساب‌اند. شکل‌دهی گسترش می‌یابد: فونتی با جانشینی بافتاری تهاجمی می‌تواند بیشتر از نویسه‌های ورودی glyph ساطع کند، و بندی که هر چند نویسه یک‌بار خط نوشتاری عوض می‌کند به‌ازای هر تعویض یک ران تولید می‌کند. سندی که برای بیشینه‌کردن هر دو مونتاژ شده یک رشته متوسط را به یک تخصیص بزرگ تبدیل می‌کند، و سرویسی که متن را از PDFهای نامطمئن شکل می‌دهد به حدی نیاز دارد که خودش انتخاب کرده نه حدی که دستگاه تحمیل می‌کند

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

اتصال در زمان اجرا، نه یک وابستگی بیلد

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

قاعده متناظر برای فراخوانندگان این است که بررسی کنید. ActiveTextShaper وقتی پلتفرم پیش‌فرضی ندارد و هیچ‌کدام پیکربندی نشده nil برمی‌گرداند، و نقطه ورود شکل‌دهی آن را به‌عنوان یک شکل‌دهنده ناموجود گزارش می‌کند نه به‌عنوان یک شکست شکل‌دهی. آن‌ها مساله‌های متفاوتی‌اند و شایسته پیام‌های متفاوت: یکی یک شکاف استقرار است، دیگری یک مشکل فونت یا متن

یک‌بار نصب کنید، پیش از آنکه چیزی شکل بدهد

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

وقتی یک بک‌اند نصب شد، اندازه‌گیری و شکستن سطر روی هر دو پلتفرم یکسان رفتار می‌کنند، چون متریک‌های ران و glyph را مصرف می‌کنند به‌جای فراخوانی مستقیم پلتفرم؛ مدل شکستن سطر در مقاله اندازه‌گیری متن و شکستن واژه‌ای سطر توصیف شده است. پلتفرم‌ها و toolchainهای پشتیبانی‌شده مؤلفه در صفحه محصول PDFium Delphi component فهرست شده‌اند