Teknisk artikkel

Bygg en arbeidsbenk for PDF-gjennomgang i Delphi med PDFium-komponenten

En arbeidsstasjon for gjennomgang av innkommende PDF-er er et lite program med én oppgave: se på hver eneste fil før noe nedstrøms får lov til å røre den. For å utføre den oppgaven må den sette sammen en håndfull evner i ett gjennomløp. Den åpner filen (uten å stole på den), leser hva filen selv hevder om seg selv, ser etter innhold som vil villede en naiv utrekker eller bære et angrep, avgjør om det i det hele tatt finnes utrekkbar tekst, og dirigerer deretter dokumentet til en kø basert på det den fant. Hopper man over inspeksjonen, blir feilene stille: en PDF kryptert med eierpassord som pakker inn et XFA-skjema, glir gjennom en tekstutrekker som tomme strenger, blir indeksert som et blankt dokument, og ingen legger merke til det før noen nedstrøms leter etter innhold som aldri ble lest. PDFium Component er et kildekodebasert VCL/LCL-viser- og inspeksjonsbibliotek for Delphi, C++Builder og Lazarus, og det eksponerer introspeksjonskallene denne arbeidsstasjonen trenger. Avsnittene nedenfor går gjennom hvilket kall som besvarer hvilket spørsmål, og de to stedene hvor det opplagte kallet gir deg et selvsikkert feil svar

Fem spørsmål som må besvares før en fil dirigeres

Fjern rutenettet og miniatyrstripen, og inntakstriage reduseres til fem spørsmål:

Diagram over en Delphi PDF inntaksarbeidsbenk som besvarer fem triagespørsmål i én billig åpning, og ruter filer til klar, gjennomgang, blokkert eller skadet tilstand
Inntakstriage svarer fem spørsmål i én billig åpning og ruter filen til klar, gjennomgang, blokkert eller skadet
  • Kan filen i det hele tatt åpnes, og under hvilket passord?
  • Hva hevder den å være: tittel, forfatter, opprettelsesdato?
  • Bærer den aktivt eller risikabelt innhold som JavaScript, et XFA-skjema eller innebygde filer?
  • Finnes det utrekkbar tekst, eller er det en skanning på vei til OCR?
  • Gitt alt dette, hvilken kø får den: rett gjennom-behandling, manuell gjennomgang eller karantene?

Hvert spørsmål tilsvarer ett eller to PDFium Component-kall. To av disse sammenhengene har skarpe hjørner som står for de fleste av de feildirigerte filene jeg har måttet feilsøke i produksjon. Dokumentmetadata finnes to forskjellige steder som kan være uenige, og kryptering hindrer ikke nødvendigvis et dokument fra å åpnes

Åpne billig: skjemautfylling av, null sider rendret

Triage bør være den billigst mulige åpningen. Å sette FormFill := False før Active := True forteller komponenten å hoppe helt over skjemautfyllingsmiljøet. Det forkorter lastetiden, og (like viktig for filer av ukjent opprinnelse) det hindrer all JavaScript på dokumentnivå fra å initialiseres. Ingen av inspeksjonsegenskapene som brukes nedenfor, krever rendering av en side, så et triage-gjennomløp trenger aldri produsere en eneste bitmap

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // ingen skjemamiljø, ingen JavaScript-initiering
    Pdf.Active := True;        // feil er stille: Active forblir bare False

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // skadet fil eller brukerpassord-lås
      Exit;                    // finally-blokken kjører fortsatt
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // la aldri instansen lekke ved en feilformet fil
  end;
end;

