Technical Article

Изграждане на работна среда за приемане и преглед на PDF в Delphi с PDFium Component

Работната среда за приемане и преглед на PDF е малка програма с една задача: да провери всеки файл, преди каквито и да било следващие процеси да получат достъп до него. За да направи това, тя трябва да обедини няколко възможности в един обход. Тя отваря файла (без да му се доверява сляпо), чете това, което файлът твърди за себе си, търси съдържание, което би подвело прост алгоритъм за извличане на текст или би пренесло атака, определя дали изобщо има текст за извличане и след това насочва документа към съответната опашка въз основа на намереното. Ако пропуснете тази проверка, грешките ще останат незабелязани: PDF файл, защитен с парола на собственика (owner password) и съдържащ XFA формуляр, преминава през процеса на извличане на текст като празни низове, индексира се като празен документ и никой не забелязва, докато потребител по-нататък не потърси съдържание, което всъщност никога не е било прочетено. PDFium Component е библиотека за преглед и инспекция с изходен код за VCL/LCL за Delphi, C++Builder и Lazarus, и тя предоставя извикванията за интроспекция, от които се нуждае тази работна среда. Разделите по-долу описват кое извикване на кой въпрос отговаря, както и двете места, където очевидното извикване ви дава напълно погрешен отговор.

Пет въпроса, на които да отговорите, преди файлът да бъде насочен

Ако премахнете мрежата с изображения и лентата с миниатюри, сортирането при приемане се свежда до пет въпроса:

  • Може ли изобщо да се отвори файлът и с коя парола?
  • За какъв се представя файлът: заглавие, автор, дата на създаване?
  • Съдържа ли активно или рисково съдържание като JavaScript, XFA формуляр или вградени файлове?
  • Има ли текст за извличане, или е сканирано изображение, предназначено за OCR?
  • Като се има предвид всичко това, коя опашка го поема: директна обработка, ръчен преглед или карантина?

Всеки въпрос съответства на едно или две извиквания в PDFium Component. Две от тези съответствия имат своите особености, които са причина за повечето неправилно насочени файлове, които е трябвало да дебъгвам в реални условия. Метаданните на документа живеят на две различни места, които могат да се разминават, а криптирането не винаги пречи на документа да се отвори.

Икономично отваряне: изключено попълване на формуляри и без рендиране на страници

Сортирането трябва да отваря файла по възможно най-икономичния начин. Задаването на FormFill := False преди Active := True указва на компонента да прескочи изцяло средата за попълване на формуляри. Това съкращава времето за зареждане и (което е също толкова важно за файлове с неизвестен произход) предотвратява инициализирането на какъвто и да е JavaScript на ниво документ. Никое от свойствата за инспекция, използвани по-долу, не изисква рендиране на страница, така че процесът на сортиране никога не произвежда растерни изображения.

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;

Проверката след присвояването не е опционална и тя е проверка, а не обработчик на изключения по конкретна причина. Когато енджинът не може да зареди файла, компонентът потиска вътрешната грешка EPdfError и оставя Active на False, вместо да я разпространява нагоре. Код, който чака изключение, с удоволствие ще прочете PageCount от документ, който никога не се е отворил. Ако работният процес по отхвърляне се нуждае от действителния текст на грешката от енджина, прочетете файла в байтов масив и извикайте претоварения метод LoadDocument, който приема TBytes â€?този път наистина предизвиква EPdfError със съответното съобщение (включително при грешка с парола). Блокът try..finally също има важно място тук. Услугите за приемане на файлове работят без надзор с седмици и никое изключение не трябва да води до изтичане на инстанцията на TPdf или да държи заключен файл, в който следващият опит ще се спъне.

Пропускателната способност рядко се превръща в пречка. При изключено попълване на формуляри и без рендиране, отварянето при сортиране се ограничава основно от входно-изходните операции (I/O) и един-единствен работен процес лесно проверява по няколко файла на секунда от локалния диск. Ако обемът на входящите документи надхвърли възможностите на един процес, разделете работата по файлове, а не по видове проверки. Петте въпроса споделят едно отваряне и разделянето им в различни процеси би умножило най-скъпата стъпка, вместо да разпредели разходите за нея.

Метаданните живеят на две места и те се разминават

Стандартът ISO 32000-1 дефинира две места за съхранение на метаданните на документа: речника с информация за документа (document information dictionary â€?клауза 14.3.3) и XMP пакет, прикачен към каталога (каталог â€?клауза 14.3.2). Свойствата Title, Author, Subject и CreationDate четат речника Info, като за всеки друг ключ се използва MetaText[], а DecodeDate се грижи за анализирането на низа с дата във формат D:YYYYMMDD.... Уловката е, че съвременните софтуерни инструменти все по-често записват метаданни само в XMP пакета â€?посока, която ISO 32000-2 прави официална, като премахва повечето ключове от речника Info в PDF 2.0. Симптомът в инструмента за приемане е съвсем ясен: работната ви среда показва празно заглавие, докато Adobe Acrobat показва такова, тъй като Acrobat се е върнал към dc:title вътре в XMP пакета, който свойствата на речника Info изобщо не докосват.

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;

