Tekninen artikkeli

XFA-rikastekstihyperlinkkien litistäminen PDF-linkeiksi Delphissä

XFA, XML Forms Architecture, on vanhentunut. ISO 32000-1 sisältää sen kohdassa §12.7 huomautuksella, että se on poistettu PDF 2.0:sta, ja nykyaikaiset katseluohjelmat pudottavat XFA-moottoreitaan yksitellen. Mikään tästä ei ole tyhjentänyt arkistoja. Hallituksen vastaanottolomakkeita, vakuutushakemuksia ja tiliotteita laadittiin XFA-muodossa suurimman osan kahdesta vuosikymmenestä, ja näitä tiedostoja saapuu yhä tänäkin päivänä sähköposteihin ja asiakirjaputkiin. Kun katseluohjelma, joka ennen renderöi ne, lopettaa sen, lomake muuttuu tyhjäksi sivuksi, jossa on paikkamerkki "avaa toisessa lukijassa" (please open in a different reader). Kestävä korjaus on litistää (flatten) XFA staattiseksi PDF-sisällöksi, jonka kuka tahansa lukija voi maalata

Tuon litistämisen vaikea osa eivät ole kentät. Tekstikentät ja valintaruudut yhdistyvät AcroForm-widgetteihin riittävän siististi. Vaikea osa on rikasteksti, jonka XFA tallentaa piirtoelementin (draw element) sisään, <exData contentType="text/html"> -lohkoon. Tuo lohko on HTML-osajoukko (subset), jossa on sisäinen muotoilu (inline styling) ja usein ankkureita. Sen saaminen sivulle tarkoittaa sekä muotoillun tekstin että elävien hyperlinkkien toistamista, ja hyperlinkit ovat kohta, jossa useimmat toteutukset luovuttavat hiljaa

Miltä XFA-rikasteksti todella näyttää

exData-runko on pieni viipale XHTML:ää. Kappale on <p>; muotoiltu merkkijakso on <span>, jolla on oma sisäinen CSS painolle, asennolle (posture), värille ja koolle; ja hyperlinkki on <a href="...">, joka kietoo sen näkyvän tekstin. Yhdellä rivillä voi olla useita jaksoja peräkkäin, kullakin erilainen muotoilu, ja yksi niistä voi olla ankkuri. Muotoilu ei ole koristelu, joka voidaan pudottaa pois. Lauseke, joka on renderöity lihavoidulla punaisella, koska se on oikeudellinen varoitus, on pysyttävä lihavoituna ja punaisena litistämisen jälkeen, tai litistetty asiakirja vääristää alkuperäistä

Joten litistysmoottori ei voi käsitellä lohkoa yhtenä merkkijonona. Sen on käytävä läpi sisäinen rakenne, ratkaistava jokaisen jakson (run) efektiivinen tyyli kerrostamalla jakson sisäinen CSS piirtoelementin perusfontin päälle ja ladottava jaksot toinen toisensa perään riville. HotPDF mallintaa jokaisen näistä ladotuista fragmenteista sisäisenä TXFARichRun-tietueena. Tietue kantaa jakson tekstin, sen ratkaistun tyylin, sen mitatun laatikon ja ankkurille sen Href:n, johon se osoittaa

Jaksojen latominen vasemmalta oikealle

Sijoittelu on kohta, jossa rikasteksti lakkaa olemasta jäsennysongelma (parsing problem) ja muuttuu ladontaongelmaksi. Jaksot jakavat rivin, joten jokainen jakso alkaa siitä, mihin edellinen päättyi. Ei ole olemassa merkkauskieltä (markup), joka tallentaisi nuo sijainnit; ne on mitattava. Moottorin sisäinen LayoutRichText-rutiini mittaa jokaisen jakson samoilla fonttimetriikoilla, jotka myöhemmin maalaavat sen, ja asettaa sitten jakson vaakasuuntaisen siirtymän kaikkien aiempien jaksoleveyksien juoksevaan summaan. Jakso yksi alkaa piirtolaatikon origosta, jakso kaksi alkaa jakson yksi leveydestä, jakso kolme kahden ensimmäisen yhdistetystä leveydestä ja niin edelleen koko rivin poikki

