PDFium Component iki v3.121.1 perskaičius anotaciją per TPdf.Annotation[] ir grąžinus įrašą atgal, į jos /AP appearance žodyną galėjo atsirasti tuščios /R ir /D gijos, net kai originale buvo tik /N. PDF/A validatoriai tokį žodyną atmeta. Nuo v3.121.1 getter praneša tik tą appearance, kurį iš tiesų perskaitė, tad nepakitęs round trip naujų dalykų nerašo. Verta šią nesėkmę suprasti detaliai, nes įprastas jos trigeris yra pataisymas, ketinęs failą padaryti atitingesnį, o ne mažiau
Kas sugenda, kai anotaciją grąžinate nepakeistą?
Trumpas atsakymas: anotacija pagimdo appearance srautus, kurių niekada neturėjo, ir failas, iki jūsų pakeitimo praeinantis PDF/A validaciją, po jos jos nebepraeina. Tipinis scenarijus vyksta taip. Kliento archyvas atkeliauja su kvadratų ir tekstų anotacijomis be Print vėliavėlės, PDF/A reikalauja, kad kiekviena anotacija spausdintųsi, tad jūs perėjate puslapius, pridedate afPrint ir grąžinate kiekvieną įrašą atgal. Niekas tame kode neliečia appearances. Įrašas iš TPdf.Annotation[] yra TPdfAnnotation, o SetAnnotationData rašo kiekvieną lauką, kurio Has* sentinelas pastatytas, — būtent taip, kaip skirta veikti HasContents / ContentsText poroms. Bėda buvo ta, kad getter HasAppearanceRollover ir HasAppearanceDown statydavo True su tuščiomis eilutėmis režimams, kurie neegzistavo, o setter sąžiningai rašydavo du tuščius srautus:
procedure MarkAnnotationsPrintable(const FileName: string);
var
Pdf: TPdf;
PageNo, I: Integer;
A: TPdfAnnotation;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
for PageNo := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := PageNo;
for I := 0 to Pdf.AnnotationCount - 1 do
begin
A := Pdf.Annotation[I];
if not (afPrint in A.Flags) then
begin
A.Flags := A.Flags + [afPrint] - [afHidden, afInvisible, afNoView];
// Iki v3.121.1 šis priskirimas taip pat rašė tuščias /AP/R ir
// /AP/D srautus, kai šaltinio anotacija turėjo tik /AP/N
Pdf.Annotation[I] := A;
end;
end;
end;
Pdf.SaveAs(ChangeFileExt(FileName, '.printable.pdf'));
finally
Pdf.Free;
end;
end;
ISO 32000-1 §12.5.5 apibrėžia appearance žodyną su trimis raktais: /N normaliajai išvaizdai, /R rollover ir /D down. /R ir /D pasirinktiniai, ir kai jų nėra, peržiūryklė grįžta prie /N. Tuščias /R srautas, tačiau, nėra „nesamas“. Tai teisėtas srautas, nupiešiantis nieko, tad peržiūryklė, gerbianti rollover appearances, akimirksniu, kai žymeklis užslenka virš anotacijos, parodo tuščią stačiakampį. PDF/A griežtesnis dar: ISO 19005-1 (su Corrigendum 2) ir ISO 19005-2 / 19005-3 anotacijos appearance žodyne leidžia tik /N. veraPDF failą, praėjusį round trip, praneša pagal 6.5.3-4 taisyklę PDF/A-1 ir 6.3.3-2 taisyklę PDF/A-2 bei PDF/A-3, o įtaisytasis TPdf.ValidatePdfA jį įrašo kaip pvaiAnnotationApDictViolation. Pakeitimas, pridėjęs Print vėliavėlę dėl vieno standarto punkto, sulaužė kitą
Kodėl FPDFAnnot_GetAP nesamai appearance grąžina 2?
PDFium iš FPDFAnnot_GetAP niekada negrąžina nulio, net kai prašomas appearance srautas neegzistuoja. Funkcija seka įprastą PDFium dviejų kvietimų modelį: perduodate nil buferį, kad gautumėte reikiamą dydį baitais, skirtinate, tada kviečiate dar kartą tekstui UTF-16LE nukopijuoti. Dydis visada įskaito UTF-16 terminatorių, tad nesamas srautas praneša 2 baitus — tuščia eilutė su jos terminatoriumi. Iki v3.121.1 getter tikrindavo ByteLength >= SizeOf(FPDF_WCHAR) — tikrinimą, kurį praeina kiekvienas kvietimas, tad visos trys HasAppearance* vėliavėlės sugrįždavo True bet kuriai anotacijai su bet kokia appearance apskritai. Round trip per įrašą tada prašydavo FPDFAnnot_SetAP kiekvienam režimui įrašyti tuščią eilutę, ir PDFium sukurdavo srautą jai laikyti. Jokios išimties, jokio įspėjimo, ir matomas puslapis atrodė identiškai — todėl defektas iškilo veraPDF fixture, o ne peržiūryklėje
Kaip v3.121.1 sprendžia, jog appearance egzistuoja
ReadAppearance, pagelbiklis GetPageAnnotation viduje, užpildantis AppearanceNormal, AppearanceRollover ir AppearanceDown, dabar rezultatą laiko turiniu tik tada, kai jis neša bent vieną simbolį virš terminatoriaus. Pirmasis kvietimas turi grąžinti daugiau nei SizeOf(FPDF_WCHAR) baitų ir lyginį baitų skaičių, nes nelyginis ilgis negali būti UTF-16. Antrasis kvietimas, kuris realiai nukopijuoja tekstą, validuojamas dar kartą: grąžintas ilgis 2 ar mažiau, arba didesnis už skirtąjį buferį, nustato HasValue atgal į False ir palieka eilutę tuščią. Rašymo pusėje niekas nesikeitė. SetAnnotationData vis dar kviečia FPDFAnnot_SetAP tik tiems režimams, kurių HasAppearance* vėliavėlė True, tad įrašas, perskaitytas iš anotacijos su vien /N, dabar grąžina tik /N. Regresijos fixture dengia abi kryptis: kvadrato anotacija su normaliąja appearance, perskaityta ir nepakitus grąžinta, praeina PDF/A-1b, PDF/A-2b ir PDF/A-3b, o ta pati anotacija su nuimta Print vėliavėle krenta ant tikėtinos vėliavėlių taisyklės ir nieko kito
Nesami ir tuščia srautai atrodo identiškai, tad getter lieka konservatyvus
Natyvioji API nesamą appearance srautą nuo esamo, bet tuščio atskirti negali, ir PDFium Component to nepretenduoja. Abu atvejai iš FPDFAnnot_GetAP grąžina tuos pačius 2 baitus, tad abu perskaitomi kaip HasAppearanceRollover = False su tuščia AppearanceRollover. Iš to dvi pasekmės, aplink kurias derėtų projektuoti. Pirma, False sentinelas reiškia „turinio neskaityta, tad grąžinant šį režimą paliksiu ramybėje“, o ne „/R rakto žodyne nėra“. Antra, įrašas negali aptikti tuščio srauto, kuris jau yra faile: senesnio build ar kito įrankio pažeistas dokumentas perskaitomas švarus, ir įrašo grąžinimas jo nei sutvarko, nei pablogina. Tokiems failams rasti reikia baitų lygio patikros — tam skirti TPdf.ValidatePdfA ir PDF/A preflight validacijos darbo eiga su PDFium Component
Kaip appearance išvalyti tyčia?
Statote sentinelą aiškiai ir perduodate tuščią eilutę; setter ją parašo. Tuščių eilučių draudimas SetAnnotationData viduje būtų buvęs akliškas šio bugo pataisymas, bet jis sulaužytų ir tuos kvietėjus, kurie appearance išvalo sąmoningai — tą patį kontraktą, kurio dėl teksto laikosi HasContents ir HasAuthor. Todėl pataisymas apsigyvena vien getter pusėje, o setter toliau gerbia tai, ko prašo kvietėjas:
// Pakeiskite rollover appearance, tada vėl jį išvalykite
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// A.HasAppearanceRollover yra True, o tekstas praėjęs abi kryptim yra 'q Q'
A.HasAppearanceRollover := True; // aiškiai pakartokite ketinimą
A.AppearanceRollover := ''; // tyčia parašykite tuščią srautą
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// Perskaitęs grįžta kaip HasAppearanceRollover = False su tuščia eilute:
// tuščias ir nesamas srautai čia neatskiriami
Turėkite omenyje, kad aiškiai išvalytas /R ar /D pagal aukščiau cituotas PDF/A taisykles vis tiek skaitomas kaip papildomas raktas. Jei tikslas — archyvinis profilis, vienintelė validuojasi forma yra ne tuščias /N ir kiti du režimai, palikti ramybėje. Bet kokia darbo eiga, perkelianti anotacijas tarp dokumentų, pavyzdžiui XFDF eksportas ir importas su PDFium Component, turėtų sekti tą pačią taisyklę: kopijuokite režimus, kuriuos realiai turėjo šaltinis, ir palikite kitus sentinelus False
Read-modify-write modelis, liekantis PDF/A saugus
Atsinaujinkite į v3.121.1 ar naujesnę, palikite appearance sentinelus lygiai tokiais, kokiais juos grąžino getter, ir prieš išsiunčiant patikrinkite išsaugotą failą. Kadangi pasenęs tuščias srautas perskaitomas kaip nesamas, patikros žingsnis turi žiūrėti į serializuotą dokumentą, o ne į įrašą, ir jis pigus, kad būtų paleidžiamas po kiekvienos partijos:
uses
PDFium, FPdfPdfa; // FPdfPdfa deklaruoja TPdfAValidationIssue
function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
Report: TPdfAValidationResult;
begin
// Validuoja dokumentą, dabar įkeltą Pdf viduje, įskaitant pakeitimus,
// atliktus per Pdf.Annotation[] nuo atvėrimo
Report := Pdf.ValidatePdfA;
Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;
Ta pati drausmė tinka bet kuriai juostai, perdažančiai ar komentuojančiai puslapius peržiūrai — darbo eigai, aprašytai kaip kurti Delphi anotacijų peržiūros darbo eigą su PDFium Component: įrašas yra momentinė to, ką variklis sugebėjo perskaityti, nuotrauka, ir sentinelas, kurio patys nustatėte, turi grįžti nepakeistas. Pilnas anotacijų API, PDF/A preflight ir natyvus PDFium variklis keliauja kartu PDFium Component for Delphi, C++Builder and Lazarus pakete