Technischer Artikel

XFA-SOM-Pfadausdrücke in Delphi: Dynamische Formulardaten

Die losLab PDF Library liest und schreibt wiederkehrende Datenelemente in dynamischen XFA-Formularen über XFA 3.3 SOM-Pfadausdrücke: GetXFAFormFieldValue und SetXFAFormFieldValue akzeptieren die Roots $data, $record und !data, den Platzhalter [*] für Vorkommen, Nachfahren- und Kind-Platzhalter-Selektoren, die Eigenschaft parent, die Klassenselektoren #dataGroup/#dataValue und einfache Prädikate im FormCalc-Stil. So kann ein Delphi-Programm jede Rechnungsposition in einem staatlichen Steuerformular mit einem einzigen Aufruf aktualisieren. Dieser Artikel dient als Referenz für die Adressierung; Informationen zum Verschieben ganzer Datensätze zwischen Dateien finden Sie in dem Begleitartikel über den FDF-, XFDF- und XFA-Formulardatenaustausch

Das Problem zeigt sich in dem Moment, in dem ein Formular aufhört, flach zu sein. Eine Umsatzsteuererklärung oder Zollerklärung, die als dynamisches XFA-Formular erstellt wurde, enthält keine zwölf Felder mit den Namen Total_Price_1 bis Total_Price_12. Sie hat ein einziges Detail-Unterformular, das einmal im Vorlagen-DOM deklariert und so oft instanziiert wird, wie es die Daten erfordern. Die Werte befinden sich in einer zweiten Struktur, dem Daten-DOM innerhalb des datasets-Pakets, wo jede Position ein wiederkehrendes <Detail>-Element unter <Receipt> ist. Flache Feldnamen können nicht „den Preis in der dritten Zeile“ oder „jede Zeile über 200“ adressieren; genau das ist die Aufgabe, die das Kapitel „Scripting Object Model“ der Adobe-XFA-3.3-Spezifikation den SOM-Ausdrücken zuweist, und es ist die Syntax, die die XFA-Feldwert-APIs der losLab PDF Library verwenden

Wie adressiert man XFA-Datenknoten aus Delphi?

Jeder datasets-Pfad beginnt an einem Root, und die losLab PDF Library akzeptiert die Standardschreibweisen austauschbar: $data is die XFA-3.3-Kurzform von xfa.datasets.data, !data ist die Kurzform mit Root bei xfa.datasets, und in Paketen, die keine Datensatzverarbeitung verwenden, löst sich $record in den äußeren Datensatz auf – das erste Element unter dem Datenknoten. Vorlagen-seitige APIs wie SetXFAFormFieldAccess akzeptieren $template und xfa.template auf dieselbe Weise. Segmente können durch Punkte oder Schrägstriche getrennt sein ($data.Receipt.Tax oder $data/Receipt/Tax). Dies ist wichtig, da GetXFAFormFieldNames Felder als Pfade mit Schrägstrichen auflistet, die direkt an die Wert- und Vorlagen-APIs übergeben werden können. Vorkommensindizes sind gemäß Spezifikation nullbasiert, sodass Detail[0] die erste Zeile ist. Zwei Escaping-Regeln halten ungewöhnliche Namen adressierbar: In einem durch Punkte getrennten Pfad steht \. für einen literalen Punkt (Line\.Item), während Pfade mit Schrägstrichen Punkte als normale Zeichen behandeln. Geschäftsdaten mit eigenem XML-Namensraum-Präfix werden anhand des lokalen Namens abgeglichen, sodass <m:Receipt> weiterhin auf Receipt reagiert

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;

Wie füllt man wiederkehrende Zeilen in einem XFA-Formular programmatisch?

Der Platzhalter [*] für Vorkommen ist das Batch-Werkzeug. Während ein numerischer Index ein einzelnes Geschwisterelement auswählt, wählt [*] alle Geschwisterelemente desselben Namens aus, sodass $data.Receipt.Detail[*].Total_Price das Preisfeld in jeder Position gleichzeitig anspricht. Beim Lesen gibt GetXFAFormFieldValue die Werte aller übereinstimmenden Knoten zurück, getrennt durch ein |-Zeichen; beim Schreiben aktualisiert SetXFAFormFieldValue jeden übereinstimmenden Knoten mit demselben Wert und gibt bei Erfolg 1 zurück. Ein Pfad, der keine Übereinstimmung findet, liefert eine leere Zeichenfolge zurück. Dies ist eine einfache Methode, um zu prüfen, ob ein Zweig existiert, bevor man hineinschreibt

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;

Nachfahren-, Kind-Platzhalter- und Eltern-Selektoren

