Teknisk artikel

Fladgørelse af XFA Rich-Text Hyperlinks til PDF-links i Delphi

XFA, XML Forms Architecture, er forældet. ISO 32000-1 har den i §12.7 med kommentaren, at den er fjernet fra PDF 2.0, og moderne fremvisere dropper deres XFA-motorer én efter én. Intet af dette har tømt arkiverne. Statens indberetningsformularer, forsikringsansøgninger og kontoudtog blev forfattet som XFA igennem størstedelen af to årtier, og disse filer ankommer stadig i indbakker og dokumentpipelines i dag. Når fremviseren, der plejede at gengive dem, holder op med det, bliver formularen til en blank side med pladsholderen "åbn venligst i en anden læser". Den varige løsning er at fladgøre XFA'en til statisk PDF-indhold, som enhver læser kan male

Den svære del af den fladgørelse er ikke felterne. Tekstfelter og afkrydsningsfelter knytter sig pænt nok til AcroForm-widgets. Den svære del er den rige tekst, som XFA gemmer i et tegneelement, i en <exData contentType="text/html">-blok. Den blok er en HTML-undergruppe med indlejret styling og ofte ankre. At få det ind på siden betyder at reproducere både den stylede tekst og de live hyperlinks, og hyperlinksene er der, hvor de fleste implementeringer stille og roligt giver op

Hvordan XFA rich text faktisk ser ud

En exData-brødtekst er en lille del af XHTML. Et afsnit er et <p>; et stylet spænd af tegn er et <span> med sin egen indlejrede CSS for tykkelse, kropsholdning, farve og størrelse; og et hyperlink er et <a href="...">, der ombryder dets synlige tekst. Én enkelt linje kan indeholde adskillige spænd på stribe, hver med forskellig styling, og en af dem kan være et anker. Stylingen er ikke dekoration, der bare kan droppes. En paragraf der er udfærdiget i fed rød, fordi den er en juridisk advarsel, skal forblive fed og rød efter fladgørelse, ellers repræsenterer det fladgjorte dokument ikke originalen på retvisende måde

Så formateringsmaskinen kan ikke behandle blokken som én streng. Den er nødt til at gennemgå den indlejrede struktur, løse hver enkelt afviklings effektive styling, ved at lægge spændets indlejrede CSS over tegneelementets grundlæggende skrifttype, og lægge afviklingerne ud, den ene efter den anden, på tværs af linjen. HotPDF modellerer hvert af disse formaterede fragmenter som en intern TXFARichRun record. En record bringer afviklingens tekst, dens valgte stil, dens opmålte felt, og, hvis det er et anker, det Href den peger på

Placering af forløb fra venstre mod højre

Positionering er der, hvor rich text holder op med at være et parser-problem, og i stedet bliver til et typesætnings-problem. Forløbene deler en linje, så hvert forløb starter der, hvor det foregående sluttede. Der er ingen opmærkning, der gemmer disse positioner; de skal opmåles. Maskinens interne LayoutRichText rutine opmåler hvert forløb med de samme typografimål, som vil male den senere, og derpå sætter den forløbets horisontale kompensation til summen af bredden fra samtlige de tidligere forløb. Forløb 1 starter ved tegnefeltets nulpunkt, forløb 2 starter fra bredden af forløb 1, forløb 3 ved den samlede bredde af de to første, og så fremdeles hele linjen hen

Dette er grunden til at justering af skrifttypens målinger betyder så meget. Layout-passet måler fremskridt; et adskilt render-pas tegner glyffer. Hvis disse to pas ikke er enige om skrifttypen, vil de bokse, som layoutet har udregnet, ikke ligge under de glyffer, der er tegnet af rendereren. HotPDF holder dem i takt med hinanden, ved at forbinde hvert enkelt forløbs endelige styling til en skrifttypespecifikation ved hjælp af den interne RunStyleToFontSpec hjælper, som stemmer overens med rendererens egne standardindstillinger, nemlig Arial ved 10 punkter. Det målte fremskridt og den indtegnede tekst passer nu sammen, og et forløbs udregnede boks ligger nu præcist henover de tegn som en læser betragter

// 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 et anker-forløb til en PDF-link-annotation

Et hyperlink i en færdig PDF er ikke en del af sideindholdet. Det er et separat objekt, en link-annotation, der er beskrevet i ISO 32000-1 §12.5.6.5. Denne annotation har et /Rect, der definerer det klikbare rektangel på siden, og en handling der udløses, når der klikkes på rektanglet. Ved et eksternt link er handlingen en URI-handling: /S /URI, med destinationsadressen som dens /URI-streng. Den synlige tekst nedenunder er ganske almindeligt sideindhold; denne annotation er en usynlig varm zone, der ligger henover det

Fladgørelsens sti følger præcis denne model. Når et forløb rummer et Href, tegner HotPDF allerførst den formaterede tekst og bygger derefter en Link-annotation, der anbringes henover forløbets boks. Det offentlige adgangspunkt for annotationen, er sidemetoden AddURILink, som skaber et /Type /Annot /Subtype /Link objekt, der indeholder en /URI-handling, og som returnerer et bibliotek med annotationen. Dens rektangel er det område, der er udmålt for forløbets boks, og som oversættes fra tegneelementets lokale koordinater, til sidens koordinater. Resultatet er et link, der lander helt præcist henover ankerets tekst, og ingen andre steder

