Робочий стіл прийому й перевірки PDF — це невелика програма з одним завданням: оглянути кожен файл, перш ніж щось нижче за потоком отримає дозвіл його торкнутися. Щоб виконати це завдання, вона мусить зібрати жменьку можливостей в один прохід. Вона відкриває файл (не довіряючи йому), читає, що файл заявляє про себе, шукає вміст, який введе в оману наївний екстрактор чи нестиме атаку, вирішує, чи є взагалі якийсь текст, придатний для вилучення, а тоді спрямовує документ у чергу на основі знайденого. Пропустіть інспекцію — і збої стануть тихими: PDF, зашифрований паролем власника, що загортає форму 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; // без середовища форм, без ініціалізації JavaScript
Pdf.Active := True; // збій тихий: Active просто лишається False
if not Pdf.Active then
begin
Rec.OpenFailed := True; // пошкоджений файл або блокування паролем користувача
Exit; // блок finally все одно виконується
end;
Rec.PageCount := Pdf.PageCount;
CollectIdentity(Pdf, IncomingPath, Rec);
CollectRiskSignals(Pdf, Rec);
finally
Pdf.Active := False;
Pdf.Free; // ніколи не витікайте екземпляр на пошкодженому файлі
end;
end;
Перевірка після присвоєння не опційна, і це саме перевірка, а не обробник винятку, не просто так. Коли рушій не може завантажити файл, компонент проковтує внутрішній EPdfError і залишає Active на False замість того, щоб пропагувати його. Код, що чекає на виняток, радо прочитає PageCount з документа, який ніколи не відкрився. Якщо робочому процесу відхилення потрібен фактичний текст помилки рушія, зчитайте файл у масив байтів і викличте перевантаження LoadDocument, яке бере TBytes; цей шлях справді підіймає EPdfError із повідомленням, включно з випадком пароля. try..finally все ще заслуговує на своє місце. Служби прийому працюють без нагляду тижнями, і жоден пізніший виняток не повинен допустити витік екземпляра TPdf чи утримати блокування, об яке спіткнеться прохід повторної спроби
Пропускна здатність рідко стає вузьким місцем. Коли заповнення форм вимкнене й немає рендеру, у відкритті для сортування домінує введення-виведення, і один робочий процес комфортно перевіряє кілька файлів за секунду з локального диска. Якщо обсяг прийому колись переросте одного робочого, розподіляйте роботу за файлом, а не за перевіркою. П'ять питань ділять одне відкриття, і розділення їх між процесами лише помножило б найдорожчий крок замість того, щоб його амортизувати
Метадані живуть у двох місцях, і вони не збігаються
ISO 32000-1 визначає два домівки для метаданих документа: словник інформації про документ (пункт 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
Rec.Author := Pdf.Author;
Rec.CreatedAt := Pdf.CreationDate; // сирий рядок дати PDF ("D:2026...")
// Порожня назва в Info не означає, що документ без назви — цей
// компонент не розкриває пакет XMP, тож перевірте сирі байти файлу
// на елемент dc:title, перш ніж довіряти порожньому значенню.
if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
Include(Rec.Flags, ifTitleInXmpOnly);
end;
Навіть груба перевірка підрядка вище заробляє своє місце: «метадані присутні, але не там, де їх шукають застарілі інструменти» — це релевантний для маршрутизації факт для будь-якого архівного конвеєра, що індексує за назвою чи автором. Якщо ваш подальший індекс читає лише словник Info, файли, позначені так, тихо стануть непридатними для пошуку
Зашифровані файли, які все одно відкриваються
Зашифрований документ не обов'язково не відкриється. Стандартний обробник безпеки (ISO 32000-1, пункт 7.6.3) розрізняє пароль користувача, потрібний для відкриття документа, і пароль власника, який лише регулює дозволи, такі як друк і копіювання. Велика частка «захищених» бізнес-документів зашифровані паролем власника й порожнім паролем користувача. Вони відкриваються без запиту, повністю розшифровуються й покладаються на те, що переглядачі добровільно поважатимуть прапорці дозволів. Це політика, а не захист, і ваші стани прийому мають відображати цю різницю
Виявлення шифрування після успішного відкриття потребує одного виклику рушія плюс запасний варіант. FPDF_GetSecurityHandlerRevision(Pdf.Document) повертає -1 для незахищених файлів і ревізію обробника в іншому разі, а Pdf.Permissions, що повертає щось відмінне від маски з усіма встановленими бітами $FFFFFFFF, є підтверджувальним сигналом. Для файлів, справді заблокованих паролем користувача, присвоюйте Password перед встановленням Active := True; якщо відкриття все одно провалюється, спрямуйте файл у заблокований стан, що запитує облікові дані у відправника через безпечний канал, а не сліпо повторює спроби. І опирайтеся спокусі трактувати «зашифрований» як автоматичний карантин. У більшості документоємних галузей зашифровані, але відкривані файли — нормальний випадок, а не підозрілий
Активний вміст: JavaScript, XFA та вбудовані файли
Три знахідки завжди повинні впливати на рішення про маршрутизацію. По-перше, JavaScript: подія OnUnsupportedFeature повідомляє про структурні можливості, такі як XFA чи 3D-вміст, коли рушій на них натрапляє, але вона не виявляє JavaScript. Натомість перевіряйте JavaScriptActionCount і трактуйте ненульовий результат як активний вміст. По-друге, XFA: коли FormType повертає ftXfaFull, видимі сторінки часто є мало чим більшим за рендер шаблону XFA, і звичайне вилучення тексту побачить шаблонний текст замість заповнених значень. По-третє, вкладення: 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 — це властивість на рівні сторінки; пройдіться сторінками, щоб
// підсумувати. Завантаження об'єкта сторінки нічого не рендерить, тож це лишається дешевим.
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 — це обхід шляху, що чекає на необережний виклик збереження. І блоклист розширень — це розтяжка, а не гарантія. Його робота — змусити людину прийняти рішення, а не засвідчити, що файл чистий
Перетворення сигналів на стани маршрутизації
Робоча модель станів потребує менше станів, ніж очікує більшість команд: готовий (немає блокерів, текст присутній), на перевірці (відкриття вдалося, але щось потребує уваги, як-от форма XFA, JavaScript, порожній текстовий шар чи назва лише в XMP), заблокований (потрібен пароль користувача) і пошкоджений (відкриття не вдалося). Записуйте докази поряд зі станом. Хеш файлу, кількість сторінок, точні прапорці й повідомлення про помилку рушія для пошкоджених файлів — усе це важливо, бо людина, яка поставить під сумнів рішення про маршрутизацію, зробить це тижнями пізніше, стосовно файлу, який відтоді, можливо, замінили чи змінили
Коли оператору справді потрібно поглянути на файл у карантині, не віддавайте його типовому переглядачу оболонки. Рендерте його всередині захищеної панелі з вимкненим виконанням скриптів та обробкою посилань — підхід, описаний у побудові безпечної поверхні попереднього перегляду PDF у Delphi. А якщо ваш прийом живить архів із вимогами відповідності, прохід сортування — природне місце, щоб запланувати глибшу перевірку; пакетна перевірка preflight за профілями PDF/A і PDF/UA підхоплює точно там, де ця інспекція зупиняється
Сторінка продукту компонента охоплює ліцензування, повний API інспекції та комплектні демо, включно з інспектором документів у стилі прийому: PDFium Component