مقال تقني

إخفاقات تحميل PDF الصامتة في Delphi: تقرير تحميل PDFium

في مكوّن PDFium لـ Delphi وLazarus، لا يرفع إسناد TPdf.Active := True استثناءً قط حين يفشل تحميل PDF: يبتلع TPdf.SetActive كل استثناء ويترك المكوّن غير نشط. لترى الخطأ الحقيقي استدعِ TPdf.LoadDocument(Options, Report) بدلًا منه. يعيد ذلك التحميل الزائد رفع الاستثناء الأصلي ويمأل TPdfLoadReport بحالة التحميل وكود خطأ PDFium الأصلي وهل اضطر جدول الإحالات المرجعية إلى إعادة بناء

تظهر المشكلة عادة في كود الدفعات. مهمة استخراج جداول تجوب مجلدًا من 13 ملف PDF واقعيًا بـ TPdf واحد مشترك، فيعود 7 منها بوصفها إخفاقات. ولا ملف من الـ 7 معطوب فعلًا. كتل except حول التحميل لا تشتعل أبدًا، ويلوم السجل أسماء ملفات خاطئة، وأول خطأ مرئي هو EPdfError مجردة عن مكوّن غير نشط، تُرفع من قراءة خاصية بعد عدة أسطر من التحميل الذي فشل فعلًا. سلوكان منفصلان يتكدان لإنتاج تلك الصورة، وكلاهما يعمل كما صُمم

لماذا لا يرفع TPdf.Active := True استثناءً حين يفشل تحميل PDF؟

يغلّف TPdf.SetActive التحميل عبر LoadDocument في try..except يبتلع كل أصناف الاستثناءات ويكتفي بترك المكوّن غير نشط. والابتلاع مقصود: يجرى الواضع نفسه حين يبدّل مصمم النماذج قيمة Active في الـ IDE، ولا يجوز أن يُسقط مسار سيئ البيئة. وفي زمن التشغيل تكتفي TPdf.Active بالبلاغ عن وجود مقبض مستند أصلي، فبعد تحميل فاشل تُقرأ False ولا يحدث شيء آخر. وكل ما رُفع ضاع، أكان EPdfError من المحلل أم خطأ تدفق أم EAccessViolation من pdfium.dll نصف مربوطة. رسائل DLL التفصيلية المشروحة في تشخيص إخفاقات تحميل pdfium.dll في Delphi لا تبلغ معالجك إلا عبر استدعاء لا يبتلعها

مسارا تحميل في مكوّن PDFium: يبتلع إسناد Active بقيمة true كل استثناء في الواضع ويؤجل الفشل إلى أول استدعاء محروس حيث يرفع CheckActive استثناء EPdfError عن مكوّن غير نشط، بينما يفحص LoadDocument مع TPdfLoadOptions وTPdfLoadReport الترويسة وstartxref وأقسام xref وعلامة نهاية الملف بايتًا بايتًا ثم يعيد رفع الاستثناء الأصلي مع السبب الحقيقي مثبتًا
الابتلاع مقصود لأن مصمم الـ IDE يتقاسم الواضع؛ وكود الدفعات يحتاج التحميل الزائد الذي يرفع ويبلّغ ويحكي قصة الملف الحقيقية
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // يبتلع SetActive أي استثناء تحميل
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // لا يُنفذ قط
end;
// وبدل ذلك يظهر الفشل هنا، في صورة EPdfError عامة:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// إصلاح أدنى للكود القائم: افحص Active بعد الإسناد مباشرة؛
// ومنذ v3.122.1 يحتفظ LastLoadReport بنص الخطأ المبتلع
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

يظهر الفشل أخيرًا عند أول استدعاء محروس. تبدأ TPdf.PageCount، مثل معظم خصائص المستند، بـ CheckActive التي ترفع EPdfError تسمي المكوّن دون الملف ودون السبب. فحص Pdf.Active بعد الإسناد مباشرة يحوّل انهيارًا منسوبًا خطأً إلى مدخل «فشل» صادق. قبل PDFiumPas v3.122.1 ضاع السبب عند تلك النقطة؛ ومنذ v3.122.1 يستبدل الإسناد الفاشل LastLoadReport بتقرير plsFailed يحمل نص الخطأ، فينجو السبب. أما كائن الاستثناء نفسه والتدقيق على مستوى البايتات فيشترطان مدخلًا مختلفًا

لماذا يفشل إعادة استخدام TPdf واحد ابتداءً من الملف الثاني؟

