Teknisk artikel

PDFium Component: PDF intake and review workbench in Delphi

En granskningsarbetsbänk för PDF-intag är ett litet program med ett jobb: titta på varje fil innan något nedströms tillåts röra den. För att göra det jobbet måste den sätta samman en handfull förmågor i en enda passering. Den öppnar filen (utan att lita på den), läser vad filen påstår om sig själv, letar efter innehåll som kommer att vilseleda en naiv extraktor eller bära en attack, bestämmer om det finns någon extraherbar text alls, och dirigerar sedan dokumentet till en kö baserat på vad den hittade. Hoppa över inspektionen och felen blir tysta: en ägarlösenordskrypterad PDF som omsluter ett XFA-formulär seglar genom en textextraktor som tomma strängar, indexeras som ett tomt dokument, och ingen märker det förrän någon nedströms letar efter innehåll som aldrig lästes. PDFium Component är ett källkods-VCL/LCL-visnings- och inspektionsbibliotek för Delphi, C++Builder och Lazarus, och det exponerar introspektionsanropen den här arbetsbänken behöver. Avsnitten nedan går igenom vilket anrop som besvarar vilken fråga, och de två platserna där det uppenbara anropet ger dig ett självsäkert felaktigt svar

Fem frågor att besvara innan en fil dirigeras

Skala bort rutnätet och miniatyrremsan, och intagstriage reduceras till fem frågor:

Diagram över en Delphi PDF-intagsarbetsbänk som besvarar fem triagefrågor i en enda billig öppning och leder filer till tillstånden redo, granskning, blockerad eller skadad
Intake-triage besvarar fem frågor i en enda billig öppning och dirigerar filen till redo, granskning, blockerad eller skadad
  • Kan filen överhuvudtaget öppnas, och under vilket lösenord?
  • Vad påstår den sig vara: titel, författare, skapandedatum?
  • Bär den aktivt eller riskabelt innehåll som JavaScript, ett XFA-formulär, eller inbäddade filer?
  • Finns det extraherbar text, eller är det en skanning på väg mot OCR?
  • Givet allt det, vilken kö får den: direktbearbetning, manuell granskning, eller karantän?

Varje fråga mappar mot ett eller två PDFium Component-anrop. Två av de mappningarna har vassa hörn som står för de flesta av de feldirigerade filerna jag har behövt felsöka i produktion. Dokumentmetadata bor på två olika platser som kan vara oense, och kryptering hindrar inte nödvändigtvis ett dokument från att öppnas

Öppna billigt: formulärifyllning av, noll sidor renderade

Triage bör vara den billigast möjliga öppningen. Att sätta FormFill := False före Active := True talar om för komponenten att hoppa över formulärifyllningsmiljön helt. Det förkortar laddningstiden, och (lika viktigt för filer av okänt ursprung) det hindrar allt JavaScript på dokumentnivå från att initieras. Ingen av inspektionsegenskaperna som används nedan kräver rendering av en sida, så en triage-passering behöver aldrig producera en enda bitmapp

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // ingen formulärmiljö, ingen JavaScript-initiering
    Pdf.Active := True;        // misslyckande är tyst: Active förblir helt enkelt False

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // skadad fil eller användarlösenordslås
      Exit;                    // finally-blocket körs ändå
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // läck aldrig instansen vid en felformad fil
  end;
end;

Kontrollen efter tilldelningen är inte valfri, och det är en kontroll snarare än en undantagshanterare av ett skäl. När motorn inte kan ladda filen sväljer komponenten det interna EPdfError och lämnar Active vid False i stället för att förmedla det vidare. Kod som väntar på ett undantag kommer glatt läsa PageCount från ett dokument som aldrig öppnades. Om avvisningsarbetsflödet behöver motorns faktiska feltext, läs filen till en bytearray och anropa LoadDocument-overloaden som tar TBytes; den vägen höjer verkligen EPdfError med meddelandet, inklusive lösenordsfallet. try..finally förtjänar fortfarande sin plats. Intagstjänster körs obevakade i veckor, och inget senare undantag får läcka TPdf-instansen eller hålla ett lås som återförsöksomgången snubblar över

Genomströmning blir sällan flaskhalsen. Med formulärifyllning inaktiverad och ingen rendering domineras en triage-öppning av I/O, och en enda arbetare inspekterar bekvämt flera filer per sekund från lokal disk. Om intagsvolymen någonsin skulle växa ur en arbetare, partitionera arbetet efter fil snarare än efter kontroll. De fem frågorna delar en öppning, och att dela upp dem över processer skulle multiplicera det dyraste steget i stället för att fördela ut det

Metadata bor på två platser, och de är oense

ISO 32000-1 definierar två hem för dokumentmetadata: dokumentinformationsordboken (klausul 14.3.3) och ett XMP-paket fäst vid katalogen (klausul 14.3.2). Egenskaperna Title, Author, Subject och CreationDate läser Info-ordboken, med MetaText[] för alla andra nycklar och DecodeDate för att parsa datumsträngen D:YYYYMMDD.... Haken är att moderna producenter i allt högre grad bara skriver XMP, en riktning ISO 32000-2 gör officiell genom att fasa ut de flesta Info-ordboksnycklar i PDF 2.0. Symptomet i ett intagsverktyg är konkret. Din arbetsbänk visar en tom titel medan Adobe Acrobat visar en, eftersom Acrobat föll tillbaka på dc:title inuti XMP-paketet, vilket Info-ordboksegenskaperna aldrig rör

