مقال تقني

إلحاق مصفوفة PDFium مقابل تقديمها في Delphi: دوران المحور

تستخدم مصفوفات PDF التآلفية اتفاقية متجه الصف الخاصة بـISO 32000-1 §8.3.3، حيث تُضرَب نقطة في المصفوفة من اليسار: point' = point * M. في PDFium Component لـDelphi وC++Builder، تلك الحقيقة الواحدة تثبّت سطح الواجهة البرمجية بأكمله لـTPdfMatrix: Multiply تُلحِق، فـM := M * Op، بينما PreMultiply تُقدِّم، فـM := Op * M

كل علّة تحويل كلاسيكية تعود جذورها إلى تذكّر تلك الجملة بالمقلوب. العلامة المائية watermark التي تدور بأناقة في ملف اختبارك وتهبط نصفها خارج الصفحة في ملف العميل. الصورة المصغَّرة التي تخرج مدوَّرة مرتين لأن الصفحة كانت تحمل بالفعل ربع دورة. الختم الذي إزاحته مثالية على A4 وينحرف على Letter. لا شيء من هذه علل رسم؛ إنها علل ترتيب ضرب، وكلها قابلة للإصلاح بمجرد أن تستطيع أن تقول بصوت عالٍ في أي فضاء مكتوبة كل عملية

اتفاقية متجه الصف التي تضبط القواعد

تخزّن TPdfMatrix العناصر الستة المسمّاة في المواصفة وتطبّقها تمامًا كما تعرّفها الصيغة، فالتحويل نفسه هو حيث يبدأ التفكير المنطقي. تحسب TPdfMatrix.TransformPoint القيمة x' = x*a + y*c + e وy' = x*b + y*d + f، وهي الصيغة ذات العناصر الستة التي يعرّفها ISO 32000-1 §8.3.4 لمُشغِّل cm الذي يدمج مصفوفة على مصفوفة التحويل الحالية. الزوج (a, b) هو الصف الأول، و(c, d) الثاني، و(e, f) صف الإزاحة. عادات متجه العمود المُكتسَبة من OpenGL أو من مساق جبر خطي ستضللك هنا، وستضللك بصمت، لأن مصفوفة بترتيب خاطئ تبقى مصفوفة صحيحة تمامًا. اقرأ مُركَّبًا composite باتفاقية الصف من اليسار إلى اليمين ويسقط ترتيب التطبيق مجانًا: بما أن point * (M * Op) يساوي (point * M) * Op، فإن عملية مُلحَقة تعمل على إحداثيات أنتجتها المصفوفة القائمة بالفعل، أي على فضاء الصفحة، بينما عملية مُقدَّمة تعمل قبل تشغيل المصفوفة القائمة، في فضاء الدخل الخاص بالكائن نفسه

var
  M: TPdfMatrix;
  Pt: FS_POINTF;
begin
  M := TPdfMatrix.Create;                // identity
  try
    // Append order: each call acts on what the previous calls produced.
    M.Scale(0.5, 0.5);                   // M := M * S   half size
    M.Rotate(90);                        // M := M * R   clockwise, degrees
    M.Translate(300, 400);               // M := M * T   then move on the page

    Pt := M.TransformPoint(0, 0);        // x*a + y*c + e, x*b + y*d + f
  finally
    M.Free;
  end;
end;

تفترض TPdfMatrix.Rotate اتجاه عقارب الساعة والدرجات افتراضيًا، مع توفر ACounterClockwise وAAngleInRadians حين تكون بياناتك المصدرية موقَّعة بطريقة أخرى. خصائص a حتى f للقراءة فقط وخاصية Handle تعيد لك FS_MATRIX الخام، وهذا ما تريده FPDFPageObj_SetMatrix. لا شيء في الفئة يخفي الأرقام الستة عنك، وهذا متعمَّد: حين يسيء تحويل التصرف، طباعة a حتى f هي أسرع تشخيص تملكه

لماذا يحتاج تقديم إزاحة إلى الجزء الخطي؟

لأن إزاحة مُقدَّمة مكتوبة في فضاء دخل المصفوفة، ويجب حملها عبر الجزء الخطي الحالي قبل أن تنضم إلى صف الإزاحة. تحسب TPdfMatrix.PreTranslate إذن e := dx*a + dy*c + e وf := dx*b + dy*d + f. الإلحاق هو الاتجاه السهل: TPdfMatrix.Translate مكتوبة في فضاء الصفحة، حيث لا شيء يحتاج تحويلًا، فهي تضيف فقط dx إلى e وdy إلى f. أي شخص "يحسّن" PreTranslate إلى عمليتي جمع قد حذف للتو الدوران والقياس من الإزاحة

M := TPdfMatrix.Create;
try
  M.Rotate(90);              // a=0, b=-1, c=1, d=0

  M.Translate(10, 0);        // append: e := e + 10
                             // -> 10 points to the right on the page

  M.Reset;
  M.Rotate(90);
  M.PreTranslate(10, 0);     // prepend: e := 10*a + 0*c + e  (unchanged)
                             //          f := 10*b + 0*d + f  (f - 10)
                             // -> 10 points along the stamp own x axis,
                             //    which after the turn points down the page
