Technical Article

Fladgøring af XFA rich-text hyperlinks til PDF-links i Delphi

XFA, XML Forms Architecture, er forældet. ISO 32000-1 har det med i §12.7 med den bemærkning, at det er fjernet fra PDF 2.0, og moderne fremvisere dropper deres XFA-motorer én efter én. Intet af dette har tømt arkiverne. Offentlige indberetningsformularer, forsikringsansøgninger og kontoudtog blev forfattet som XFA i størstedelen af to årtier, og disse filer ankommer stadig i indbakker og dokumentpipelines i dag. Når den fremviser, der plejede at rendere dem, holder op med det, forvandles formularen til en tom side med en pladsholder, der siger "åbn venligst i en anden læser". Den holdbare løsning er at fladgøre (flatten) XFA til statisk PDF-indhold, som enhver læser kan tegne.

Den svære del af denne fladgøring er ikke felterne. Tekstbokse og afkrydsningsfelter afbildes fint nok til AcroForm-widgets. Den svære del er den rich text, som XFA gemmer i et draw-element i en <exData contentType="text/html">-blok. Den blok er et HTML-undersæt med indbygget (inline) styling og ofte ankre (hyperlinks). At få det ind på siden betyder, at man skal reproducere både den stylede tekst og de aktive hyperlinks, og hyperlinkene er der, hvor de fleste implementeringer stille og roligt giver op.

Hvordan XFA rich text rent faktisk ser ud

En exData-krop er en lille bid XHTML. Et afsnit er et <p>; en stylet gruppe af tegn er en <span> med sin egen indbyggede CSS for fed, kursiv, farve og størrelse; og et hyperlink er et <a href="...">, der omslutter den synlige tekst. En enkelt linje kan indeholde flere spans i træk, hver med forskellig styling, og en af dem kan være et anker. Stylingen er ikke dekoration, der bare kan udelades. En klausul, der renderes i fed rød skrift, fordi det er en juridisk advarsel, skal forblive fed og rød efter fladgøringen, ellers misrepræsenterer det fladgjorte dokument originalen.

Så fladgøringsmotoren kan ikke behandle blokken som én streng. Den skal gennemgå den indbyggede struktur, finde hver kørslens (run) effektive stil ved at lægge span'ens indbyggede CSS over draw-elementets basisskrift og udlægge kørslerne én efter en hen over linjen. HotPDF modellerer hvert af disse udlagte fragmenter som en intern TXFARichRun-post. Posten bærer kørslens tekst, dens løste stil, dens målte boks og, for et anker, den Href, det peger på.

Udlægning af kørslerne fra venstre mod højre

Placering er der, hvor rich text holder op med at være et parsingproblem og bliver et satsproblem (typesetting). Kørslerne deler en linje, så hver kørsel begynder, hvor den forrige sluttede. Der er ingen opmærkning, der registrerer disse positioner; de skal måles. Motorens interne LayoutRichText-rutine måler hver kørsel med de samme skrifttypemetrikker, som senere vil tegne den, og sætter derefter kørslens vandrette forskydning til den løbende sum af all tidligere kørselsbredder. Kørsel ét starter ved draw-boksens startpunkt, kørsel to starter ved bredden af kørsel ét, kørsel tre ved den kombinerede bredde af de to første osv. hen over linjen.

Det er grunden til, at skrifttypejustering ved måling betyder så meget. Layout-passet måler bredder (advances); et separat render-pas tegner glyffer. Hvis de to gennemgange er uenige om skrifttypen, vil de bokse, som layoutet har beregnet, ikke sidde under de glyffer, som rendereren tegner. HotPDF holder dem i trit ved at afbilde hver kørslens løste stil til en skrifttypespecifikation via den interne hjælper RunStyleToFontSpec, som matcher renderens egne standarder for Arial på 10 punkter. Den målte bredde og den tegnede tekst stemmer så overens, og en kørsels beregnede boks dækker reelt de tegn, som en læser ser.

// 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;

Fra en anker-kørsel til en PDF-linkannotering

Et hyperlink i en færdig PDF er ikke en del af sideindholdet. Det er et separat objekt, en linkannotering (Link annotation), beskrevet i ISO 32000-1 §12.5.6.5. Annoteringen har et /Rect, der definerer det klikbare rektangel på siden, og en handling, der udløses, når der klikkes på rektanglet. For et eksternt link er handlingen en URI-handling: /S /URI med måladressen som dens /URI-streng. Den synlige tekst nedenunder er almindeligt sideindhold; annoteringen er den usynlige aktive zone, der er lagt hen over det.

Fladgøringsstien følger præcis denne model. Når en kørsel indeholder en Href, HotPDF først tegner den stylede tekst og bygger derefter en linkannotering over kørslens boks. Det offentlige indgangspunkt for den annotering er sidemetoden AddURILink, som opretter objektet /Type /Annot /Subtype /Link med en /URI-handling og returnerer annoteringsordbogen. Dens rektangel er kørslens målte boks, oversat fra draw-elementets lokale koordinater til sidekoordinater. Resultatet er et link, der lander præcist på ankerteksten og intet andet sted.

