مقال تقني

دقة HotPDF في Delphi: وحدات الرسم و UserWidth

في HotPDF Component تعرّف THotPDF.Resolution وحدة الرسم: كل إحداثي X و Y، وكل هامش، والحجم الممرر إلى SetFont، ونتائج TextWidth و GetWideTextWidth تقاس بوحدة 1/Resolution بوصة. ولا تتبعه THPDFPage.Width و Height وتبقيان بالنقاط، فحدود التخطيط يجب أن تأتي من UserWidth و UserHeight للقراءة فقط. والسبب المعتاد لمس Resolution هو نقل بوابة: محرك تقارير يفكر أصلاً بـ 1/96 أو 1/144 بوصة أسهل نقلاً حين يتحدث جانب PDF بالوحدة نفسها مما حين يحصل كل موضع استدعاء على معامل تحويل. وذلك يعمل جيداً طالما تعرف أي الأعداد انتقلت إلى الوحدة الجديدة وأيها بقيت

ماذا تغيّر THotPDF.Resolution فعلاً؟

تغيّر THotPDF.Resolution طريقة قراءة HotPDF للأعداد التي تمررها فقط؛ والـ PDF الذي تكتبه هو نفسه. والمضبط سطران: تخزن SetResolution القيمة وتضبط DocScale := Value / 72. ومن ثم تقسم XProjection و YProjection كل إحداثي على DocScale في طريقه إلى تدفق المحتوى، وتقسم SetFont الحجم بالطريقة نفسها قبل تسجيله. فضاء المستخدم في PDF افتراضه 1/72 بوصة (ISO 32000-1 §8.3.2.3)، فعند الدقة الافتراضية 72 يكون الإسقاط هوية وعند 144 تكون وحدة الرسم نصف نقطة. ولا يُكتب مدخل /UserUnit. تلك الخاصية صفحة، المضافة في PDF 1.6، شيء منفصل يكشفه HotPDF بوصفه THPDFPage.SetUserUnit. وتفصيلة تمسك القادمين من دروس TextOut: إحداثيات الصفحة تجري من الزاوية العلوية اليسرى مع نمو Y نزولاً، لأن YProjection تحسب أعلى MediaBox ناقص Y المقيس، ويظل ذلك صحيحاً عند كل Resolution

كيف تعرّف THotPDF.Resolution وحدة الرسم في Delphi: يخزن المضبط DocScale بوصفه Resolution على 72، ثم تقسم XProjection و YProjection و SetFont كل إحداثي وحجم في طريقه إلى تدفق المحتوى، فتكون الدقة 72 تعييناً هوياً والدقة 144 تجعل وحدة الرسم نصف نقطة بينما تجري الصفحة من أعلى اليسار مع Y نزولاً
لا شيء في ملف المخرج يتحرك — معنى الأعداد التي تمررها وحده ما يتغير، ولهذا يظهر تدفق المحتوى نفسه عند 72 وعند 144
var
  Pdf: THotPDF;
  Page: THPDFPage;
  Margin: Single;
  Title: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Resolution := 144;               // وحدة رسم واحدة = 1/144 بوصة
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // ‏A4: ‏Width = 595، ‏UserWidth = 1190
    Margin := 144;                       // بوصة واحدة بوحدات الرسم
    Page.SetFont('Arial', [fsBold], 28); // ‏28/144 بوصة، خط 14 pt
    Title := 'INVOICE 2026-0417';
    // محاذاة يمينية على حافة الصفحة مقاسة بالوحدة نفسها
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // خط 1 pt
    Page.MoveTo(Margin, Margin + 48);
    Page.LineTo(Page.UserWidth - Margin, Margin + 48);
    Page.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

لماذا يخالف Page.Width إحداثياتك عند الدقة 144؟

تبلّغ THPDFPage.Width و Height الصفحة بالنقاط مهما كانت دقة المستند، بينما إحداثياتك بوحدة 1/Resolution بوصة، فعند 144 تبدو الصفحة بنصف عرضها الحقيقي. وصفحة A4 تقرأ Width = 595 و Height = 842 عند الدقة 72 وما زالت تقرأ 595 و 842 عند 144، حيث الحافة اليمنى فعلياً عند X = 1190. وتعيد UserWidth و UserHeight، المضافتان في v2.766.0، القيمة Width * DocScale، أي حجم الصفحة بالوحدة التي ترسم بها. وقبل وجودهما خلطت المكتبة بين الاثنين داخلياً، والأعراض عند الدقة 144 كانت درامية: فقرات التف عند كل محرف، ودفعت THPDFTable.Render كل صف إلى صفحة جديدة، ورسم كل من مستورد HTML ومُسطّح XFA محتواه بنصف الحجم، والنموذج المسطّح مزدحم في الزاوية العلوية اليسرى. وتخطيط الفقرات وعرض الجداول واستيراد HTML وتوسيط EMF وقص صفحة WMF وتشخيصات التخطيط تقرأ الآن كلها حجم وحدة المستخدم. ويجب أن يفعل كود تخطيطك ذلك أيضاً: أي شيء يقارن ضد إحداثي رسم (هامش أيمن، أو اختبار فاصل صفحات، أو حساب توسيط) يعود إلى UserWidth و UserHeight، لا إلى Width و Height أبداً

