مقال تقني

قياس نص PDF للتخطيط والتفاف الكلمات في Delphi

يعد الاستدعاء الذي يضع النص على صفحة PDF مباشرًا. أنت تعطي AddText سلسلة، وخطًا، وحجمًا، وموضعًا، وتظهر الصور الرمزية. ما لا تفعله هو إخبارك بمدى عرض تلك السلسلة بمجرد رسمها، ولا تقسم سلسلة طويلة عبر عدة أسطر. يقوم استدعاء واحد برسم امتداد واحد من النص في موضع واحد. إذا كان الامتداد أوسع من العمود الذي تقصد أن يتناسب معه، فإنه ببساطة يتجاوز الحافة، ولا شيء في استدعاء الرسم يحذرك. اللحظة التي تريد فيها فقرة بدلاً من تسمية واحدة، القطعة المفقودة هي عرض السلسلة بالخط والحجم المختارين، والتي يتم قياسها قبل تثبيتها على الصفحة

هذه هي مشكلة التخطيط الكلاسيكية. لالتفاف فقرة في عمود، يجب أن تعرف، كلمة بكلمة، مقدار المساحة الأفقية التي سيستغرقها كل سطر مرشح، ويجب أن تعرف ذلك قبل رسم أي شيء. التفاف الكلمات عبارة عن حلقة قياس ملفوفة حول استدعاء رسم، والارتباط الذي يرسم فقط يمنحك النصف الثاني. يسد دعم قياس النص في مكون PDFium هذه الفجوة بوظيفتين، MeasureText و MeasureTextWidth، اللتين تبلغان عن النطاق المعروض لسلسلة دون وضع علامة على أي صفحة

لماذا القياس عبارة عن مساعد فئة، وليس طريقة جديدة في TPdf

يصل دعم القياس كمساعد فئة Delphi لـ TPdf، يعيش في وحدته الخاصة، بدلاً من طرق جديدة مثبتة في فئة TPdf. مساعد الفئة هو ميزة لغة تتيح لك إرفاق طرق بنوع موجود من خارج إعلانه. بمجرد أن تكون الوحدة في النطاق، يتم استدعاء الطرق الجديدة تمامًا كما لو كانت تنتمي إلى الفئة، لذلك تُقرأ طريقة المساعد على أنها Pdf.MeasureTextWidth(...) دون كائن منفصل للبناء أو التمرير

السبب في وضعها في طبقات بهذه الطريقة هو الفصل. يبقى النوع الأساسي TPdf كما هو، دون إضافة حقل ودون المساس بأي توقيع موجود، لذلك فإن المشروع الذي لا يحتاج أبدًا إلى تخطيط لا يحمل أبدًا كود القياس. المشروع الذي يحتاجه يضيف وحدة واحدة إلى جملة uses وتضيء الطرق. تصبح الإمكانية اختيارية على مستوى وحدة واحدة، وهي أنظف طريقة لتوسيع نوع لا تملكه أو لا تريد إزعاجه

uses
  PDFium, FPdfView, FPdfEdit,
  FPdfMeasure;   // the helper unit; brings MeasureText into scope on TPdf

// With the unit in scope the methods read as members of TPdf:
var
  W, H: Double;
begin
  Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
  // W and H are now the rendered width and height in PDF user units
end;

القياس دون لمس الصفحة

يجب أن يكون القياس خاليًا من الآثار الجانبية. يجب أن يبلغ عن عرض دون ترك أي شيء وراءه، لأنك تستدعيه مرات عديدة أثناء تحديد التخطيط ويجب أن تبدو الصفحة تمامًا كما لو لم تقم بالقياس على الإطلاق. التقنية التي تجعل هذا ممكنًا هي بناء كائن نصي، وطلب حجمه، والتخلص منه قبل إرفاقه بالصفحة

