مقاله فنی

تراز کامل برای متن PDF در Delphi با HotPDF

تراز کامل طرح‌بندی‌ای است که باعث می‌شود یک ستون از متن در هر دو لبه چپ و راست در یک خط قرار گیرد، ظاهری که از یک کتاب چاپ شده یا یک گزارش رسمی انتظار دارید. توصیف آن آسان است و به طرز شگفت‌آوری اشتباه انجام دادن آن نیز آسان است، زیرا پاسخ به این سوال که "فضای اضافی به کجا می‌رود" برای انگلیسی همانند ژاپنی نیست، و به این دلیل که روش ساده‌لوحانه برای اندازه‌گیری هر خط، یک صفحه سریع را به صفحه‌ای کند تبدیل می‌کند. HotPDF از طریق یک فراخوانی جعبه-طرح‌بندی (box-layout call) واحد، ترازسازی آگاه از اسکریپت را به شما می‌دهد، و در زیر آن فراخوانی، یک اصلاح عملکرد کلاسیک قرار دارد که به تنهایی ارزش درک کردن را دارد

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

تراز کامل در واقع به چه چیزی نیاز دارد

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

قانونی که تراز خوب را از بد جدا می‌کند، این است که فضای خالی را کجا قرار می‌دهید. اسکریپتی که کلمات را با فاصله بین آن‌ها می‌نویسد، مانند انگلیسی و بقیه خانواده لاتین، دارای درزهای طبیعی در هر فاصله بین کلمات است. عریض‌تر کردن این فاصله‌ها برای چشم نامرئی است زیرا خوانندگان از قبل می‌پذیرند که فاصله‌های بین کلمات متفاوت است. اسکریپتی که بدون فاصله بین کلمات نوشته می‌شود، مانند کاراکترهای هان (Han) چینی، کانا (kana) ژاپنی، یا هانگول (Hangul) کره‌ای، چنین درزهایی ندارد. در آنجا فضای خالی باید به طور مساوی بین گلیف‌های مجاور پخش شود، اصلی که حروف‌چین‌های ژاپنی به آن kintou-waritsuke یا فاصله‌گذاری یکنواخت می‌گویند. اعمال کشش فاصله کلمات به سبک لاتین روی یک خط CJK، یا چپاندن تمام فضای خالی در تنها جایی که یک خط CJK به طور اتفاقی دارای فاصله است، رودخانه‌ها و شکاف‌هایی (rivers and gaps) را ایجاد می‌کند که نشان‌دهنده خروجی آماتور است

چگونه HotPDF تصمیم می‌گیرد که فضا به کجا برود

HotPDF این تصمیم را برای هر شکاف، نه برای هر خط، می‌گیرد. هنگامی که یک خط را تراز می‌کند، روی هر جفت گلیف مجاور حرکت می‌کند و می‌پرسد که آیا مرز قابل کششی بین آن‌ها قرار دارد یا خیر. یک مرز زمانی قابل کشش است که هر طرف یک فاصله یا تب (tab) باشد، یعنی حالت لاتین، یا زمانی که هر دو طرف کاراکترهای قابل شکستن CJK باشند، یعنی حالت فاصله‌گذاری یکنواخت. این مرزها را می‌شمارد، فضای خالی خط را به طور مساوی بین آن‌ها تقسیم می‌کند، و آن سهم را به هر شکاف واجد شرایط اضافه می‌کند

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

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

چرا خط آخر به حال خود رها می‌شود

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

HotPDF خط پایانی را بر اساس موقعیت تشخیص می‌دهد. همانطور که متن را در خطوط می‌پیچد (wraps)، می‌داند که خطی که به تازگی جدا کرده است چه زمانی به پایان رشته ارائه‌شده می‌رسد. آن خط پایانی با تراز ساده به چپ ساطع (emitted) می‌شود و عرض طبیعی خود را حفظ می‌کند. هر خط قبل از آن به هر دو لبه تراز می‌شود. شکست‌های خط سخت (Hard line breaks) که در متن می‌نویسید، همانطور که نوشته شده‌اند رعایت می‌شوند، بنابراین یک خط کوتاه عمدی نیز هرگز کشیده نمی‌شود. خواننده یک بلوک مستطیلی تمیز از متن را می‌بیند که خط آخر آن به طور طبیعی به پایان می‌رسد، که همان چیزی است که چشم انتظار دارد

