Technisch artikel

Bouw een PDF-intake-reviewwerkbank in Delphi met PDFium

Een PDF-intake-reviewwerkbank is een klein programmaatje met één taak: naar elk bestand kijken voordat iets downstream het mag aanraken. Om die taak te doen moet het een handvol mogelijkheden in één pass samenbrengen. Het opent het bestand (zonder het te vertrouwen), leest wat het bestand over zichzelf beweert, zoekt naar inhoud die een naïeve extractor zal misleiden of een aanval draagt, beslist of er überhaupt extraheerbare tekst is, en routeert het document dan naar een queue op basis van wat het vond. Sla de inspectie over en de falen zijn stille: een met eigenaarswachtwoord versleutelde PDF die een XFA-formulier omhult vaart door een tekstextractor als lege strings, wordt geïndexeerd als een blanco document, en niemand merkt het tot iemand downstream op zoek gaat naar inhoud die nooit gelezen is. PDFium Component is een source-code VCL/LCL-viewer en inspectiebibliotheek voor Delphi, C++Builder en Lazarus, en stelt de introspectie-aanroepen bloot die deze werkbank nodig heeft. De secties hieronder lopen door welke aanroep welke vraag beantwoordt, en de twee plaatsen waar de voor de hand liggende aanroep u een zelfverzekerd fout antwoord geeft

Vijf vragen om te beantwoorden voordat een bestand gerouteerd wordt

Stript u het raster en de thumbnail-strip af, dan reduceert intaketriage zich tot vijf vragen:

Diagram van een Delphi PDF-intakewerkbank die vijf triagevragen beantwoordt in één goedkope open en bestanden routeert naar de statussen ready, review, blocked of damaged
Intake-triage beantwoordt vijf vragen in één goedkope openactie en routeert het bestand naar ready, review, blocked of damaged
  • Kan het bestand überhaupt geopend worden, en onder welk wachtwoord?
  • Wat beweert het te zijn: titel, auteur, aanmaakdatum?
  • Draagt het actieve of riskante inhoud zoals JavaScript, een XFA-formulier of ingebedde bestanden?
  • Is er extraheerbare tekst, of is het een scan op weg naar OCR?
  • Gegeven al dat, naar welke queue gaat het: directe verwerking, handmatige review of quarantaine?

Elke vraag beeldt zich op één of twee PDFium Component-aanroepen. Twee van die afbeeldingen hebben scherpe hoeken die verantwoordelijk zijn voor de meeste verkeerd gerouteerde bestanden die ik in productie heb moeten debuggen. Document-metadata leeft op twee verschillende plaatsen die het oneens kunnen zijn, en encryptie hoeft een document niet tegen te houden bij het openen

Goedkoop openen: form-fill uit, nul pagina's gerenderd

Triage hoort de goedkoopst mogelijke open te zijn. FormFill := False zetten vóór Active := True vertelt de component de form-fill-omgeving geheel over te slaan. Dat verkort de laadtijd, en (net zo belangrijk voor bestanden van onbekende herkomst) het voorkomt dat documentniveau-JavaScript initialiseert. Geen van de inspectie-eigenschappen die hieronder gebruikt worden vereist het renderen van een pagina, dus een triage-pass hoeft nooit één bitmap te produceren

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // geen form-fill-omgeving, geen JavaScript-initialisatie
    Pdf.Active := True;        // mislukking is stil: Active blijft simpelweg False

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // beschadigd bestand of vergrendeling met gebruikerswachtwoord
      Exit;                    // het finally-blok draait nog steeds
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // lek de instantie nooit bij een misvormd bestand
  end;
end;

De check na de toewijzing is niet optioneel, en het is een check in plaats van een exception-handler om een reden. Wanneer de engine het bestand niet kan laden, slikt de component de interne EPdfError weg en laat Active op False staan in plaats van die te propageren. Code die op een exception wacht leest vrolijk PageCount uit een document dat nooit opende. Als de afwijzingsworkflow de werkelijke fouttekst van de engine nodig heeft, lees het bestand in een byte-array en roep de LoadDocument-overload die TBytes neemt; dat pad raiset wél EPdfError met de melding, inclusief het wachtwoordgeval. Het try..finally verdient nog steeds zijn plaats. Intakeservices draaien wekenlang onbeheerd, en geen latere exception mag de TPdf-instantie lekken of een lock vasthouden waar de retry-pass over struikelt

Doorput wordt zelden de flessenhals. Met form-fill uit en geen rendering wordt een triage-open gedomineerd door I/O, en één worker inspecteert comfortabel enkele bestanden per seconde vanaf lokale schijf. Als intakevolume ooit één worker ontgroeit, partitioneer het werk op bestand in plaats van op check. De vijf vragen delen één open, en ze over processen verspreiden vermenigvuldigt de duurste stap in plaats van die af te schrijven

Metadata leeft op twee plaatsen, en ze zijn het oneens

ISO 32000-1 definieert twee thuishavens voor document-metadata: de document information dictionary (clausule 14.3.3) en een XMP-pakket dat aan de catalogus hangt (clausule 14.3.2). De eigenschappen Title, Author, Subject en CreationDate lezen de Info-dictionary, met MetaText[] voor elke andere key en DecodeDate om de D:YYYYMMDD...-datumstring te parsen. De adder is dat moderne producers steeds vaker alleen XMP schrijven, een richting die ISO 32000-2 officieel maakt door de meeste Info-dictionary-keys in PDF 2.0 te deprecaten. Het symptoom in een intakereeds is concreet. Uw werkbank toont een lege titel terwijl Adobe Acrobat er een toont, omdat Acrobat terugviel op dc:title binnen het XMP-pakket, wat de Info-dictionary-eigenschappen nooit aanraken

