Tehnični članak

Round-tripi videzov pripomb v Delphiju s PDFium

V PDFium Component pred v3.121.1 je lahko branje pripombe prek TPdf.Annotation[] in dodelitev zapisa nazaj dodalo prazna vnosa /R in /D v njen slovar videzov /AP, tudi kadar je izvirnik nosil le /N. Validirniki PDF/A ta slovar zavrnejo. Od v3.121.1 pridobivalnik prijavi le videz, ki ga je dejansko prebral, tako da nespremenjen round-trip ne zapiše ničesar novega. Napako se splača razumeti podrobno, ker je običajni sprožilec popravek, ki naj bi datoteko naredil bolj skladno, ne manj

Diagram round-tripa pripomb v PDFium Component, kjer dodajanje afPrint prek TPdf.Annotation[] in SetAnnotationData zapiše tudi prazna toka /R in /D prek FPDFAnnot_SetAP, iz čistega slovarja videzov PDF A naredi slovar, ki ga veraPDF zavrne, dokler v3.121.1 ne prijavi le videzov, ki jih je dejansko prebral
Branje pripombe in njen nespremenjen zapis nazaj je dodalo prazna toka videzov rollover in down, in to je tisto, ki podre PDF/A, ne zastavica Print, ki ste jo nameravali dodati

Kaj gre narobe, ko pripombo zapišete nazaj nespremenjeno?

Kratek odgovor: pripomba dobi tokove videzov, ki jih ni imela, in datoteka, ki je pred vašim urejanjem prestala validacijo PDF/A, jo po njem podere. Tipičen scenarij teče takole. Arhiv stranke pride s kvadratnimi in besedilnimi pripombami brez zastavice Print, PDF/A zahteva, da se vsaka pripomba natisne, zato se sprehodite po straneh, dodate afPrint in vsak zapis dodelite nazaj. Nič v tej kodi se ne dotakne videzov. Zapis iz TPdf.Annotation[] je TPdfAnnotation, SetAnnotationData pa zapiše vsako polje, katerega marker Has* je nastavljen, kar je točno tako, kot delujeta para HasContents / ContentsText. Težava je bila, da je pridobivalnik nastavil HasAppearanceRollover in HasAppearanceDown na True s praznimi nizi za načine, ki niso obstajali, nastavljavnik pa je dolžno zapisal dva prazna toka:

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];
          // Pred v3.121.1 je ta dodelitev zapisala tudi prazna toka /AP/R in
          // /AP/D, kadar je izvorna pripomba imela le /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 definira slovar videzov s tremi vnosi: /N za normalni videz, /R za rollover in /D za down. /R in /D sta neobvezna, kadar ju ni, pa si gledalnik vrne /N. Prazen tok /R pa ni odsoten. Je veljaven tok, ki ne naslika ničesar, zato gledalnik, ki spoštuje videze rollover, pokaže prazen pravokotnik v trenutku, ko se kazalec premakne nad pripombo. PDF/A je še strožji: ISO 19005-1 (s Corrigendum 2) in ISO 19005-2 / 19005-3 dovoljujeta le /N v slovarju videzov pripombe. veraPDF prijavi round-trip datoteko pod pravilom 6.5.3-4 za PDF/A-1 in pravilom 6.3.3-2 za PDF/A-2 ter PDF/A-3, vgrajeni TPdf.ValidatePdfA pa jo našteje kot pvaiAnnotationApDictViolation. Ureditev, ki je dodala zastavico Print, da zadovolji eno odstavko standarda, pa je podrila drugo

Slovar videzov pripombe iz ISO 32000-1 z vnosi normalni, rollover in down: PDFium vrne 2 bajta za manjkajoči tok in za obstoječi prazen, tako da se oba prek TPdf prebereta kot brez vsebine, samo preizkus na ravni bajtov, kot je TPdf.ValidatePdfA, pa najde prazen tok, ki ga PDF/A ne dovoljuje
Manjkajoči /R se vrne na /N; prazen /R naslika prazen pravokotnik in še vedno podre PDF/A, prek zapisa pa sta nedločljiva

Zakaj FPDFAnnot_GetAP vrne 2 za manjkajoči videz?

PDFium iz FPDFAnnot_GetAP nikoli ne vrne ničle, tudi kadar zahtevani tok videza ne obstaja. Funkcija sledi običajnemu dvoklicnemu vzorcu PDFium: podajte nil medpomnilnik, da dobite potrebno velikost v bajtih, rezervirajte, nato pa pokličite znova, da se prekopira besedilo UTF-16LE. Velikost vedno vključuje zaključevalnik UTF-16, zato manjkajoči tok prijavi 2 bajta, prazen niz skupaj z njegovim zaključevalnikom. Pridobivalnik pred v3.121.1 je preizkusil ByteLength >= SizeOf(FPDF_WCHAR), preizkus, ki ga prestane vsak klic, tako da so se vsi trije markerji HasAppearance* vrnili True za vsako pripombo s kakršnim koli videzom. Round-trip skozi zapis je nato od FPDFAnnot_SetAP zahteval, da shrani prazen niz za vsak način, PDFium pa je ustvaril tok, da ga nosi. Brez izjeme, brez opozorila, vidna stran pa je zgledala identična, zato se je napaka pokazala v fixture veraPDF, ne v gledalniku

Kako FPDFAnnot_GetAP prijavi manjkajoči tok videza v PDFium: dvoklicni vzorec vedno vrne vsaj dva bajta za zaključevalnik UTF-16, stara vrata s primerjavo s SizeOf(FPDF_WCHAR) so prestala vsak klic in nastavila vse markerje HasAppearance na true, vrata v3.121.1 pa zahtevajo več kot zaključevalnik plus sodo število bajtov
Dva bajta sta kodiran prazen niz, ne dokaz, da videz obstaja; popravljen pridobivalnik vse, kar je na dolžini zaključevalnika ali pod njo, obravnava kot brez vsebine, zapis nazaj pa ostane tiho

