Technical Article

Izrada radnog stola za unos i pregled PDF-a u Delphiju s PDFium komponentom

Radni stol za unos i pregled PDF-a (PDF intake review workbench) mali je program s jednim zadatkom: pregledati svaku datoteku prije nego što je bilo koji daljnji proces smije dotaknuti. Da bi obavio taj posao, on mora spojiti nekoliko mogućnosti u jedan prolaz. Otvara datoteku (bez povjerenja u nju), čita ono što datoteka tvrdi o sebi, traži sadržaj koji bi mogao zavarati naivni ekstraktor ili sadržavati napad, odlučuje postoji li uopće tekst koji se može izdvojiti, a zatim usmjerava dokument u red čekanja na temelju onoga što je pronašao. Preskočite inspekciju i neuspjesi će proći neopaženo: PDF šifriran vlasničkom lozinkom koji omotava XFA obrazac prolazi kroz ekstraktor teksta kao prazan niz, indeksira se kao prazan dokument i nitko ništa ne primjećuje sve dok netko u kasnijem procesu ne počne tražiti sadržaj koji zapravo nikada nije pročitan. PDFium komponenta (PDFium Component) je VCL/LCL preglednik i knjižnica za inspekciju s izvornim kodom za Delphi, C++Builder i Lazarus te izlaže pozive za introspekciju koji su potrebni ovom radnom stolu. Odjeljci u nastavku opisuju koji poziv odgovara na koje pitanje te dva mjesta na kojima vam očigledan poziv daje samouvjereno pogrešan odgovor.

Pet pitanja na koja treba odgovoriti prije usmjeravanja datoteke

Kada uklonite mrežu i traku sa sličicama, trijaža unosa svodi se na pet pitanja:

  • Može li se datoteka uopće otvoriti i pod kojom lozinkom?
  • Što tvrdi da jest: naslov, autor, datum stvaranja?
  • Sadrži li aktivni ili rizični sadržaj kao što je JavaScript, XFA obrazac ili ugrađene datoteke?
  • Postoji li tekst koji se može izdvojiti ili se radi o skenu koji ide na OCR?
  • S obzirom na sve to, u koji red čekanja ide: izravna obrada, ručni pregled ili karantena?

Svako se pitanje mapira na jedan ili dva poziva PDFium komponente. Dva od tih mapiranja imaju oštre kutove koji objašnjavaju većinu pogrešno usmjerenih datoteka koje sam morao otklanjati u produkciji. Metapodatci dokumenta žive na dva različita mjesta koja se mogu razlikovati, a šifriranje ne sprječava nužno otvaranje dokumenta.

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

Trijaža bi trebala biti najjeftinije moguće otvaranje. Postavljanje FormFill := False prije Active := True govori komponenti da u potpunosti preskoči okruženje za popunjavanje obrazaca. To skraćuje vrijeme učitavanja i (što je jednako važno za datoteke nepoznatog podrijetla) sprječava pokretanje JavaScripta na razini dokumenta. Nijedno od svojstava inspekcije korištenih u nastavku ne zahtijeva iscrtavanje stranice, pa prolaz trijaže nikada ne mora stvoriti niti jednu 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;

Provjera nakon dodjele nije neobavezna, i to je provjera, a ne rukovatelj iznimkama s razlogom. Kada pokretač ne može učitati datoteku, komponenta guta internu iznimku EPdfError i ostavlja Active na False umjesto da je propagira. Kod koji čeka iznimku veselo će pročitati PageCount iz dokumenta koji se nikada nije otvorio. Ako tijek rada odbijanja treba stvarni tekst pogreške pokretača, pročitajte datoteku u niz bajtova i pozovite preopterećenu metodu LoadDocument koja prima TBytes; ta putanja podiže iznimku EPdfError s porukom, uključujući slučaj lozinke. Blok try..finally i dalje ima svoje mjesto. Usluge unosa rade bez nadzora tjednima, pa nijedna kasnija iznimka ne smije uzrokovati curenje instance TPdf ili zadržati lokot na koji će se sljedeći pokušaj spotaknuti.

Propusnost rijetko postaje usko grlo. S isključenim popunjavanjem obrazaca i bez iscrtavanja, jeftino otvaranje za trijažu uglavnom ovisi o I/O operacijama, pa jedan radnik ugodno pregledava nekoliko datoteka u sekundi s lokalnog diska. Ako volumen unosa ikada preraste jednog radnika, raspodijelite posao po datoteci, a ne po vrsti provjere. Pet pitanja dijeli jedno otvaranje, a njihovo dijeljenje na više procesa umnožilo bi najskuplji korak umjesto da ga raspodijeli.

Metapodatci žive na dva mjesta i razlikuju se

ISO 32000-1 definira dva doma za metapodatke dokumenta: rječ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 rječnik Info, uz MetaText[] za bilo koji drugi ključ i DecodeDate za parsiranje niza datuma D:YYYYMMDD.... Problem je u tome što moderni generatori sve više pišu samo XMP, što ISO 32000-2 čini službenim ukidanjem većine ključeva rječnika Info u PDF-u 2.0. Simptom u alatu za unos je konkretan: vaš radni stol prikazuje prazan naslov dok ga Adobe Acrobat prikazuje, jer se Acrobat vratio na dc:title unutar XMP paketa, koji svojstva rječnika Info nikada ne dotiču.

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 gruba provjera podnizova iznad opravdava svoje postojanje: "metapodatci su prisutni, ali ne tamo gdje ih naslijeđeni alati traže" činjenica je važna za usmjeravanje u bilo kojem arhivskom cjevovodu koji indeksira prema naslovu ili autoru. Ako vaš indeks u nastavku čita samo rječnik Info, datoteke označene na ovaj način tiho će postati nepretražive.