Sjekken etter tildelingen er ikke valgfri, og det er en sjekk snarere enn en exception-håndterer av en grunn. Når motoren ikke klarer å laste filen, sluker komponenten den interne EPdfError og lar Active forbli False i stedet for å videreformidle den. Kode som venter på et unntak, vil villig lese PageCount fra et dokument som aldri ble åpnet. Hvis avvisningsarbeidsflyten trenger motorens faktiske feiltekst, må man lese filen inn i et byte-array og kalle LoadDocument-overloaden som tar TBytes; den stien utløser faktisk EPdfError med meldingen, inkludert passord-tilfellet. try..finally fortjener fortsatt sin plass. Inntakstjenester kjører ubemannet i flere uker, og ingen senere unntak får lekke TPdf-instansen eller holde på en lås som gjenforsøksrunden vil snuble over

Gjennomstrømning blir sjelden flaskehalsen. Med skjemautfylling deaktivert og ingen rendering domineres en triage-åpning av I/O, og én enkelt arbeider inspiserer uten problemer flere filer i sekundet fra lokal disk. Hvis inntaksvolumet noen gang vokser fra én arbeider, bør arbeidet partisjoneres etter fil snarere enn etter sjekk. De fem spørsmålene deler én åpning, og å splitte dem over flere prosesser ville mangedoblet det dyreste trinnet i stedet for å amortisere det

Metadata finnes to steder, og de er uenige

ISO 32000-1 definerer to hjem for dokumentmetadata: dokumentets informasjons-dictionary (punkt 14.3.3) og en XMP-pakke knyttet til katalogen (punkt 14.3.2). Egenskapene Title, Author, Subject og CreationDate leser Info-dictionaryet, med MetaText[] for enhver annen nøkkel og DecodeDate for å tolke D:YYYYMMDD...-datostrengen. Fellen er at moderne produsenter i økende grad kun skriver XMP, en retning ISO 32000-2 gjør offisiell ved å fase ut de fleste Info-dictionary-nøkler i PDF 2.0. Symptomet i et inntaksverktøy er konkret. Arbeidsstasjonen din viser en tom tittel mens Adobe Acrobat viser én, fordi Acrobat falt tilbake på dc:title inne i XMP-pakken, som Info-dictionary-egenskapene aldri rører

Diagram over PDF-metadata som bor to steder, Info-ordboken og XMP-pakken, som kan være uenige om tittelen i et Delphi inntaksverktøy
Dokumentmetadata bor i Info-ordboken og i XMP-pakken, og de to hjemmene kan være uenige om tittelen
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // verdi fra Info-dictionary
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // rå PDF-datostreng ("D:2026...")

  // En tom Info-tittel betyr ikke at dokumentet er uten tittel. Komponenten
  // eksponerer ikke XMP-pakken, så undersøk de rå filbytene for
  // dc:title-elementet før den tomme verdien antas å være riktig.
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

Selv den grovkornede substreng-undersøkelsen ovenfor fortjener sin plass: "metadata til stede, men ikke der eldre verktøy leter" er et dirigeringsrelevant faktum for enhver arkivpipeline som indekserer på tittel eller forfatter. Hvis den nedstrøms indeksen din bare leser Info-dictionaryet, blir filer merket på denne måten stille usøkbare

Krypterte filer som likevel åpner

Et kryptert dokument mislykkes ikke nødvendigvis med å åpne. Standardsikkerhetshåndtereren (ISO 32000-1 punkt 7.6.3) skiller mellom et brukerpassord, som kreves for å åpne dokumentet, og et eierpassord som bare regulerer tillatelser som utskrift og kopiering. En stor andel av "beskyttede" forretningsdokumenter er kryptert med et eierpassord og et tomt brukerpassord. De åpner uten å spørre, dekrypterer fullt ut, og er avhengige av at visere frivillig respekterer tillatelsesflaggene. Det er policy, ikke beskyttelse, og inntakstilstandene dine bør gjenspeile forskjellen

