Articol tehnic

Expresii de cale XFA SOM în Delphi: Date dinamice de formular

losLab PDF Library citește și scrie noduri de date repetate în formulare dinamice XFA prin intermediul expresiilor de cale XFA 3.3 SOM: GetXFAFormFieldValue și SetXFAFormFieldValue acceptă rădăcinile $data, $record și !data, caracterele joker de apariție [*], selectori de descendenți și selectori joker pentru copii, proprietatea parent, selectori de clasă #dataGroup/#dataValue și predicate simple în stil FormCalc, permițând unui program Delphi să actualizeze fiecare linie de factură dintr-un formular fiscal guvernamental cu un singur apel. Acest articol este referința de adresare; pentru mutarea unor seturi întregi de date între fișiere, consultați articolul însoțitor despre schimbul de date de formular FDF, XFDF și XFA

Problema apare în momentul în care un formular nu mai este plat. O declarație de TVA sau o declarație vamală construită ca formular dinamic XFA nu are douăsprezece câmpuri numite Total_Price_1 până la Total_Price_12; are un subformular Detail declarat o singură dată în DOM-ul șablonului și instanțiat de atâtea ori cât cer datele. Valorile se află într-un al doilea arbore, DOM-ul de Date (Data DOM) din interiorul pachetului de seturi de date, unde fiecare element de linie este un element <Detail> repetat sub <Receipt>. Numele de câmpuri plate nu se pot adresa la „prețul de pe a treia linie” sau „fiecare linie de peste 200”; aceasta este exact sarcina pe care capitolul Scripting Object Model al specificației Adobe XFA 3.3 o atribuie expresiilor SOM, și este sintaxa pe care o folosesc API-urile pentru valori de câmpuri XFA ale losLab PDF Library

Cum se adresează nodurile de date XFA din Delphi?

Fiecare cale de seturi de date începe de la o rădăcină, iar losLab PDF Library acceptă denumirile standard în mod interschimbabil: $data este forma scurtă XFA 3.3 pentru xfa.datasets.data, !data este forma scurtă cu rădăcina în xfa.datasets, iar în pachetele care nu utilizează procesarea înregistrărilor, $record se rezolvă la înregistrarea de date externă, primul element de sub nodul de date. API-urile de pe partea de șablon, cum ar fi SetXFAFormFieldAccess, acceptă $template și xfa.template în același mod. Segmentele pot fi delimitate prin puncte sau prin slash ($data.Receipt.Tax sau $data/Receipt/Tax), ceea ce contează deoarece GetXFAFormFieldNames enumeră câmpurile ca căi delimitate prin slash, care pot fi utilizate direct în API-urile de valori și șabloane. Indecșii de apariție au baza zero conform specificației, deci Detail[0] este primul rând. Două reguli de ecranare (escaping) mențin adresabile numele neobișnuite: într-o cale delimitată prin puncte, \. reprezintă un punct literal (Line\.Item), în timp ce căile delimitate prin slash tratează punctele ca pe niște caractere obișnuite. Datele de afaceri care poartă propriul prefix de spațiu de nume XML sunt potrivite după numele local, astfel încât <m:Receipt> răspunde în continuare la Receipt

