مقال تقني

أخطاء الدوران المزدوج والتكبير الملائم في PDFium بـDelphi

تقبل دالة FPDF_RenderPageBitmap في مكوّن PDFium معامل دوران يضيفه PDFium دائمًا فوق أيًّا كان الدوران الذي تحمله الصفحة بالفعل في إدخال /Rotate الخاص بها، بحيث تؤدي قراءة دوران صفحة مخزَّن وتغذية القيمة نفسها مرة أخرى إلى استدعاء الرسم إلى تدوير الصفحة مرتين. يظهر الخطأ نفسه بالضبط في حسابات التكبير الملائم: تحجيم صورة مصغّرة من عرض وارتفاع الصفحة غير المُدارَين ينتج نسبة عرض إلى ارتفاع خاطئة كلما كانت /Rotate تساوي 90 أو 270 درجة، لأن الصورة النقطية المرسومة تخرج بعرض وارتفاع متبادلين

الفشل سهل الملاحظة بمجرد معرفة ما تبحث عنه، وسهل التفويت قبل ذلك. تصل دفعة من الفواتير الممسوحة ضوئيًا بمزيج من أصول طولية وعرضية، ويصحح أحدهم نصفها بدوران 90 درجة في Acrobat قبل الأرشفة، ويرسم شريط الصور المصغّرة في عارض Delphi مبني على PDFium تلك الصفحات تحديدًا جانبيًا، أو مقلوبة، أو مضغوطة في صندوق بشكل الاتجاه الخاطئ. لا شيء يُطلق استثناءً. لا شيء يسجّل خطأً. البكسلات ببساطة خاطئة، وفقط للمجموعة الفرعية من الصفحات التي أدارها أحدهم لاحقًا — بالضبط نوع الخلل الذي ينجو من تمريرة ضمان جودة كاملة مقابل ملف PDF اختباري غير مُدار ثم يظهر في الإنتاج على الصفحة 47 من ملف حقيقي

لماذا يدور 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، متوقعًا أن يجعل الاستدعاء الصفحة قائمة (upright). صفحة محفوظة بالفعل بـ/Rotate 90 تُعرض بشكل صحيح، مُدارة، في أي عارض متوافق، بما في ذلك PDFium؛ أضف ro90 مرة أخرى فوق ذلك وستدور الصفحة إلى 180 درجة بدلًا من الـ90 المقصودة، بينما تدور صفحة بلا دوران على الإطلاق ربع دورة غير مرغوب فيها دون سبب

// 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 مكانه في الواجهة البرمجية لمهمة مختلفة حقًا: إضافة دوران للعرض فقط لا علاقة له باتجاه الصفحة المخزَّن، النوع الذي يطبّقه زر شريط أدوات تدوير-العرض دون لمس الملف الكامن. تحتفظ 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 طولية تحمل /Rotate 90 لا تزال تبلّغ عن نحو 595 في 842 نقطة، رغم أن PDFium يرسمها، بشكل صحيح، بنحو 842 في 595 بمجرد أن يسري الدوران، وينتهي صندوق ملائم محسوب من الزوج غير المُدار بشكل الاتجاه الخاطئ كليًا

FPDF_GetPageSizeByIndex مثال ملموس واحد على استدعاء يبلّغ عن ذلك الحجم الجوهري غير المُدار بالتصميم، ما يجعله مناسبًا لمسح أبعاد الصفحات دون تحميل كل صفحة وخطيرًا لحسابات التكبير الملائم التي تنسى مراعاة ذلك. يتبع الإصلاح مباشرة من تسمية المشكلة: تحقق من دوران الصفحة قبل إجراء حساب الملاءمة، وبادل العرض والارتفاع كلما كان ذلك الدوران 90 أو 270 درجة، واحسب صندوق الملاءمة من الزوج المتبادَل، ومرّر مع ذلك ro0 إلى استدعاء الرسم الفعلي، لأن PDFium يبقى الجهة الوحيدة التي تطبّق الدوران الحقيقي

الحصول على صور مصغّرة صحيحة دون إعادة اختراع حسابات الملاءمة

تحمل TPdf.RenderPageThumbnail هذا الإصلاح بالفعل، بحيث يكون أقصر مسار إلى صورة مصغّرة صحيحة استدعاءها بدلًا من إعادة تجميع منطق الملاءمة والدوران يدويًا. عند إعطائها فهرس صفحة يبدأ من 1 وعرضًا وارتفاعًا أقصيين، تحسب RenderPageThumbnail صندوق ملاءمة، وتصححه لأجل /Rotate بقيمة 90 أو 270 داخليًا، وتُعيد صورة نقطية يملكها المستدعي دون إزعاج الصفحة الحالية للمستند أو إطلاق حدث 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 لا تغطي سوى حالة الصورة النقطية الواحدة. تحتاج شبكة صور مصغّرة مخصصة، أو شريط معاينة طباعة، أو مربع حوار اختيار صفحات يخطط عدة صفحات مقابل صناديق مستقلة حسابات الملاءمة نفسها الواعية بالدوران دون الرغبة بالضرورة في صورة نقطية جديدة لكل بلاطة، وتعتمد أوضاع تكبير ملاءمة الصفحة وملاءمة العرض الخاصة بـTPdfView نفسها داخليًا على الفكرة نفسها بالضبط، مختارة بين عرض الصفحة وارتفاعها لحساب نسبة التكبير بناءً على دوران العرض الحالي قبل مقارنته بمنطقة العميل المتاحة. إذا كان أداء التكبير والتمرير في عارض من ذلك النوع هو المشكلة التالية في القائمة، فإن المقالة المرافقة حول التخزين المؤقت للرسم والتكبير السلس في عارض Delphi مبني على PDFium تلتقط بالضبط حيث ينتهي التحجيم الصحيح

اكتشاف دوران مزدوج قبل أن يكتشفه عميل

يحمل الدوران المزدوج توقيعًا بصريًا موثوقًا واحدًا: صفحة أُديرت 90 درجة في الطريق تخرج وكأنها أُديرت 180 نسبة إلى بقية المستند، لا 90، لأن ro90 الإضافية تكدّست فوق ro90 الخاصة بالصفحة نفسها بدلًا من استبدالها. لن يلتقط هذا أبدًا تجهيز اختبار مبني فقط من صفحات /Rotate 0، بما أن إضافة ro0 إلى ro0 لا تزال ro0 ويبقى الخلل غير مرئي؛ يحتاج التجهيز على الأقل صفحة واحدة محفوظة بـ/Rotate 90 وأخرى بـ/Rotate 270 قبل أن يمكن الوثوق بمسار شيفرة الصور المصغّرة أو التكبير الملائم

خط أنابيب الصفحة-إلى-صورة-نقطية الأساسي المشروح في رسم صفحات PDF إلى JPEG عبر مكوّن PDFium يرسم بالفعل الصفحات المُدارة بشكل صحيح دون أي شيفرة حالة خاصة، تحديدًا لأنه يترك Rotation عند افتراضيه ro0 ويترك PDFium يطبّق /Rotate بنفسه. لا يظهر خلل الدوران المزدوج إلا بمجرد أن تبدأ شيفرة التطبيق بقراءة PageRotation مرة أخرى وتغذيتها في مكان لا تنتمي إليه

استدعاءات الرسم الواعية بالدوران وتحجيم الصور المصغّرة الموصوفة هنا جزء من مكوّن PDFium لـDelphi وC++Builder، إلى جانب بقية واجهات الرسم والعرض واستخراج النص المبنية على فئتي TPdf وTPdfView نفسيهما