رندرکنندهٔ صفحه در HotPDF Delphi Component حالا متن را با حساب کردن جابهجایی هر گلایف در text space پیش میبرد، یعنی tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th همانطور که ISO 32000-1 §9.4.4 تعریفش میکند، و بعد ماتریس متن را از بخش خطیاش با HPDFTranslateTextMatrix جلو میبرد. بریدن بهازای هر فریم q ذخیره و روی Q بازیابی میشود، اما یک ناحیهٔ GDI فقط وقتی گرفته میشود که آن فریم واقعاً clip را عوض کند. هر دو fix در HotPDF 2.754.0 رسیدند و هر دو از صفحات دنیای واقعی آمدند که با کلمههای فروپاشیده رندر میشدند یا نواحی clip از Q شان نشت میکردند. باگ اول حسابی است که تا وقتی تولیدکننده اندازهٔ فونتش را داخل ماتریس بنویسد درست به نظر میرسد. دومی یک fix درستی است که تقریباً شتابگیری رندر موازی را خرجمان کرد و راهی که سرعت را پس گرفتیم، اگر هر device PDF پشتیبانیشده از GDI مینویسی، ارزش دانستنش را دارد
چرا وقتی یک PDF از Tf 1 استفاده میکند متن به یک کلوخه فرومیریزد؟
چون کد پیشروی قدیمی یک فاصلهٔ text space را مستقیم به مؤلفهٔ انتقال Tm اضافه میکرد، انگار که text space و user space همیشه مقیاس یکسان دارند. بسیاری از تولیدکنندههای واقعی اندازهٔ فونت را با Tf روی 1 میگذارند و اندازهٔ واقعی را در ماتریس متن حمل میکنند. با /F1 1 Tf و 12 0 0 12 72 700 Tm، یک گلایف 500 واحدی در text space به اندازهٔ 0.5 پیش میرود که وقتی Tm مقیاسش میکند روی صفحه 6 نقطه میشود. رندرکنندهٔ قدیمی Tm.e := Tm.e + Adv را اجرا میکرد و قلم را 0.5 نقطه جلو میبرد. هر گلایف یکدوازدهم نویسه بعد از قبلی مینشست، پس یک خط متن بدنه بهشکل یک لکهٔ تیره در حاشیهٔ چپ رندر میشد در حالی که همان فایل در هر viewer دیگری بینقص به نظر میرسید
// content stream از تولیدکنندهای که اندازه را در Tm کدگذاری میکند نه Tf:
// BT
// /F1 1 Tf
// 12 0 0 12 72 700 Tm
// [(Hel) 30 (lo) -250 (world)] TJ
// ET
// پیشروی قدیمی (سادهشده): فاصله به Tm.e اضافه میشد انگار user space است
Adv := W * FontSize / 1000; // 0.5 برای گلایفی 500 واحدی
if (HorizScale <> 0) and (HorizScale <> 100) then
Adv := Adv * HorizScale / 100; // Th فقط روی عرض
Adv := Adv + CharSpace; // Tc با Th مقیاس نمیشود
if Code = 32 then
Adv := Adv + WordSpace * FontSize / 1000; // Tw اشتباهاً با Tfs مقیاس شده
Tm.e := Tm.e + Adv; // از Tm.a و Tm.b و Tm.c و Tm.d چشمپوشی میکند
// تنظیم TJ قدیمی: بدون Th و باز هم فقط Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;
میانبُر Tm.e تنها نقص آن بلوک نبود. فاصلهگذاری کلمه یعنی Tw در واحدهای text space بدون مقیاس بیان میشود، اما کد قدیمی آن را در FontSize / 1000 ضرب میکرد، پس زیر Tf 12 یک خط justify شده تقریباً همهٔ فاصلهٔ بین کلمههایش را از دست میداد. مقیاسبندی افقی Th روی عرض گلایف اعمال میشد اما نه روی Tc یا Tw، و تنظیم kerning در TJ اصلاً از آن عبور نمیکرد. مسیر بدون-رسم که متن نامرئی در حالت رندر 3 — همان جنس لایههای متنی OCR — را پیش میبرد و متن داخل optional content پنهان، یک کپی خصوصی از همان حساب داشت، پس هر چیزی که بعد از یک اجرای نامرئی رسم میشد از موقعیت غلط شروع میشد. باگهای وضعیت متن در یک رندرکننده بهندرت بلند شکست میخورند: مثل باگهای ایندکس عملوند و نام منبع که روزی Tc و Tw و Tz را بدون حتی یک خطا صفر میکردند، اینها صفحات قابلباور روی خروجی خود کتابخانه تولید میکردند و فقط روی فایلهای تولیدکنندههای دیگر میشکستند
ISO 32000-1 §9.4.4 پیشروی گلایف را چطور تعریف میکند؟
ISO 32000-1 §9.4.4 پیشروی را کاملاً در text space تعریف میکند و آن را بهعنوان یک ماتریس انتقال روی ماتریس متن اعمال میکند، پس جواب این است که اول tx را حساب کنی و بگذاری Tm مقیاس و چرخش و skew را انجام بدهد. برای نوشتار افقی، tx برابر ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th است، که در آن w0 عرض گلایف در هزارمهای em است، Tj تنظیم TJ است و Th برابر Tz تقسیم بر 100. Tm جدید میشود [1 0 0 1 tx 0] × Tm، که در HotPDF همان هلپر HPDFTranslateTextMatrix است: X و Y را از طریق ضرایب ماتریس a و b و c و d اضافه میکند بهجای آنکه مستقیم در e و f بنویسد. طبق §9.3.3، Tw فقط روی کد نویسهٔ تکبایتی 32 اعمال میشود، پس کدهای CID چندبایتی هرگز روی مسیر افقی فاصلهگذاری کلمه نمیگیرند. همان هلپر حالا Td و TD و T* و operatorهای ' و " و تنظیمهای TJ و مسیر متن پنهان را میراند، یعنی یک تابع واحد مالک قاعده است
procedure HPDFTranslateTextMatrix(var Matrix: THPDFAffineMatrix; X, Y: Double);
begin
Matrix.e := Matrix.e + Matrix.a * X + Matrix.c * Y;
Matrix.f := Matrix.f + Matrix.b * X + Matrix.d * Y;
end;
// پیشروی گلایف افقی، ISO 32000-1 9.4.4
W := HPDFFontCharWidth(F, Code);
Adv := W * State.Text.FontSize / 1000 + State.Text.CharSpace;
if (Code = 32) and not F.CID2Byte then
Adv := Adv + State.Text.WordSpace; // Tw در text space، بدون مقیاس
Adv := Adv * State.Text.HorizScale / 100; // Th روی کل مجموع اعمال میشود
HPDFTranslateTextMatrix(Tm, Adv, 0);
// عنصر عددی TJ: همان فضا، همان Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);
جایگذاری گلایف هم باید همان منطق را دنبال میکرد. وقتی خطوط دور embed شده در دسترس نیست و رندرکننده به TextOutW مربوط به GDI برمیگردد، حالا ماتریس کامل گلایف را از CTM × Tm × rise × مقیاس em میسازد، شامل Th، و آن را با SetWorldTransform در حالت GM_ADVANCED داخل یک جفت SaveDC / RestoreDC نصب میکند. فونت GDI با ارتفاع ثابت 1000 واحدی ساخته میشود و ترنسفورم اندازهگیری را انجام میدهد، پس متن چرخیده و کج جهتش را نگه میدارد بهجای آنکه ایستاده در یک نقطهٔ مبدأ ترنسفورمشده رسم شود. حالت نوشتار عمودی یک عدمتقارن عمدی است: فونت WMode 1 روی محور y با متریک عمودیاش پیش میرود و مقیاسبندی افقی روی آن محور اعمال نمیشود
q/Q واقعاً چه چیزی را در وضعیت گرافیک PDF ذخیره میکند؟
ISO 32000-1 §8.4.2 مسیر برش جاری را بخشی از وضعیت گرافیک فهرست میکند، پس Q باید clip را دقیقاً همانطور که در q متناظر بود بازیابی کند، نه فقط پارامترهای عددی. HotPDF از قبل پشتهٔ وضعیت گرافیک با CTM و رنگها و پارامترهای خط و وضعیت متن را نگه میداشت، اما GDI clip را در device context نگه میدارد، بیرون از آن پشته. پس یک کپی از وضعیت عددی همه چیز را بازیابی میکرد جز clip، و clipی که با W n داخل یک بلوک q ... Q نصب میشد به بریدن هر عملیات بعدی روی صفحه ادامه میداد. Form XObjectها مسیر دومی به همان شکست اضافه کردند، چون §8.10 به فرم یک save و restore ضمنی دور محتوایش میدهد و محتوای فرم دنیای واقعی گاهی operatorهای q خودش را نامتوازن رها میکند هرچند spec میخواهد جفت شوند. رندرکننده حالا قبل از اجرای یک فرم CaptureClipBeforeChange و SaveDC را صدا میزند و بعد از تمام شدن فرم هر ناحیهٔ ذخیرهشدهٔ عمیقتر از عمق ورود را دور میریزد و RestoreDC را صدا میزند، پس هر HRGN ذخیرهشده دقیقاً یک مسیر آزادسازی دارد
گرفتن تنبل clip با THPDFSavedClipState
fixی که عرضه شد بهازای هر q یک رکورد THPDFSavedClipState ذخیره میکند اما بخش پرهزینه را تا اولین باری که فریم clip را عوض میکند عقب میاندازد. رکورد handle ناحیه و عمق پشتهای که به آن تعلق دارد و device contextی که از آن گرفته شده و یک فلگ Captured را نگه میدارد. DevPushState فقط عمق و DC را پر میکند و آرایهٔ فریم را با دو برابر کردن از 16 بزرگ میکند، پس یک content stream پر از q 1 0 0 1 x y cm ... Q هیچ object GDI ای تخصیص نمیدهد. operatorهایی که میخواهند برش را عوض کنند، یعنی رسم مسیر با یک W یا W* معلق، operator n، پر کردن الگو و ورود به فرم، اول CaptureClipBeforeChange را صدا میزنند
procedure THPDFPageRenderer.CaptureClipBeforeChange;
var
Index, ClipResult: Integer;
Region: HRGN;
begin
ClearSavedClipRegions(FGSStack.Count);
Index := FSavedClipCount - 1;
if (Index < 0) or FSavedClips[Index].Captured or
(FSavedClips[Index].StackDepth <> FGSStack.Count) or
(FSavedClips[Index].DC <> FDC) then Exit; // از قبل ذخیره شده یا مال ما نیست
Region := CreateRectRgn(0, 0, 0, 0);
if Region = 0 then RaiseLastOSError;
ClipResult := GetClipRgn(FDC, Region); // 0 یعنی اصلاً clip وجود ندارد
if ClipResult <= 0 then
begin
DeleteObject(Region);
Region := 0;
if ClipResult < 0 then RaiseLastOSError;
end;
FSavedClips[Index].Region := Region;
FSavedClips[Index].Captured := True;
end;
procedure THPDFPageRenderer.DevPopState;
var
Index: Integer;
begin
ClearSavedClipRegions(FGSStack.Count);
Index := FSavedClipCount - 1;
if (Index >= 0) and (FSavedClips[Index].StackDepth = FGSStack.Count) then
begin
if FSavedClips[Index].Captured and (FSavedClips[Index].DC = FDC) then
SelectClipRgn(FDC, FSavedClips[Index].Region); // Region برابر 0 برش را حذف میکند
if FSavedClips[Index].Region <> 0 then
DeleteObject(FSavedClips[Index].Region);
Dec(FSavedClipCount);
end;
FGSStack.Pop;
end;
هزینهٔ اندازهگیریشدهٔ نسخهٔ مشتاق دلیل وجود این طراحی است. اولین پیادهسازی درست بهازای هر q یک ناحیهٔ GDI میساخت و میخواند و در صفحههایی که بیشترشان ترنسفورمهای عددی بودند، threadهای رندر وقتشان را صرف رقابت سر objectهای ناحیهٔ GDI میکردند بهجای رستر کردن. پایپلاین رندر موازی از سود انتظاریاش به حدود 1.13 تا 1.20 برابر توانعملیاتی تکنخی افت کرد و گیت شتاب 1.5 برابری در سویت بنچمارک را رد نکرد. با گرفتن تنبل و ظرفیت فریم بازیافتشده، همان بنچمارک گیت اصلی 1.5 برابری را دوباره قبول میشود. antialiasing گلایف کوچک TrueType در همان انتشار رسید و مظنون بدیهی بود، اما رگرسیون به تخصیص ناحیه برمیگشت، که یادآور خوبی است که قبل از سرزنش تازهترین قابلیت، اندازه بگیر
مرزهای این رویکرد کجاست؟
clip ذخیرهشده یک ناحیهٔ GDI در پیکسلهای device است، پس برای بیتمپی که رندر میشود دقیق و برای هر مقصد دیگری بیمعناست. به همین دلیل هر فریم device context خودش را ثبت میکند و DevPopState وقتی DC عوض شده بازیابی را رد میکند، مثلاً وقتی یک گروه شفافیت در بیتمپ لایهٔ خودش رندر میشود. برگرداندن صفر از GetClipRgn نتیجهای مشروع است به معنای نبود clip و بازیابی آن با SelectClipRgn(FDC, 0) همان کاری است که درست، یک clipی که در q متناظر وجود نداشت را حذف میکند. در سمت متن، fix این را درست میکند که هر گلایف کجا میرود اما عرض نمیسازد: اگر فونتی آرایهٔ /Widths اش را جا انداخته باشد و برنامهٔ embed شده در دسترس نباشد، پیشروی همچنان فقط به اندازهٔ fallback عرض خوب است. وقتی این حوزه را رگرسیونتست میکنی، دستکم یک fixture با Tf 1 و یک Tm مقیاسشده، یکی با Tz و Tw غیرصفر، و یکی با clipی داخل q ... Q و محتوایی بیرون آن نگه دار، چون هیچکدام در اسنادی که خود کتابخانه تولید میکند ظاهر نمیشوند
اگر رندرکننده را از کد اپلیکیشن میرانی، در الگوی فراخوانی که در رندر کردن یک صفحهٔ PDF به بیتمپ توصیف شد هیچ چیز عوض نمیشود و صفحههایی که قبلاً خطوط لکهدار یا محتوای بریده نشان میدادند روی 2.754.0 و بعدش بهسادگی درست رندر میشوند. جزئیات مربوط به کامپوننت و نسخههای پشتیبانیشدهٔ Delphi و C++Builder و لایسنس در صفحهٔ محصول HotPDF Delphi PDF Component است