Articol tehnic

Construirea unui banc de testare pentru revizuirea PDF-urilor în Delphi

Un banc de lucru pentru revizuirea la recepție a fișierelor PDF este un program mic cu o singură sarcină: să examineze fiecare fișier înainte ca orice component din aval să aibă voie să îl atingă. Pentru a face asta, trebuie să reunească într-o singură trecere un mănunchi de capabilități. Deschide fișierul (fără să aibă încredere în el), citește ce pretinde fișierul despre sine, caută conținut care ar induce în eroare un extractor naiv sau ar purta un atac, decide dacă există vreun text extractibil și apoi direcționează documentul către o coadă în funcție de ce a găsit. Săriți peste inspecție, iar eșecurile devin tăcute: un PDF criptat cu parolă de proprietar care încapsulează un formular XFA trece printr-un extractor de text ca șiruri goale, este indexat ca document gol, iar nimeni nu observă până când cineva din aval nu caută conținut care nu a fost citit niciodată. PDFium Component este o bibliotecă de vizualizare și inspecție VCL/LCL cu cod sursă pentru Delphi, C++Builder și Lazarus, iar el expune apelurile de introspecție de care are nevoie acest banc de lucru. Secțiunile de mai jos parcurg care apel răspunde la care întrebare și cele două locuri unde apelul evident vă oferă un răspuns greșit cu încredere deplină

Cinci întrebări la care trebuie răspuns înainte ca un fișier să fie direcționat

Îndepărtați grila și banda de miniaturi, iar triajul la recepție se reduce la cinci întrebări:

Diagramă a unui banc de lucru de primire PDF Delphi, răspunzând la cinci întrebări de triaj într-o singură deschidere ieftină și dirijând fișierele către stările de pregătit, de recenzat, blocat sau deteriorat
Triage-ul de intake răspunde la cinci întrebări într-o deschidere ieftină și dirijează fișierul către ready, review, blocked sau damaged
  • Poate fi deschis fișierul, și sub ce parolă?
  • Ce pretinde că este: titlu, autor, dată de creare?
  • Poartă conținut activ sau riscant, precum JavaScript, un formular XFA sau fișiere încorporate?
  • Există text extractibil, sau este o scanare destinată OCR-ului?
  • Ținând cont de toate acestea, în ce coadă ajunge: procesare directă, revizuire manuală sau carantină?

Fiecare întrebare se mapează pe unul sau două apeluri PDFium Component. Două dintre aceste mapări au colțuri ascuțite care explică majoritatea fișierelor direcționate greșit pe care a trebuit să le depanez în producție. Metadatele documentului trăiesc în două locuri diferite care pot fi în dezacord, iar criptarea nu împiedică neapărat deschiderea unui document

Deschidere ieftină: form fill dezactivat, zero pagini randate

Triajul ar trebui să fie cea mai ieftină deschidere posibilă. Setarea FormFill := False înainte de Active := True îi spune componentei să ocolească integral mediul form-fill. Asta scurtează timpul de încărcare și, la fel de important pentru fișierele de origine necunoscută, împiedică inițializarea oricărui JavaScript la nivel de document. Niciuna dintre proprietățile de inspecție folosite mai jos nu necesită randarea unei pagini, așa că o trecere de triaj nu trebuie niciodată să producă un singur bitmap

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // fără mediu de formular, fără inițializare JavaScript
    Pdf.Active := True;        // eșecul este silențios: Active rămâne pur și simplu False

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // fișier deteriorat sau blocat cu parolă de utilizator
      Exit;                    // blocul finally tot rulează
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // nu lăsați niciodată instanța negestionată la un fișier malformat
  end;
end;

Verificarea de după atribuire nu este opțională, și există un motiv pentru care este o verificare, nu un handler de excepție. Când motorul nu poate încărca fișierul, componenta înghite EPdfError-ul intern și lasă Active pe False în loc să îl propage. Codul care așteaptă o excepție va citi fericit PageCount dintr-un document care nu s-a deschis niciodată. Dacă fluxul de respingere are nevoie de textul real al erorii motorului, citiți fișierul într-un array de octeți și apelați supraîncărcarea LoadDocument care ia TBytes; acea cale chiar ridică EPdfError cu mesajul, inclusiv în cazul parolei. try..finally își câștigă totuși locul. Serviciile de recepție rulează nesupravegheate săptămâni la rând, iar nicio excepție ulterioară nu are voie să lase instanța TPdf negestionată sau să rețină un blocaj de care trecerea de reîncercare se va împiedica

