PDFium Componentissa ennen v3.121.1:ää annotaation lukeminen TPdf.Annotation[]in kautta ja tietueen sijoittaminen takaisin saattoi lisätä tyhjät /R- ja /D-merkinnät sen /AP-esityssanakirjaan, vaikka alkuperäinen kantoi vain /N:n. PDF/A-validaattorit hylkäävät kyseisen sanakirjan. Versiosta v3.121.1 alkaen getteri raportoi vain esityksen jonka se oikeasti luki, joten muuttumaton round trip ei kirjoita mitään uutta. Virhe kannattaa ymmärtää tarkasti, koska tyypillinen laukaisija on korjaus joka pyrkii tekemään tiedostosta sääntöjenmukaisemman, ei vähemmän
Mikä menee pieleen kun kirjoitat annotaation takaisin muuttumattomana?
Lyhyt vastaus: annotaatio saa esitysstreamit joita sillä ei koskaan ollut, ja tiedosto joka läpäisi PDF/A-validoinnin ennen muokkaustasi kaatuu sen jälkeen. Tyypillinen skenaario etenee näin. Asiakasarkisto saapuu neliö- ja tekstiannotaatioilla joilta puuttuu Print-lippu, PDF/A vaatii jokaisen annotaation tulostuvan, joten käyt läpi sivut, lisäät afPrintin ja sijoitat jokaisen tietueen takaisin. Mikään kyseisessä koodissa ei koske esityksiä. Tietue TPdf.Annotation[]ista on TPdfAnnotation, ja SetAnnotationData kirjoittaa jokaisen kentän jonka Has*-sentinelli on asetettu, mikä on täsmälleen se miten HasContents / ContentsText-parit on tarkoitettu toimiviksi. Ongelma oli, että getteri asetti HasAppearanceRolloverin ja HasAppearanceDownin arvoon True tyhjillä merkkijonoilla modeille joita ei ollut olemassa, ja setteri kirjoitti tunnollisesti kaksi tyhjää streamia:
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];
// Ennen v3.121.1:tä tämä sijoitus kirjoitti myös tyhjät /AP/R- ja
// /AP/D-streamit kun lähdeannotaatiolla oli vain /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 määrittelee esityssanakirjan kolmella merkinnällä: /N normaalille esitykselle, /R rolloverille ja /D downille. /R ja /D ovat valinnaisia, ja kun ne puuttuvat, katselin palautuu /N:ään. Tyhjä /R-stream ei kuitenkaan ole poissa. Se on kelvollinen stream joka ei maalaa mitään, joten katselin joka kunnioittaa rollover-esityksiä näyttää tyhjän suorakulmion heti kun osoitin liukuu annotaation yli. PDF/A on vielä tiukempi: ISO 19005-1 (Corrigendum 2:n kanssa) ja ISO 19005-2 / 19005-3 sallivat annotaation esityssanakirjassa vain /N:n. veraPDF raportoi round trip -tiedoston säännöllä 6.5.3-4 PDF/A-1:ssä ja säännöllä 6.3.3-2 PDF/A-2:ssa ja PDF/A-3:ssa, ja sisäänrakennettu TPdf.ValidatePdfA listaa sen pvaiAnnotationApDictViolationina. Muokkaus joka lisäsi Print-lipun tyydyttääkseen standardin yhden pykälän rikkoi toisen
Miksi FPDFAnnot_GetAP palauttaa 2:n puuttuvalle esitykselle?
PDFium ei koskaan palauta nollaa funktiosta FPDFAnnot_GetAP, vaikka pyydetty esitysstream ei olisi olemassakaan. Funktio noudattaa tavallista PDFiumin kaksikutsuista kaavaa: annetaan nil-puskuri saadaksesi tarvittavan koon tavuina, varataan muisti, ja kutsutaan uudelleen kopioidaksesi UTF-16LE-tekstin. Koko sisältää aina UTF-16-terminaattorin, joten puuttuva stream raportoi 2 tavua, tyhjä merkkijono plus sen terminaattori. v3.121.1:ää edeltänyt getteri testasi ehtoa ByteLength >= SizeOf(FPDF_WCHAR), tarkistus jonka jokainen kutsu läpäisee, joten kaikki kolme HasAppearance*-lippua palautuivat True:na mille tahansa annotaatiolle jolla oli ylipäätään jokin esitys. Round trip tietueen kautta pyysi sitten FPDFAnnot_SetAPia tallentamaan tyhjän merkkijonon jokaiselle modille, ja PDFium loi streamin kantamaan sen. Ei poikkeusta, ei varoitusta, ja näkyvä sivu näytti identtiseltä, mistä syystä vika nousi pintaan veraPDF-fixtuurissa eikä katselimessa
Miten v3.121.1 päättää että esitys on olemassa
ReadAppearance, apufunktio GetPageAnnotationin sisällä joka täyttää AppearanceNormalin, AppearanceRolloverin ja AppearanceDownin, kohtelee tulosta sisältönä vain kun se kantaa vähintään yhtä merkkiä terminaattorin lisäksi. Ensimmäisen kutsun on palautettava enemmän kuin SizeOf(FPDF_WCHAR) tavua ja parillinen tavumäärä, koska pariton pituus ei voi olla UTF-16:a. Toinen kutsu, joka oikeasti kopioi tekstin, validoidaan uudelleen: palautunut pituus 2 tai vähemmän, tai suurempi kuin varattu puskuri, nollaa HasValuein arvoon False ja jättää merkkijonon tyhjäksi. Kirjoituspuolella mikään ei muuttunut. SetAnnotationData kutsuu yhä FPDFAnnot_SetAPia vain modeille joiden HasAppearance*-lippu on True, joten tietue joka luettiin annotaatiosta jolla on vain /N kirjoittaa nyt takaisin vain /N:n. Regressiofixtuuri kattaa molemmat suunnat: neliöannotaatio normaalilla esityksellä, luettuna ja muuttumattomana takaisin kirjoitettuna, läpäisee PDF/A-1b:n, PDF/A-2b:n ja PDF/A-3b:n, kun taas sama annotaatio jolta Print-lippu on poistettu kaatuu odotettuun lippusääntöön eikä mihinkään muuhun
Puuttuva ja tyhjä stream näyttävät identtisiltä, joten getteri pysyy konservatiivisena
Natiivi API ei erota puuttuvaa esitysstreamia streamista joka on olemassa mutta tyhjä, eikä PDFium Component esitä muuta. Molemmat tapaukset palauttavat samat 2 tavua funktiosta FPDFAnnot_GetAP, joten molemmat lukevat takaisin arvoina HasAppearanceRollover = False tyhjällä AppearanceRolloverilla. Siitä seuraa kaksi seurausta joiden ympärille kannattaa suunnitella. Ensiksi, False-sentinelli tarkoittaa ”sisältöä ei luettu, joten takaisinkirjoitus jättää tämän modin rauhaan”, ei sitä että ”/R-avain on poissa sanakirjasta”. Toiseksi, tietue ei voi havaita tiedostossa jo olevaa tyhjää streamia: vanhemman buildin tai toisen työkalun vahingoittama dokumentti lukee takaisin puhtaana, ja tietueen sijoittaminen takaisin ei korjaa eikä pahenna sitä. Löytääksesi kyseiset tiedostot tarvitset tavutason tarkistuksen, mihin TPdf.ValidatePdfA ja PDF/A-preflight-validointiprosessi PDFium Componentilla on olemassa
Miten tyhjennät esityksen tahallaan?
Asetat sentinellin eksplisiittisesti ja välität tyhjän merkkijonon; setteri kirjoittaa sen. Tyhjien merkkijonojen estäminen SetAnnotationDataissa olisi ollut tümpä korjaus tähän bugiin, mutta se olisi myös rikkonut soittajat jotka tyhjentävät esityksen tahallaan, sama sopimus jota HasContents ja HasAuthor noudattavat tekstille. Korjaus asuu siis kokonaan getterissä, ja setteri kunnioittaa yhä mitä ikinä soittaja pyytää:
// Korvaa rollover-esiintymä ja tyhjennä se sitten uudelleen
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// A.HasAppearanceRollover on True ja teksti pyörähtää takaisin arvona 'q Q'
A.HasAppearanceRollover := True; // vahvista tarkoitus eksplisiittisesti uudelleen
A.AppearanceRollover := ''; // kirjoita tyhjä stream tahallaan
Pdf.Annotation[0] := A;
A := Pdf.Annotation[0];
// Lukee takaisin arvoina HasAppearanceRollover = False tyhjällä merkkijonolla:
// tyhjä stream ja puuttuva ovat tässä erottamattomia
Pidä mielessä että eksplisiittisesti tyhjennetty /R tai /D lasketaan yhä ylimääräiseksi avaimeksi yllä lainattujen PDF/A-sääntöjen alla. Jos kohde on arkistoprofiili, ei-tyhjän /N:n kirjoittaminen ja kahden muun modin jättäminen koskematta on ainoa muoto joka validee. Mikä tahansa prosessi joka siirtää annotaatioita dokumenttien välillä, kuten XFDF-vienti ja tuonti PDFium Componentilla, tekee saman säännön mukaan: kopioi modit joita lähteellä oikeasti oli ja jätä loput sentinellit arvoon False
Lue–muokkaa–kirjoita-kaava joka pysyy PDF/A-varmana
Päivitä v3.121.1:een tai uudempaan, jätä esityssentinellit täsmälleen niin kuin getteri palautti ne, ja validoi tallennettu tiedosto ennen kuin lähetät sen matkaan. Koska vanhentunut tyhjä stream lukee takaisin poissa olevana, varmennusaskeleen on katsottava serialisoituun dokumentiin eikä tietueeseen, ja se on halpa ajaa jokaisen erän jälkeen:
uses
PDFium, FPdfPdfa; // FPdfPdfa määrittelee TPdfAValidationIssuen
function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
Report: TPdfAValidationResult;
begin
// Validoi Pdfiin ladatun dokumentin, mukaan lukien muokkaukset
// jotka on tehty Pdf.Annotation[]:n kautta avaamisen jälkeen
Report := Pdf.ValidatePdfA;
Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;
Sama kuri pätee mihin tahansa paneeliin joka värää tai annotoi sivuja katselmointia varten, prosessi jota käsittelee artikkeli Delphi-annotaatiokatselmointiprosessin rakentamisesta PDFium Componentilla: tietue on tilannekuva siitä mitä kone pystyi lukemaan, ja sentinelli jota et itse asettanut kulkee takaisin muuttumattomana. Koko annotaatio-API, PDF/A-preflight ja natiivi PDFium-kone toimitetaan yhdessä PDFium Component for Delphi, C++Builder and Lazarus -paketissa