هزینه اندازه‌گیری که ترازسازی را کند می‌کرد

برای تراز کردن یک خط باید عرض دقیق آن را بدانید، و باید پیشروی (advance) هر گلیف را بدانید تا بتوانید فضای اضافی را به دقت قرار دهید. اولین پیاده‌سازی این اعداد را به روشی بدیهی به دست آورد. آن، کل خط را با یک پرس‌وجوی عرض کامل یونیکد اندازه گرفت، سپس پیشوندها را یکی پس از دیگری اندازه گرفت تا پیشروی هر گلیف را از طریق تفاوت‌گیری (differencing) بازیابی کند. برای خطی از N گلیف، این یعنی N+1 فراخوانی به موتور اندازه‌گیری، و هر فراخوانی یک رفت‌وبرگشت (round-trip) کامل GDI است، که از سیستم عامل می‌خواهد متن را شکل داده و اندازه بگیرد و پاسخ را برگرداند

این کار برای هر خط ارزان به نظر می‌رسد. اما در سراسر یک صفحه اینطور نیست. یک صفحه A4 متراکم از متن بدنه را در نظر بگیرید، تقریباً چهل و پنج خط با حدود هشتاد کاراکتر در هر خط. با N+1 رفت‌وبرگشت در هر خط، این یعنی حدود 81 رفت‌وبرگشت برای هر خط و تقریباً 3645 برای صفحه، که تقریباً تمام آن‌ها صرف اندازه‌گیری مجدد متنی می‌شود که موتور لحظاتی پیش به آن نگاه کرده بود. در یک کار دسته‌ای (batch job) که هزاران صفحه تولید می‌کند، این سربار (overhead) بر زمان چیدمان مسلط می‌شود، و هر رفت‌وبرگشت از مرز بین فرآیند شما و زیرسیستم گرافیکی عبور می‌کند

یک فراخوانی به جای N به علاوه یک

این اصلاح نوعی تغییر است که کوچک به نظر می‌رسد اما نتیجه بزرگی دارد. GDI از قبل می‌تواند عرض کل رشته و موقعیت هر گلیف را در یک پرس‌وجوی واحد گزارش کند. HotPDF آن را از طریق GetWideCharAdvances در دسترس قرار می‌دهد، که یک آرایه را با پیشروی طبیعی هر گلیف، از جمله کرنینگ (kerning)، پر می‌کند و عرض کل را در یک فراخوانی به جای N+1 برمی‌گرداند. روتین ترازسازی، که در داخل به نام _HPDFEmitJustifiedWideLine شناخته می‌شود، همه پیشروی‌ها را یک بار درخواست می‌کند، فضای خالی را محاسبه می‌کند، آن را در سراسر مرزهای قابل کشش توزیع می‌کند و خط را ساطع می‌کند

برای همان صفحه A4 اندازه‌گیری هر خط از حدود 81 رفت‌وبرگشت به یکی کاهش می‌یابد، بنابراین صفحه از تقریباً 3645 رفت‌وبرگشت به حدود 45 کاهش می‌یابد، که نزدیک به هشتاد برابر کاهش است. خروجی بایت به بایت یکسان است، زیرا هیچ چیزی در مورد اندازه‌گیری تغییر نکرده است به جز اینکه چند بار درخواست می‌شود. همان موتور GDI، همان معیارهای فونت، همان کرنینگ اعداد یکسانی را تغذیه می‌کنند. فقط تعداد رفت‌وبرگشت‌ها کاهش یافته است. وقتی اندازه‌گیری از قبل درست است، بهینه‌سازی مناسب این است که دیگر مکرراً آن را درخواست نکنید، نه اینکه آن را تقریب بزنید