Debitul devine rareori blocajul. Cu form fill dezactivat și fără randare, o deschidere de triaj este dominată de I/O, iar un singur worker inspectează confortabil mai multe fișiere pe secundă de pe discul local. Dacă volumul de recepție ajunge vreodată să depășească un singur worker, partiționați munca pe fișier, nu pe verificare. Cele cinci întrebări partajează o singură deschidere, iar împărțirea lor între procese ar multiplica cel mai costisitor pas în loc să îl amortizeze

Metadatele trăiesc în două locuri, iar acestea nu concordă

ISO 32000-1 definește două locuri pentru metadatele documentului: dicționarul de informații al documentului (clauza 14.3.3) și un pachet XMP atașat catalogului (clauza 14.3.2). Proprietățile Title, Author, Subject și CreationDate citesc dicționarul Info, cu MetaText[] pentru orice altă cheie și DecodeDate pentru a interpreta șirul de dată D:YYYYMMDD.... Capcana este că producătorii moderni scriu tot mai des doar XMP, o direcție pe care ISO 32000-2 o oficializează depreciind majoritatea cheilor din dicționarul Info în PDF 2.0. Simptomul într-un instrument de recepție este concret. Bancul dvs. de lucru arată un titlu gol, în timp ce Adobe Acrobat afișează unul, deoarece Acrobat a recurs la dc:title din interiorul pachetului XMP, pe care proprietățile dicționarului Info nu îl ating niciodată

Diagramă a metadatelor PDF trăind în două locuri, dicționarul Info și pachetul XMP, care pot fi în dezacord despre titlu într-un instrument Delphi de primire
Metadatele documentului trăiesc în dicționarul Info și în pachetul XMP, iar cele două case pot dezacorda despre titlu
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // valoare din dicționarul Info
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // șir brut de dată PDF ("D:2026...")

  // Un titlu Info gol nu înseamnă că documentul este netitrat. Componenta
  // nu expune pachetul XMP, așa că verificați octeții bruți ai fișierului
  // pentru elementul dc:title înainte de a avea încredere în câmpul gol.
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

Chiar și verificarea rudimentară de subșir de mai sus își justifică prezența: „metadate existente, dar nu acolo unde se uită instrumentele vechi” este un fapt relevant pentru direcționare, pentru orice pipeline de arhivare care indexează după titlu sau autor. Dacă indexul dvs. din aval citește doar dicționarul Info, fișierele marcate astfel vor deveni în tăcere de negăsit prin căutare

Fișiere criptate care se deschid oricum

Un document criptat nu eșuează neapărat la deschidere. Handler-ul de securitate standard (ISO 32000-1, clauza 7.6.3) face distincție între o parolă de utilizator, necesară pentru a deschide documentul, și o parolă de proprietar care doar controlează permisiuni precum tipărirea și copierea. O mare parte din documentele de afaceri „protejate” sunt criptate cu o parolă de proprietar și o parolă de utilizator goală. Ele se deschid fără solicitare, se decriptează complet și se bazează pe bunăvoința vizualizatoarelor de a respecta indicatorii de permisiune. Aceasta este o politică, nu o protecție, iar stările dvs. de recepție ar trebui să reflecte diferența

Detectarea criptării după o deschidere reușită necesită un apel de motor plus un mecanism de rezervă. FPDF_GetSecurityHandlerRevision(Pdf.Document) returnează -1 pentru fișierele neprotejate și revizia handler-ului în celelalte cazuri, iar faptul că Pdf.Permissions returnează altceva decât masca cu toți biții setați $FFFFFFFF este semnalul care confirmă. Pentru fișierele blocate cu adevărat cu parolă de utilizator, atribuiți Password înainte de a seta Active := True; dacă deschiderea eșuează în continuare, direcționați fișierul către o stare blocată care solicită credențiale de la expeditor printr-un canal securizat, în loc să reîncercați orbește. Și rezistați tentației de a trata „criptat” ca pe o carantină automată. În majoritatea industriilor cu volum mare de documente, fișierele criptate dar deschise cu succes reprezintă cazul normal, nu cel suspect

Conținut activ: JavaScript, XFA și fișiere încorporate