Diagram över PDF-metadata som lever på två ställen, Info-ordboken och XMP-paketet, som kan vara oense om titeln i ett Delphi intagsverktyg
Dokumentmetadata bor i Info-ordlistan och i XMP-paketet, och de två hemmen kan vara oense om titeln
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // Info-ordbokens värde
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // rå PDF-datumsträng ("D:2026...")

  // En tom Info-titel betyder inte att dokumentet saknar titel.
  // Komponenten exponerar inte XMP-paketet, så sondera de råa fil-
  // byten efter dc:title-elementet innan du litar på den tomma
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

Även den råa delsträngssonden ovan förtjänar sin plats: "metadata finns, men inte där äldre verktyg letar" är ett dirigeringsrelevant faktum för varje arkivpipeline som indexerar på titel eller författare. Om ditt nedströmsindex bara läser Info-ordboken kommer filer flaggade på det här sättet tyst att bli osökbara

Krypterade filer som öppnas ändå

Ett krypterat dokument misslyckas inte nödvändigtvis med att öppnas. Standardsäkerhetshanteraren (ISO 32000-1 klausul 7.6.3) skiljer på ett användarlösenord, som krävs för att öppna dokumentet, från ett ägarlösenord som bara spärrar behörigheter som utskrift och kopiering. En stor andel "skyddade" affärsdokument är krypterade med ett ägarlösenord och ett tomt användarlösenord. De öppnas utan att fråga, dekrypterar fullständigt, och förlitar sig på att visare frivilligt respekterar behörighetsflaggorna. Det är policy, inte skydd, och dina intagstillstånd bör återspegla skillnaden

Att upptäcka kryptering efter en lyckad öppning tar ett motoranrop plus en reservlösning. FPDF_GetSecurityHandlerRevision(Pdf.Document) returnerar -1 för oskyddade filer och hanterarrevisionen annars, och att Pdf.Permissions returnerar något annat än masken $FFFFFFFF med alla bitar satta är den bekräftande signalen. För genuint användarlösenordslåsta filer, tilldela Password före du sätter Active := True; om öppningen fortfarande misslyckas, dirigera filen till ett blockerat tillstånd som begär autentiseringsuppgifter från avsändaren via en säker kanal snarare än att försöka igen blint. Och stå emot frestelsen att behandla "krypterad" som automatisk karantän. I de flesta dokumenttunga branscher är krypterade-men-öppningsbara filer normalfallet, inte det misstänkta

Aktivt innehåll: JavaScript, XFA och inbäddade filer

Tre fynd bör alltid nå dirigeringsbeslutet. Först, JavaScript: händelsen OnUnsupportedFeature rapporterar strukturella funktioner som XFA eller 3D-innehåll när motorn stöter på dem, men den upptäcker inte JavaScript. Kontrollera JavaScriptActionCount i stället och behandla ett resultat skilt från noll som aktivt innehåll. För det andra, XFA: när FormType returnerar ftXfaFull är de synliga sidorna ofta inte mycket mer än en rendering av XFA-mallen, och konventionell textextraktion kommer att se schablontext snarare än de ifyllda värdena. För det tredje, bilagor: en PDF är ett containerformat, och AttachmentCount talar om för dig om den här bär passagerare

Diagram över PDF-intagsrisksignaler i Delphi: krypteringshanterarens revision, räkning av JavaScript-åtgärder, XFA-formulärtyp, och farliga bilagor
Krypteringstillstånd och antalet JavaScript, XFA och bilagor är signalerna som måste överleva in i dirigeringsbeslutet
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 är en per-sida-egenskap; gå igenom sidorna för att
  // summera det. Att ladda ett sidobjekt renderar ingenting, så det förblir billigt
  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;

Två detaljer i den loopen förtjänar uppmärksamhet. Bilagsnamnet kommer inifrån dokumentet, så återanvänd det aldrig som en utdatasökväg utan att sanera det först; ett inbäddat namn som ..\..\start.exe är en sökvägstraversering som väntar på ett oaktsamt sparningsanrop. Och en blocklista över filändelser är en snubbeltråd, inte en garanti. Dess jobb är att tvinga fram ett mänskligt beslut, inte att intyga att filen är ren

Att omvandla signaler till dirigeringstillstånd

En användbar tillståndsmodell behöver färre tillstånd än de flesta team förväntar sig: ready (inga hinder, text närvarande), review (öppning lyckades men något behöver ögon, som ett XFA-formulär, JavaScript, ett tomt textlager, eller en titel bara i XMP), blocked (användarlösenord krävs), och damaged (öppning misslyckades). Registrera beviset tillsammans med tillståndet. Filhashen, sidantalet, de exakta flaggorna, och motorns felmeddelande för skadade filer spelar alla roll, eftersom personen som ifrågasätter ett dirigeringsbeslut kommer att göra det veckor senare, mot en fil som sedan dess kan ha ersatts eller ändrats

När en operatör faktiskt behöver titta på en karantänsatt fil, ge den inte till standardskalvisaren. Rendera den inuti en härdad panel med skriptning och länkhantering inaktiverad, tillvägagångssättet beskrivet i att bygga en säker PDF-förhandsgranskningsyta i Delphi. Och om ditt intag matar ett arkiv med efterlevnadskrav, är triage-passeringen den naturliga platsen att schemalägga en djupare kontroll; batchpreflight-validering mot PDF/A- och PDF/UA-profiler tar vid precis där den här inspektionen slutar

Komponentens produktsida täcker licensiering, det fullständiga inspektions-API:et, och de medföljande demonstrationerna, inklusive en dokumentinspektör i intagsstil: PDFium Component