مقاله فنی

باگ‌های چرخش دوگانه و زوم-متناسب در PDFium در Delphi

تابع FPDF_RenderPageBitmap در PDFium Component یک آرگومان چرخش می‌پذیرد که PDFium همیشه آن را بالای هر چرخشی که صفحه از پیش در ورودی /Rotate خودش حمل می‌کند می‌افزاید، پس خواندن چرخش ذخیره‌شده‌ی یک صفحه و سپردن همان مقدار دوباره به فراخوانی رندر، صفحه را دوبار می‌چرخاند. همان اشتباه دقیقاً در محاسبات زوم-متناسب هم نمایان می‌شود: اندازه‌گذاری یک بندانگشتی از عرض و ارتفاع نچرخیده‌ی صفحه، هر زمان که /Rotate برابر ۹۰ یا ۲۷۰ درجه باشد، نسبت تصویر اشتباهی تولید می‌کند، چون بیت‌مپ رندرشده با عرض و ارتفاع جابه‌جا بیرون می‌آید

این شکست وقتی بدانید به‌دنبال چه چیزی بگردید آسان برای دیدن است، و تا آن زمان آسان برای از دست‌دادن. یک دسته از فاکتورهای اسکن‌شده با ترکیبی از اصل‌های portrait و landscape می‌رسد، کسی نیمی از آن‌ها را با یک چرخش ۹۰درجه‌ای در Acrobat پیش از آرشیوکردن صاف می‌کند، و نوار بندانگشتی در یک نمایشگر Delphi ساخته‌شده روی PDFium آن صفحات مشخص را کج، وارونه، یا فشرده‌شده درون یک جعبه‌ی شکل‌گرفته برای جهت اشتباه رندر می‌کند. هیچ چیزی استثنایی throw نمی‌کند. هیچ چیزی خطایی لاگ نمی‌کند. پیکسل‌ها صرفاً اشتباه هستند، و فقط برای زیرمجموعه‌ای از صفحات که کسی بعداً چرخانده — دقیقاً همان نوع باگی که از یک پاس کامل QA در برابر یک PDF تست نچرخیده جان سالم به‌در می‌برد و بعد در تولید در صفحه‌ی ۴۷ یک PDF واقعی نمایان می‌شود

چرا PDFium صفحه را دوبار می‌چرخاند؟

PDFium مقدار /Rotate خودِ صفحه را به‌طور خودکار هر بار که یک بیت‌مپ رندر می‌کند اعمال می‌کند، صرف‌نظر از اینکه چه چیزی به رندرر پاس داده شده. پارامتر چرخش در FPDF_RenderPageBitmap، در معرض دید گذاشته‌شده در PDFiumPas به‌عنوان مقادیر TRotation ro0، ro90، ro180 و ro270 روی TPdf.RenderPage، TPdf.RenderTile و TPdf.RenderPageThumbnail، زاویه‌ای را که یک صفحه باید در نهایت داشته باشد تنظیم نمی‌کند؛ پارامتر چرخش تنظیم می‌کند چقدر چرخش اضافی روی هر چیزی که دیکشنری صفحه از پیش مشخص کرده لایه‌بندی شود، به همین دلیل هر یک از آن متدها آن را به‌طور پیش‌فرض ro0 قرار می‌دهد

TPdf.PageRotation همان مقدار /Rotate را از طریق FPDFPage_GetRotation می‌خواند، و کد برنامه اغلب به آن به دلایلی نیاز دارد که هیچ ربطی به رندر ندارد، مثل تصمیم‌گیری درباره‌ی چیدمان یک حاشیه‌نویسی در فضای صفحه. تله یک خط تکی است: سپردن PageRotation به آرگومان Rotation در RenderPage، با انتظار اینکه فراخوانی صفحه را به راست‌قامت نرمال کند. صفحه‌ای که از پیش با /Rotate 90 ذخیره شده، در هر نمایشگر مطابق، PDFium شامل، به‌درستی و چرخیده نمایش داده می‌شود؛ ro90 را دوباره روی آن اضافه کنید و صفحه به ۱۸۰ درجه به‌جای ۹۰ مورد نظر می‌چرخد، درحالی‌که صفحه‌ای بدون هیچ چرخشی اصلاً یک ربع چرخش ناخواسته بدون هیچ دلیلی می‌گیرد

// Wrong: PageRotation already reflects /Rotate, and PDFium applies
// it automatically on every render -- passing it again as Rotation
// doubles the angle
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, Pdf.PageRotation, []);

// Right: leave Rotation at its ro0 default and let PDFium apply the
// page's own /Rotate exactly once
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, ro0, []);

پارامتر Rotation واقعاً برای چیست

