Technical Article

Izrada radnog stola za pregled prihvata PDF-a u Delphi-ju pomoću PDFium komponente

Radni sto za pregled prihvata PDF-a je mali program sa jednim zadatkom: pregledati svaku datoteku pre nego što bilo šta dalje u lancu obrade dobije priliku da je dodirne. Da bi obavio taj posao, on mora da objedini nekoliko mogućnosti u jedan prolaz. On otvara datoteku (bez poverenja u nju), čita šta datoteka tvrdi o sebi, traži sadržaj koji bi mogao da zavara jednostavan ekstraktor ili nosi napad, odlučuje da li uopšte postoji tekst koji se može izdvojiti, a zatim usmerava dokument u red čekanja na osnovu onoga što je pronašao. Preskočite inspekciju i neuspesi će biti tihi: PDF šifrovan lozinkom vlasnika koji obmotava XFA obrazac prolazi kroz ekstraktor teksta kao prazan string, biva indeksiran kao prazan dokument, i niko ne primećuje dok neko dalje u lancu ne potraži sadržaj koji nikada nije pročitan. PDFium Component je VCL/LCL čitač sa izvornim kodom i biblioteka za inspekciju za Delphi, C++Builder i Lazarus, i izlaže pozive introspekcije koji su potrebni ovom radnom stolu. Odeljci u nastavku prolaze kroz to koji poziv odgovara na koje pitanje, kao i kroz dva mesta gde vam očigledan poziv daje samouvereno pogrešan odgovor.

Pet pitanja na koja treba odgovoriti pre nego što se datoteka usmeri

Uklonite mrežu i traku sa sličicama, i trijaža prihvata se svodi na pet pitanja:

  • Može li se datoteka uopšte otvoriti i pod kojom lozinkom?
  • Šta ona tvrdi da jeste: naslov, autor, datum kreiranja?
  • Da li nosi aktivan ili rizičan sadržaj kao što su JavaScript, XFA obrazac ili ugrađene datoteke?
  • Da li postoji tekst koji se može izdvojiti ili je u pitanju skeniranje namenjeno za OCR?
  • S obzirom na sve to, koji red čekanja je preuzima: direktna obrada, ručni pregled ili karantin?

Svako pitanje se mapira na jedan ili dva poziva PDFium komponente. Dva od tih mapiranja imaju oštre uglove koji objašnjavaju većinu pogrešno usmerenih datoteka koje sam morao da debagujem u produkciji. Metapodaci dokumenta žive na dva različita mesta koja se mogu razilaziti, a enkripcija ne zaustavlja nužno otvaranje dokumenta.

Otvorite jeftino: isključeno popunjavanje obrazaca, nula iscrtanih stranica

Trijaža bi trebala biti najjeftinije moguće otvaranje. Postavljanje FormFill := False pre nego što Active := True govori komponenti da potpuno preskoči okruženje za popunjavanje obrazaca. To skraćuje vreme učitavanja i (što je podjednako važno za datoteke nepoznatog porekla) sprečava inicijalizaciju JavaScript-a na nivou dokumenta. Nijedno od svojstava inspekcije korišćenih u nastavku ne zahteva iscrtavanje stranice, tako da prolaz trijaže nikada ne mora da proizvede nijednu bitmapu.

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // no form environment, no JavaScript init
    Pdf.Active := True;        // failure is silent: Active simply stays False

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // damaged file or user-password lock
      Exit;                    // the finally block still runs
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // never leak the instance on a malformed file
  end;
end;

Provera nakon dodele vrednosti nije opciona, i to je provera, a ne rukovalac izuzecima sa razlogom. Kada motor ne može da učita datoteku, komponenta guta interni EPdfError i ostavlja Active na False umesto da ga propagira. Kod koji čeka izuzetak će rado pročitati PageCount iz dokumenta koji se nikada nije otvorio. Ako tok rada odbijanja zahteva stvarni tekst greške motora, pročitajte datoteku u niz bajtova i pozovite preopterećenje LoadDocument koje prima TBytes; ta putanja podiže EPdfError sa porukom, uključujući slučaj lozinke. Blok try..finally i dalje nalazi svoje mesto. Usluge prihvata rade bez nadzora nedeljama, i nijedan kasniji izuzetak ne sme da procuri instancu TPdf ili drži zaključavanje preko kojeg će se prolaz za ponovni pokušaj saplesti.

Propusna moć retko postaje usko grlo. Sa onemogućenim popunjavanjem obrazaca i bez iscrtavanja, otvaranjem u svrhu trijaže dominira I/O rad, i jedan radnik komotno pregleda nekoliko datoteka u sekundi sa lokalnog diska. Ako obim prihvata ikada preraste jednog radnika, podelite posao po datoteci, a ne po proveri. Pet pitanja dele jedno otvaranje, a njihovo deljenje na više procesa bi pomnožilo najskuplji korak umesto da ga amortizuje.

Metapodaci žive na dva mesta i oni se ne slažu

ISO 32000-1 definiše dva mesta za metapodatke dokumenta: rečnik informacija o dokumentu (klauzula 14.3.3) i XMP paket pridružen katalogu (klauzula 14.3.2). Svojstva Title, Author, Subject i CreationDate čitaju Info rečnik, uz MetaText[] za bilo koji drugi ključ i DecodeDate za parsiranje D:YYYYMMDD... stringa datuma. Zamka je u tome što moderni generatori sve više pišu samo XMP, smer koji ISO 32000-2 ozvaničava ukidanjem većine ključeva Info rečnika u PDF-u 2.0. Simptom u alatki za prihvat je konkretan. Vaš radni sto prikazuje prazan naslov dok Adobe Acrobat prikazuje naslov, jer se Acrobat vratio na dc:title unutar XMP paketa, što svojstva Info rečnika nikada ne dodiruju.

procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // Info dictionary value
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // raw PDF date string ("D:2026...")

  // An empty Info title does not mean the document is untitled. The
  // component does not expose the XMP packet, so probe the raw file
  // bytes for the dc:title element before trusting the blank.
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

Čak i jednostavna provera podstringa pronalazi svoje mesto: „metapodaci su prisutni, ali ne tamo gde ih nasleđene alatke traže“ jeste činjenica relevantna za usmeravanje za bilo koji arhivski lanac koji indeksira po naslovu ili autoru. Ako vaš nizvodni indeks čita samo Info rečnik, datoteke označene na ovaj način će tiho postati nepretražive.

Šifrovane datoteke koje se ipak otvaraju

Šifrovani dokument ne mora nužno da padne pri otvaranju. Standardni rukovalac bezbednošću (ISO 32000-1 klauzula 7.6.3) razlikuje korisničku lozinku, potrebnu za otvaranje dokumenta, od lozinke vlasnika koja samo ograničava dozvole kao što su štampanje i kopiranje. Veliki udeo „zaštićenih“ poslovnih dokumenata je šifrovan lozinkom vlasnika i praznom korisničkom lozinkom. Oni se otvaraju bez pitanja, potpuno se dešifruju i oslanjaju se na to da će čitači dobrovoljno poštovati zastavice dozvola. To je politika, a ne zaštita, i vaša stanja prihvata bi trebala da odražavaju tu razliku.

Otkrivanje enkripcije nakon uspešnog otvaranja zahteva jedan poziv motora i rezervno rešenje. FPDF_GetSecurityHandlerRevision(Pdf.Document) vraća -1 za nezaštićene datoteke i reviziju rukovaoca u suprotnom, a Pdf.Permissions koje vraća bilo šta osim maske sa svim postavljenim bitovima $FFFFFFFF jeste potvrđujući signal. Za datoteke koje su zaista zaključane korisničkom lozinkom, dodelite Password pre nego što postavite Active := True; ako otvaranje i dalje ne uspe, usmerite datoteku u blokirano stanje koje zahteva akredititive od pošiljaoca putem bezbednog kanala umesto slepog ponovnog pokušaja. Izbegnite iskušenje da „šifrovano“ tretirate kao automatski karantin. U većini industrija sa obimnom dokumentacijom, šifrovane datoteke koje se mogu otvoriti su normalan slučaj, a ne sumnjiv.

Aktivan sadržaj: JavaScript, XFA i ugrađene datoteke

Tri nalaza bi uvek trebala uticati na odluku o usmeravanju. Prvo, JavaScript: događaj OnUnsupportedFeature izveštava o strukturnim funkcijama kao što su XFA ili 3D sadržaj kada ih motor sretne, ali ne otkriva JavaScript. Umesto toga proverite JavaScriptActionCount i tretirajte rezultat različit od nule kao aktivan sadržaj. Drugo, XFA: kada FormType vrati ftXfaFull, vidljive stranice su često jedva nešto više od iscrtavanja XFA šablona, a konvencionalna ekstrakcija teksta će videti šablon umesto popunjenih vrednosti. Treće, prilozi: PDF je kontejnerski format, a AttachmentCount vam govori da li ovaj nosi putnike.

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 is a per-page property; walk the pages to total
  // it. Loading a page object renders nothing, so this stays cheap.
  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;

Dva detalja u toj petlji zaslužuju pažnju. Ime priloga dolazi iz unutrašnjosti dokumenta, tako da ga nikada nemojte ponovo koristiti kao izlaznu putanju bez prethodnog čišćenja; ugrađeno ime poput ..\..\start.exe je prelazak putanje (path traversal) koji čeka na neoprezan poziv za čuvanje. Pored toga, lista blokiranih ekstenzija je nagazna mina, a ne garancija. Njen posao je da iznudi ljudsku odluku, a ne da sertifikuje datoteku kao čistu.

Pretvaranje signala u stanja usmeravanja

Funkcionalan model stanja zahteva manje stanja nego što većina timova očekuje: spremno (nema blokada, tekst prisutan), pregled (otvaranje uspelo ali nešto zahteva pažnju, kao što je XFA obrazac, JavaScript, prazan sloj teksta ili naslov samo u XMP-u), blokirano (potrebna lozinka korisnika) i oštećeno (otvaranje nije uspelo). Zabeležite dokaze uporedo sa stanjem. Heš datoteke, broj stranica, tačne zastavice i poruka o grešci motora za oštećene datoteke su važni, jer će osoba koja dovodi u pitanje odluku o usmeravanju to učiniti nedeljama kasnije, u odnosu na datoteku koja je u međuvremenu možda zamenjena ili izmenjena.

Kada operater zaista treba da pogleda karantinsku datoteku, nemojte je predavati podrazumevanom sistemskom čitaču. Iscrtajte je unutar zaštićenog panela sa onemogućenim skriptovanjem i rukovanjem vezama, što je pristup opisan u izgradnji bezbedne PDF površine za pregled u Delphi-ju. A ako vaš prihvat hrani arhivu sa zahtevima usklađenosti, prolaz trijaže je prirodno mesto za planiranje dublje provere; grupna preflight validacija u odnosu na PDF/A i PDF/UA profile nastavlja tačno tamo gde se ova inspekcija zaustavlja.

Stranica proizvoda pokriva licenciranje, kompletan API za inspekciju i prateće demo programe, uključujući inspektor dokumenata u stilu prihvata: PDFium Component.