Technický článek

Vytvoření kontrolního panelu pro příjem PDF v Delphi s PDFium

Pracoviště pro vstupní kontrolu PDF je malý program s jedním úkolem: podívat se na každý soubor dřív, než se ho smí dotknout cokoli po proudu. Aby tento úkol splnil, musí do jednoho průchodu sestavit hrstku schopností. Otevře soubor (aniž by mu důvěřoval), přečte, co soubor sám o sobě tvrdí, hledá obsah, který zmate naivní extraktor nebo nese útok, rozhodne, jestli je v něm vůbec nějaký extrahovatelný text, a pak dokument nasměruje do fronty podle toho, co v něm našel. Vynechte kontrolu a selhání budou tichá: PDF šifrované heslem vlastníka, které v sobě balí formulář XFA, projede textovým extraktorem jako prázdné řetězce, zaindexuje se jako prázdný dokument, a nikdo si toho nevšimne, dokud někdo po proudu nebude hledat obsah, který nikdy nebyl přečten. PDFium Component je zdrojová knihovna VCL/LCL pro prohlížení a inspekci pro Delphi, C++Builder a Lazarus, a vystavuje introspekční volání, která toto pracoviště potřebuje. Následující sekce procházejí tím, které volání odpovídá na kterou otázku, a dvě místa, kde vám zjevné volání dá sebejistě špatnou odpověď

Pět otázek, na které je třeba odpovědět, než je soubor směrován

Odstraňte mřížku a pruh náhledů, a vstupní třídění se zredukuje na pět otázek:

Diagram pracoviště příjmu PDF v Delphi odpovídajícího na pět triážních otázek jedním laciným otevřením a směrujícího soubory do stavů ready, review, blocked nebo damaged
Intake triage odpoví na pět otázek v jediném levném otevření a nasměruje soubor na ready, review, blocked, nebo damaged
  • Dá se soubor vůbec otevřít, a pod jakým heslem?
  • Za co se sám vydává: název, autor, datum vzniku?
  • Nese aktivní nebo rizikový obsah, jako je JavaScript, formulář XFA nebo vložené soubory?
  • Je v něm extrahovatelný text, nebo je to sken mířící na OCR?
  • S ohledem na to vše, do které fronty patří: přímé zpracování, ruční kontrola, nebo karanténa?

Každá otázka se mapuje na jedno nebo dvě volání PDFium Component. Dvě z těchto mapování mají ostré hrany, které odpovídají za většinu špatně nasměrovaných souborů, které jsem musel v produkci ladit. Metadata dokumentu žijí na dvou různých místech, která si mohou odporovat, a šifrování nemusí dokumentu nutně zabránit v otevření

Otevřete levně: vyplňování formulářů vypnuté, žádná stránka vykreslená

Třídění by mělo být nejlevnějším možným otevřením. Nastavení FormFill := False před Active := True řekne komponentě, ať prostředí pro vyplňování formulářů úplně přeskočí. To zkrátí čas načítání, a (stejně důležité u souborů neznámého původu) to zabrání inicializaci jakéhokoli JavaScriptu na úrovni dokumentu. Žádná z inspekčních vlastností použitých níže nevyžaduje vykreslení stránky, takže třídicí průchod nikdy nemusí vyprodukovat jedinou bitmapu

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // žádné prostředí formuláře, žádná inicializace JavaScriptu
    Pdf.Active := True;        // selhání je tiché: Active prostě zůstane False

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // poškozený soubor nebo zámek uživatelským heslem
      Exit;                    // blok finally se pořád spustí
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // nikdy neuniknout instanci u poškozeného souboru
  end;
end;

