Tekninen artikkeli

AP-streamittomien PDF-annotaatioiden flattenointi Delphissä

HotPDF v2.743.0 flattenoi PDF-annotaatiot, joilla ei ole /AP-ulkoasustreamia, sen sijaan että ohittaisi ne hiljaisesti. FlattenLoadedAnnotations ohjaa nyt ulkoasuttoman widgetin EnsureLoadedFieldAppearanceStream-metodille ja rakentaa ulkoasuttomalle markup-annotaatiolle Form XObjectin annotaation omista ominaisuuksista, joten /NeedAppearances-lomakkeeseen kirjoitetut arvot säilyvät sivusisällössä eivätkä katoa flattenoinnissa. Muutoksen pakottanut vika näyttää no-opilta. Asiakas lähettää selaimesta PDF:ksi tulostetun täytetyn hakemuslomakkeen. Lataat sen HotPDF:llä, kutsut FlattenLoadedAnnotations-metodia, saat paluuarvoksi 0, tallennat ja toimitat dokumentin jossa hakijan kirjoittaman nimen ja summan kohdalla on tyhjiä laatikoita. Mikään ei nostanut virhettä eikä kirjannut lokiin. Arvot olivat koko ajan tiedostossa, kunkin kentän /V-merkinnässä, ja flattenointikierros kulki niiden ohi koska millään widgetillä ei ollut leivottavaa ulkoasustreamia

Miksi selaimesta tulostetun lomakkeen flattenointi hukkaa kirjoitetut arvot?

Koska /NeedAppearances-lomake tallentaa arvon mutta ei arvon kuvaa. ISO 32000-1 12.7.2 sallii interaktiivisen lomakkeen asettaa /NeedAppearances true -merkinnän AcroForm-dictionaryyn, mikä kertoo katseluohjelmalle että se rakentaa jokaisen kentän visuaalisen pinnan avaushetkellä arvoista /V, /DA ja /Q. Halpoja lomakkeita tuottavat tuottajat — selaimen tulostuspolut, palvelinpuolen täyttäjät ja jotkin skannauksen etupäät — tarttuvat tähän mahdollisuuteen eivätkä kirjoita /AP-tietoa lainkaan. ISO 32000-1 12.5.5:n ulkoasualgoritmin mukainen flattenointi on siirtotyö: ota annotaation normaali ulkoasustream, kohdista sen /BBox sen /Rect-alueeseen, kutsu sitä sivun sisältöstreamista Do-operaattorilla ja poista sitten annotaatio. Ilman lähdestreamia ei ole mitään siirrettävää. Alkuperäinen HotPDF-toteutus v2.386.0:sta lähtien käsitteli tämän "skip"-tilanteena, mikä on yksinään puolusteltavaa mutta kokonaisuutena tuhoisaa: juuri flattenointia eniten tarvitsevat dokumentit kantavat vähiten todennäköisesti ulkoasuja. Sama aukko nielaisi markup-annotaatiot — katselmointityökalun Highlightin, redline-kierroksen Squaren ja Ink-allekirjoituksen — aina kun tuottaja luotti katseluohjelman piirtävän ne

Missä HotPDF liittää synteesin FlattenLoadedAnnotationsiin

Kytkentäkohta on tarkoituksella myöhäinen: vasta kun ulkoasun haku epäonnistuu, ei ennen sitä. FlattenLoadedAnnotations pyytää edelleen ensin normaalia ulkoasua GetLoadedAnnotationAppearanceStream-metodilta, ja annotaatio jolla se jo on leivotaan täsmälleen kuten v2.386.0:ssa. Vain nil-tulos annotaatiolle jolla on epädegeneroitunut /Rect-alue eikä hidden-lippua siirtyy synteesipolulle. Järjestys on tärkeä: dokumentin tekijä joka näki vaivan kirjoittaa /AP-tiedon saa takaisin omat tavunsa eikä HotPDF:n jälleenrakennusta niistä

NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
if (NStrm= nil) and (RR> RL) and (RT> RB) and ((FlagsValue and 2)= 0) then
begin
  if Subtype= 'Widget' then
  begin
    FieldIdx:= GetLoadedFormFieldIndexForAnnotation(Indices[PgI], AnI, WidgetIdx);
    if FieldIdx>= 0 then
      EnsureLoadedFieldAppearanceStream(FieldIdx);
    // kysy uudelleen: generaattori on liittänyt widgetiin /AP /N:n
    NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
  end
  else
    NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;

Tästä kaksi annotaatioperhettä erkanevat. Widget ratkaistaan takaisin omistavaan kenttään GetLoadedFormFieldIndexForAnnotation-metodilla ja annetaan EnsureLoadedFieldAppearanceStream-metodille, kentän ulkoasugeneraattorille joka on ollut tässä Delphi PDF -kirjastossa v2.328.0:sta asti. Sen uudelleenkäyttö oman toisen kenttärenderöijän kirjoittamisen sijaan on koko asian ydin — se kattaa jo Type0-fontit, rivien rivityksen, quaddingin, checkbox- ja radio-/AS-tilat sekä /MK-kierron, saman mekanismin joka on AcroForm-kenttien lisäämisen jo ladattuun PDF:ään taustalla. Kaikki muu menee markup-syntetisaattorille. Kutsujan kannalta mikään muu ei muutu: sama yhden rivin flattenointikutsu palauttaa nyt nollaa suuremman määrän dokumenteilla jotka aiemmin palauttivat nollan

Doc:= THotPDF.Create(nil);
try
  Doc.LoadFromFile('needappearances-form.pdf');
  // v2.743.0: AP:ttomat widgetit ja markup syntetisoidaan ja leivotaan
  Flattened:= Doc.FlattenLoadedAnnotations;          // kaikki sivut, kaikki alatyypit
  // Flattened:= Doc.FlattenLoadedAnnotations('1-3', 'Highlight');
  if Flattened= 0 then
    raise Exception.Create('nothing was flattened');
  Doc.SaveLoadedDocument('flattened.pdf');
finally
  Doc.Free;
end;

Miksi QuadPoints ja InkList päätyvät väärään paikkaan?

Koska nämä koordinaatit ovat sivun user spacessa kun taas syntetisoitu ulkoasustream piirtää omassa /BBox-spacessaan, eivätkä nämä kaksi origoa ole sama piste. ISO 32000-1:n taulukko 176 määrittelee tekstin markup-annotaatioiden /QuadPoints-koordinaatit oletusarvoisessa user spacessa ja taulukko 174 tekee saman viiva-annotaation /L-päätepisteille; /InkList noudattaa samaa käytäntöä. HotPDF antaa syntetisoidulle Formille /BBox-alueen [0 0 W H], jonka origo sijaitsee /Rect-alueen vasemmassa alakulmassa. Siksi jokainen /QuadPoints-, /L- tai /InkList-arvosta poimittu piste täytyy siirtää vähentämällä negatoitu /Rect-alueen vasen alakulma ennen sen kirjoittamista sisältöstreamiin. Jos tämä tehdään väärin, sivun 700 pistettä ylempänä olevaan riviin kohdistettu korostus piirretään 700 pistettä oman laatikkonsa yläpuolelle, mikä käytännössä tarkoittaa ettei se piirry mihinkään. Korjaus on yksi vähennys koordinaattia kohden ja se yhdistyy bake-vaiheen myöhemmin tuottamaan cm-operaattoriin — tuo matriisi kohdistaa /BBox-alueen takaisin /Rect-alueeseen, joten kaksi vaihetta kumoavat toisensa ja absoluuttinen geometria korjaantuu

// /L-päätepisteet ovat sivun user spacessa (ISO 32000-1 Table 174); Formin
// BBox-alku on /Rect:n vasemmassa alakulmassa, joten siirrä -(RL, RB)
X1:= ArrNum(LA, 0, 0)- RL;
Y1:= ArrNum(LA, 1, 0)- RB;
X2:= ArrNum(LA, 2, 0)- RL;
Y2:= ArrNum(LA, 3, 0)- RB;
StrokeOp:= ColorOp(DArr('C'), true);
if StrokeOp= '' then
  StrokeOp:= '0 G';