Diagram van PDF-metadata die op twee plekken leeft, het Info-woordenboek en het XMP-pakket, die het over de titel oneens kunnen zijn in een Delphi intake-tool
Documentmetadata leeft in de Info dictionary en in het XMP-pakket, en de twee woningen kunnen het oneens zijn over de titel
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // waarde uit de Info-dictionary
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // ruwe PDF-datumstring ("D:2026...")

  // Een lege Info-titel betekent niet dat het document geen titel heeft. Het
  // component stelt het XMP-pakket niet bloot, dus doorzoek de ruwe bestands-
  // bytes op het dc:title-element voordat u de lege waarde vertrouwt.
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

Zelfs de ruwe substring-probe hierboven verdient zijn plaats: "metadata aanwezig, maar niet waar legacy-tools kijken" is een routeringsrelevant feit voor elke archiefpijplijn die op titel of auteur indexeert. Als uw downstream-index alleen de Info-dictionary leest, worden bestanden die zo gemarkeerd zijn stil onvindbaar

Versleutelde bestanden die toch openen

Een versleuteld document faalt niet noodzakelijk bij het openen. De standaard security handler (ISO 32000-1 clausule 7.6.3) onderscheidt een gebruikerswachtwoord, vereist om het document te openen, van een eigenaarswachtwoord die slechts permissies gate zoals afdrukken en kopiëren. Een groot aandeel "beschermde" zakelijke documenten is versleuteld met een eigenaarswachtwoord en een leeg gebruikerswachtwoord. Ze openen zonder prompt, ontcijferen volledig, en leunen op viewers die vrijwillig de permissieflags eren. Dat is beleid, geen bescherming, en uw intake-statussen moeten het verschil weerspiegelen

Encryptie detecteren na een geslaagde open vergt één engine-aanroep plus een fallback. FPDF_GetSecurityHandlerRevision(Pdf.Document) retourneert -1 voor onbeschermden en anders de handler-revisie, en Pdf.Permissions dat iets anders dan het alles-bit-gezette $FFFFFFFF-masker retourneert is het bevestigende signaal. Voor echt met gebruikerswachtwoord vergrendelde bestanden, wijs Password toe vóór Active := True; als het openen alsnog faalt, routeer het bestand naar een geblokkeerde status die credentials bij de afzender opvraagt via een beveiligd kanaal in plaats van blind te retrien. En weersta de verleiding om "versleuteld" als automatische quarantaine te behandelen. In de meeste documentintensieve bedrijfstakken zijn versleuteld-maar-openbare bestanden het normale geval, niet het verdachte

Actieve inhoud: JavaScript, XFA en ingebedde bestanden

Drie bevindingen moeten altijd de routeringsbeslissing bereiken. Ten eerste, JavaScript: het event OnUnsupportedFeature rapporteert structurele features zoals XFA of 3D-content wanneer de engine ze tegenkomt, maar detecteert geen JavaScript. Controleer in plaats daarvan JavaScriptActionCount en behandel een resultaat ongelijk nul als actieve inhoud. Ten tweede, XFA: wanneer FormType ftXfaFull retourneert, zijn de zichtbare pagina's vaak weinig meer dan een rendering van het XFA-template, en conventionele tekstextractie ziet boilerplate in plaats van de ingevulde waarden. Ten derde, bijlagen: een PDF is een containerformaat, en AttachmentCount vertelt u of deze passagiers meedraagt

Diagram van PDF-intakerisicosignalen in Delphi: versleutelingshandler-revisie, JavaScript-actietellingen, XFA-formuliertype en gevaarlijke bijlagen
Versleutelingsstatus en de JavaScript-, XFA- en bijlage-aantallen zijn de signalen die moeten overleven tot in de routeringsbeslissing
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 een per-pagina-eigenschap; loop door de pagina's om het totaal
  // te bepalen. Het laden van een paginaobject rendert niets, dus dit blijft goedkoop.
  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;

Twee details in die lus verdienen aandacht. De bijlagen-naam komt van binnen het document, dus hergebruik die nooit als uitvoerpad zonder eerst te saneren; een ingebedde naam als ..\..\start.exe is een path-traversal die wacht op een onachtzame save-aanroep. En een extensie-blocklist is een draad, geen garantie. Zijn taak is een menselijke beslissing afdwingen, niet het bestand schoon certificeren

Van signalen naar routeringsstatussen

Een werkbaar statusmodel heeft minder statussen nodig dan de meeste teams verwachten: klaar (geen blokkades, tekst aanwezig), review (openen geslaagd maar iets vraagt ogen, zoals een XFA-formulier, JavaScript, een lege tekstlaag of een titel alleen in XMP), geblokkeerd (gebruikerswachtwoord vereist), en beschadigd (openen gefaald). Leg het bewijs vast naast de status. De bestandshash, paginatelling, de exacte flags, en de engine-foutmelding voor beschadigde bestanden doen er allemaal toe, want de persoon die een routeringsbeslissing in twijfel trekt doet dat weken later, tegen een bestand dat inmiddels vervangen of gewijzigd kan zijn

Wanneer een operator inderdaad naar een bestand in quarantaine moet kijken, lever het dan niet uit aan de standaard shell-viewer. Render het binnen een verhard paneel met scripting en koppelingsafhandeling uit, de aanpak beschreven in een veilig PDF-voorbeeldoppervlak bouwen in Delphi. En als uw intake een archief voedt met conformiteitseisen, dan is de triage-pass de natuurlijke plek om een diepere check in te plannen; batch preflight-validatie tegen PDF/A- en PDF/UA-profielen pakt precies op waar deze inspectie stopt

De productpagina van de component behandelt licentiëring, de volledige inspectie-API en de meegeleverde demo's, waaronder een intake-stijl documentinspecteur: PDFium Component