التسلسل هو أربعة استدعاءات PDFium. ينشئ FPDFPageObj_NewTextObj كائنًا نصيًا مقابل المستند، نظرًا لاسم الخط وحجمه. يضبط FPDFText_SetText السلسلة التي يحملها الكائن. يقرأ FPDFPageObj_GetBounds مربع الإحاطة الخاص بالكائن. يحرر FPDFPageObj_Destroy الكائن. بشكل حاسم، لا شيء في هذا التسلسل يستدعي واجهة برمجة تطبيقات إدراج الصفحة. يتم إنشاء الكائن والاستعلام عنه وتدميره بشكل منعزل، لذلك يظل المستند دون تغيير عند إرجاع الوظيفة. إنه مسبار يمكن التخلص منه، مخرجه الوحيد هو الأرقام الأربعة لمربع الإحاطة الخاص به

هذه هي الطريقة القوية للقيام بذلك لأن PDFium لا يعرض عرض تقدم مريح لكل صورة رمزية يمكنك جمعه بنفسك. تعتمد مقاييس الصور الرمزية على برنامج الخط، وعلى الترميز، وعلى كيفية تحميل PDFium للوجه، ولا يوجد استدعاء عام يمنحك تقدم كل حرف في سلسلة. من ناحية أخرى، يتم حساب مربع الإحاطة لكائن نصي حقيقي بواسطة نفس الآلية التي ستضع الصور الرمزية للرسم، لذلك فهو يعكس النطاق المعروض الفعلي بدلاً من التقريب. يعد بناء كائن واحد يمكن التخلص منه وقراءة حدوده هو القياس الأكثر موثوقية الذي يمكن أن تقدمه المكتبة

// The shape of MeasureText, expressed against the verified PDFium calls.
// A text object is built, measured, and destroyed; no page is involved.
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
  FontSize: Single; out Width, Height: Double);
var
  TextObject: FPDF_PAGEOBJECT;
  L, B, R, T: Single;
begin
  Width  := 0;
  Height := 0;
  if Self.Document = nil then
    Exit;
  TextObject := FPDFPageObj_NewTextObj(Self.Document,
    FPDF_BYTESTRING(AnsiString(Font)), FontSize);
  if TextObject = nil then
    Exit;
  try
    if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
      Exit;
    if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
    begin
      Width  := R - L;
      Height := T - B;
    end;
  finally
    FPDFPageObj_Destroy(TextObject);   // probe discarded, page untouched
  end;
end;

إحداثيات ووحدات النتيجة

يعود مربع الإحاطة كأربعة حواف، يسار، أسفل، يمين، وأعلى، ويستنتج البعدان عن طريق الطرح. العرض هو اليمين ناقص اليسار والارتفاع هو الأعلى ناقص الأسفل. كلاهما معبر عنه في وحدات مستخدم PDF، حيث تمثل الوحدة الواحدة واحدًا من اثنين وسبعين من البوصة، وهي نفس مساحة الإحداثيات التي تضع فيها النص على الصفحة. لا توجد وحدة جهاز مخفية ولا توجد بكسلات متضمنة في هذه المرحلة. عرض 36 يعني نصف بوصة من الصفحة، مهما كانت دقة العرض النهائية

يعمل المحور الرأسي بالطريقة التي يحددها PDF، مع زيادة Y لأعلى، ولهذا السبب يكون الارتفاع هو الأعلى ناقص الأسفل بدلاً من العكس. هذا التفصيل يهم عندما تقدم مؤشرًا لأسفل عمود. تقيس ارتفاع السطر، ثم تطرحه من خط الأساس الحالي للعثور على السطر التالي، لأن التحرك لأسفل الصفحة يعني التحرك نحو Y أصغر. إذا كانت وجهتك شاشة وليست ورقًا، فستقوم بتحويل وحدات المستخدم إلى بكسلات الجهاز باستخدام دقة العرض: قيمة في وحدات المستخدم مضروبة في DPI ومقسومة على 72 تعطي بكسلات، لذلك يمكن مطابقة عرض العمود الذي حددته بالنقاط مقابل مسار مقاس قبل أن تقرر أين يذهب الفاصل

ماذا يحدث في المدخلات التالفة

تتم كتابة الوظائف لتفشل بهدوء. إذا لم يكن هناك مستند مفتوح، أو إذا تعذر إنشاء الكائن النصي، تكون النتيجة نطاقًا صفريًا بدلاً من استثناء مرفوع. تتم تهيئة العرض والارتفاع إلى الصفر في الأعلى ولا تتم الكتابة فوقهما إلا بمجرد قراءة مربع الإحاطة بنجاح. سلسلة فارغة، مستند مفقود، خط لا تستطيع المكتبة حله إلى كائن، كل من هذه يرجع صفرًا بدلاً من الرمي