Kako v3.121.1 odloči, da videz obstaja

ReadAppearance, pomočnik znotraj GetPageAnnotation, ki polni AppearanceNormal, AppearanceRollover in AppearanceDown, rezultat obravnava kot vsebino šele, kadar nosi vsaj en znak čez zaključevalnik. Prvi klic mora vrniti več kot SizeOf(FPDF_WCHAR) bajtov in sodo število bajtov, saj liha dolžina ne more biti UTF-16. Drugi klic, ki dejansko prekopira besedilo, se ponovno preveri: vrnjena dolžina 2 ali manj ali pa večja od rezerviranega medpomnilnika ponastavi HasValue na False in pusti niz prazen. Na strani pisanja se nič ni spremenilo. SetAnnotationData še vedno pokliče FPDFAnnot_SetAP le za načine, katerih zastavica HasAppearance* je True, tako da zapis, prebran iz pripombe, ki ima le /N, zdaj zapiše nazaj le /N. Regresijski fixture pokriva obe smeri: kvadratna pripomba z normalnim videzom, prebrana in nespremenjena zapisana nazaj, prestane PDF/A-1b, PDF/A-2b in PDF/A-3b, ista pripomba z odvzeto zastavico Print pa podre na pričakovanem pravilu zastavice in nič drugega

Manjkajoči in prazni tokovi zgledajo identično, zato pridobivalnik ostane previden

Izvorni API ne more ločiti manjkajočega toka videza od obstoječega, a praznega, PDFium Component se pa ne dela, da lahko. Oba primera vrneta iste 2 bajta iz FPDFAnnot_GetAP, tako da se oba prebereta kot HasAppearanceRollover = False s praznim AppearanceRollover. To ima dve posledici, okrog katerih naj bi oblikovali. Prvič, marker False pomeni »ni bilo prebrane vsebine, zato bo zapis nazaj ta način pustil pri miru«, ne »ključa /R ni v slovarju«. Drugič, zapis ne more zaznati praznega toka, ki je že v datoteki: dokument, poškodovan s starejšo gradnjo ali z drugim orodjem, se prebere kot čist, dodelitev zapisa nazaj ga ne popravi in ne poslabša. Da najdete take datoteke, potrebujete preizkus na ravni bajtov, zato sta tu TPdf.ValidatePdfA in delovni tok validacije PDF/A preflight s PDFium Component

Kako namerno počistite videz?

Marker nastavite izrecno in podate prazen niz; nastavljavnik ga zapiše. Zavračanje praznih nizov v SetAnnotationData bi bila za to napako kruta rešitev, podrla pa bi tudi klicatelje, ki videz namerno počistijo — isto pogodbo, ki ji za besedilo sledita HasContents in HasAuthor. Zato popravek živi v celoti v pridobivalniku, nastavljavnik pa še naprej ugodi kar koli zahteva klicatelj:

// Zamenjaj videz rollover, nato ga znova počisti
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;

A := Pdf.Annotation[0];
// A.HasAppearanceRollover je True in se besedilo round-tripa kot 'q Q'
A.HasAppearanceRollover := True;   // izrecno ponovi namero
A.AppearanceRollover := '';        // namerno zapiši prazen tok
Pdf.Annotation[0] := A;

A := Pdf.Annotation[0];
// Prebere se kot HasAppearanceRollover = False s praznim nizom:
// prazen tok in manjkajoči sta tu nedločljiva

Upoštevajte, da izrecno izpraznjen /R ali /D še vedno šteje kot dodaten ključ pod pravili PDF/A, navedenimi zgoraj. Če je cilj arhivski profil, je pisanje nepraznega /N in puščanje drugih dveh načinov nedotaknjenih edina oblika, ki prestane validacijo. Vsak delovni tok, ki prestavlja pripombe med dokumenti, kot je izvoz in uvoz XFDF s PDFium Component, naj sledi istemu pravilu: prekopirajte načine, ki jih je vir dejansko imel, preostale markerje pa pustite False

Vzorec branje-spremeni-zapiši, ki ostane varen za PDF/A

Nadgradite na v3.121.1 ali pozneje, markerje videzov pustite točno take, kot jih je vrnil pridobivalnik, in shranjeno datoteko preverite, preden jo odpošljete. Ker se zastarel prazen tok prebere kot odsoten, mora korak preverjanja pogledati serializiran dokument, ne zapisa, in je dovolj poceni, da ga poženete po vsaki seriji:

uses
  PDFium, FPdfPdfa;  // FPdfPdfa deklarira TPdfAValidationIssue

function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
  Report: TPdfAValidationResult;
begin
  // Preveri dokument, ki je trenutno naložen v Pdf, vključno z urejanji,
  // narejenimi prek Pdf.Annotation[] od odpiranja
  Report := Pdf.ValidatePdfA;
  Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;

Ista disciplina velja za vsako podokno, ki strani pobarva ali označi za pregled, delovni tok, ki ga pokriva gradnja delovnega toka pregledovanja pripomb v Delphiju s PDFium Component: zapis je posnetek tistega, kar je pogon zmogel prebrati, marker, ki ga niste nastavili sami, pa naj potuje nazaj nespremenjen. Celoten API pripomb, PDF/A preflight in izvorni pogon PDFium prihajajo skupaj v PDFium Component za Delphi, C++Builder in Lazarus