// 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;

Hvorfor den aktive zone skal komme fra målte bredder

Det er fristende at forestille sig at lokalisere linket ved at søge på siden efter dets synlige tekst og tegne rektanglet omkring det, der findes. Det fungerer ikke, og årsagen er fundamental for, hvordan fladgjort tekst gemmes. De stylede kørsler tegnes med indlejrede subset-skrifttyper. En subset-skrifttype omnummererer de glyffer, den beholder, så sidens indholdsstrøm indeholder heksadecimale CID-koder, ikke de oprindelige tegnkoder. Bytes på siden er ikke de bogstaver, et menneske læser, og de er ikke søgbare som tekst. En søgning efter ankerets overskrift finder intet, fordi den overskrift ikke findes som bogstavelig tekst nogen steder i strømmen.

Det eneste pålidelige anker for rektanglet er den geometri, layout-passet allerede har produceret. Hver kørselers forskydning og målte bredde blev beregnet, mens linjen blev sat, før nogen glyf blev omnummereret, og de beskriver, hvor teksten fysisk vil dukke op. HotPDF tager derfor linkrektanglet direkte fra kørslens udlagte boks frem for fra nogen tekstsøgning. Fordi målingen brugte render-skrifttypen, er boksen korrekt uanset subsetting. Geometri overlever kodningen; det gør tekst ikke. Det er hele argumentet for placering baseret på målte bredder, og det er grunden til, at en fladgører, der forsøger at eftermontere links ved hjælp af tekstsøgning, producerer aktive zoner (hit zones), der driver eller forsvinder.

Kørsel af fladgøringen fra din kode

For en PDF, der allerede indeholder en XFA-pakke, er indgangspunktet FlattenLoadedXFA. Indlæs dokumentet, kald metoden, og gem resultatet. Parameteren Editable bestemmer, hvad der sker med formularfelterne: Angiv True for at beholde dem som udfyldbare AcroForm-widgets, eller False for at markere hver widget som skrivebeskyttet (read-only), så outputtet er en fastlåst registrering. De rich-text draw-blokke med deres stylede kørsler og linkannoteringer produceres under alle omstændigheder. Funktionen returnerer antallet af widgets, den udsendte.

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;

Læs altid XFAFlattenWarnings efter kaldet. Listen ryddes i starten af hver fladgøring og akkumulerer en linje for hvert element, som motoren afviste at rendere: en ikke-understøttet felttype, et tegnebillede, der ikke kunne afkodes, en exData-blok uden brugbare spans. Ingen af disse udløser en undtagelse (exception), så en tom advarselsliste er dit bevis på, at alt blev afbildet korrekt, og en ikke-tom liste fortæller dig præcist, hvilke originaler du skal efterse. Når du har den rå XFA som XDP-bytes i stedet for en indlæst PDF, tager søskendemetoden ApplyXFAAsAcroForm disse bytes direkte og deler samme kodesti og advarselsadfærd. Den komplementære AddXFAPacket-metode går den anden vej og indlejrer en XFA-pakke i et dokument, du er ved at bygge.

Bekræftelse af resultatet i en læser

Åbn den fladgjorte fil i Acrobat eller en hvilken som helst aktuel fremviser, og kontroller to ting. For det første, at rich text blev renderet med sin styling intakt: de fede kørsler er fede, de farvede kørsler bærer deres farve, og spans sidder i den rigtige rækkefølge på linjen frem for at overlappe eller løbe uden for boksen. For det second, at hyperlinkene er aktive. Hold markøren over et anker, og statuslinjen skal vise måladressen; klik på den, og URI-handlingen skal åbne den. Brug fremviserens annoteringsinspektør til at bekræfte, at hver enkelt er en ægte /Link-annotering, hvis /Rect omslutter ankerteksten og sidder over indhold, der nu er almindelige tegnede glyffer frem for formular-renderet XFA. Den kombination – stylede statisk tekst plus rigtige Link-annoteringer på de rigtige rektangler – er det, der gør, at det fladgjorte dokument overlever de XFA-motorer, det ikke længere har brug for.

Fladgøring af selve felterne, dvs. tekstbokse, afkrydsningsfelter og valgmulighedslister, der omgiver denne rich text, er dækket i vores gennemgang af fladgøring af XFA-formularer til AcroForm-widgets. For den bredere historie om at bygge og placere linkannoteringer manuelt, ud over dem som fladgøringsstien genererer, se arbejdet med PDF-annoteringer i HotPDF. Begge bygger på den samme annoterings- og formularmodel, som leveres med HotPDF Component til Delphi og C++Builder.