Τεχνικό Άρθρο

Σιωπηλές αποτυχίες load PDF στο Delphi: load report PDFium

Στο PDFium Component for Delphi and Lazarus, η ανάθεση TPdf.Active := True δεν σηκώνει ποτέ όταν ένα PDF αποτυγχάνει να φορτώσει: το TPdf.SetActive πιάνει κάθε εξαίρεση και αφήνει το component ανενεργό. Για να δείτε το πραγματικό σφάλμα, καλέστε αντ' αυτού το TPdf.LoadDocument(Options, Report). Εκείνη η υπερφόρτωση ξανασηκώνει την πρωτότυπη εξαίρεση και γεμίζει ένα TPdfLoadReport με την κατάσταση load, τον native κωδικό σφάλματος του PDFium και αν ο πίνακας cross-reference χρειάστηκε ξαναχτίσιμο

Το πρόβλημα συνήθως αναδύεται σε κώδικα παρτίδας. Μια δουλειά εξαγωγής πινάκων περπατά έναν φάκελο με 13 PDF του πραγματικού κόσμου με ένα κοινόχρηστο TPdf, και 7 από αυτά επανέρχονται ως αποτυχίες. Κανένα από τα 7 αρχεία δεν είναι στην πραγματικότητα χαλασμένο. Τα blocks except γύρω από το load δεν ανάβουν ποτέ, το log κατηγορεί τα λάθος ονόματα αρχείων, και το πρώτο ορατό σφάλμα είναι ένα γυμνό EPdfError για ανενεργό component, σηκωμένο από μια ανάγνωση ιδιότητας αρκετές γραμμές μετά το load που όντως απέτυχε. Δύο ξεχωριστές συμπεριφορές στοιβάζονται για να παράγουν εκείνη την εικόνα, και αμφότερες δουλεύουν όπως σχεδιάστηκαν

Γιατί το TPdf.Active := True δεν σηκώνει όταν ένα PDF αποτυγχάνει να φορτώσει;

Το TPdf.SetActive τυλίγει το LoadDocument σε try..except που καταπίνει κάθε κλάση εξαίρεσης και απλώς αφήνει το component ανενεργό. Η κατάποση είναι σκόπιμη: ο ίδιος setter τρέχει όταν ένας form designer εναλλάσσει το Active στο IDE, και ένα κακό μονοπάτι δεν πρέπει να σπάσει το IDE. Σε run time το TPdf.Active απλώς αναφέρει αν υπάρχει native handle εγγράφου, οπότε μετά από αποτυχημένο load διαβάζεται False και τίποτα άλλο δεν συμβαίνει. Ό,τι σηκώθηκε έχει χαθεί, αν ήταν EPdfError από τον parser, σφάλμα stream ή EAccessViolation από ένα μισοδεμένο pdfium.dll. Τα αναλυτικά μηνύματα DLL που περιγράφει το διάγνωση αποτυχιών load του pdfium.dll στο Delphi φτάνουν στον handler σας μόνο μέσω κλήσης που δεν τα καταπίνει

Δύο μονοπάτια load στο PDFium Component: η ανάθεση Active true καταπίνει κάθε εξαίρεση στον setter και αναβάλλει την αποτυχία στην πρώτη guarded κλήση, όπου το CheckActive σηκώνει EPdfError για ανενεργό component, ενώ το LoadDocument με TPdfLoadOptions και TPdfLoadReport ελέγχει το header, το startxref, τα xref και τον marker τέλους αρχείου, μετά ξανασηκώνει την πρωτότυπη εξαίρεση με την πραγματική αιτία προσαρτημένη
Η κατάποση είναι σκόπιμη επειδή ο designer του IDE μοιράζεται τον setter· ο κώδικας παρτίδας χρειάζεται την υπερφόρτωση που σηκώνει, αναφέρει και λέει την πραγματική ιστορία του αρχείου
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // Το SetActive καταπίνει οποιαδήποτε εξαίρεση load
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]));

// Μινιμαλικό fix για υπάρχοντα κώδικα: τεστάρετε το Active αμέσως μετά την ανάθεση·
// από το v3.122.1 το LastLoadReport κρατά το κείμενο του καταπιωμένου σφάλματος
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

Η αποτυχία τελικά εμφανίζεται στην πρώτη guarded κλήση. Το TPdf.PageCount, όπως οι περισσότεες ιδιότητες εγγράφου, ξεκινά με CheckActive, που σηκώνει EPdfError ονομάζοντας το component αλλά όχι το αρχείο και όχι την αιτία. Το να τεστάρετε το Pdf.Active αμέσως μετά την ανάθεση μετατρέπει ένα κακόλογο crash σε τίμια εγγραφή «απέτυχε». Πριν το PDFiumPas v3.122.1 η αιτία χανόταν σε εκείνο το σημείο· από το v3.122.1 η αποτυχημένη ανάθεση αντικαθιστά το LastLoadReport με αναφορά plsFailed που κουβαλά το κείμενο του σφάλματος, οπότε η αιτία επιβιώνει. Το ίδιο το exception object και ο έλεγχος σε επίπεδο bytes εξακολουθούν να απαιτούν διαφορετικό σημείο εισόδου