TPdf.FileName لا يُسند إلا والمكوّن غير نشط، فيرفض المثيل المشترك الملف الثاني قبل أن يحاول تحميله أصلًا. تبدأ TPdf.SetFileName بـ CheckInactive، ويحمي الحارس نفسه Password وFormFill. بعد أول تحميل ناجح يبقى المثيل نشطًا، فيرفع الإسناد التالي استثناء، وإن التقطت حلقة الدفعة ذلك الاستثناء وتابعت، هبط الخطأ تحت اسم الملف الجديد بينما المستند القديم ما يزال مفتوحًا. وبالمزج مع إخفاقات التحميل المبتلعة يتوقف السجل عن مطابقة الواقع. في إعادة إنتاج الـ 13 ملفًا بلّغ مثيل مشترك عن 7 إخفاقات، بينما فتح TPdf.Create(nil) جديد لكل مستند الملفات الـ 13 كلها. وضبط Active := False بين الملفات ينجح كذلك، لكن مثيل واحد لكل مستند يُبقي كل ملف معزولًا بالبناء

خط زمني لمثيل TPdf مشترك في PDFium يفشل ابتداءً من الملف الثاني: بعد أول تحميل يبقى المثيل نشطًا، ويرفع إسناد FileName التالي استثناءً في CheckInactive قبل أي محاولة تحميل، وتسجل حلقة الدفعة الخطأ تحت اسم الملف الجديد بينما المستند القديم مفتوح، وهو الفخ وراء 7 إخفاقات زائفة في دفعة من 13 ملفًا
يحرس SetFileName بـ CheckInactive، فيرفض المثيل المشترك الملف الثاني قبل تجربته؛ اعزل كل مستند بـ TPdf خاص به فيطابق السجل الواقع من جديد

ماذا يمنحك TPdf.LoadDocument مع TPdfLoadReport؟

ترفع TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) الاستثناء الحقيقي وتخبرك كذلك بما جرى في صورة مهيكلة. يحمّل التحميل الزائد الخاص بالملفات FileName؛ وتأخذ التحميلات الزائدة الشقيقة TBytes أو مؤشرًا وحجمًا، ويغطي LoadCustomDocument(AStream, AOwnsStream, Options, Report) التدفقات. يتحقق كل منها من الخيارات، ويفحص أن المثيل غير نشط، ويجري تدقيقًا بايتيًا للترويسة وstartxref وأقسام xref وعلامة %%EOF، ثم ينفذ التحميل الأصلي. ويحده التدقيق حدودًا من النوع نفسه الذي تناقشه ميزانيات موارد المحلل لملفات PDF غير موثوقة: يضبط TPdfLoadOptions.Default قيمة AuditByteLimit إلى 256 MiB وMaxIssues إلى 256 وMaxXrefSections إلى 1024 وMaxXrefEntries إلى 4,000,000. وعند الفشل تضبط الطريقة Report.Status := plsFailed وتعيد الرفع؛ ولأن Report يُكتب في موضعه، تنجو محتوياته من الاستثناء، وتُخزن نسخة في TPdf.LastLoadReport

تجيب حقول التقرير عن الأسئلة التي يحتاجها سجل دفعات فعلًا. قيمة Status إحدى plsNotAttempted وplsLoaded وplsLoadedWithRecovery وplsRejected وplsFailed. ويحمل NativeErrorCode نتيجة FPDF_GetLastError، فيفصل FPDF_ERR_PASSWORD (4) كلمة السر الغائبة أو الخاطئة عن ملف متضرر يُبلَّغ عنه بـ FPDF_ERR_FORMAT (3). وتقول UsedRecovery وCrossReferenceTableValid وRecoveryRoute هل اضطر PDFium إلى إعادة بناء جدول xref، ويسرد Issues كل مادة تدقيق بـ Code وSeverity وOffset وObjectNumber وMessageText، مع ضبط IssuesTruncated حين يقتطع MaxIssues القائمة

خط أنابيب LoadDocument في مكوّن PDFium وتقرير TPdfLoadReport الخاص به: يرفع التحقق من الخيارات وفحص عدم النشاط قبل وجود أي تقرير، ويمشي التدقيق البايتي في الترويسة وstartxref وأقسام xref وعلامة نهاية الملف، ويسجل التحميل الأصلي قيمة FPDF_GetLastError، وتتفرع النواتج إلى محمّل أو محمّل مع تعاف بعد إعادة بناء xref أو رفض صارم أو فشل
يجيب Status وNativeErrorCode وقائمة المسائل عمّا يحتاجه سجل دفعات؛ والتدقيق البايتي لا يضيفه إلا تحميل زائد بالخيارات، بينما يسجل فشل Active := True منذ v3.122.1 قيمة plsFailed في LastLoadReport
uses
  SysUtils, Classes, TypInfo, FPdfView, PDFium;

procedure ProcessBatch(Files, Log: TStrings);
var
  I: Integer;
  Pdf: TPdf;
  Options: TPdfLoadOptions;
  Report: TPdfLoadReport;
begin
  Options := TPdfLoadOptions.Default(plmCompatible);
  for I := 0 to Files.Count - 1 do
  begin
    Pdf := TPdf.Create(nil);          // مثيل واحد لكل مستند
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // يمتلئ Report حتى مع رفع LoadDocument استثناءً
          if Report.NativeErrorCode = FPDF_ERR_PASSWORD then
            Log.Add(Files[I] + ': password required')
          else
            Log.Add(Format('%s: %s (%s)', [Files[I],
              GetEnumName(TypeInfo(TPdfLoadStatus), Ord(Report.Status)),
              E.Message]));
          Continue;
        end;
      end;
      if Report.UsedRecovery then
        Log.Add(Files[I] + ': opened after PDFium rebuilt the xref table');
      ExtractTables(Pdf, Log);
    finally
      Pdf.Free;
    end;
  end;