Result:= _FloatToStrR(BW)+ ' w '#10+ StrokeOp+ #10+
  _FloatToStrR(X1)+ ' '+ _FloatToStrR(Y1)+ ' m '+
  _FloatToStrR(X2)+ ' '+ _FloatToStrR(Y2)+ ' l S'#10;

Mitä syntetisoitu markup-ulkoasu oikeasti piirtää

Markup-syntetisaattori lukee vain annotaatio-dictionaryn eikä mitään muuta, mikä pitää tuloksen ennustettavana ja rehellisenä sen suhteen mitä se ei voi tietää. FreeText ja Stamp piirtävät /Contents-sisällön käyttäen /DA-arvosta parsittua fonttia ja väriä, kohdistettuna /Q-arvon mukaan ja 2 pisteen sisäetäisyydellä. Square ja Circle piirtävät re- tai neljän kaaren Bezier-ääriviivan joka piirretään /C-värillä ja täytetään /IC-värillä kun se on annettu, leveydellä joka tulee /BS /W-arvosta. Line ja Ink piirtävät kärkensä. Highlight täyttää jokaisen nelikulmion, kun taas Underline, StrikeOut ja Squiggly piirtävät viivan nelikulmion alareunaan, keskelle tai yhden pisteen siksakina. Arvo 1:n alle jäävä /CA muuttuu ca-merkinnän sisältäväksi ExtGState-tilaksi, johon viitataan streamin alussa muodossa /GSA gs

Tekstin koodaus päätetään /DA-arvon nimeämästä AcroFormin /DR /Font-merkinnästä. Jos kyseisen fontin /Subtype on Type0, HotPDF kirjoittaa merkkijonon UTF-16BE-heksaliteraalina yhdessä FEFF-tavujärjestysmerkin kanssa; muuten se kirjoittaa escapeutetun literaalimerkkijonon, jossa sulkeet ja kenoviivat on escapeutettu ja yli 126 olevat tavut kirjoitetaan oktaalina. /DA-arvon Tf-operaattori emittoidaan ennen BT-operaattoria, mikä on laillista koska tekstin tila säilyy tekstiobjektien rajan yli, ja samalla vältytään /DA-merkkijonon pilkkomiselta. Kaksi rajoitusta on syytä sanoa suoraan. Rivin leveyden rivitystä ja quaddingia varten arvioidaan puoli-emin ja täyden emin heuristiikalla eikä oikeilla fonttimetriikoilla, joten suhteellisella fontilla kohdistus on lähellä mutta ei tarkka. Ja subtype jolle ei ole mitään syntetisoitavaa — Popup, Link tai Stamp jonka ainoa sisältö on ikoninnimi — palauttaa arvon nil ja jätetään koskemattomaksi aivan kuten ennenkin

Väliaikainen /Annots-vaihto joka rankaisee hyödyllisestä siivouksesta

FlattenOneWidget, FlattenLoadedFormFields-metodin käyttämä widget-kohtainen polku, on aliasointiansa joka täytyy ottaa huomioon jokaisessa jaettuun flattenointisilmukkaan tehtävässä muutoksessa. Se korvaa sivun /Annots-arvon tilapäisesti yhden alkion taulukolla, jotta yleinen flattenointikierros käsittelee yhtä widgetiä, ja palauttaa sitten alkuperäisen PHPDFDictionaryItem-osoittimen finally-lohkossa. Palautus kirjoittaa takaisin dictionary-slottiin jonka se otti talteen ennen kutsua

DictItem:= PHPDFDictionaryItem(PageObj.Items.Items[AnnotsIndex]);
Item:= DictItem^.Value;
TemporaryAnnots:= THPDFArrayObject.Create(nil);
TemporaryAnnots.AddObject(Target);
DictItem^.Value:= TemporaryAnnots;
try
  Result:= FlattenLoadedAnnotations(IntToStr(PageIndex+ 1), 'Widget')= 1;