Дори грубото търсене на подниз по-горе си струва усилията: „наличнÐ?метаданни, но не там, където търсят по-старите инструментиâ€?е факт, важен за насочването в рамките на всеки архив, който индексира документи по заглавие или автор. Ако вашият индекс след това чете само речника Info, файловете, маркирани по този начин, тихомълком ще станат неоткриваеми при търсене.

Криптирани файлове, които се отварят въпреки това

Криптираният документ не винаги се проваля при отваряне. Стандартният механизъм за сигурност (ISO 32000-1, клауза 7.6.3) разграничава потребителската парола (user password), необходима за отваряне на документа, от паролата на собственика (owner password), която само ограничава права като печат и копиране. Голяма част от „защитенитеâ€?бизнес документи са криптирани с парола на собственика и празна потребителска парола. Те се отварят без подкана, декриптират се напълно и разчитат на това, че визуализаторите ще изберат да зачетат флаговете за права. Това е политика, а не реална защита, и състоянията на процеса ви на приемане трябва да отразяват тази разлика.

Откриването на криптиране след успешно отваряне изисква едно извикване към енджина плюс резервен вариант. FPDF_GetSecurityHandlerRevision(Pdf.Document) връща -1 за незащитени файлове и съответната версия на механизма в противен случай, а връщането на маска, различна от пълната $FFFFFFFF от Pdf.Permissions, е потвърждаващият сигнал. За наистина заключени с потребителска парола файлове, присвоете Password преди да зададете Active := True â€?ако отварянето все още се проваля, насочете файла към състояние на блокиран, което изисква данни за достъп от подателя през защитен канал, вместо да опитвате сляпо отново. И избягвайте изкушението да третирате всяко „криптиранеâ€?като повод за незабавна карантина. В повечето сфери, работещи с голям обем документи, криптираните, но лесни за отваряне файлове са обичаен случай, а не подозрителен.

Активно съдържание: JavaScript, XFA и вградени файлове

Три констатации винаги трябва да влияят на решението за насочване. Първо, JavaScript: събитието OnUnsupportedFeature съобщава за структурни характеристики като XFA или 3D съдържание, когато енджинът ги срещне, но не засича JavaScript. Вместо това проверете JavaScriptActionCount и третирайте ненулев резултат като активно съдържание. Второ, XFA: когато FormType върне ftXfaFull, видимите страници често са просто изобразяване на XFA шаблона и конвенционалното извличане на текст ще прочете общи шаблони (boilerplate) вместо попълнените стойности. Трето, прикачени файлове (attachments): PDF е контейнерен формат и AttachmentCount ви показва дали този файл носи други със себе си.

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;

Два детайла в този цикъл заслужават внимание. Името на приложения файл идва от вътрешността на документа, така че никога не го използвайте като изходен път без предварително саниране â€?вградено име като ..\..\start.exe е уязвимост за преминаване по пътя (path traversal), чакаща невнимателно записване на файл. Освен това списъкът с забранени разширения е само предупредителен сигнал, а не гаранция. Неговата задача е да наложи вземането на човешко решение, а не да сертифицира файла като безопасен.

Превръщане на сигналите в състояния на насочване

Един работещ модел на състоянията се нуждае от по-малко стъпки, отколкото повечето екипи очакват: ready (готов â€?без пречки, наличен текст), review (за преглед â€?отварянето е успешно, но нещо изисква внимание, например XFA формуляр, JavaScript, празен текстов слой или заглавие само в XMP), blocked (блокиран â€?необходима е потребителска парола) и damaged (повреден â€?отварянето е неуспешно). Записвайте доказателствата заедно със състоянието. Хешът на файла, броят на страниците, точните флагове и съобщението за грешка от енджина за повредени файлове â€?всичко това е от значение, тъй като човекът, който оспорва решението за насочване, ще го направи седмици по-късно, срещу файл, който междувременно може да е бил заменен или променен.

Когато се налага оператор да разгледа карантиниран файл, не го предавайте на стандартния визуализатор на системата. Рендирайте го в защитен панел с изключени скриптове и управление на връзките â€?подход, описан в статията за изграждане на защитена повърхност за предварителен преглед на PDF в Delphi. А ако вашият процес захранва архив с изисквания за съответствие, проверката при приемане е естественото място за насрочване на по-подробен анализ; статията за пакетна префлайт (preflight) валидация спрямо PDF/A и PDF/UA профили продължава точно оттам, където тази инспекция спира.

Продуктовата страница на компонента обхваща лицензирането, пълния приложен програмен интерфейс (API) за инспекция и включените демонстрационни проекти, включително инспектор на документи за приемане: PDFium Component.