Kontrola za přiřazením není volitelná, a je to kontrola, ne obsluha výjimky, z konkrétního důvodu. Když engine nedokáže soubor načíst, komponenta pohltí interní EPdfError a nechá Active na False místo toho, aby ji propagovala dál. Kód, který čeká na výjimku, si vesele přečte PageCount z dokumentu, který se nikdy neotevřel. Pokud workflow pro odmítnutí potřebuje skutečný chybový text enginu, přečtěte soubor do pole bajtů a zavolejte přetížení LoadDocument, které bere TBytes; tato cesta EPdfError se zprávou skutečně vyvolá, včetně případu s heslem. try..finally si pořád zaslouží své místo. Vstupní služby běží bez dozoru celé týdny, a žádná pozdější výjimka nesmí uniknout instanci TPdf ani podržet zámek, o který pak zakopne opakovaný pokus

Propustnost se málokdy stane úzkým hrdlem. S vypnutým vyplňováním formulářů a bez vykreslování třídicímu otevření dominuje I/O, a jeden worker pohodlně prověří několik souborů za vteřinu z lokálního disku. Pokud objem vstupu jednou přerostě jednoho workera, rozdělte práci podle souboru, ne podle kontroly. Pět otázek sdílí jedno otevření, a jejich rozdělení napříč procesy by znásobilo nejdražší krok místo toho, aby ho amortizovalo

Metadata žijí na dvou místech, a odporují si

ISO 32000-1 definuje dva domovy pro metadata dokumentu: slovník informací o dokumentu (klauzule 14.3.3) a paket XMP připojený ke katalogu (klauzule 14.3.2). Vlastnosti Title, Author, Subject a CreationDate čtou Info dictionary, s MetaText[] pro jakýkoli jiný klíč a DecodeDate pro parsování datového řetězce D:YYYYMMDD.... Háček je v tom, že moderní producenti stále víc zapisují jen XMP, směr, který ISO 32000-2 zoficiálňuje tím, že v PDF 2.0 zavrhuje většinu klíčů Info dictionary. Příznak v nástroji pro vstupní kontrolu je konkrétní. Vaše pracoviště ukáže prázdný název, zatímco Adobe Acrobat nějaký zobrazí, protože Acrobat spadl zpátky na dc:title uvnitř paketu XMP, kterého se vlastnosti Info dictionary nikdy nedotknou

Diagram metadat PDF žijících na dvou místech, slovník Info a paket XMP, jež se můžou rozejít v titulu v příjmovém nástroji Delphi
Metadata dokumentu žijí ve slovníku Info i v paketu XMP a obě domácnosti se můžou neshodnout ohledně titulu
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // hodnota z Info dictionary
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // syrový datový řetězec PDF („D:2026...")

  // Prázdný title v Info neznamená, že dokument nemá název.
  // Komponenta nevystavuje paket XMP, takže syrové bajty souboru
  // prohledejte na element dc:title, než uvěříte prázdné hodnotě.
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

I ta hrubá sonda na podřetězec výše si zaslouží své místo: „metadata jsou přítomná, ale ne tam, kam se dívají starší nástroje" je fakt relevantní pro směrování v jakémkoli archivním pipeline, který indexuje podle názvu nebo autora. Pokud váš navazující index čte jen Info dictionary, soubory takto označené se potichu stanou nevyhledatelnými

Šifrované soubory, které se stejně otevřou

Šifrovaný dokument se nutně neotevře neúspěšně. Standardní bezpečnostní handler (ISO 32000-1 klauzule 7.6.3) rozlišuje uživatelské heslo, vyžadované k otevření dokumentu, od hesla vlastníka, které jen brání oprávněním, jako je tisk a kopírování. Velký podíl „chráněných" firemních dokumentů je zašifrován heslem vlastníka a prázdným uživatelským heslem. Otevřou se bez výzvy, plně se dešifrují a spoléhají na to, že prohlížečky dobrovolně budou respektovat příznaky oprávnění. To je politika, ne ochrana, a vaše vstupní stavy by měly tento rozdíl odrážet