پارامتر Rotation جایگاهش در API را برای کاری واقعاً متفاوت به‌دست می‌آورد: افزودن یک چرخش فقط-نما که هیچ ربطی به جهت ذخیره‌شده‌ی یک صفحه ندارد، از همان نوعی که یک دکمه‌ی نوار ابزار چرخش-نما بدون لمس‌کردن فایل زیرین اعمال می‌کند. TPdfView به دقیقاً همین دلیل این دو مفهوم را به‌عنوان دو ویژگی جداگانه نگه می‌دارد. TPdfView.PageRotation همان /Rotate خودِ صفحه را آینه می‌کند و، از طریق FPDFPage_SetRotation، می‌تواند یک مقدار جدید را به سند بازنویسی کند؛ TPdfView.Rotation یک ویژگی گذرا و فقط-نما است که به‌طور پیش‌فرض ro0 است و هرگز فایل را لمس نمی‌کند. خواندن ویژگی اول و نوشتنش در دومی کل باگ در یک جمله است

// View-only: rotates what the user sees, changes nothing in the file
procedure TViewerForm.RotateViewClick(Sender: TObject);
begin
  case PdfView.Rotation of
    ro0:   PdfView.Rotation := ro90;
    ro90:  PdfView.Rotation := ro180;
    ro180: PdfView.Rotation := ro270;
    ro270: PdfView.Rotation := ro0;
  end;
end;

// Persistent: rewrites the page's own /Rotate entry in the document
procedure TViewerForm.RotatePageClick(Sender: TObject);
begin
  case PdfView.PageRotation of
    ro0:   PdfView.PageRotation := ro90;
    ro90:  PdfView.PageRotation := ro180;
    ro180: PdfView.PageRotation := ro270;
    ro270: PdfView.PageRotation := ro0;
  end;
end;

چرا اندازه‌گذاری زوم-متناسب به همان روش می‌شکند؟

اندازه‌گذاری زوم-متناسب به یک دلیل تصویر-آینه‌ای می‌شکند: محاسبات از یک جفت عدد اشتباه شروع می‌شود نه از زاویه‌ی اشتباه. یک روش معمول برای اندازه‌گذاری یک جعبه‌ی بندانگشتی از PDFium عرض و ارتفاع یک صفحه را می‌خواهد، آن نسبت تصویر را با جعبه‌ی موجود مقایسه می‌کند، و بزرگ‌ترین مستطیلی که در آن جا می‌گیرد را محاسبه می‌کند — که برای یک صفحه‌ی نچرخیده تمیز کار می‌کند. همان محاسبات وقتی عرض و ارتفاع از یک فراخوانی می‌آیند که اندازه‌ی ذاتی و نچرخیده‌ی صفحه را گزارش می‌دهد، برای یک صفحه‌ی /Rotate 90 یا /Rotate 270 بی‌سروصدا شکست می‌خورد: یک صفحه‌ی A4 portrait که /Rotate 90 حمل می‌کند همچنان تقریباً ۵۹۵ در ۸۴۲ نقطه گزارش می‌دهد، هرچند PDFium آن را، به‌درستی، تقریباً ۸۴۲ در ۵۹۵ رندر می‌کند به‌محض اینکه چرخش اعمال شود، و یک جعبه‌ی متناسب محاسبه‌شده از جفت نچرخیده در نهایت کاملاً برای جهت اشتباه شکل می‌گیرد

FPDF_GetPageSizeByIndex یک مثال مشخص از یک فراخوانی است که از نظر طراحی آن اندازه‌ی ذاتی و نچرخیده را گزارش می‌دهد، که آن را برای اسکن‌کردن ابعاد صفحه بدون بارگذاری هر صفحه راحت می‌کند و برای محاسبات زوم-متناسبی که فراموش می‌کند آن را در نظر بگیرد پرخطر. رفع اشکال مستقیماً از نام‌گذاری مسئله پیروی می‌کند: چرخش صفحه را پیش از انجام محاسبات متناسب بررسی کنید، عرض و ارتفاع را هر زمان که آن چرخش ۹۰ یا ۲۷۰ درجه باشد جابه‌جا کنید، جعبه‌ی متناسب را از جفت جابه‌جاشده محاسبه کنید، و همچنان ro0 را به فراخوانی رندر واقعی پاس دهید، چون PDFium همچنان همان کسی است که چرخش واقعی را اعمال می‌کند

درست‌کردن بندانگشتی‌ها بدون دوباره‌اختراع‌کردن محاسبات متناسب