الفخ الأول: إسناد Width أو Height يحوّل الصفحة إلى نقاط

ضبط Page.Width أو Page.Height يغير الصفحة بصمت إلى UserDefined، وصفحة UserDefined تتجاهل DocScale كلياً، فكل ما ترسمه عليها بعد ذلك بالنقاط لا بوحدة 1/Resolution بوصة. والمضبط قديم ويأخذ نقاطاً بتصميم، ولهذا تُرك معناه وشأنه. وإسقاط صفحة UserDefined مجرد X + MinX، وتخزن SetFont الحجم دون تغيير. وعند الدقة 144 تكون النتيجة صفحة يخرج محتواها فجأة بضعف حجم الصفحة قبلها. وارتكبت المكتبة هذا الخطأ بنفسها: كانت صفحات استمرار الفقرات تنسخ حجم الصفحة السابقة عبر Width، فتحولت كل صفحة فائض إلى نقاط. تلك الصفحات تنسخ الآن Size و Orientation ودقة الصفحة بدلاً منها، ولا ترجع إلى Width و Height إلا إذا كانت الصفحة الأصلية UserDefined أصلاً

مخرجان، حسب ما تحتاج. إن كانت ورقة قياسية تكفي فاضبط Page.Size و Page.Orientation وواصل الرسم بوحدة الدقة خاصتك. وإن احتجت فعلاً حجم صفحة خاصاً فاقبل أنها صفحة نقاط وارسم بالنقاط؛ يساوي هناك UserWidth قيمة Width، فيبقى كود التخطيط الذي يقرأ UserWidth دائماً يعمل على النوعين معاً. واختبار الوحدة يثبّت ذلك: عند الدقة 144 تبلّغ صفحة A4 ‏UserWidth بقيمة 1190، لكن بعد Width := 500 و Height := 400 تبلّغ 500 و 400. والصفحات المحمّلة تتصرف بالطريقة نفسها، لأن صفحة معاد بناؤها من PDF قائم لا تعرف سوى MediaBox بالنقاط وترسم بالنقاط. والصفحات التي أنشأها هذا المستند تحفظ وحداتها حين تبتعد وتعود عبر CurrentPageNumber، وقد كان ذلك هو الحال منذ v2.766.26

لماذا يخالف Page.Width إحداثياتك عند الدقة 144 في HotPDF: تبقى Width و Height بالنقاط بينما الرسم بوحدة 1/144 بوصة، فتبلّغ صفحة A4 القيمة 595 بينما حافتها اليمنى عند UserWidth بقيمة 1190، وإسناد Width يحول الصفحة إلى UserDefined التي تتجاهل DocScale، فتلف الفقرات لكل محرف وتنكسر الجداول لكل صف ويهبط نصف أحجام SetFont
أي شيء يقارن ضد إحداثي رسم يعود إلى UserWidth و UserHeight — وعلى صفحة نقاط UserDefined يتطابق الاثنان، فينجو كود التخطيط نفسه في الحالين

الفخ الثاني: لماذا تخرج أحجام الخطوط نصف الحجم؟

حجم خط بدأ نقاطاً يخرج نصف الحجم عند الدقة 144 لأن SetFont تعامل وسيط حجمها بوصفه وحدات رسم وتحوله إلى نقاط قبل تخزينه. داخلياً تخزن SetFont القيمة ASize / DocScale * DPI في كائن الخط الحالي، فالقيمة المخزنة نقاط دائماً. وتعثرت المكتبة في ذلك مرتين: كان الاحتياط الخطي في WideTextOutBoxEx وصفحة استمرار الفقرات كلاهما يسلم تلك القيمة النقطية المخزنة إلى SetFont، فقيسها مرة ثانية وأنصفت النص. ولا يستطيع كودك قراءة الحجم المخزن، لكن العيب نفسه يظهر كلما بلغت قيمة نقطية من مكان آخر SetFont: ‏TFont.Size من نموذج VCL، أو حجم في تعريف تقرير، أو طول CSS من نوع pt. حوّلها أولاً، وأدخل دقة الصفحة الخاصة وحالة UserDefined في المعامل، كما يفعل تشغيل الملفات التعريفية حين يعيد تشغيل Canvas الصفحة (انظر كيف يستورد HotPDF رسومات EMF و WMF المتجهة لذلك المسار):