Γιατί η επαναχρησιμοποίηση ενός TPdf αποτυγχάνει από το δεύτερο αρχείο και μετά;

Το TPdf.FileName μπορεί να ανατεθεί μόνο ενώ το component είναι ανενεργό, οπότε ένα κοινόχρηστο instance απορρίπτει το δεύτερο αρχείο πριν καν επιχειρήσει να το φορτώσει. Το TPdf.SetFileName ξεκινά με CheckInactive, και ο ίδιος guard προστατεύει τα Password και FormFill. Μετά από το πρώτο επιτυχημένο load το instance μένει ενεργό, η επόμενη ανάθεση σηκώνει εξαίρεση, και αν ο βρόχος παρτίδας την πιάσει και προχωρήσει, το σφάλμα προσγειώνεται κάτω από το νέο όνομα αρχείου ενώ το παλιό έγγραφο είναι ακόμα ανοιχτό. Ανακατεμένο με τις καταπιωμένες αποτυχίες load, το log παύει να ταιριάζει την πραγματικότητα. Στην αναπαραγωγή των 13 αρχείων ένα κοινόχρηστο instance ανέφερε 7 αποτυχίες, ενώ ένα φρέσκο TPdf.Create(nil) ανά έγγραφο άνοιξε και τα 13. Ορισμός Active := False ανάμεσα στα αρχεία επίσης δουλεύει, αλλά ένα instance ανά έγγραφο κρατά κάθε αρχείο απομονωμένο εκ κατασκευής

Χρονοδιάγραμμα κοινόχρηστου TPdf του PDFium που αποτυγχάνει από το δεύτερο αρχείο και μετά: μετά το πρώτο load το instance μένει ενεργό, η επόμενη ανάθεση FileName σηκώνει εξαίρεση στο CheckInactive πριν από οποιαδήποτε προσπάθεια load, και ο βρόχος παρτίδας καταλογογραφεί το σφάλμα κάτω από το νέο όνομα αρχείου ενώ το παλιό έγγραφο είναι ακόμα ανοιχτό, η παγίδα πίσω από 7 ψευδείς αποτυχίες σε παρτίδα 13 αρχείων
Το SetFileName φρουρείται με CheckInactive, οπότε ένα κοινόχρηστο instance απορρίπτει το δεύτερο αρχείο πριν το δοκιμάσει· απομονώστε κάθε έγγραφο με δικό του TPdf και το log ξαναταιριάζει την πραγματικότητα

Τι σας δίνει το TPdf.LoadDocument με TPdfLoadReport;

Το TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) σηκώνει την πραγματική εξαίρεση και επίσης σας λέει τι συνέβη σε δομημένη μορφή. Η υπερφόρτωση αρχείου φορτώνει το FileName· αδελφές υπερφορτώσεις παίρνουν TBytes ή pointer και μέγεθος, και το LoadCustomDocument(AStream, AOwnsStream, Options, Report) καλύπτει streams. Καθεμία επικυρώνει τις options, ελέγχει ότι το instance είναι ανενεργό, τρέχει έλεγχο σε επίπεδο bytes του header, του startxref, των xref τμημάτων και του marker %%EOF, μετά εκτελεί το native load. Ο έλεγχος φράσσεται από τα ίδια είδη ορίων που συζητά το budgets πόρων parser για μη αξιόπιστα PDF: το TPdfLoadOptions.Default θέτει AuditByteLimit σε 256 MiB, MaxIssues σε 256, MaxXrefSections σε 1024 και MaxXrefEntries σε 4.000.000. Σε αποτυχία η μέθοδος θέτει Report.Status := plsFailed και ξανασηκώνει· επειδή το Report γράφεται επιτόπου, τα περιεχόμενά του επιβιώνουν της εξαίρεσης, και ένα αντίγραφο αποθηκεύεται στο TPdf.LastLoadReport

Τα πεδία της αναφοράς απαντούν τις ερωτήσεις που χρειάζεται πραγματικά ένα log παρτίδας. Το 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 κόβει τη λίστα