TPdf.RenderPageThumbnail از پیش این رفع اشکال را حمل می‌کند، پس کوتاه‌ترین مسیر به یک بندانگشتی درست فراخوانی آن است به‌جای دوباره‌مونتاژکردن منطق متناسب-و-چرخش دستی. با دریافت یک شماره‌ی صفحه‌ی مبنا-۱ و حداکثر عرض و ارتفاع، RenderPageThumbnail یک جعبه‌ی متناسب محاسبه می‌کند، آن را داخلاً برای یک /Rotate برابر ۹۰ یا ۲۷۰ تصحیح می‌کند، و یک بیت‌مپ متعلق-به-فراخواننده برمی‌گرداند بدون اینکه صفحه‌ی فعلی سند یا شلیک یک رویداد OnPageChange را مختل کند — که برای یک نوار بندانگشتی که کنار یک نمایشگر زنده روی همان نمونه‌ی TPdf ساخته شده اهمیت دارد

// PageW, PageH are a page's own (unrotated) dimensions in points, for
// example from FPDF_GetPageSizeByIndex, which reports size before
// /Rotate is applied
function FitBox(PageW, PageH: Double; Rotation: TRotation;
  MaxW, MaxH: Integer; out FitW, FitH: Integer): Boolean;
var
  PgW, PgH, Swap: Integer;
begin
  PgW := Round(PageW);
  PgH := Round(PageH);
  if PgW < 1 then PgW := 1;
  if PgH < 1 then PgH := 1;

  if Rotation in [ro90, ro270] then
  begin
    Swap := PgW;
    PgW := PgH;
    PgH := Swap;
  end;

  Result := (MaxW > 0) and (MaxH > 0);
  if not Result then
    Exit;

  if PgW * MaxH > PgH * MaxW then
  begin
    FitW := MaxW;
    FitH := (MaxW * PgH) div PgW;
  end
  else
  begin
    FitH := MaxH;
    FitW := (MaxH * PgW) div PgH;
  end;
end;

کمکی FitBox در هر صورت ارزش نگه‌داشتن دارد، چون RenderPageThumbnail فقط حالت تک-بیت‌مپ را پوشش می‌دهد. یک شبکه‌ی بندانگشتی سفارشی، یک نوار پیش‌نمایش چاپ، یا یک دیالوگ انتخاب‌گر-صفحه که چند صفحه را در برابر جعبه‌های مستقل چیدمان می‌کند، به همان محاسبات متناسب آگاه-از-چرخش نیاز دارد بدون اینکه لزوماً یک بیت‌مپ تازه برای هر کاشی بخواهد، و حالت‌های زوم fit-page و fit-width خودِ TPdfView داخلاً به همان ایده تکیه می‌کنند، انتخاب بین عرض و ارتفاع یک صفحه برای محاسبات نسبت-زوم بر اساس چرخش فعلی نما پیش از مقایسه‌ی آن با ناحیه‌ی کلاینت موجود. اگر کارایی زوم و اسکرول در آن نوع نمایشگر مسئله‌ی بعدی در فهرست است، مقاله‌ی همراه درباره‌ی کش رندر و زوم روان در یک نمایشگر Delphi مبتنی بر PDFium دقیقاً از همان‌جایی ادامه می‌دهد که اندازه‌گذاری درست تمام می‌شود

یافتن یک چرخش دوگانه پیش از اینکه یک مشتری آن را بیابد

یک چرخش دوگانه یک امضای بصری قابل‌اعتماد دارد: صفحه‌ای که در ورودی ۹۰ درجه چرخیده بود، نسبت به بقیه‌ی سند ۱۸۰ چرخیده به‌نظر می‌رسد، نه ۹۰، چون ro90 اضافی روی ro90 خودِ صفحه انباشته شد به‌جای اینکه جایگزینش کند. یک fixture تست که فقط از صفحات /Rotate 0 ساخته شده هرگز این را نمی‌گیرد، چون افزودن ro0 به ro0 همچنان ro0 است و باگ نامرئی می‌ماند؛ یک fixture به حداقل یک صفحه با /Rotate 90 و یکی با /Rotate 270 نیاز دارد پیش از اینکه یک مسیر کد بندانگشتی یا زوم-متناسب قابل‌اعتماد باشد

خط لوله‌ی پایه‌ی صفحه-به-بیت‌مپ که در رندرکردن صفحات PDF به JPEG با PDFium Component پوشش داده شده، از پیش صفحات چرخیده را بدون هیچ کد مورد-خاصی به‌درستی رندر می‌کند، دقیقاً چون Rotation را در پیش‌فرض ro0 خودش می‌گذارد و اجازه می‌دهد PDFium خودش /Rotate را اعمال کند. باگ چرخش-دوگانه فقط زمانی ظاهر می‌شود که کد برنامه شروع به خواندن PageRotation و تغذیه‌ی آن جایی که به آن تعلق ندارد بکند

فراخوانی‌های رندر آگاه-از-چرخش و اندازه‌گذاری بندانگشتی که در اینجا توصیف شد بخشی از کامپوننت PDFium برای Delphi و C++Builder هستند، در کنار بقیه‌ی APIهای رندر، نمایش، و استخراج متن که روی همان کلاس‌های TPdf و TPdfView ساخته شده‌اند