يُبقي هذا الاختيار حلقة القياس بسيطة، لأن حلقة تعمل على آلاف الكلمات ليست المكان المناسب لمعالجة الاستثناءات في كل تكرار. التكلفة هي أن المتصل يحمل الشيك. العرض الصفري هو حارس، وليس حقيقة حول النص، لذلك يجب أن يحترس الكود الذي يقسم على عرض مقاس أو يفترض قيمة موجبة من الصفر قبل الوثوق به. عامل الصفر على أنه "تعذر القياس" والعقد واضح؛ تجاهله والمدخلات التالفة بهدوء تصبح تخطيطًا بعمود من الصور الرمزية المتداخلة

التفاف كلمات جشع مبني على القياس

مع وجود وظيفة عرض في متناول اليد، فإن التفاف الكلمات هو حلقة جشعة قصيرة. تقوم بتقسيم الفقرة إلى كلمات، والاحتفاظ بسطر حالي، ولكل كلمة تقيس ما سيكون عليه السطر إذا قمت بإلحاق تلك الكلمة. بينما لا يزال خط التجربة يتناسب مع عرض العمود، تستمر في الإضافة؛ عندما يفيض، تقوم بمسح السطر الحالي بـ AddText وتبدأ سطرًا جديدًا بالكلمة التي لم تتناسب. يتم التراكم بالكامل باستخدام MeasureTextWidth، والشيء الوحيد الذي يصل إلى الصفحة هو سطر تأكدت بالفعل من أنه يناسب

procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
  FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
  Words: TArray<string>;
  Line, Trial: WideString;
  I: Integer;
  Y: Double;
begin
  Words := string(Para).Split([' ']);
  Line  := '';
  Y     := TopY;
  for I := 0 to High(Words) do
  begin
    if Line = '' then
      Trial := Words[I]
    else
      Trial := Line + ' ' + Words[I];
    // Measure the candidate line before drawing anything.
    if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
    begin
      Pdf.AddText(Line, Font, FontSize, X, Y);   // flush the line that fit
      Y    := Y - LineHeight;                    // Y decreases going down
      Line := Words[I];                          // overflowing word starts next line
    end
    else
      Line := Trial;
  end;
  if Line <> '' then
    Pdf.AddText(Line, Font, FontSize, X, Y);      // flush the final line
end;

تقيس الحلقة خط التجربة بدلاً من قياس كل كلمة وجمعها، لأن عرض السطر ليس مجموع عروض كلماته. تساهم المسافات بين الكلمات، ويلتقط المسار المقاس ذلك مباشرة. القاعدة الجشعة، وضع أكبر عدد ممكن من الكلمات كما يسمح العمود والكسر عند آخر كلمة تناسبه، هي نفس القاعدة التي تسد الفجوة بين AddText الخام وفقرة حقيقية. لم يكن استدعاء الرسم هو الجزء الصعب أبدًا. القياس الذي يجب أن يسبقه هو، وهذا بالضبط ما يوفره المساعد

أين يناسب هذا

القياس هو الطبقة بين إنشاء المحتوى وعرضه، لذلك يقترن بشكل طبيعي ببقية سير عمل المستند من البداية. إذا كنت تقوم بتجميع الصفحات ووضع النص في المقام الأول، فإن الأساس يكمن في إنشاء مستندات PDF من البداية باستخدام مكون PDFium في Delphi، حيث تتم تغطية AddText وإعداد الصفحة بالكامل. عندما يكون الخط الذي تقيسه مهمًا بقدر أهمية السلسلة، لأن المقاييس تعتمد على الوجه، يوضح تحليل خصائص خط PDF باستخدام مكون PDFium في Delphi كيف تبلغ المكتبة عن معلومات الخط التي تدفع مربعات الإحاطة تلك. يبني كلاهما على نفس الارتباط، مكون PDFium لـ Delphi و Lazarus، حيث يتم شحن مساعد القياس جنبًا إلى جنب مع واجهات برمجة تطبيقات المستند والصفحة والنص الموضحة عبر هذه المدونة