Tästä syystä mittausfontin kohdistuksella on niin paljon merkitystä. Asetteluvaihe (layout pass) mittaa etenemisiä (advances); erillinen renderöintivaihe piirtää glyyfejä. Jos nämä kaksi vaihetta ovat eri mieltä fontista, asettelun laskemat laatikot eivät istu renderöijän maalaamien glyyfien alla. HotPDF pitää ne tahdissa kartoittamalla kunkin jakson ratkaistun tyylin fonttimääritykseen sisäisen RunStyleToFontSpec-apulaisen kautta, joka vastaa renderöijän omia oletusasetuksia (Arial, 10 pistettä). Mitattu eteneminen ja piirretty teksti ovat silloin yhtä mieltä, ja jakson laskettu laatikko aidosti peittää lukijan näkemät merkit

// Conceptual shape of one laid-out run. The engine builds an array of these
// internally; you never construct them yourself, but the fields explain how a
// link's hit box is derived from measured geometry rather than from text.
type
  TRichRunInfo = record
    Dx, Dy : Double;       // top-left, relative to the draw-box origin
    W, H   : Double;       // measured run box (width from the layout pass)
    Text   : AnsiString;   // the run's visible characters
    Href   : AnsiString;   // URI target for an <a> run, '' otherwise
  end;

Ankkurijaksosta PDF-Link-merkinnäksi

Hyperlinkki valmiissa PDF-tiedostossa ei ole osa sivun sisältöä. Se on erillinen objekti, Link-merkintä, joka on kuvattu kohdassa ISO 32000-1 §12.5.6.5. Merkinnässä on /Rect, joka määrittelee napsautettavan suorakulmion sivulla, ja toiminto (action), joka laukeaa, kun suorakulmiota napsautetaan. Ulkoiselle linkille toiminto on URI-toiminto: /S /URI, jonka kohdeosoite on sen /URI-merkkijonona. Näkyvä teksti sen alla on tavallista sivun sisältöä; merkintä on näkymätön aktiivinen alue (hot zone), joka on asetettu sen päälle

Litistyspolku noudattaa täsmälleen tätä mallia. Kun jakso kantaa Href-arvoa, HotPDF piirtää ensin muotoillun tekstin ja rakentaa sitten Link-merkinnän jakson laatikon päälle. Julkinen aloituskohta tuolle merkinnälle on sivun metodi AddURILink, joka luo /Type /Annot /Subtype /Link -objektin /URI-toiminnolla ja palauttaa merkintäsanakirjan (annotation dictionary). Sen suorakulmio on jakson mitattu laatikko, käännettynä piirtoelementin paikallisista koordinaateista sivun koordinaatteihin. Tuloksena on linkki, joka laskeutuu tarkalleen ankkuritekstiin eikä minnekään muualle

// The same public API the flatten path uses for each anchor run. It produces
// an ISO 32000-1 12.5.6.5 Link annotation: /Subtype /Link with a /URI action
// over the given rectangle. The optional description fills /Contents so a
// screen reader can announce the target.
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // page-space hit box for the run
  Annot := Pdf.CurrentPage.AddURILink(LinkRect,
    'https://www.example.gov/appeal', 'File an appeal online');
end;

Miksi osuma-alueen (hit box) on tultava mitatuista leveyksistä

On houkuttelevaa kuvitella sijoittavansa linkki etsimällä sivulta sen näkyvä teksti ja piirtämällä suorakulmio minkä tahansa löytyneen ympärille. Se ei toimi, ja syy on perustavanlaatuinen sille, miten litistetty teksti tallennetaan. Muotoillut jaksot maalataan upotetuilla osajoukko-fonteilla (subset fonts). Osajoukko-fontti numeroi uudelleen glyyfit, jotka se säilyttää, joten sivun sisältövirta (content stream) pitää sisällään heksadesimaalisia CID-koodeja, ei alkuperäisiä merkkikoodeja. Sivulla olevat tavut eivät ole kirjaimia, joita ihminen lukee, eivätkä ne ole haettavissa tekstinä. Haku ankkurin kuvatekstillä ei löydä mitään, koska kyseistä kuvatekstiä ei ole olemassa kirjaimellisena tekstinä missään virrassa

Ainoa luotettava ankkuri suorakulmiolle on geometria, jonka asetteluvaihe (layout pass) jo tuotti. Kunkin jakson siirtymä ja mitattu leveys laskettiin virratettaessa riviä, ennen kuin yhtäkään glyyfiä numeroitiin uudelleen, ja ne kuvaavat, missä teksti fyysisesti ilmestyy. Siksi HotPDF ottaa linkkisuorakulmion suoraan jakson alaslasketusta laatikosta (laid-down box) minkään tekstin haun sijaan. Koska mittauksessa käytettiin renderöintifonttia, laatikko on oikea riippumatta alijoukon muodostamisesta (subsetting). Geometria selviää koodauksesta; teksti ei. Se on koko peruste mitatun leveyden sijoittelulle, ja siksi litistäjä (flattener), joka yrittää asentaa linkkejä jälkikäteen tekstihaulla, tuottaa osuma-alueita (hit zones), jotka ajelehtivat tai katoavat

