У PDFium Component для Delphi і Lazarus присвоєння TPdf.Active := True ніколи не підіймає, коли PDF провалює завантаження: TPdf.SetActive ловить кожен виняток і лишає компонент неактивним. Щоб побачити справжню помилку, викличте натомість TPdf.LoadDocument(Options, Report). Той перевантажений виклик повторно підіймає оригінальний виняток і заповнює TPdfLoadReport статусом завантаження, нативним кодом помилки PDFium і тим, чи довелося перебудовувати таблицю cross-reference
Проблема зазвичай вилазить у batch-коді. Задача вилучення таблиць обходить теку з 13 реальних PDF одним спільним TPdf, і 7 з них повертаються як збої. Жоден із тих 7 файлів насправді не зламаний. Блоки except довкола завантаження ніколи не спрацьовують, лог звинувачує неправильні імена файлів, а перша видима помилка — голий EPdfError про неактивний компонент, піднятий з читання властивості кількома рядками після завантаження, яке справді провалилося. Дві окремі поведінки накладаються, даючи ту картину, і обидві працюють як задумано
Чому TPdf.Active := True не підіймає, коли PDF провалює завантаження?
TPdf.SetActive загортає LoadDocument у try..except, що ковтає кожен клас винятків і просто лишає компонент неактивним. Ковтання навмисне: той самий сетер працює, коли дизайнер форм клацає Active в IDE, і поганий шлях не мусить валити IDE. У run time TPdf.Active просто звітує, чи існує нативний хендл документа, тож після проваленого завантаження він читається False, і більше нічого не відбувається. Все, що було піднято, зникло — чи то EPdfError від парсера, чи помилка потоку, чи EAccessViolation від напівприв'язаної pdfium.dll. Детальні DLL-повідомлення, описані в статті про діагностику збоїв завантаження pdfium.dll у Delphi, доходять до вашого хендлера лише через виклик, який їх не ковтає
Pdf.FileName := FileName;
try
Pdf.Active := True; // SetActive ковтає будь-який виняток завантаження
except
on E: Exception do
Log.Add(FileName + ': ' + E.Message); // ніколи не виконується
end;
// Збій вилазить тут замість цього, як загальний EPdfError:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));
// Мінімальний фікс для наявного коду: тестуйте Active одразу після присвоєння;
// від v3.122.1 LastLoadReport тримає текст проковтнутої помилки
Pdf.Active := True;
if not Pdf.Active then
Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);
Збій зрештою вилазить на першому захищеному виклику. TPdf.PageCount, як і більшість властивостей документа, стартує з CheckActive, який підіймає EPdfError, називаючи компонент, але не файл і не причину. Тестування Pdf.Active одразу після присвоєння перетворює краш із чужою атрибуцією на чесний запис «failed». До PDFiumPas v3.122.1 причина втрачалася в той момент; від v3.122.1 провалене присвоєння замінює LastLoadReport звітом plsFailed, що несе текст помилки, тож причина виживає. Сам об'єкт винятку і байтовий аудит досі потребують іншої точки входу
Чому повторне використання одного TPdf провалюється від другого файлу?
TPdf.FileName можна присвоювати лише поки компонент неактивний, тож спільний інстанс відкидає другий файл ще до того, як спробує його завантажити. TPdf.SetFileName стартує з CheckInactive, і той самий охоронець захищає Password і FormFill. Після першого успішного завантаження інстанс лишається активним, наступне присвоєння підіймає, і якщо batch-петля ловить той виняток і рухається далі, помилка приземляється під новим іменем файлу, поки старий документ досі відкритий. У суміші з проковтнутими збоями завантаження лог перестає збігатися з реальністю. У 13-файловій репродукції спільний інстанс звітував 7 збоїв, тоді як свіжий TPdf.Create(nil) на документ відкривав усі 13. Установка Active := False між файлами теж працює, але один інстанс на документ тримає кожен файл ізольованим за конструкцією
Що дає TPdf.LoadDocument з TPdfLoadReport?
TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) підіймає справжній виняток і ще розповідає, що сталося, у структурній формі. Файловий перевантажений виклик завантажує FileName; побратими беруть TBytes чи вказівник і розмір, а LoadCustomDocument(AStream, AOwnsStream, Options, Report) покриває потоки. Кожен валідує опції, перевіряє, що інстанс неактивний, ганяє байтовий аудит заголовка, startxref, секцій xref і маркера %%EOF, а потім виконує нативне завантаження. Аудит обмежений того сорту межами, які розбирає стаття про ресурсні бюджети парсера для недовірених PDF: TPdfLoadOptions.Default ставить AuditByteLimit у 256 МіБ, MaxIssues у 256, MaxXrefSections у 1024 і MaxXrefEntries у 4 000 000. При провалі метод ставить Report.Status := plsFailed і повторно підіймає; оскільки Report пишеться на місці, його вміст переживає виняток, а копія зберігається в TPdf.LastLoadReport
Поля звіту відповідають на питання, які batch-лог справді потребує. Status — одне з plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected чи plsFailed. NativeErrorCode тримає FPDF_GetLastError, тож FPDF_ERR_PASSWORD (4) відділяє відсутній чи неправильний пароль від пошкодженого файлу, звітованого як FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid і RecoveryRoute кажуть, чи мусив PDFium перебудувати таблицю xref, а Issues перелічує кожну знахідку аудиту з Code, Severity, Offset, ObjectNumber і MessageText, з установленим IssuesTruncated, коли MaxIssues обрізав список
uses
SysUtils, Classes, TypInfo, FPdfView, PDFium;
procedure ProcessBatch(Files, Log: TStrings);
var
I: Integer;
Pdf: TPdf;
Options: TPdfLoadOptions;
Report: TPdfLoadReport;
begin
Options := TPdfLoadOptions.Default(plmCompatible);
for I := 0 to Files.Count - 1 do
begin
Pdf := TPdf.Create(nil); // один інстанс на документ
try
Pdf.FileName := Files[I];
try
Pdf.LoadDocument(Options, Report);
except
on E: Exception do
begin
// Report заповнений, хоча LoadDocument підняв
if Report.NativeErrorCode = FPDF_ERR_PASSWORD then
Log.Add(Files[I] + ': password required')
else
Log.Add(Format('%s: %s (%s)', [Files[I],
GetEnumName(TypeInfo(TPdfLoadStatus), Ord(Report.Status)),
E.Message]));
Continue;
end;
end;
if Report.UsedRecovery then
Log.Add(Files[I] + ': opened after PDFium rebuilt the xref table');
ExtractTables(Pdf, Log);
finally
Pdf.Free;
end;
end;
end;
Коли варто завантажувати з plmStrict?
Користуйтеся plmStrict щоразу, коли мовчки відремонтований файл гірший за відхилений, — скажімо, приймання до архіву, робота з доказами чи конвеєр підписування. PDFium тихо реконструює зламану таблицю cross-reference (ISO 32000-1 §7.5.4), скануючи файл на предмет об'єктів, — чудово для переглядача і проблема для всього, що мусить обробити рівно ті байти, які йому дали. Після нативного завантаження компонент питає FPDF_DocumentHasValidCrossReferenceTable. У режимі plmCompatible перебудова дає plsLoadedWithRecovery плюс попередження plicNativeCrossReferenceRebuild. У режимі plmStrict компонент вивантажує документ, ставить plsRejected, додає plicStrictModeRejected і підіймає EPdfError із «Strict PDF load rejected the document». Суворий режим також відкидає будь-яку помилку аудиту, а TPdfLoadOptions.Default(plmStrict) вмикає RequireFinalEndOfFileMarker, що підвищує відсутній %%EOF чи дані після фінального (§7.5.5) з попередження до помилки. Аудит xref доповнює перевірки на рівні об'єктів зі статті про валідацію object- і xref-потоків з PDFium VCL
function AcceptForArchive(const FileName: string; out Reason: string): Boolean;
var
Pdf: TPdf;
Report: TPdfLoadReport;
I: Integer;
begin
Result := False;
Reason := '';
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
try
Pdf.LoadDocument(TPdfLoadOptions.Default(plmStrict), Report);
Result := True; // валідний xref, без помилок аудиту
except
on E: EPdfError do
begin
Reason := E.Message;
for I := 0 to High(Report.Issues) do
if Report.Issues[I].Severity = plisError then
Reason := Reason + sLineBreak + Format(' at offset %d: %s',
[Report.Issues[I].Offset, Report.Issues[I].MessageText]);
end;
end;
finally
Pdf.Free;
end;
end;
Де TPdf.LastLoadReport перестає говорити правду?
TPdf.LastLoadReport повний лише після перевантаженого виклику LoadDocument, що бере опції, бо лише ті перевантаження ганяють байтовий аудит. Успішне Active := True пише звіт у compatible-режимі без байтового аудиту, тож AuditAttempted лишається False. До PDFiumPas v3.122.1 провалене не писало нічого, що означало: на спільному інстансі LastLoadReport досі описував попередній файл, часто з заспокійливим plsLoaded. Від v3.122.1 кожне провалене завантаження замінює звіт: провалене Active := True, яке досі лишає компонент неактивним без підйому, і провалений звичайний виклик LoadDocument чи LoadCustomDocument записують plsFailed з текстом помилки, знову без аудиту. Ще дві прогалини важать на практиці. Валідація опцій і CheckInactive працюють до ініціалізації звіту, тож від'ємний AuditByteLimit чи вже активний інстанс підіймають, не виробляючи звіту. І NativeErrorCode осмислений лише коли PDFium справді пробував розбір; для відсутнього файлу обгортка підіймає до запуску PDFium, тож логуйте ErrorMessage і текст винятку замість нього
Практичне правило коротке. Тримайте Active := True для переглядачів, прив'язаних до дизайнера, де неактивний компонент — прийнятний наслідок. Усюди інше, і передусім у batch- та серверному коді, створюйте один TPdf на документ, викликайте LoadDocument(Options, Report), ловіть виняток, який він підіймає, і логуйте Report.Status, NativeErrorCode і помилкові Issues разом з іменем файлу. Ціна — кілька рядків на місце виклику, і кожен збій отримує атрибуцію правильному файлу зі справжньою причиною
API звіту завантаження, суворий режим і байтовий аудит виходять з PDFium Component для Delphi, C++Builder і Lazarus, поруч з рендерингом, вилученням тексту, заповненням форм і валідацією PDF/A