Trei constatări ar trebui să ajungă întotdeauna la decizia de direcționare. Prima, JavaScript: evenimentul OnUnsupportedFeature raportează funcționalități structurale precum XFA sau conținut 3D pe măsură ce motorul le întâlnește, dar nu detectează JavaScript. Verificați în schimb JavaScriptActionCount și tratați un rezultat diferit de zero drept conținut activ. A doua, XFA: când FormType returnează ftXfaFull, paginile vizibile sunt adesea puțin mai mult decât o randare a șablonului XFA, iar extragerea convențională de text va vedea text generic în loc de valorile completate. A treia, atașamente: un PDF este un format de tip container, iar AttachmentCount vă spune dacă acesta poartă pasageri

Diagramă a semnalelor de risc la primirea PDF în Delphi: revizia handler-ului de criptare, numărul de acțiuni JavaScript, tipul de formular XFA și atașamentele periculoase
Starea de criptare și numărările JavaScript, XFA și atașamente sunt semnalele care trebuie să supraviețuiască până în decizia de dirijare
procedure CollectRiskSignals(Pdf: TPdf; var Rec: TIntakeRecord);
var
  i, PageNo: Integer;
  Ext: string;
begin
  Rec.IsEncrypted := Assigned(FPDF_GetSecurityHandlerRevision) and
    (FPDF_GetSecurityHandlerRevision(Pdf.Document) <> -1);
  Rec.HasForms := Pdf.FormType <> ftNone;
  Rec.IsXfa := Pdf.FormType = ftXfaFull;
  Rec.HasJavaScript := Pdf.JavaScriptActionCount > 0;

  // AnnotationCount este o proprietate per pagină; parcurgeți paginile pentru
  // a-l totaliza. Încărcarea unui obiect pagină nu randează nimic, deci rămâne ieftin.
  Rec.Annotations := 0;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    Inc(Rec.Annotations, Pdf.AnnotationCount);
  end;

  Rec.Attachments := Pdf.AttachmentCount;

  for i := 0 to Rec.Attachments - 1 do
  begin
    Ext := LowerCase(ExtractFileExt(string(Pdf.AttachmentName[i])));
    if (Ext = '.exe') or (Ext = '.js') or (Ext = '.vbs') or (Ext = '.dll') then
      Include(Rec.Flags, ifDangerousAttachment);
  end;
end;

Două detalii din acea buclă merită atenție. Numele atașamentului provine din interiorul documentului, așa că nu îl refolosiți niciodată ca și cale de ieșire fără să îl igienizați mai întâi; un nume încorporat precum ..\..\start.exe este un path traversal care așteaptă un apel de salvare neatent. Iar o listă de excludere după extensie este un declanșator de alarmă, nu o garanție. Rolul ei este să forțeze o decizie umană, nu să certifice fișierul ca fiind curat

Transformarea semnalelor în stări de direcționare

Un model de stări funcțional necesită mai puține stări decât se așteaptă majoritatea echipelor: ready (fără blocaje, text prezent), review (deschiderea a reușit, dar ceva necesită atenție umană, precum un formular XFA, JavaScript, un strat de text gol sau un titlu prezent doar în XMP), blocked (necesită parolă de utilizator) și damaged (deschiderea a eșuat). Înregistrați dovezile alături de stare. Hash-ul fișierului, numărul de pagini, indicatorii exacți și mesajul de eroare al motorului pentru fișierele deteriorate contează toate, deoarece persoana care pune la îndoială o decizie de direcționare va face asta săptămâni mai târziu, în raport cu un fișier care poate fi între timp înlocuit sau modificat

Când un operator chiar trebuie să se uite la un fișier pus în carantină, nu îl încredințați vizualizatorului implicit al shell-ului. Randați-l într-un panou securizat, cu scriptarea și gestionarea link-urilor dezactivate, abordarea descrisă în construirea unei suprafețe de previzualizare PDF securizate în Delphi. Iar dacă recepția dvs. alimentează o arhivă cu cerințe de conformitate, trecerea de triaj este locul firesc pentru a programa o verificare mai profundă; validarea preflight în lot față de profilurile PDF/A și PDF/UA preia exact de unde se oprește această inspecție

Pagina de produs a componentei tratează licențierea, API-ul complet de inspecție și demonstrațiile incluse, printre care un inspector de documente în stil recepție: PDFium Component