Technical Article

تحليل خصائص خطوط PDF باستخدام PDFium VCL في Delphi

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

يعرض المكوّن هذه المعلومات عبر الكائنين TPdf وTPdfView نفسيهما اللذين تستخدمهما للرسم واستخراج النص. لا يوجد كائن منفصل لفتح «جدول الخطوط». بعد تحليل نص الصفحة، ترتبط خصائص الخط بفهرس المحرف، فتقرأها محرفًا واحدًا في كل مرة. هذا التصميم يطابق طريقة تخزين PDF للمعلومة من الأصل: فقد تنتقل الصفحة الواحدة بين الخطوط عشرات المرات، والجواب الصادق الوحيد عن سؤال «ما الخط الذي كُتب به هذا المستند» هو «يعتمد على المحرف الذي تقصده»

قراءة الخط المرتبط بمحرف واحد

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

procedure DescribeFontAt(Pdf: TPdf; CharIndex: Integer);
var
  Report: TStringList;
  PtSize: Single;
begin
  Report := TStringList.Create;
  try
    PtSize := Pdf.FontSize[CharIndex];

    Report.Add('Character : ' + Pdf.Character[CharIndex]);
    Report.Add('Family    : ' + Pdf.FontFamilyName[CharIndex]);
    Report.Add('Base name : ' + Pdf.FontBaseName[CharIndex]);
    Report.Add('Weight    : ' + IntToStr(Pdf.FontWeight[CharIndex]));
    Report.Add('Italic    : ' + IntToStr(Pdf.FontItalicAngle[CharIndex]) + ' deg');
    Report.Add('Size      : ' + FormatFloat('0.0', PtSize) + ' pt');
    Report.Add('Ascent    : ' + FormatFloat('0.0', Pdf.FontAscent[CharIndex, PtSize]));
    Report.Add('Descent   : ' + FormatFloat('0.0', Pdf.FontDescent[CharIndex, PtSize]));
    Report.Add('Embedded  : ' + BoolToStr(Pdf.FontIsEmbedded[CharIndex], True));

    ShowMessage(Report.Text);
  finally
    Report.Free;
  end;
end;

بعض التواقيع قد تفاجئ من يأتي من مكتبات أخرى. تأخذ FontAscent وFontDescent وسيطين، فهرس المحرف وحجمًا بالنقاط، لأن PDFium يعيد هذه المقاييس بوحدات فضاء الغليف التي لا تصبح بكسلات إلا عندما تضربها في الحجم الذي حُدد به النص. مرّر القيمة التي قرأتها بالفعل من FontSize[CharIndex]، وستحصل على الارتفاع والنزول بالنقاط نفسها التي يستخدمها بقية التخطيط. قيمة النزول تعود سالبة لأنها تقيس ما تحت خط الأساس. واسم العائلة واسم الأساس سلسلتان منفصلتان عمدًا: فاسم الأساس هو الإدخال الخام /BaseFont في ملف PDF، وغالبًا ما يحمل بادئة تقسيم فرعي مثل ABCDEF+، بينما اسم العائلة هو الاسم المنظف الذي يعتمده العارض

تحويل نقرة إلى فهرس محرف

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

procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Index: Integer;
begin
  if not PdfView.Active then
    Exit;

  // 4 px of slack in each direction so a near-miss still hits the glyph.
  Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
  if Index < 0 then
    Exit;                      // clicked between glyphs; leave the panel alone

  PdfView.CurrentCharIndex := Index;
  DescribeFontAt(PdfView.Pdf, Index);
end;

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

التضمين هو الخاصية الحاسمة

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

procedure ReportNonEmbeddedFonts(Pdf: TPdf);
var
  Embedded, External: TStringList;
  I: Integer;
  Name: string;