Detekce šifrování po úspěšném otevření zabere jedno volání enginu plus fallback. FPDF_GetSecurityHandlerRevision(Pdf.Document) vrací -1 pro nechráněné soubory a jinak revizi handleru, a Pdf.Permissions vracející cokoli jiného než masku se všemi bity nastavenými $FFFFFFFF je potvrzující signál. U opravdu heslem uživatele uzamčených souborů přiřaďte Password před nastavením Active := True; pokud otevření pořád selže, nasměrujte soubor do blokovaného stavu, který si vyžádá přihlašovací údaje od odesílatele přes bezpečný kanál, místo aby to slepě zkoušel znovu. A odolejte pokušení zacházet s „šifrováno" jako s automatickou karanténou. Ve většině odvětví náročných na dokumenty jsou šifrované, ale otevíratelné soubory běžný případ, ne podezřelý

Aktivní obsah: JavaScript, XFA a vložené soubory

Tři nálezy by měly vždy dorazit až k rozhodnutí o směrování. Za prvé, JavaScript: událost OnUnsupportedFeature hlásí strukturální funkce, jako XFA nebo 3D obsah, ve chvíli, kdy na ně engine narazí, ale JavaScript nedetekuje. Místo toho zkontrolujte JavaScriptActionCount a nenulový výsledek berte jako aktivní obsah. Za druhé, XFA: když FormType vrátí ftXfaFull, viditelné stránky jsou často málo víc než vykreslení šablony XFA, a konvenční extrakce textu uvidí obecnou šablonu místo vyplněných hodnot. Za třetí, přílohy: PDF je kontejnerový formát, a AttachmentCount vám řekne, jestli tenhle konkrétní soubor veze cestující

Diagram rizikových signálů příjmu PDF v Delphi: revize šifrovacího handleru, počty akcí JavaScriptu, typ formuláře XFA a nebezpečné přílohy
Stav šifrování a počty JavaScriptu, XFA i příloh jsou signály, jež musejí přežít až do rozhodnutí o směrování
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 je vlastnost po stránkách; projít stránky a sečíst
  // je. Načtení objektu stránky nic nevykresluje, takže to zůstává levné.
  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 detaily v této smyčce si zaslouží pozornost. Název přílohy pochází zevnitř dokumentu, takže ho nikdy nepoužívejte jako výstupní cestu bez předchozí sanitizace; vložený název jako ..\..\start.exe je path traversal čekající na neopatrné volání uložení. A blocklist přípon je jen tripwire, ne záruka. Jeho úkolem je vynutit lidské rozhodnutí, ne osvědčit, že je soubor čistý

Přeměna signálů na směrovací stavy

Funkční model stavů potřebuje méně stavů, než většina týmů čeká: ready (žádné blokátory, text přítomen), review (otevření uspělo, ale něco potřebuje lidské oči, jako formulář XFA, JavaScript, prázdná textová vrstva nebo název jen v XMP), blocked (vyžadováno uživatelské heslo) a damaged (otevření selhalo). Zaznamenávejte důkazy spolu se stavem. Hash souboru, počet stránek, přesné příznaky a chybová zpráva enginu u poškozených souborů, to vše záleží, protože člověk, který rozhodnutí o směrování zpochybní, to udělá o týdny později, proti souboru, který mezitím mohl být nahrazen nebo upraven

Když si operátor opravdu potřebuje prohlédnout soubor v karanténě, nedávejte mu ho do výchozí prohlížečky shellu. Vykreslete ho uvnitř zpevněného panelu s vypnutým skriptováním a zpracováním odkazů, přístup popsaný v budování bezpečného náhledového povrchu PDF v Delphi. A pokud vaše vstupní kontrola krmí archiv s požadavky na shodu, třídicí průchod je přirozené místo, kam naplánovat hlubší kontrolu; dávková preflight validace proti profilům PDF/A a PDF/UA navazuje přesně tam, kde tato inspekce končí

Produktová stránka komponenty pokrývá licencování, plné inspekční API a přibalená dema, včetně inspektoru dokumentů ve stylu vstupní kontroly: PDFium Component