Å oppdage kryptering etter en vellykket åpning krever ett motorkall pluss en reserveløsning. FPDF_GetSecurityHandlerRevision(Pdf.Document) returnerer -1 for ubeskyttede filer og ellers håndtererrevisjonen, og at Pdf.Permissions returnerer noe annet enn masken med alle biter satt, $FFFFFFFF, er det bekreftende signalet. For filer som reelt er låst med brukerpassord, tildeles Password før Active := True settes; hvis åpningen fortsatt mislykkes, dirigeres filen til en blokkert tilstand som ber om påloggingsinformasjon fra avsenderen gjennom en sikker kanal i stedet for å prøve på nytt blindt. Og motstå fristelsen til å behandle "kryptert" som automatisk karantene. I de fleste dokumenttunge bransjer er krypterte-men-åpnebare filer normaltilfellet, ikke det mistenkelige

Aktivt innhold: JavaScript, XFA og innebygde filer

Tre funn bør alltid nå frem til dirigeringsbeslutningen. For det første JavaScript: OnUnsupportedFeature-hendelsen rapporterer strukturelle egenskaper som XFA eller 3D-innhold etter hvert som motoren støter på dem, men den oppdager ikke JavaScript. Sjekk i stedet JavaScriptActionCount, og behandl et resultat forskjellig fra null som aktivt innhold. For det andre XFA: når FormType returnerer ftXfaFull, er de synlige sidene ofte lite mer enn en rendering av XFA-malen, og konvensjonell tekstutrekking vil se standardtekst i stedet for de utfylte verdiene. For det tredje vedlegg: en PDF er et containerformat, og AttachmentCount forteller deg om denne har passasjerer med

Diagram over PDF inntak risikosignaler i Delphi: krypteringsbehandler-revisjon, JavaScript-handlingstelling, XFA-skjematype og farlige vedlegg
Krypteringstilstand og JavaScript-, XFA- og vedleggsantallene er signalene som må overleve inn i ruteringsbeslutningen
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 er en egenskap per side; gå gjennom sidene for å
  // summere den. Å laste et sideobjekt rendrer ingenting, så dette forblir billig.
  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;

To detaljer i den løkken fortjener oppmerksomhet. Vedleggsnavnet kommer fra inne i dokumentet, så gjenbruk det aldri som en utdatasti uten å sanere det først; et innebygd navn som ..\..\start.exe er en stitraversering som venter på et uforsiktig lagre-kall. Og en filtype-blokkliste er en snublesnor, ikke en garanti. Jobben dens er å tvinge frem en menneskelig beslutning, ikke å attestere at filen er ren

Å gjøre signaler om til dirigeringstilstander

En brukbar tilstandsmodell trenger færre tilstander enn de fleste team forventer: klar (ingen blokkeringer, tekst til stede), gjennomgang (åpning lyktes, men noe trenger et blikk, som et XFA-skjema, JavaScript, et tomt tekstlag eller en tittel bare i XMP), blokkert (brukerpassord kreves), og skadet (åpning mislyktes). Registrer bevisene sammen med tilstanden. Filens hash, sidetall, de nøyaktige flaggene og motorens feilmelding for skadede filer betyr alle noe, fordi personen som stiller spørsmål ved en dirigeringsbeslutning, vil gjøre det uker senere, mot en fil som i mellomtiden kan ha blitt erstattet eller endret

Når en operatør faktisk trenger å se på en karantenefil, må den ikke overleveres til standard-shell-viseren. Render den inne i en herdet rute med skripting og lenkehåndtering deaktivert, tilnærmingen beskrevet i bygging av en sikker PDF-forhåndsvisningsflate i Delphi. Og hvis inntaket ditt mater et arkiv med samsvarskrav, er triage-gjennomløpet det naturlige stedet å planlegge en dypere kontroll; batch-preflight-validering mot PDF/A- og PDF/UA-profiler tar over nøyaktig der denne inspeksjonen stopper

Komponentens produktside dekker lisensiering, det fulle inspeksjons-API-et og de medfølgende demoene, inkludert en dokumentinspektør i inntaksstil: PDFium Component