Drei strukturelle Selektoren decken die Fälle ab, in denen Sie den Feldnamen, aber nicht seine genaue Tiefe kennen. Der Nachfahren-Selektor .. findet Übereinstimmungen in jeder Tiefe unter dem aktuellen Knoten: $data..Total_Price findet das erste Total_Price-Element irgendwo unter dem Daten-Root, und ein Schreibvorgang über einen Nachfahrenpfad aktualisiert diesen ersten übereinstimmenden Knoten. Beachten Sie die Grenze: Ein Vorkommensindex nach einer Nachfahren-Übereinstimmung springt nicht über Geschwisterzweige hinweg. Wenn sich also $data..Total_Price[0] innerhalb der ersten Detail-Zeile auflöst, gibt $data..Total_Price[1] ein leeres Ergebnis zurück, anstatt zur nächsten Zeile zu springen; verwenden Sie Detail[*], wenn Sie alle ansprechen möchten. Der Kind-Platzhalter .* entspricht jedem direkten Kindelement und filtert die verbleibenden Segmente: $data.Receipt.*.Total_Price erreicht das Total_Price-Feld in jedem Kindzweig von Receipt, ohne ein gleichnamiges Zusammenfassungsfeld auszuwählen, das direkt auf Receipt selbst liegt. Schließlich steigt die Eigenschaft parent eine Ebene nach oben, was XFA 3.3 bewusst von .. trennt: $data.Receipt.Detail[1].parent.Tax beginnt bei der zweiten Position, steigt zu Receipt auf und landet bei dessen Geschwisterwert Tax

Klassenselektoren und Prädikate: #dataGroup, #dataValue, .[expression]

Die #class-Syntax adressiert Daten-DOM-Knoten nach Objektklasse statt nach Namen. Dies ist der praktische Weg, wenn XML-Tags Zeichen enthalten, die sich nur schwer als Skriptnamen schreiben lassen. Die losLab PDF Library ordnet #dataGroup Elementen zu, die Element-Kinder haben, und #dataValue Blatt-Elementen ohne Element-Kinder. Beide lassen sich mit numerischen oder [*]-Vorkommen kombinieren: $data.Receipt.#dataGroup[0].#dataValue[0] wählt den ersten Wert innerhalb der ersten Gruppe unter Receipt aus. Der Prädikaten-Selektor .[expression] filtert gleichnamige Geschwisterelemente nach Inhalt. Unterstützt werden einfache Vergleiche eines Kindwerts mit einem Literal unter Verwendung relationaler Operatoren wie >, < und <=. Wenn ein Prädikat auf mehrere Zeilen zutrifft, werden Lesevorgänge mit | zusammengeführt und Schreibvorgänge aktualisieren jede Übereinstimmung

// 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]');

Welche XFA-SOM-Funktionen werden nicht unterstützt?

Die SOM-Unterstützung in der losLab PDF Library beschränkt sich auf Datasets und Vorlagen-Feldpfade, und die Grenzen sind explizit statt Best-Effort definiert. Wenn Sie diese im Voraus kennen, erspart Ihnen das die Fehlersuche bei einem Pfad, der stillschweigend eine leere Zeichenfolge zurückgibt

  • Prädikate führen kein beliebiges FormCalc oder JavaScript aus: keine Funktionsaufrufe (ein contains(...)-Prädikat gibt ein leeres Ergebnis zurück), keine zusammengesetzten booleschen Ausdrücke, sondern nur ein einfacher child op literal-Vergleich
  • $record funktioniert nur in Paketen ohne Datensatzverarbeitung; dataWindow-Datensatz-Paging und Datensatzgruppen-Rotation sind nicht implementiert
  • Die Auflösung erfolgt direkt gegen das Daten-DOM und das Vorlagen-DOM; die relative Auflösung des Form-DOMs – die zusammengeführte Ansicht, die ein Viewer zur Laufzeit erstellt – wird nicht durchgeführt
  • Die Attribut-Ladesemantik des Daten-DOMs wird nicht angewendet; die Bibliothek liest das datasets-Paket als rohes XML, sodass Pfade Elemente adressieren, keine Attribute, die zu Knoten heraufgestuft wurden

Diese Grenzen stören bei der Batch-Befüllung selten, da ein Befüller Daten anhand ihrer Struktur und nicht anhand von Skriptlogik adressiert. Wenn sie doch stören, ist der Rettungsanker die Paketebene: Lesen Sie die gesamte datasets-XML aus, transformieren Sie sie in Delphi und schreiben Sie sie wieder zurück

Schreiben des gesamten Pakets mit SetXFAFromString

SetXFAFromString ist dieser Einstiegspunkt auf Paketebene: Er installiert eine vollständige XDP-Zeichenfolge, Vorlage und Datasets zusammen, und erstellt den AcroForm-Container in einem neuen Dokument, falls noch keines vorhanden ist. Er akzeptiert sowohl UTF-16-codierte Pakete mit Byte-Reihenfolgemarken (BOM) als auch UTF-8. In der Praxis wird häufig die von der Behörde bereitgestellte XDP-Datei als feste Vorlage geladen, per SetXFAFromString eingelesen, und dann über SOM-adressierte SetXFAFormFieldValue-Aufrufe die flüchtigen rechnungsspezifischen Werte vor dem Speichern aktualisiert. Da XFA eines von zwei Formularmodellen ist, die eine PDF-Datei enthalten kann, hat die klassische AcroForm-Seite ihre eigene Automatisierungsgeschichte, die in dem Artikel über interaktive Formularaktionen und JavaScript behandelt wird

Die hier gezeigten XFA-Feldwert-, Enumerations- und Paket-APIs sind in der losLab PDF Library für Delphi, C# und VB.NET enthalten; die Produktseite enthält die vollständige Referenz zur Formularverarbeitung