// وحدات الرسم لكل نقطة على الصفحة الحالية. تعكس الإسقاط
// الذي تستخدمه HotPDF: ‏1 على صفحة مقاسة عبر Width/Height، وإلا
// (دقة المستند / 72) * (دقة الصفحة / 72)
function UnitsPerPoint(Pdf: THotPDF): Single;
begin
  if Pdf.CurrentPage.Size = UserDefined then
    Result := 1
  else
    Result := (Pdf.Resolution / 72) * (Pdf.CurrentPage.Resolution / 72);
end;

procedure SetFontFromVcl(Pdf: THotPDF; Font: TFont);
begin
  // ‏TFont.Size بالنقاط؛ وتتوقع SetFont وحدات رسم
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

تطبق المكتبة القاعدة نفسها على ثوابتها النقطية الخاصة. الخط ذو الـ 12 نقطة الذي تبدأ به كل صفحة جديدة يُضرب الآن في معامل الوحدات لكل نقطة الداخلي، فهو 12 نقطة عند أي دقة. و DrawChart، الذي هوامشه وأحجام عناوينه وعرض خطوطه كلها نقاط مكتوبة يدوياً، يعمل الآن والمقياس مضبوط مؤقتاً إلى 1. أما ما يبقى بوحدات الرسم عمداً فهي افتراضات معاملات عامة مثل حجم وحدة DrawQRCode وحجم خط الجدول الافتراضي: فهي جزء من عقد الـ API، وعند الدقة 144 تعني نصف ما تعنيه عند 72. وإن كنت تقيس تقاريرك من قالب فدليل مخرجات التقارير بخطوط وصور في HotPDF يغطي من أين تأتي تلك القيم عادة

كيف تتحقق أن التخطيط مستقل عن الدقة؟

الاختبار الأوثق مقارنة بايتات: اعرض الصفحة نفسها عند الدقة 72 ثم عند 144 مع مضاعفة كل إحداثي وحجم، ويجب أن تكون تدفقات المحتوى غير المضغوطة متطابقة. كلتا التشغيلتين تهبطان على قيم نقطية نفسها بعد الإسقاط، فأي اختلاف قيمة تخطت التحويل. هكذا تفحص حزمة اختبارات HotPDF الفقرات والجداول واستيراد HTML وتسطيح XFA والأقواس والملفات التعريفية والصور. والأسلوب نفسه يعمل لكود تقاريرك الخاص بأدوات احتواء تكاد لا تكون:

procedure RenderPage(const FileName: string; Res: Integer; K: Single);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.Compression := cmNone;       // تدفقات محتوى مقروءة
    Pdf.FileName := FileName;
    Pdf.Resolution := Res;
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 10 * K);
    Pdf.CurrentPage.TextOut(36 * K, 36 * K, 0, 'Line 1');
    Pdf.CurrentPage.Rectangle(36 * K, 60 * K, 200 * K, 40 * K);
    Pdf.CurrentPage.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

// ‏RenderPage('r72.pdf', 72, 1) و RenderPage('r144.pdf', 144, 2)
// يجب أن تنتجا تدفقات محتوى صفحة متطابقة بايتاً ببايت
كيف تتحقق من استقلالية الدقة في كود HotPDF Delphi: اعرض التخطيط المطابق مرتين، مرة عند الدقة 72 بمقياس 1 ومرة عند 144 بمضاعفة كل إحداثي وحجم خط، ثم اطلب تدفقات محتوى غير مضغوطة متطابقة بايتاً ببايت — وعدم التطابق يشير إلى صفحة حُولت إلى UserDefined عبر Width أو قيمة نقطية غير محولة بلغت SetFont
كلتا التشغيلتين تهبطان على قيم نقطية نفسها بعد الإسقاط، فأي اختلاف عدد تخطى تحويله — الأداة نفسها التي تعتمدها حزمة اختبارات HotPDF

افحص المعاملات التي تحمل أعداداً: ‏Td و Tm و Tf و re و w ومصفوفات TJ. وستبقى بايتات مستوى الملف تختلف في تاريخ الإنشاء وفي /ID، فقارن التدفقات لا الملفات كاملة. وعدم التطابق يشير دائماً تقريباً إلى أحد الفخين أعلاه: صفحة غُيّر حجمها عبر Width، أو قيمة نقطية ممرت مباشرة إلى SetFont. وإن كنت جديداً على استدعاءات الرسم نفسها فابدأ بـ جولة TextOut في HotPDF للحجم والنمط والدوران، ثم عد وحوّل الدقة متى صار تخطيطك يقرأ UserWidth. وتفاصيل الـ API الكاملة وتحميلات التجربة على صفحة HotPDF Delphi PDF component