A PDFium Component v3.121.1 előtt egy annotáció TPdf.Annotation[]-en át való kiolvasása és a rekord visszaírása üres /R és /D bejegyzéseket adhatott hozzá az /AP megjelenésszótárához, akkor is, ha az eredeti csak /N-t hordozott. Ezt a szótárt a PDF/A validátorok elutasítják. A v3.121.1 óta a getter csak akkor jelent megjelenést, ha valóban olvasott, így egy változatlan round trip semmi újat nem ír. A hiba megérdemli a részletes megértést, mert a megszokott kiváltó ok egy olyan javítás, amely a fájlt szabálykövetőbbé akarta tenni, nem kevésbé
Mi romlik el, ha egy annotációt változatlanul ír vissza?
A rövid válasz: az annotáció olyan megjelenésstreameket kap, amelyekkel sosem rendelkezett, és egy fájl, amely a szerkesztése előtt átment a PDF/A ellenőrzésen, utána megbukik rajta. A tipikus forgatókönyv így fut. Egy vevői archívum olyan négyzet- és szöveges annotációkkal érkezik, amelyekből hiányzik a Print jelző, a PDF/A megköveteli minden annotáció nyomtatását, ezért végigsétál az oldalakon, hozzáadja az afPrint-et, és minden rekordot visszaír. Az a kód semmilyen megjelenéshez nem nyúl. A TPdf.Annotation[]-ből érkező rekord egy TPdfAnnotation, a SetAnnotationData pedig minden mezőt kiír, amelynek Has* őrzője be van állítva — pontosan úgy működnek a HasContents / ContentsText párok, ahogy kell. A baj az volt, hogy a getter HasAppearanceRollover-t és HasAppearanceDown-t True-ra állította üres karakterláncokkal a nem létező módokhoz, a setter pedig lelkiismeretesen kiírott két üres streamet:
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];
// A v3.121.1 előtt ez a hozzárendelés üres /AP/R és
// /AP/D streameket is írt, ha a forrás annotációnak csak /AP/N-je volt
Pdf.Annotation[I] := A;
end;
end;
end;
Pdf.SaveAs(ChangeFileExt(FileName, '.printable.pdf'));
finally
Pdf.Free;
end;
end;
Az ISO 32000-1 §12.5.5 három bejegyzéssel definiálja a megjelenésszótárat: /N a normál megjelenéshez, /R a rolloverhez, /D a lenyomáshoz. A /R és a /D opcionális, ha hiányoznak, a megjelenítő a /N-hez tér vissza. Egy üres /R stream viszont nem hiányzó. Érvényes stream, amely semmit nem fest, így a rollover megjelenéseket tisztelő néző üres téglalapot mutat abban a pillanatban, amikor a mutató az annotáció fölé ér. A PDF/A még szigorúbb: az ISO 19005-1 (a 2. sz. hibajavítással) és az ISO 19005-2 / 19005-3 csak /N-t enged az annotáció megjelenésszótárában. A veraPDF a körbejárt fájlt a 6.5.3-4-es szabály alatt jelenti PDF/A-1-nél, a 6.3.3-2-es alatt PDF/A-2-nél és PDF/A-3-nál, a beépített TPdf.ValidatePdfA pedig pvaiAnnotationApDictViolation-ként sorolja fel. A szerkesztés, amely a Print jelzőt adta hozzá a szabvány egyik klauzulája kedvéért, megtört egy másikat
Miért ad 2-t a FPDFAnnot_GetAP egy hiányzó megjelenésre?
A PDFium sosem ad vissza nullát a FPDFAnnot_GetAP-ból, akkor sem, ha a kért megjelenésstream nem létezik. A függvény a bevett PDFium két hívásos mintát követi: nil puffert ad át, hogy megkapja a szükséges méretet bájtokban, lefoglal, majd újrahív a UTF-16LE szöveg lemásolásáért. A méret mindig tartalmazza a UTF-16 lezárót, így egy hiányzó stream 2 bájtot jelent, egy üres karakterláncot a lezárójával. A v3.121.1 előtti getter a ByteLength >= SizeOf(FPDF_WCHAR) feltételt tesztelte, amelyen minden hívás átment, így mindhárom HasAppearance* jelző True-ként jött vissza bármely, bármilyen megjelenéssel bíró annotációra. A rekordon átmenő round trip aztán megkérte az FPDFAnnot_SetAP-t, hogy minden módhoz üres karakterláncot tároljon, a PDFium pedig megteremtette a streamet, amely tartsa. Kivétel nélkül, figyelmeztetés nélkül, a látható oldal pedig változatlanul nézett ki — ezért bukkant fel a hiba egy veraPDF tesztállományban, nem egy nézőben
Hogyan dönti el a v3.121.1, hogy létezik-e megjelenés
A ReadAppearance, az a segéd a GetPageAnnotation-ben, amely feltölti az AppearanceNormal-t, az AppearanceRollover-t és az AppearanceDown-t, ma már csak akkor kezel eredményt tartalomként, ha legalább egy karaktert hordoz a lezárón túl. Az első hívásnak többet kell visszaadnia, mint SizeOf(FPDF_WCHAR) bájt, és páros bájtszámot, hiszen páratlan hosszúság nem lehet UTF-16. A második hívás, amely ténylegesen másolja a szöveget, újra validálódik: 2 vagy annál kisebb visszaadott hossz, vagy a lefoglalt puffernél nagyobb, False-ra állítja a HasValue-t, és üresen hagyja a karakterláncot. Az író oldalon semmi sem változott. A SetAnnotationData továbbra is csak olyan módokra hívja az FPDFAnnot_SetAP-t, amelyek HasAppearance* jelzője True, így a csak /N-nel bíró annotációból olvasott rekord ma már csak /N-t ír vissza. A regressziós tesztállomány mindkét irányt lefedi: egy normál megjelenéssel bíró négyzetannotáció, változatlanul kiolvasva és visszaírva, átment PDF/A-1b-n, PDF/A-2b-n és PDF/A-3b-n, míg ugyanaz az annotáció Print jelzője nélkül a várt jelzőszabályon bukik meg, és semmi máson
A hiányzó és az üres stream egyformán néz ki, ezért a getter konzervatív marad
A natív API nem tudja megkülönböztetni a hiányzó megjelenésstreamet a létező, de ürestől, és a PDFium Component nem tesz úgy, mintha tudná. Mindkét eset ugyanazt a 2 bájtot adja a FPDFAnnot_GetAP-ból, így mindkettő HasAppearanceRollover = False-ként olvas vissza, üres AppearanceRollover-rel. Ennek két következménye van, amelyre érdemes tervezni. Először: a False őrző azt jelenti, hogy „nem olvasódott tartalom, tehát a visszaírás békén hagyja ezt a módot”, nem azt, hogy „az /R kulcs hiányzik a szótárból”. Másodszor: a rekord nem észleli azt az üres streamet, amely már a fájlban ül: egy régebbi build vagy másik eszköz által rongált dokumentum tisztán olvas vissza, és a rekord visszaírása sem javítja, sem rontja. Az ilyen fájlok megtalálásához bájtszintű ellenőrzés kell, amire a TPdf.ValidatePdfA és a PDF/A előellenőrzési munkafolyamat a PDFium Componenttel való
Hogyan töröljön megjelenést szándékosan?
Expliciten állítsa be az őrzőt, és adjon át üres karakterláncot; a setter kiírja. Az üres karakterláncok tiltása a SetAnnotationData-ban a nyers javítás lett volna erre a hibára, de azokat a hívókat is megtörte volna, amelyek szándékosan törölnek megjelenést — ugyanaz a szerződés, amelyet a HasContents és a HasAuthor követ a szövegnél. Ezért a javítás teljes egészében a getterben él, a setter pedig továbbra is azt írja, amit a hívó kér:
// Cserélje a rollover megjelenést, majd törölje újra
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// A HasAppearanceRollover True, és a szöveg 'q Q'-ként jön vissza
A.HasAppearanceRollover := True; // a szándékot expliciten újrakijelenti
A.AppearanceRollover := ''; // szándékosan üres streamet ír
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// HasAppearanceRollover = False-ként olvas vissza üres karakterlánccal:
// egy üres stream és egy hiányzó itt megkülönböztethetetlen
Tartsa észben, hogy egy expliciten kiürített /R vagy /D az idézett PDF/A szabályok alatt is további kulcsnak számít. Ha a cél egy archív profil, egy nem üres /N írása és a másik két mód békén hagyása az egyetlen forma, amely ellenőriz. Minden munkafolyamat, amely annotációkat költöztet dokumentumok közt, például az XFDF export és import a PDFium Componenttel, kövesse ugyanezt a szabályt: a forrás által ténylegesen birtokolt módokat másolja, a többi őrzőt hagyja False-on
Olvass-módosít-ír minta, amely PDF/A-biztos marad
Változzon v3.121.1-re vagy későbbire, hagyja a megjelenés-őrzőket pontosan úgy, ahogy a getter visszaadta, és ellenőrizze a mentett fájlt, mielőtt kiszállítja. Mert egy avas üres stream hiányzóként olvas vissza, az ellenőrzési lépésnek a szerializált dokumentumot kell néznie, nem a rekordot, és elég olcsó ahhoz, hogy minden tétel után lefusson:
uses
PDFium, FPdfPdfa; // az FPdfPdfa deklarálja a TPdfAValidationIssue-t
function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
Report: TPdfAValidationResult;
begin
// Érvényesíti a Pdf-be jelenleg betöltött dokumentumot, a megnyitás óta
// a Pdf.Annotation[]-en át tett szerkesztéseket is beleértve
Report := Pdf.ValidatePdfA;
Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;
Ugyanez a fegyelem vonatkozik minden olyan panelre, amely átfest vagy megjegyzésekkel lát el oldalakat felülvizsgálathoz — ezt a munkafolyamatot az annotációs felülvizsgálati munkafolyamat építése Delphiben, PDFium Componenttel tárgyalja: a rekord annak a pillanatképe, amit a motor olvasni tudott, és egy olyan őrző, amelyet Ön nem állított, változatlanul utazzon vissza. A teljes annotációs API, a PDF/A előellenőrzés és a natív PDFium motor együtt szállul a Delphihez, C++Builderhez és Lazarushoz készült PDFium Componentben