// 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 træffeltet skal komme fra opmålte bredder

Det frister, hvis man forestiller sig at kunne placere linket, ved at gennemsøge siden for dens synlige tekst, for derefter at indtegne rektanglet rundt om det man eventuelt har fundet. Dette fungerer bare ikke, og grunden til det udspringer af de grundlæggende metoder for fladgjort tekst. De stylede forløb tegnes op, ved hjælp af indlejrede undersæt-skrifttyper (subset fonts). Et skrifttype-undersæt omnummererer de glyffer som den beholder, så strømmen af sideindhold indeholder hexadecimale CID-koder, men ikke de originale koder for tegnene. Sideindholdets bytes er ikke udgjort af de bogstaver, et menneske kan læse, og der kan ikke søges efter dem, som hvis det var tekst. En søgning efter ankerets overskrift finder ikke noget, fordi den overskrift ganske enkelt ikke eksisterer i strømmen, som om det var ægte tekst

Det eneste troværdige ankerpunkt for rektanglet, er den geometri som layout-passet allerede har produceret. Hvert forløbs forskydning og opmålte bredde blev udregnet under formningen af linjen, før en glyph blev omnummereret, og disse udregninger beskriver nøjagtigt, hvor teksten rent fysisk vil dukke op. Af den grund anbringer HotPDF link-rektanglet, lige præcist ud fra det faldende forløbs boks, i stedet for en form for tekstsøgning. Fordi der i opmålingen blev anvendt skrifttypen fra rendereren, vil boksen altid være korrekt uafhængigt af dens subsetting. Geometrien kan modstå kodningen; det kan teksten ikke. Dér har man det grundlæggende argument for den positionsplacering, der sker igennem de udmålte bredder, og dermed også grunden til at en fladgører (flattener), der forsøger at tilpasse links ud fra en tekstsøgning, vil generere nogle varme zoner, der forflytter sig, eller som kan forsvinde helt og holdent

Sæt fart i fladgørelsen fra din kode

For en PDF, der allerede indeholder en XFA-pakke, er adgangspunktet FlattenLoadedXFA. Indlæs dokumentet, fremkald metoden og gem resultatet. Editable-parameteren bestemmer, hvad der sker med felterne i formularen: benyt True for at beholde dem som widgets, der kan udfyldes med AcroForm, eller False for at markere enhver widget som en form for "read-only", så resultatet i stedet bliver en "frossen" og låst registrering. Uanset, hvad man gør, bliver rich text tegneblokkene, med deres formaterede forløb og link-annotationer, dannet hver eneste gang. Funktionen returnerer en sammentælling af de widgets, den har afgivet

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;

Husk altid at læse XFAFlattenWarnings efter kaldet. Listen tømmes automatisk i starten af hver fladgørelse, og gemmer en linje for alle de elementer, som motoren ikke formåede at renderere: en ikke-understøttet type felt, et tegnebillede der ikke kunne blive afkodet, en exData-blok uden nogen brugbare afviklinger. Intet af dette vil udløse en undtagelse, så en tom liste over advarsler, vil således være dit endegyldige bevis på at alt har mappet, og omvendt vil en liste der ikke er tom, fortælle dig med 100 procent nøjagtighed, hvilke originaler det er der skal inspiceres. Skulle du rumme XFA, i sin reneste og mest uforarbejdede form, som XDP bytes, der ikke var indlæst i en PDF, og the sibling metoden ApplyXFAAsAcroForm i stedet tager imod disse bytes direkte, og dermed også deler med andre både via den samme kodesti, samt samme advarselsadfærd. Den komplementære metode AddXFAPacket arbejder lige akkurat i den modsatte retning og indlejrer derimod en XFA-pakke, ned i det dokument, du er ved at udforme

Bekræftelse af et resultat hos en læser

Åbn den fladgjorte fil i Acrobat, eller enhver aktuel fremviser, og tjek to ting. For det første, at rich text er gengivet med sin styling intakt: de fede forløb er fede, de farvede forløb bærer deres farve, og spændene sidder i den rigtige rækkefølge på linjen, i stedet for at overlappe hinanden eller løbe uden for boksen. For det andet, at hyperlinkene er live. Hold musen over et anker, og statuslinjen bør vise måladressen; klik på det, og URI-handlingen bør åbne det. Brug fremviserens annotation-inspektør til at bekræfte, at hver enkelt er en ægte /Link-annotation, hvis /Rect omfavner ankerets tekst, og sidder oven på indhold, der nu er helt almindelige malede glyffer, i stedet for form-gengivet XFA. Den kombination, statisk stylet tekst plus rigtige Link-annotationer på de rigtige rektangler, er det, der får det fladgjorte dokument til at overleve de XFA-motorer, det ikke længere har brug for

Fladgørelse af selve felterne, tekstboksene, afkrydsningsfelterne og valglisterne, der omgiver denne rige tekst, er dækket af vores gennemgang af fladgørelse af XFA-formularer til AcroForm widgets. Hvad angår den bredere historie om at bygge og placere Link-annotationer i hånden, udover dem fladgørelsens sti frembringer, kan du henvise til arbejde med PDF-annotationer i HotPDF. Begge bygger på den samme model for annotationer og formularer, der leveres med HotPDF Component til Delphi og C++Builder