finally
  M.Free;
end;

عدم التماثل نفسه يجري عبر زوج القياس، ويستحق معرفة أي العناصر يلمسها كل واحد قبل أن تصحح واحدة منها في الثالثة صباحًا. تضرب TPdfMatrix.PreScale الصفوف، مقيسة a وb بـscaleX وc وd بـscaleY، وتترك الإزاحة دون مساس لأن الإزاحة حدثت بالفعل في مصب downstream التحويل. أما TPdfMatrix.Scale المُلحِقة فتضرب الأعمدة بدلًا من ذلك، آخذة a وc وe بـscaleX وb وd وf بـscaleY، فيتقيّس الإزاحة القائمة مع كل شيء آخر. كلاهما مساران أحاديا الغرض يتخطيان الجداء العام ذا العناصر الستة، وكلاهما يحافظ على دلالات التركيب الخاصة بالصيغة العامة بدقة

أين تذهب الإزاحتان في دوران حول محور pivot؟

حول العملية، لا حول المصفوفة بأكملها، وبذلك الترتيب. تُلحق TPdfMatrix.RotateAt بـTranslate(-pivot)، ثم الدوران، ثم Translate(+pivot)، وهو ما يتركّب تحت اتفاقية متجه الصف كـTranslate(-pivot) * Op * Translate(pivot). ذلك التسلسل هو ما يبقي المحور ثابتًا تحت العملية الجديدة بينما ما يزال يسمح للمصفوفة القائمة بإنتاج إحداثياتها أولًا وتسليمها. اكتب الزوج بالترتيب المعاكس، كما كان سيكون صحيحًا في مكتبة متجه عمود، ويدور الكائن حول نقطة الأصل بدل الدوران في مكانه، وهذا بالضبط كيف تنتهي علامة مائية مُمركَزة خارج صندوق القص crop box

procedure RotateStampAboutPageCenter(AObj: FPDF_PAGEOBJECT;
  const AAngleDegrees, APageWidth, APageHeight: Single);
var
  M: TPdfMatrix;
  Raw: FS_MATRIX;
begin
  if not FPDFPageObj_GetMatrix(AObj, Raw) then
    raise Exception.Create('Page object carries no matrix');
  M := TPdfMatrix.Create(Raw);
  try
    // Appends Translate(-pivot) * Rotate * Translate(+pivot) in one call.
    M.RotateAt(AAngleDegrees, APageWidth / 2, APageHeight / 2);
    Raw := M.Handle;
    FPDFPageObj_SetMatrix(AObj, Raw);
  finally
    M.Free;
  end;
end;

التركيب نفسه يدعم ScaleAt وSkewAt وHorizontalFlipAt وVerticalFlipAt وCentralFlipAt، فبمجرد أن تثق بالنمط للدوران يمكنك الوثوق به للبقية. TPdfMatrix.CentralFlip تستحق إفرادًا: فهي تنفي العناصر الستة جميعًا لتمنحك دورة 180 درجة دون أي مثلثات trigonometry على الإطلاق، ما يعني لا cos لقيمة كان يفترض أن تكون صفرًا تمامًا ولا انجراف متراكم حين تطبّقها في حلقة. إن كنت تضع علامات مكرَّرة بدل تدوير واحدة، فإن آلية الوضع نفسه مشروحة في أختام صفحات قابلة لإعادة الاستخدام بكائنات Form XObject، وعمل المصفوفة هنا يجلس مباشرة فوقه

ماذا يخبرك TryDecompose عن مصفوفة؟

تُبلغ TPdfMatrix.TryDecompose عن الإزاحة والقياس والدوران والقص shear والمحدِّد determinant وعلامة انعكاس تحت اتفاقية قياس-ثم-دوران، وتُبلغ عنها بأمانة كافية لتكون مفيدة للقرارات لا للتسجيل فقط. تأتي ScaleX من طول الصف الأول، Sqrt(a*a + b*b)، فهي موجبة دومًا. أما ScaleY فهي Determinant / ScaleX، ما يجعلها موقَّعة. يأتي الدوران من ArcTan2(-b, a) بالدرجات، والقص من الجداء النقطي dot product للصفين مُطبَّعًا بكلا القياسين

تلك العلامة على ScaleY هي الجزء الذي يحذفه الناس، وحذفها علّة حقيقية لا تجميلية. المحدِّد السالب يعني أن المصفوفة تحتوي انعكاسًا. أجبر كلا عاملي القياس على الموجب لجعل الأرقام تبدو أنظف وستكون قد رمِيت الانعكاس بعيدًا، فمصفوفة أُعيد بناؤها من التفكيك تعود معكوسة: النص يُقرأ بالمقلوب، وصفحة ممسوحة ضوئيًا تنقلب، وشعار مستورَد يواجه الاتجاه الخاطئ. حقل IsReflected موجود كي لا تضطر إلى استنتاجه أبدًا. هذا أيضًا الفحص الذي يمنع الدوران المزدوج الكلاسيكي، حيث يضيف كود دورة عرض إلى صفحة تحمل واحدة بالفعل؛ نسخة جانب العارض من تلك المشكلة مشروحة في ملاءمة الصورة المصغَّرة والتكبير والدوران المزدوج