var
  Lib: TPDFlib;
  Tax: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('vat-return.pdf', '');
    // Equivalent addresses for the same data node
    Tax := Lib.GetXFAFormFieldValue('Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('xfa.datasets.data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$record.Tax');
    // Slash form with a zero-based occurrence index
    Lib.SetXFAFormFieldValue('$record/Detail[0]/Total_Price', '251.00');
    Lib.SaveToFile('vat-return-updated.pdf');
  finally
    Lib.Free;
  end;
end;

Cum se completează programatic rândurile repetate într-un formular XFA?

Caracterul joker de apariție [*] este instrumentul pentru procesare în lot. Acolo unde un index numeric selectează un singur frate (sibling), [*] selectează toți frații cu același nume, astfel încât $data.Receipt.Detail[*].Total_Price adresează simultan câmpul de preț de pe fiecare linie de produs. La citire, GetXFAFormFieldValue returnează valorile tuturor nodurilor potrivite unite printr-un separator |; la scriere, SetXFAFormFieldValue actualizează fiecare nod potrivit cu aceeași valoare și returnează 1 în caz de succes. O cale care nu se potrivește cu nimic este citită înapoi ca un șir gol, ceea ce reprezintă o modalitate simplă de a verifica dacă o ramură există înainte de a scrie în ea

var
  Lib: TPDFlib;
  Prices: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('invoice.pdf', '');
    // All Detail rows at once: '250.00|60.00'
    Prices := Lib.GetXFAFormFieldValue('$data.Receipt.Detail[*].Total_Price');
    // Reset one column across every repeated row in a single call
    Lib.SetXFAFormFieldValue('$data.Receipt.Detail[*].Surcharge', '0.00');
    Lib.SaveToFile('invoice-updated.pdf');
  finally
    Lib.Free;
  end;
end;

Selectori de descendenți, selectori joker pentru copii și selectori părinte

Trei selectori structurali acoperă cazurile în care cunoașteți numele câmpului, dar nu și adâncimea sa exactă. Selectorul de descendenți .. face potriviri la orice adâncime sub nodul curent: $data..Total_Price găsește primul Total_Price oriunde sub rădăcina de date, iar o scriere printr-o cale descendentă actualizează acel prim nod potrivit. Rețineți limita: un index de apariție după o potrivire descendentă nu trece peste ramurile surori, deci dacă $data..Total_Price[0] se rezolvă în interiorul primului rând Detail, $data..Total_Price[1] returnează un rezultat gol în loc să sară la rândul următor; folosiți Detail[*] când le doriți pe toate. Caracterul joker pentru copii .* se potrivește cu fiecare copil direct și lasă segmentele rămase să filtreze: $data.Receipt.*.Total_Price ajunge la valoarea Total_Price din interiorul fiecărei ramuri copil a Receipt fără a selecta și un câmp de rezumat cu același nume aflat direct pe Receipt în sine. În cele din urmă, proprietatea parent urcă un nivel, ceea ce specificația XFA 3.3 separă în mod deliberat de ..: $data.Receipt.Detail[1].parent.Tax începe la al doilea articol, urcă la Receipt și ajunge la valoarea fratelui său Tax

Selectori de clasă și predicate: #dataGroup, #dataValue, .[expresie]

Sintaxa #class adresează nodurile Data DOM după clasa de obiecte în loc de nume, ceea ce este calea practică atunci când etichetele XML conțin caractere care sunt greu de ortografiat ca nume de script. losLab PDF Library asociază #dataGroup cu elementele care au elemente copil și #dataValue cu elementele frunză fără copii, ambele putându-se combina cu apariții numerice sau cu [*]: $data.Receipt.#dataGroup[0].#dataValue[0] selectează prima valoare din primul grup sub Receipt. Selectorul de predicat .[expresie] filtrează frații cu același nume în funcție de conținut. Predicatele acceptate sunt comparații simple ale unei valori copil cu un literal, utilizând operatori relaționali precum >, < și <=; când un predicat se potrivește cu mai multe rânduri, citirile sunt returnate unite prin |, iar scrierile actualizează fiecare potrivire

// Flag every line whose Total_Price exceeds 200
Lib.SetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price > 200].Review_Flag', '1');

// Descriptions of all low-value lines, '|'-joined when several match
Desc := Lib.GetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price <= 200].Description');

// Class-based addressing when tag names resist script spelling
Total := Lib.GetXFAFormFieldValue(
  '$data.Receipt.#dataGroup[0].#dataValue[0]');

Ce caracteristici XFA SOM nu sunt acceptate?

Suportul SOM din losLab PDF Library este limitat la căile pentru seturi de date (datasets) și șabloane de câmpuri, iar limitele sunt explicite și nu pe baza celui mai bun efort. Cunoașterea acestora în avans scutește de depanarea unei căi care returnează silențios un șir gol

  • Predicatele nu execută FormCalc sau JavaScript arbitrar: nu există apeluri de funcții (un predicat contains(...) returnează un rezultat gol), nu există expresii booleene compuse, ci doar o singură comparație copil operator literal
  • $record funcționează numai în pachete fără procesare de înregistrări; paginarea înregistrărilor dataWindow și rotația grupurilor de înregistrări nu sunt implementate
  • Rezolvarea se face direct pe Data DOM și template DOM; rezolvarea relativă Form DOM, adică vizualizarea îmbinată pe care un vizualizator o construiește la execuție, nu este efectuată
  • Semantica de încărcare a atributelor Data DOM nu este aplicată; biblioteca citește pachetul de seturi de date ca XML brut, astfel încât căile adresează elemente, nu atribute promovate la noduri

Aceste limitări afectează rareori activitatea de completare în lot, deoarece cel care completează adresează datele prin structură și nu prin logică scriptată. Când apar, alternativa este nivelul de pachet: citiți întregul XML de seturi de date, transformați-l în Delphi și scrieți-l înapoi

Scrierea de pachete complete cu SetXFAFromString

SetXFAFromString reprezintă acel punct de intrare la nivel de pachet: instalează un șir complet XDP, șablonul și seturile de date împreună, creând containerul AcroForm într-un document nou dacă nu există încă unul, și acceptă pachete codificate UTF-16 cu semne de ordine a octeților (BOM), precum și UTF-8. O structură comună în producție este păstrarea fișierului XDP emis de agenție ca o resursă statică, încărcarea lui cu SetXFAFromString și apoi rularea de apeluri SetXFAFormFieldValue cu adrese SOM pentru valorile volatile specifice fiecărei facturi înainte de salvare. Deoarece XFA este unul dintre cele două modele de formulare pe care le poate conține un PDF, partea clasică AcroForm are propria sa poveste de automatizare, prezentată în articolul despre acțiuni interactive pe formulare și JavaScript

API-urile XFA pentru valoarea câmpului, enumerare și pachet prezentate aici sunt incluse în losLab PDF Library pentru Delphi, C# și VB.NET; pagina produsului conține referința completă pentru manipularea formularelor