Litistyksen ohjaaminen koodistasi

PDF-tiedostolle, joka sisältää jo XFA-paketin, aloituskohta on FlattenLoadedXFA. Lataa asiakirja, kutsu metodia ja tallenna tulos. Editable-parametri päättää, mitä lomakekentille tapahtuu: välitä True pitääksesi ne täytettävinä AcroForm-widgetteinä, tai False merkitäksesi jokaisen widgetin vain luku -tilaan, jotta tuloste on jäädytetty tietue. Rikastekstin piirtolohkot (rich-text draw blocks) muotoiltuine jaksoineen ja linkkimerkintöineen tuotetaan joka tapauksessa. Funktio palauttaa sen lähettämien widgettien määrän

var
  Pdf: THotPDF;
  Emitted, i: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('xfa_appeal_form.pdf');
    // True keeps fields fillable; False freezes them read-only.
    Emitted := Pdf.FlattenLoadedXFA(True);

    // Anything the engine could not map is reported, not raised.
    for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
      Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);

    Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
    Writeln('Widgets emitted: ', Emitted);
  finally
    Pdf.Free;
  end;
end;

Lue aina XFAFlattenWarnings kutsun jälkeen. Luettelo tyhjennetään jokaisen litistyksen alussa, ja siihen kertyy rivi jokaiselle elementille, jota moottori kieltäytyi renderöimästä: tukematon kenttätyyppi, piirtokuva, joka ei purkaudu, exData-lohko, jossa ei ole käyttökelpoisia jaksoja. Mikään näistä ei nosta poikkeusta, joten tyhjä varoituslista on todisteesi siitä, että kaikki kartoitettiin, ja ei-tyhjä lista kertoo tarkalleen, mitkä alkuperäiskappaleet on tarkistettava. Kun pidät raakaa XFA:ta XDP-tavuina (XDP bytes) ladatun PDF:n sijaan, sisarmetodi ApplyXFAAsAcroForm ottaa kyseiset tavut suoraan ja jakaa saman koodipolun ja saman varoituskäyttäytymisen. Täydentävä AddXFAPacket-metodi kulkee toiseen suuntaan ja upottaa XFA-paketin asiakirjaan, jota olet rakentamassa

Tuloksen vahvistaminen lukijassa

Avaa litistetty tiedosto Acrobatissa tai missä tahansa nykyisessä katseluohjelmassa ja tarkista kaksi asiaa. Ensinnäkin rikasteksti on renderöity sen muotoilu säilyttäen: lihavoidut jaksot ovat lihavoituja, värilliset jaksot kantavat värinsä, ja jaksot istuvat oikeassa järjestyksessä rivillä sen sijaan, että ne menisivät päällekkäin tai juoksisivat ulos laatikosta. Toiseksi hyperlinkit ovat eläviä. Vie hiiren osoitin ankkurin päälle, ja tilapalkin tulisi näyttää kohdeosoite; napsauta sitä, ja URI-toiminnon tulisi avata se. Käytä katseluohjelman merkintätarkastajaa (annotation inspector) vahvistaaksesi, että jokainen on aito /Link-merkintä, jonka /Rect halaa ankkuritekstiä ja istuu sisällön päällä, joka on nyt vain maalattuja glyyfejä lomakkeen renderöimän XFA:n sijaan. Tämä yhdistelmä, muotoiltu staattinen teksti ja oikeat Link-merkinnät oikeilla suorakulmioilla, saa litistetyn asiakirjan elämään pidempään kuin XFA-moottorit, joita se ei enää tarvitse

Itse kenttien, tätä rikastekstiä ympäröivien tekstikenttien, valintaruutujen ja valintalistojen litistäminen käsitellään katsauksessamme XFA-lomakkeiden litistämisestä AcroForm-widgeteiksi. Laajemman tarinan Link-merkintöjen rakentamisesta ja sijoittamisesta käsin, yli niiden, joita litistyspolku luo, löydät artikkelista työskentely PDF-merkintöjen kanssa HotPDF:ssä. Molemmat rakentuvat samalle merkintä- ja lomakemallille (annotation and forms model), joka toimitetaan HotPDF Componentin mukana Delphille ja C++Builderille