PDFium Component for Delphi and Lazarus priskyrus TPdf.Active := True, kai PDF nepakyla, išimtis niekada nekyla: TPdf.SetActive pagavęs visas išimtis palieka komponentą neaktyvų. Norėdami matyti tikrąją klaidą, vietoj to kvieskite TPdf.LoadDocument(Options, Report). Ta perkrova iš naujo pakelia originalią išimtį ir užpildo TPdfLoadReport įkėlimo statusu, natyviuoju PDFium klaidos kodu ir tuo, ar kryžminę atskaitos lentelę teko atstatyti
Problema paprastai iškyla paketiniame kode. Lentelių ištraukimo užduotis pereina 13 realaus pasaulio PDF aplanką su vienu bendru TPdf, ir 7 iš jų grįžta kaip nepavykę. Nė vienas iš tų 7 failų iš tikrųjų nesugadintas. except blokai aplink įkėlimą niekada nesužibs, žurnalas kaltins neteisingus failų vardus, o pirmoji matoma klaida — nuogas EPdfError apie neaktyvų komponentą, pakeltas iš savybės skaitymo keliomis eilutėmis po to, kas realiai nepavyko. Du atskiri elgsenos susikaupia ir pagamina tą paveikslą, ir abi veikia kaip suprojektuota
Kodėl TPdf.Active := True nepakelia išimties, kai PDF nepakyla?
TPdf.SetActive apvelkia LoadDocument su try..except, praryjančiu kiekvieną išimčių klasę, ir tiesiog palieka komponentą neaktyvų. Praryjimas sąmoningas: tas pats setteris vyksta, kai formų dizaineris IDE perjungia Active, o blogas kelias neturi sugriauti IDE. Vykdymo metu TPdf.Active tiesiog praneša, ar egzistuoja natyvusis dokumento handle, tad po nepavykusio įkėlimo jis skaitomas kaip False, ir nieko daugiau neįvyksta. Kas besikėlė — dingsta, ar tai buvo EPdfError iš parserio, srauto klaida ar EAccessViolation iš pusiau prikabinto pdfium.dll. Išsamiosios DLL žinutės, aprašytos pdfium.dll įkėlimo nepavykimų diagnostikoje Delphi, pas jus pasieks tik per kvietimą, kuris jų nepraryja
Pdf.FileName := FileName;
try
Pdf.Active := True; // SetActive praryja bet kokią įkėlimo išimtį
except
on E: Exception do
Log.Add(FileName + ': ' + E.Message); // niekada nevyksta
end;
// Nesėkmė čia vietoj to ir iškyla, kaip bendras EPdfError:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));
// Minimalus esamo kodo pataisymas: patikrinkite Active iškart po priskirimo;
// nuo v3.122.1 LastLoadReport laiko prarytos klaidos tekstą
Pdf.Active := True;
if not Pdf.Active then
Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);
Nesėkmė galiausiai iškyla pirmajame saugotame kvietime. TPdf.PageCount, kaip ir dauguma dokumento savybių, prasideda CheckActive, keliančiu EPdfError, įvardijantį komponentą, bet ne failą ir ne priežastį. Pdf.Active patikrinimas iškart po priskirimo neteisingai priskirtą kritimą paverčia sąžiningu „nepavyko“ įrašu. Iki PDFiumPas v3.122.1 priežastis tuomet būdavo prarandama; nuo v3.122.1 nepavykęs priskirimas LastLoadReport pakeičia plsFailed ataskaita, nešančia klaidos tekstą, tad priežastis išgyvena. Pats išimties objektas ir baitų lygio auditas vis dar reikalauja kito įėjimo taško
Kodėl vieno TPdf pakartotinis naudojimas krinta nuo antro failo?
TPdf.FileName galima priskirti tik neaktyviam komponentui, tad bendras egzempliorius antrąjį failą atmeta dar prieš bandydamas jį įkelti. TPdf.SetFileName prasideda CheckInactive, ir tą patį saugiklį laiko Password bei FormFill. Po pirmos sėkmingos įkelties egzempliorius lieka aktyvus, kitas priskirimas kelia išimtį, ir, jei paketų ciklas tą išimtį pagauna ir eina toliau, klaida nukrenta po naujuoju failo vardu, kol senas dokumentas vis dar atvertas. Sumaišytas su prarytais įkėlimo nepavykimais, žurnalas nustoja atitikti realybę. 13 failų atkūrime bendras egzempliorius pranešė 7 nesėkmes, o šviežias TPdf.Create(nil) kiekvienam dokumentui atvėrė visus 13. Active := False nustatymas tarp failų irgi veikia, bet po vieną egzempliorių dokumentui laiko kiekvieną failą struktūriškai izoliuotą
Ką duoda TPdf.LoadDocument su TPdfLoadReport?
TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) pakelia tikrąją išimtį ir be to pasakoja, kas įvyko, struktūruota forma. Failo perkrova įkelia FileName; seserų perkrovos ima TBytes arba rodyklę ir dydį, o LoadCustomDocument(AStream, AOwnsStream, Options, Report) dengia srautus. Kiekviena iš jų validuoja parinktis, tikrina, jog egzempliorius neaktyvus, paleidžia baitų lygio auditą per antraštę, startxref, xref sekcijas ir %%EOF ženklą, o tada atlieka natyvųjį įkėlimą. Auditas ribojamas tomis pačiomis ribomis, aptartomis parserio išteklių biudžetuose nepatikėtiniems PDF: TPdfLoadOptions.Default nustato AuditByteLimit 256 MiB, MaxIssues 256, MaxXrefSections 1024 ir MaxXrefEntries 4 000 000. Nesėkmės atveju metodas stato Report.Status := plsFailed ir pakelia iš naujo; kadangi Report rašomas vietoje, jo turinys išgyvena išimtį, o kopija saugoma TPdf.LastLoadReport
Ataskaitos laukai atsako į klausimus, kurių paketiniam žurnalui realiai reikia. Status — vienas iš plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected ar plsFailed. NativeErrorCode laiko FPDF_GetLastError, tad FPDF_ERR_PASSWORD (4) nesamą ar neteisingą slaptažodį atskiria nuo pažeisto failo, pranešamo kaip FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid ir RecoveryRoute sako, ar PDFium teko atstatyti xref lentelę, o Issues išvardija kiekvieną audito radinį su Code, Severity, Offset, ObjectNumber ir MessageText, su IssuesTruncated, kai MaxIssues sąrašą nukerpa
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); // vienas egzempliorius dokumentui
try
Pdf.FileName := Files[I];
try
Pdf.LoadDocument(Options, Report);
except
on E: Exception do
begin
// Report užpildytas, nors LoadDocument ir pakėlė
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;
Kada verta įkelti su plmStrict?
plmStrict naudokite visada, kai tyliai sutvarkytas failas blogesnis už atmestąjį — archyvo priėmime, įrodymų tvarkyme ar pasirašymo konvejeryje. PDFium ramiai atkuria sugadintą kryžminę atskaitos lentelę (ISO 32000-1 §7.5.4) skenavęs failą ieškodamas objektų, kas puiku peržiūryklei ir problema bet kam, kas turi apdoroti lygiai tuos baitus, kuriuos gavo. Po natyviosios įkelties komponentas klausia FPDF_DocumentHasValidCrossReferenceTable. plmCompatible režime atstatymas duoda plsLoadedWithRecovery plus plicNativeCrossReferenceRebuild perspėjimą. plmStrict režime komponentas dokumentą iškrauna, stato plsRejected, prideda plicStrictModeRejected ir kelia EPdfError su „Strict PDF load rejected the document“. Griežtas režimas taip pat atmeta bet kokią audito klaidą, o TPdfLoadOptions.Default(plmStrict) įjungia RequireFinalEndOfFileMarker, kuri trūkstamą %%EOF ar duomenis po galutiniojo (§7.5.5) pakelia iš perspėjimo iki klaidos. Xref auditas papildo objektų lygio tikras objektų ir xref srautų validavime su 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; // teisėtas xref, jokių audito klaidų
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;
Kur TPdf.LastLoadReport nustoja sakyti tiesą?
TPdf.LastLoadReport pilnas tik po LoadDocument perkrovos, imančios parinktis, nes tik tos perkrovos paleidžia baitų auditą. Sėkmingas Active := True rašo suderinamojo režimo ataskaitą be baitų audito, tad AuditAttempted lieka False. Iki PDFiumPas v3.122.1 nepavykęs nerašydavo nieko, kas reiškė, jog bendrame egzemplioriuje LastLoadReport vis dar apibūdindavo ankstesnįjį failą, dažnai su guostingu plsLoaded. Nuo v3.122.1 kiekvienas nepavykęs įkėlimas pakeičia ataskaitą: nepavykęs Active := True, vis dar paliekantis komponentą neaktyvų be išimties, ir nepavykęs paprastas LoadDocument ar LoadCustomDocument kvietimas užrašo plsFailed su klaidos tekstu, vėl be audito. Dar du plyšiai praktikoje svarbūs. Parinkčių validacija ir CheckInactive vyksta dar prieš ataskaitos inicijavimą, tad neigiamas AuditByteLimit ar jau aktyvus egzempliorius pakelia išimtį nepagaminęs ataskaitos. Ir NativeErrorCode prasmingas tik tada, kai PDFium realiai bandė išskaidyti; nesamam failui apvalkalas kelia išimtį dar prieš PDFium vykdymą, tad žurnalui rašykite ErrorMessage ir išimties tekstą
Praktinė taisyklė trumpa. Active := True laikykite dizaineriui surištoms peržiūryklėms, kur neaktyvus komponentas — priimtina baigtis. Visur kitur, ir pirmiausia paketiniame bei serveriniame kode, sukurkite po vieną TPdf dokumentui, kvieskite LoadDocument(Options, Report), pagaukite pakeltą išimtį ir žurnalui užrašykite Report.Status, NativeErrorCode ir klaidos lygio Issues kartu su failo vardu. Kaina — keletas eilučių vienam kvietimo taškui, ir kiekviena nesėkmė atitenka teisingam failui su tikrąja priežastimi
Load report API, griežtas režimas ir baitų lygio auditas keliauja kartu su PDFium Component for Delphi, C++Builder and Lazarus, šalia atvaizdavimo, teksto ištraukimo, formų užpildymo ir PDF/A validacijos