چگونه خط به صفحه می‌رسد

پس از تسهیم فضای خالی، HotPDF خط را با ExtTextOut و یک آرایه پیشروی برای هر گلیف، یعنی آرایه Dx ساطع می‌کند. هر ورودی فاصله‌ای از مبدأ یک گلیف تا گلیف بعدی است، که پیشروی طبیعی آن گلیف به اضافه سهم آن از فضای خالی در صورت وجود مرز قابل کشش به دنبال آن است. این مستقیماً روی مدل تصویرسازی PDF نگاشت می‌شود. متن موقعیت‌گذاری‌شده با عملگر TJ نوشته می‌شود، آرایه‌ای که اجراهای گلیف را با تنظیمات افقی صریح در هم می‌آمیزد، و مقادیر Dx دقیقاً به آن تنظیمات تبدیل می‌شوند. به همین دلیل است که فضای اضافی به جای اینکه با کاراکترهای پرکننده (padding) جعل شود، بین گلیف‌ها در موقعیت‌های زیر-نقطه‌ای (sub-point) دقیق قرار می‌گیرد، و چرا یک خط HotPDF تراز شده، اگر یک ابزار پایین‌دستی آن را دوباره بخواند، به درستی اندازه‌گیری می‌شود

شما خودتان ExtTextOut را برای پاراگراف‌های تراز شده فراخوانی نمی‌کنید. نقطه ورود WideTextOutBox است، که یک رشته یونیکد را در یک جعبه می‌پیچد و ترازی را که می‌خواهید اعمال می‌کند. این متن را به خطوطی تقسیم می‌کند که با عرض جعبه متناسب است، هر خط را در طول ارتفاع جعبه قرار می‌دهد، و تعداد کاراکترهایی را که توانسته قبل از تمام شدن فضای عمودی در آن جای دهد، برمی‌گرداند. تراز توسط ساختار شمارشی (enum) ترازسازی انتخاب می‌شود

type
  THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);

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

تراز کردن یک پاراگراف در عمل

یک مثال کامل یک سند ایجاد می‌کند، یک فونت تنظیم می‌کند، و یک پاراگراف را در یک جعبه با تراز کامل می‌ریزد. همین کد، متن لاتین و CJK را بدون تغییر پرچم (flag change) تراز می‌کند، زیرا آگاهی از اسکریپت در زیر API قرار دارد

uses
  HPDFDoc;

procedure JustifyParagraph;
var
  Pdf: THotPDF;
  Body: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Justified.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', 11);

    Body :=
      'Full justification spreads the slack on each filled line so both ' +
      'edges meet the column, while the last line keeps its natural width. ' +
      'For scripts with word gaps the space lands between words; for ' +
      'scripts without them it spreads evenly between glyphs.';

    // X, Y, LineSpacing, BoxWidth, BoxHeight, Text, Align
    Pdf.CurrentPage.WideTextOutBox(72, 72, 4, 380, 240, Body, jtJustify);

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

برای رسم همان بلوک با تراز به چپ، وسط، یا راست، فقط آرگومان نهایی را به jtLeft، jtCenter، یا jtRight تغییر دهید. پیچش (wrapping)، قرار دادن خط و مقدار بازگشتی یکسان باقی می‌مانند. عرض اندازه‌گیری شده که هر چهار مسیر را هدایت می‌کند از GetWideTextWidth می‌آید، پرس‌وجوی عرض آگاه از یونیکد که یک WideString را در جایی که اندازه‌گیری بایت به بایت قدیمی هر چیزی فراتر از Latin-1 را اشتباه اندازه می‌گرفت، به درستی اندازه می‌گیرد، و این همان چیزی است که باعث می‌شود جعبه متن CJK و جفت‌های جانشین (surrogate-pair) را از ابتدا در مکان مناسب بپیچد

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