end;

متى تحمّل بوضع plmStrict؟

استخدم plmStrict كلما كان الملف المرمم بصمت أسوأ من الملف المرفوض، كما في استقبال الأرشيف أو معالجة الأدلة أو خط أنابيب توقيع. يعيد PDFium بناء جدول إحالات مرجعية معطوب بصمت (ISO 32000-1 §7.5.4) بمسح الملف بحثًا عن كائنات، وهذا ممتاز لعارض ومشكلة لكل ما يجب أن يعالج البايتات الممررة إليه بالضبط. بعد التحميل الأصلي يسأل المكوّن FPDF_DocumentHasValidCrossReferenceTable. في وضع plmCompatible ينتج إعادة البناء plsLoadedWithRecovery مع تحذير plicNativeCrossReferenceRebuild. وفي وضع plmStrict يفرغ المكوّن المستند ويضبط plsRejected ويضيف plicStrictModeRejected ويرفع EPdfError بالرسالة "Strict PDF load rejected the document". ويرفض الوضع الصارم أيضًا أي خطأ تدقيق، ويشعل TPdfLoadOptions.Default(plmStrict) خيار RequireFinalEndOfFileMarker الذي يرقّي %%EOF غائبة أو بيانات بعدها (§7.5.5) من تحذير إلى خطأ. ويتكامل تدقيق xref مع الفحوص على مستوى الكائنات في التحقق من تدفقات الكائنات وxref مع PDFium VCL

function AcceptForArchive(const FileName: string; out Reason: string): Boolean;
var
  Pdf: TPdf;
  Report: TPdfLoadReport;
  I: Integer;
begin
  Result := False;
  Reason := '';
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    try
      Pdf.LoadDocument(TPdfLoadOptions.Default(plmStrict), Report);
      Result := True;               // xref صالح، بلا أخطاء تدقيق
    except
      on E: EPdfError do
      begin
        Reason := E.Message;
        for I := 0 to High(Report.Issues) do
          if Report.Issues[I].Severity = plisError then
            Reason := Reason + sLineBreak + Format('  at offset %d: %s',
              [Report.Issues[I].Offset, Report.Issues[I].MessageText]);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

أين يتوقف TPdf.LastLoadReport عن قول الحقيقة؟

لا يكتمل TPdf.LastLoadReport إلا بعد تحميل زائد لـ LoadDocument يأخذ خيارات، لأن تلك التحميلات وحدها تجري التدقيق البايتي. يكتب نجاح Active := True تقرير وضع متوافق بلا تدقيق بايتي، فتبقى AuditAttempted قيمتها False. قبل PDFiumPas v3.122.1 كان الفشل لا يكتب شيئًا، أي أن LastLoadReport على مثيل مشترك كان ما يزال يصف الملف السابق، غالبًا بـ plsLoaded مطمئنة. ومنذ v3.122.1 يستبدل كل تحميل فاشل التقرير: فشل Active := True، الذي ما يزال يترك المكوّن غير نشط دون رفع، وفشل استدعاء LoadDocument أو LoadCustomDocument المجرد يسجلان plsFailed مع نص الخطأ، ودون تدقيق كذلك. فجوتان أخريان تهمان عمليًا. يجري التحقق من الخيارات وCheckInactive قبل تهيئة التقرير، فقيمة AuditByteLimit سالبة أو مثيل نشط سلفًا يرفعان دون إنتاج تقرير. وNativeErrorCode ذو معنى فقط حين حاول PDFium التحليل فعلًا؛ فالملف الغائب يرفع الغلاف استثناءً قبل أن يعمل PDFium، فسجّل ErrorMessage ونص الاستثناء بدلًا منه

القاعدة العملية قصيرة. أبقِ Active := True للعارضين المرتبطين بالمصمم حيث يكون المكوّن غير النشط نتيجة مقبولة. وفي كل مكان آخر، وقبل كل شيء في كود الدفعات والخوادم، أنشئ TPdf واحدًا لكل مستند، واستدعِ LoadDocument(Options, Report)، والتقط الاستثناء الذي يرفعه وسجّل Report.Status وNativeErrorCode ومسائل Issues من مستوى الخطأ مع اسم الملف. الثمن بضعة أسطر لكل موضع استدعاء، وينسب كل فشل إلى ملفه الصحيح بسببِه الحقيقي

واجهة تقرير التحميل والوضع الصارم والتدقيق البايتي تُشحن مع PDFium Component لـ Delphi وC++Builder وLazarus، إلى جانب الرسم واستخراج النصوص وملء النماذج والتحقق من PDF/A