Το pipeline LoadDocument του PDFium Component και το TPdfLoadReport του: η επικύρωση options και ο έλεγχος ανενεργότητας σηκώνουν πριν υπάρξει καμία αναφορά, ένας έλεγχος bytes περπατά το header, το startxref, τα xref τμήματα και τον marker τέλους αρχείου, το native load καταγράφει FPDF_GetLastError, και τα αποτελέσματα διακλαδώνονται σε loaded, loaded με ανάκτηση μετά από ξαναχτίσιμο xref, αυστηρή απόρριψη ή αποτυχία
Το Status, το NativeErrorCode και η λίστα ευρημάτων απαντούν τι χρειάζεται ένα log παρτίδας· μόνο μια υπερφόρτωση με options προσθέτει τον έλεγχο bytes, ενώ από το v3.122.1 ένα αποτυχημένο Active := True καταγράφει επίσης 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);          // ένα instance ανά έγγραφο
    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 όποτε ένα σιωπηλά επιδιορθωμένο αρχείο είναι χειρότερο από ένα απορριφθέν, όπως είσοδος αρχειοθέτησης, διαχείριση αποδείξεων ή pipeline υπογραφής. Το PDFium ξαναχτίζει ήσυχα έναν χαλασμένο πίνακα cross-reference (ISO 32000-1 §7.5.4) σαρώνοντας το αρχείο για objects, που είναι υπέροχο για viewer και πρόβλημα για οτιδήποτε πρέπει να επεξεργαστεί ακριβώς τα bytes που έλαβε. Μετά το native load, το component ρωτά το FPDF_DocumentHasValidCrossReferenceTable. Σε mode plmCompatible ένα ξαναχτίσιμο δίνει plsLoadedWithRecovery συν προειδοποίηση plicNativeCrossReferenceRebuild. Σε mode plmStrict το component ξεφορτώνει το έγγραφο, θέτει plsRejected, προσθέτει plicStrictModeRejected και σηκώνει EPdfError με «Strict PDF load rejected the document». Το strict mode απορρίπτει επίσης κάθε σφάλμα ελέγχου, και το TPdfLoadOptions.Default(plmStrict) ανάβει το RequireFinalEndOfFileMarker, που προάγει λείπαν %%EOF ή δεδομένα μετά τον τελικό (§7.5.5) από προειδοποίηση σε σφάλμα. Ο έλεγχος xref συμπληρώνει τους ελέγχους επιπέδου objects του επαλήθευση object και xref streams με 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 που παίρνει options, επειδή μόνο εκείνες οι υπερφορτώσεις τρέχουν τον έλεγχο bytes. Ένα επιτυχημένο Active := True γράφει αναφορά compatible mode χωρίς έλεγχο bytes, οπότε το AuditAttempted μένει False. Πριν το PDFiumPas v3.122.1 ένα αποτυχημένο δεν έγραφε τίποτα, που σήμαινε ότι σε κοινόχρηστο instance το LastLoadReport περιέγραφε ακόμα το προηγούμενο αρχείο, συχνά με καθησυχαστικό plsLoaded. Από το v3.122.1 κάθε αποτυχημένο load αντικαθιστά την αναφορά: ένα αποτυχημένο Active := True, που εξακολουθεί να αφήνει το component ανενεργό χωρίς να σηκώνει εξαίρεση, και μια αποτυχημένη κλήση σκέτου LoadDocument ή LoadCustomDocument καταγράφουν plsFailed με το κείμενο του σφάλματος, πάλι χωρίς έλεγχο. Δύο ακόμα κενά μετράνε στην πράξη. Η επικύρωση options και το CheckInactive τρέχουν πριν αρχικοποιηθεί η αναφορά, οπότε αρνητικό AuditByteLimit ή ήδη ενεργό instance σηκώνει εξαίρεση χωρίς να παράγει αναφορά. Και το NativeErrorCode έχει νόημα μόνο όταν το PDFium όντως επιχείρησε το parse· για λείπαν αρχείο ο wrapper σηκώνει εξαίρεση πριν τρέξει το PDFium, οπότε καταλογογραφήστε ErrorMessage και το κείμενο της εξαίρεσης αντί αυτού

Ο πρακτικός κανόνας είναι σύντομος. Κρατήστε Active := True για viewers δεμένους με designer όπου ένα ανενεργό component είναι αποδεκτό αποτέλεσμα. Παντού αλλού, και πάνω απ' όλα σε κώδικα παρτίδας και server, δημιουργήστε ένα TPdf ανά έγγραφο, καλέστε LoadDocument(Options, Report), πιάστε την εξαίρεση που σηκώνει και καταλογογραφήστε Report.Status, NativeErrorCode και τα Issues επιπέδου σφάλματος μαζί με το όνομα αρχείου. Το κόστος είναι λίγες γραμμές ανά call site, και κάθε αποτυχία αποδίδεται στο σωστό αρχείο με την πραγματική της αιτία

Το API load report, το strict mode και ο έλεγχος σε επίπεδο bytes έρχονται με το PDFium Component for Delphi, C++Builder and Lazarus, μαζί με απεικόνιση, εξαγωγή κειμένου, συμπλήρωση φορμών και επικύρωση PDF/A