Šifrirane datoteke koje se ipak otvaraju

Šifrirani dokument ne mora nužno zakazati pri otvaranju. Standardni sigurnosni rukovatelj (ISO 32000-1 klauzula 7.6.3) razlikuje korisničku lozinku (user password), potrebnu za otvaranje dokumenta, od vlasničke lozinke (owner password) koja samo ograničava dopuštenja kao što su ispis i kopiranje. Velik dio "zaštićenih" poslovnih dokumenata šifriran je vlasničkom lozinkom i praznom korisničkom lozinkom. Otvaraju se bez pitanja, potpuno se dešifriraju i oslanjaju se na to da će preglednici dobrovoljno poštovati zastavice dopuštenja. To je pravilo ponašanja, a ne zaštita, i vaša stanja unosa trebala bi odražavati tu razliku.

Otkrivanje šifriranja nakon uspješnog otvaranja zahtijeva jedan poziv pokretača plus rezervnu varijantu. FPDF_GetSecurityHandlerRevision(Pdf.Document) vraća -1 za nezaštićene datoteke, a inače reviziju rukovatelja, dok je Pdf.Permissions koji vraća vrijednost različitu od maske sa svim postavljenim bitovima $FFFFFFFF potvrdni signal. Za datoteke koje su stvarno zaključane korisničkom lozinkom, dodijelite Password prije postavljanja Active := True; ako otvaranje i dalje ne uspije, usmjerite datoteku u blokirano stanje koje zahtijeva vjerodajnice od pošiljatelja putem sigurnog kanala umjesto slijepog ponovnog pokušaja. I uzalud se oduprite iskušenju da "šifrirano" automatski tretirate kao karantenu. U većini industrija s puno dokumenata, šifrirane datoteke koje se mogu otvoriti normalan su slučaj, a ne sumnjiva pojava.

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

Tri nalaza trebala bi uvijek utjecati na odluku o usmjeravanju. Prvo, JavaScript: događaj OnUnsupportedFeature javlja strukturne značajke kao što su XFA ili 3D sadržaj kako ih pokretač susreće, ali ne otkriva JavaScript. Umjesto toga provjerite JavaScriptActionCount i tretirajte rezultat različit od nule kao aktivni sadržaj. Drugo, XFA: kada FormType vrati ftXfaFull, vidljive su stranice često tek iscrtani predložak XFA-a, a uobičajena ekstrakcija teksta vidjet će generički predložak umjesto popunjenih vrijednosti. Treće, privitci: PDF je kontejnerski format, a AttachmentCount vam govori nosi li ovaj 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 pozornost. Naziv privitka dolazi iz unutrašnjosti dokumenta, pa ga nikada nemojte ponovno koristiti kao izlaznu putanju bez prethodne sanitacije; ugrađeno ime poput ..\..\start.exe je sigurnosni propust prelaska putanje (path traversal) koji čeka neoprezan poziv za spremanje. A popis blokiranih ekstenzija je žičana zamka (tripwire), a ne jamstvo. Njegov je posao iznuditi ljudsku odluku, a ne potvrditi da je datoteka sigurna.

Pretvaranje signala u stanja usmjeravanja

Upotrebljiv model stanja treba manje stanja nego što većina timova očekuje: ready (spremno - nema prepreka, tekst prisutan), review (pregled - otvaranje uspjelo, ali nešto zahtijeva pažnju, poput XFA obrasca, JavaScripta, praznog sloja teksta ili naslova samo u XMP-u), blocked (blokirano - potrebna korisnička lozinka) i damaged (oštećeno - otvaranje nije uspjelo). Zapišite dokaze uz stanje. Hash datoteke, broj stranica, točne zastavice i poruka o pogrešci pokretača za oštećene datoteke - sve je to važno, jer će osoba koja preispituje odluku o usmjeravanju to učiniti tjednima kasnije, u odnosu na datoteku koja je u međuvremenu možda zamijenjena ili izmijenjena.

Kada operater doista treba pogledati datoteku u karanteni, nemojte je predati zadanom pregledniku sustava. Iscrtajte je unutar zaštićenog okna s isključenim skriptiranjem i rukovanjem poveznicama, što je pristup opisan u članku o izradi sigurne površine za pregled PDF-a u Delphiju. A ako vaš unos hrani arhivu sa zahtjevima sukladnosti, prolaz trijaže je prirodno mjesto za planiranje dublje provjere; grupna validacija prije leta (preflight) u odnosu na PDF/A i PDF/UA profile nastavlja točno tamo gdje ova inspekcija staje.

Stranica proizvoda komponente pokriva licenciranje, puni inspekcijski API i priložene demonstracije, uključujući inspektor dokumenata u stilu unosa: PDFium komponenta (PDFium Component).