begin
  Embedded := TStringList.Create;
  External := TStringList.Create;
  try
    Embedded.Sorted := True;
    Embedded.Duplicates := dupIgnore;
    External.Sorted := True;
    External.Duplicates := dupIgnore;

    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      Name := Pdf.FontBaseName[I];
      if Name = '' then
        Continue;              // generated spaces and the like have no font
      if Pdf.FontIsEmbedded[I] then
        Embedded.Add(Name)
      else
        External.Add(Name);
    end;

    if External.Count > 0 then
      ShowMessage(IntToStr(External.Count) +
        ' non-embedded font(s):' + sLineBreak + External.Text)
    else
      ShowMessage('All ' + IntToStr(Embedded.Count) +
        ' font(s) on this page are embedded.');
  finally
    Embedded.Free;
    External.Free;
  end;
end;

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

استخراج الخط المضمّن

عندما يكون الخط مضمنًا يمكنك قراءة بايتاته مباشرة. تعيد FontData برنامج الخط الخام، وهو نفس بيانات TrueType أو CFF التي يحملها ملف PDF، وهي كافية لكتابة ملف خط مستقل أو لمطابقة الخط مع مكتبة معروفة. وتعيد مصفوفة فارغة عندما لا يكون الخط مضمنًا، لذلك يحمي فحص التضمين مع فحص الطول عملية الكتابة معًا

procedure SaveEmbeddedFont(Pdf: TPdf; CharIndex: Integer;
  const OutputFile: string);
var
  Data: TBytes;
  Stream: TFileStream;
begin
  if not Pdf.FontIsEmbedded[CharIndex] then
  begin
    ShowMessage('That glyph''s font is not embedded; nothing to extract.');
    Exit;
  end;

  Data := Pdf.FontData[CharIndex];
  if Length(Data) = 0 then
    Exit;

  Stream := TFileStream.Create(OutputFile, fmCreate);
  try
    Stream.WriteBuffer(Data[0], Length(Data));
  finally
    Stream.Free;
  end;
  ShowMessage('Wrote ' + IntToStr(Length(Data)) + ' bytes.');
end;

هذه البايتات هي المجموعة الفرعية المضمنة لا الخط التجاري الأصلي، لذا فما تسترجعه يغطي عادةً فقط المحارف التي استخدمها المستند فعلًا. وهذا مناسب جدًا للتحليل الجنائي والتحقق، لكنه سيئ لإعادة الاستخدام؛ فمجموعة فرعية من Times New Roman تحتوي ثلاثين محرفًا ليست خطًا يمكنك تثبيته والكتابة به. تعامل مع الاستخراج على أنه طريقة لفحص ما شُحن، لا أداة لاسترداد الخط. وإذا احتجت إلى اسم الأساس المطابق لوضع تسمية على الناتج، فاقرأ FontBaseName[CharIndex] إلى جانب البيانات، ثم أزل بادئة التقسيم الفرعي إذا أردت الاسم الخام للعائلة

فهم قيمة الوزن

FontWeight يعيد فئة الوزن الرقمية، على نفس مقياس 100 إلى 900 الذي يستخدمه CSS، حيث يمثل 400 الوضع العادي و700 الغامق. يعرض PDFium ما يعلنه الخط، وهذا لا يكون دائمًا مضاعفًا لمئة؛ فقد يعلن الوجه 350 أو 650، والتعامل مع أي قيمة عند 600 أو فوقها على أنها «غامقة بما يكفي» يصمد أفضل من اختبار 700 بالضبط. وزاوية الميل المائل إشارة مرافقة: فالقيمة غير الصفرية، وغالبًا ما تكون سالبة، تعني أن الوجه مائل أو تصميمًا مائلًا حقيقيًا، والصفر يعني قائمًا. معًا يتيحان لك التمييز بين تشغيل غامق مائل وتشغيل عادي من غير رسم أي شيء، وهو نوع الفحص الذي تحتاجه مرحلة فحص تمهيدي أو تدقيق إتاحة على نطاق واسع

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

خصائص الخط المعروضة هنا جزء من PDFium Delphi VCL Component