var
  D: TPdfMatrixDecomposition;
begin
  if M.TryDecompose(D) then
  begin
    // D.ScaleX is always positive; D.ScaleY carries the determinant sign.
    if D.IsReflected then
      Log('mirrored, ScaleY = %.3f', [D.ScaleY]);

    if Abs(D.RotationDegrees) > 0.5 then
      SkipDisplayRotation;      // the object already carries its own turn
  end
  else
    UseIdentityFallback;        // near-singular or non-finite: no answer
end;

ملاءمة مستطيل داخل آخر دون تخمين

تبني TPdfMatrix.TryCreateRectMapping مصفوفة المصدر-إلى-الهدف نيابة عنك وتأخذ TPdfMatrixFitMode من pmfStretch أو pmfContain أو pmfCover. تُطبِّع كلا المستطيلين أولًا، لأن مستطيلات PDF ليست مُلزَمة بالوصول باليسار أسفل اليمين أو الأسفل أسفل الأعلى، ثم تشتق قياسي X وY مستقلين: pmfStretch تبقيهما مستقلين، وpmfContain تأخذ الأصغر وتُمركِز الصندوق الأسود letterbox، وpmfCover تأخذ الأكبر وتُمركِز القص. النظير MapRectToRect يُلحق التخطيط نفسه على مصفوفة قائمة، وNewRectMapping ترفع EPdfMatrixError حيث تعيد صيغة Try القيمة False. هذه هي البدائية تحت كل وضع خلية في التركيب N-up وإعادة ترتيب الصفحات، حيث يجب أن تهبط كل صفحة مصدر داخل خلية محسوبة دون إعادة اشتقاق الحساب لكل تخطيط

المصفوفات المنحطَّة ومسار الفشل الصادق

المدخلات المنتهية لا تضمن نتيجة منتهية، فكود الملاءمة يحسب بـDouble ثم يعيد فحص المرشَّح Single المُضيَّق للانتهائية قبل نشره؛ فتخطيط يحتوي لانهاية لا يُعاد أبدًا كما لو كان صحيحًا. الانضباط نفسه يحكم القلب inversion. ترفض TPdfMatrix.TryGetInverse مصفوفة باستخدام حد نسبي، مقارنة المحدِّد مع إبسيلون مضروبًا بمربع أكبر عنصر خطي بدل ثابت محدَّد، وهذا ما يبقي الفحص ذا معنى سواء كانت وحداتك نقاطًا أو ميكرومترات. تنسحب TryDecompose بالطريقة نفسها، رافضة حين يقع طول الصف الأول أو ScaleY المشتقة عند إبسيلون أو أدناه

اختر أسلوب الفشل الذي يناسب موضع الاستدعاء بدل لفّ كل شيء في try-except من باب العادة. تعيد TryInvert وTryGetInverse وTryInverseTransformPoint وTryTransformBounds وTryCreateRectMapping القيمة False وتترك أهدافها دون مساس، وهذا يناسب اختبار الإصابة hit-testing والحلقات لكل كائن حيث ينبغي تخطي كائن منحطّ لا اعتباره فادحًا. أما Invert وInverseCopy وInverseTransformPoint وMapRectToRect وTransformBounds فترفع EPdfMatrixError بدلًا من ذلك، وهذا يناسب كود الإعداد حيث تعني مصفوفة شاذة singular أن المستدعي حسب شيئًا خاطئًا. للعمل الدفعي، تخصّص TransformPoints وTransformRects مصفوفة نتيجتهما مرة واحدة بالضبط، وتعيد TransformPointsInPlace وTransformRectsInPlace استخدام مخزنك، وتراكم TryTransformBounds صندوق الحدود في تمريرة واحدة بدل تجسيد نقاط محوَّلة أولًا

لا شيء من هذا رياضيات استثنائية. إنها اتفاقية واحدة، مُطبَّقة باتساق، بواجهة برمجية مسمّاة بحيث تكون الاتفاقية مرئية عند موضع الاستدعاء: Multiply والأفعال البسيطة تُلحِق، عائلة Pre تُقدِّم، عائلة At تُحيط العملية بزوج محورها. اكتب الترتيب في تعليق بجانب أي مُركَّب تبنيه، لأن الكود الذي يُقرأ بشكل صحيح اليوم هو الكود الذي سيعكسه أحدهم بعد ستة أشهر. المرجع الكامل لـTPdfMatrix، إلى جانب واجهات برمجة كائن الصفحة والرسم التي تغذيها هذه التحويلات، يعيش مع PDFium Component لـDelphi وC++Builder