finally
  DictItem^.Value:= Item;   // roikkuva jos sisempi silmukka vapautti tämän alkion
  TemporaryAnnots.Free;
end;

Lisää jaettuun sisempään silmukkaan järkevältä näyttävä siivous — DeleteValue('Annots') kun taulukko tyhjenee, jotta tallennettu sivu ei kanna vestigiaalista tyhjää taulukkoa — ja kutsu vapauttaa juuri sen dictionary-alkion johon DictItem osoittaa. finally kirjoittaa sitten roikkuvan osoittimen kautta ja prosessi kuolee virheeseen "Invalid pointer operation". Kaksi olemassa olevaa testiä löysi tämän heti, mikä on ainoa syy siihen että kyseessä on alaviite eikä tukipyyntö. Sääntö yleistyy: ennen jaettuun silmukkaan lisättävää siivousta tarkista kutsujista alias- tai vaihtosopimukset. Tyhjä /Annots-taulukko on kosmeettinen haitta eikä sen vuoksi kannata vaihtaa osoittimen elinikätakuuta pois

Mikä jää leipomatta ja mitä flattenointi maksaa

Piilotetut annotaatiot jätetään tarkoituksella ulkopuolelle. Annotaatio jonka /F-kokonaisluvussa on bittipaikka 2 asetettuna on piilotettu ISO 32000-1 12.5.3:n mukaan, ja kun sillä ei myöskään ole /AP-tietoa on todellinen houkutus syntetisoida ulkoasu ja leipoa se muiden tavoin. Se olisi tietoturvaseurauksia sisältävä bugi: näkymättömän muistiinpanon leipominen sivusisältöön tekee siitä näkyvän kaikille jotka avaavat tiedoston. HotPDF jättää nämä annotaatiot täsmälleen paikoilleen eikä laske niitä paluuarvoon. Ole yhtä selkeä käyttäjille myös niiden kohteiden hinnasta jotka todella leivotaan. Flattenointi on peruuttamaton — annotaatio poistetaan sivun /Annots-taulukosta ja sen visuaalinen esitys on nyt sivusisältöä, joten kentän arvoa ei voi enää muokata, kommenttiketjua ei ole, /AS-tilaa ei voi vaihdella eikä rakenteellista dataa voi palauttaa muuten kuin alkuperäisestä tiedostosta. Flattenoi kopio, säilytä alkuperäinen ja käytä flattenoitua vasta kun dokumentti lakkaa olemasta lomake ja muuttuu tietueeksi. Jos ongelmasi on XFA-pohjainen eikä ulkoasuton, erillinen HotPDF:n XFA–AcroForm-flattenointipolku on oikea aloituspaikka, ja jos rakennat lomaketta vasta, AcroForm-kenttien toimintojen ja validoinnin kytkemistä käsittelevät muistiinpanot kattavat kirjoituspuolen

Yksi varmennusta koskeva huomautus, koska muuten menetät siihen iltapäivän. ExtractLoadedPageGlyphs ei laskeudu Form XObjectien sisään, ja leivottu ulkoasu elää yhden sellaisen sisällä — sivun sisältöstreamissa on vain q ... cm /FlatAn<n> Do Q-sekvenssi. Siksi glyphien poiminta flattenoidulta sivulta ei ilmoita mitään, ja tämä on oikea toimintatapa eikä hukkunut bake. Varmista joko tavutasolla tarkistamalla /FlatAn-resurssinimi, Do-kutsu ja /Subtype /Form, tai renderöintiputken kautta, joka laajentaa XObjectit

Annotaatioiden flattenointi näyttää kolmelta siirtoriviltä siihen asti kun vastaan tulevat dokumentit joita ihmiset oikeasti tuottavat. Jos työskentelet täytettyjen lomakkeiden, katselmointimerkintöjen tai arkistotulosteen parissa Delphillä tai C++Builderilla, kannattaa lukea miten HotPDF Delphi PDF -komponentti käsittelee AcroFormien ja annotaatioiden ladattujen dokumenttien puolta ennen kuin rakennat sen päälle oman ulkoasugeneraattorin