Műszaki cikk

Annotáció megjelenések round tripje Delphiben, PDFiummal

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é

A PDFium Component annotáció round tripjének diagramja, ahol az afPrint hozzáadása a TPdf.Annotation[]-en és a SetAnnotationData-n át üres /R és /D streameket is ír a FPDFAnnot_SetAP által, egy PDF/A-tiszta megjelenésszótárból veraPDF által elutasítottat faragva, míg a v3.121.1 csak a ténylegesen olvasott megjelenéseket jelenti
Egy annotáció kiolvasása és változatlan visszaírása üres rollover és down megjelenésstreameket adott hozzá, és ez buktatja meg a PDF/A-t, nem az a Print jelző, amelyet hozzáadni szándékozott

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

Az annotáció megjelenésszótára az ISO 32000-1-ből normál, rollover és down bejegyzésekkel: a PDFium 2 bájtot ad vissza hiányzó streamre és létező üresre egyaránt, így mindkettő tartalom nélküliként olvas vissza a TPdf-en át, míg csak egy bájtszintű ellenőrzés, mint a TPdf.ValidatePdfA, találja meg azt az üres streamet, amelyet a PDF/A tilt
A hiányzó /R a /N-re esik vissza; egy üres /R üres téglalapot fest, és így is megbukik a PDF/A-n, a rekordon át pedig a kettő megkülönböztethetetlen

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 jelenti a FPDFAnnot_GetAP a hiányzó megjelenésstreamet a PDFiumban: a két hívásos minta mindig legalább két bájtot ad vissza a UTF-16 lezáróért, a régi, SizeOf(FPDF_WCHAR)-ral összehasonlító kapun minden hívás átment, és mind a HasAppearance őrzőket true-ra állította, a v3.121.1-es kapu pedig többet követel a lezárónál, páros bájtszámmal
A két bájt a kódolt üres karakterlánc, nem bizonyíték megjelenés létezésére; a javított getter a lezáró hosszán vagy alatti bármit tartalom nélküliként kezel, a visszaírás pedig csendben marad

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