HotPDF는 TXFAWidgetRuntime으로 델파이에서 동적 XFA 폼을 채웁니다. 모든 필드 편집을 하나의 트랜잭션으로 다루는 호스트 중립 위젯 계층입니다. 스냅샷, 검증, 계산, 리플로우, 그리고 전체 발행 또는 전체 롤백. 여러분의 VCL이나 FMX 호스트 안에서 단일 스레드로 돌고, 설치된 Acrobat을 필요로 하지 않으며, 무엇이든 할당하기 전에 모든 예산을 집행합니다
시나리오는 정부나 보험 업무용 문서 소프트웨어를 출시해 본 사람이라면 익숙합니다. 클레임 양식이나 세금 신고서가 PDF로 도착하는데, 페이지 콘텐츠는 “Please wait... if this message is not eventually replaced” 알림 하나뿐이고 모든 진짜 필드는 Adobe Acrobat만 렌더링하는 XFA 패킷에 삽니다. 사용자는 여러분의 애플리케이션 안에서 그것을 채우고 싶어 합니다. 래스터화로 빠져나올 수도 없습니다. 데이터가 입력되면 폼이 행을 늘리고, 셋째 행 뒤의 레이아웃은 파일에 실려온 레이아웃이 아니기 때문입니다
동적 XFA가 여전히 풀 가치가 있는 문제인 이유
동적 XFA가 남아 있는 이유는 배포된 폼이 그것을 실어 나른 포맷보다 오래 사는 것입니다. ISO 32000-1 §12.7.8은 XFA를 XDP 패킷 스트림을 담은 AcroForm 사전의 /XFA 항목으로 기술하고, ISO 32000-2는 그 메커니즘 전체를 폐기 예정으로 표합니다. 폐기 예정 표시는 로드맵에서 지웠을 뿐 현장에서 지우지 않았고, XFA 3.3 명세로 작성된 폼은 여전히 발급되고 여전히 법적 구속력을 지닙니다. 정적 XFA는 평범한 위젯 어노테이션으로 환원되고, ApplyXFAAsAcroForm을 호출하면 HotPDF가 그렇게 하는데, 트레이드오프는 XFA 폼을 AcroForm 필드로 평탄화하기에서 다룹니다. 동적 XFA는 다른 짐승입니다. occur 범위, 자라는 텍스트, calculate 스크립트가 필드 집합을 데이터의 함수로 만들므로, 사용자가 타이핑을 끝내기 전까지는 평탄화할 고정 어노테이션 목록이 없습니다. TXFAWidgetRuntime이 채우는 간극이 바로 그것입니다. XFA DOM을 살아 있게 유지하고, 받아들여진 편집마다 레이아웃을 재계산하며, 그려지고 히트 테스트될 위치 잡힌 위젯의 평면 배열을 호스트에 건넵니다
런타임이 호스트 애플리케이션에 건네는 것은 무엇인가
기하와 상태를 건네고, UI 툴킷을 가정하는 것은 아무것도 건네지 않습니다. TXFAWidgetRuntime은 WidgetCount와 Widgets[I]를 TXFAWidgetState 레코드로 노출하는데, ID, Name, Kind, PageIndex, PDF 포인트 단위 Bounds, Value, EditValue, 그리고 Focused, Editing, ReadOnly, Valid 플래그를 실으며, 그리기, 캐럿 그리기, 키보드 라우팅은 여러분 코드에 남습니다. 위젯 신원은 안정적이고 서수적입니다. 각 위젯은 name[n] 형태의 ID를 받는데, n은 레이아웃 순서에서 그 필드 이름의 이전 출현 횟수입니다. 그래서 반복 서브폼의 둘째 행은 amount[1]입니다. 그 신원이 재빌드를 살아남게 하는 것이고, FocusWidget, BeginEdit, DispatchEvent, HitTest가 모두 말하는 것이기도 합니다. THotPDF 인스턴스에 이미 열려 있는 문서에는 CreateLoadedXFAWidgetRuntime이 XDP 패킷을 추출하고, 첫 페이지 박스를 레이아웃 페이지 크기로 삼으며, 파일이 XFA를 전혀 갖지 않으면 nil을 반환합니다
var
Pdf: THotPDF;
Runtime: TXFAWidgetRuntime;
WidgetID: AnsiString;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('claim-dynamic.pdf');
Runtime := Pdf.CreateLoadedXFAWidgetRuntime; // /XFA가 없으면 nil
if Runtime = nil then
Exit;
try
for I := 0 to Runtime.WidgetCount - 1 do
Memo1.Lines.Add(Format('%s p%d [%.1f %.1f %.1f %.1f] = %s',
[string(Runtime.Widgets[I].ID), Runtime.Widgets[I].PageIndex,
Runtime.Widgets[I].Bounds.Left, Runtime.Widgets[I].Bounds.Top,
Runtime.Widgets[I].Bounds.Right, Runtime.Widgets[I].Bounds.Bottom,
string(Runtime.Widgets[I].Value)]));
// 페이지 공간 히트 테스트, 최상위 위젯이 이긴다
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
필드가 커밋될 때 무엇이 원자적이어야 하는가
편집이 만질 수 있는 전부인데, 필드 값보다 훨씬 많습니다. CommitEdit는 무엇이든 쓰기 전에 CaptureSnapshot을 부르고, 그 스냅샷은 네 가지를 덮습니다. TXFADocument.SaveToBytes의 직렬화된 XFA DOM, TXFAWidgetState 상호작용 레코드의 전체 배열, LastCalculationPasses와 LastReflowPasses 카운터, 그리고 현재 Warnings.Count입니다. 노드 값만 저장하는 것이 유혹적인 지름길이고 그것은 틀립니다. calculate 스크립트나 해석되지 않은 바인딩은 EnsureValueNode를 불러 편집이 시작될 때 존재하지 않던 데이터 노드를 실체화할 수 있습니다. 값만 복원해서는 그것들을 지울 방법이 없으므로, 거부된 편집이 datasets 패킷에 영구 구조 잔여물을 남깁니다. 커밋 순서 자체는 엄격합니다. 후보 값을 쓰고, 편집된 필드에 validate를 돌리고, calculate를 고정점까지 돌리고, 레이아웃이 안정될 때까지 리플로우합니다. 그리고 어느 단계에서든 실패는 FailAndRestore로 흘러듭니다. 이것은 스냅샷 바이트를 새 TXFADocument로 적재하고, 위젯 목록을 재빌드하고, 기록된 상호작용 상태를 재적용하고, 카운터를 리셋하고, Warnings를 스냅샷 길이로 다시 자릅니다. LastDiagnostic은 실패 시 이유를 담고, 복원 자체가 예외를 던지는 병적 사례에는 문자 그대로 XFA transaction rollback failed를 담습니다
function EditAmount(Runtime: TXFAWidgetRuntime;
const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
Current: UnicodeString;
begin
Result := False;
if not Runtime.BeginEdit(AWidgetID) then
Exit; // 읽기 전용이거나 그런 위젯이 없다
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // 잘못된 범위 또는 분할된 서러게이트
Exit;
end;
Result := Runtime.CommitEdit; // 전부 아니면 전무
if not Result then
// 문서, 위젯, 카운터, 경고는 이미 편집 전 상태로 돌아갔다.
// 포커스된 위젯은 그저 invalid로 표시될 뿐이다
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection은 자기 노트를 받을 만합니다. 기형 입력을 가장 싸게 거부하는 지점이기 때문입니다. UTF-16 서러게이트 쌍을 가르는 선택을 거부하고, 짝이 없는 상위나 하위 서러게이트를 담은 교체 텍스트를 거부하며, MaxValueChars보다 긴 결과는 무엇이든 거부합니다. 그것을 키 입력 계층에서 잡으면 트랜잭션 기계가 반쯤 쓰인 천체면 문자를 되감을 필요가 결코 없습니다
사유 목록으로 재빌드하고, 한 번의 스왑으로 발행한다
위젯 재빌드는 반쯤 끝난 상태로 관찰되어서는 결코 안 됩니다. 그래서 RebuildWidgets는 완전히 별도의 소유 TObjectList를 빌드하고 끝에 단일 배정으로 그 자리에 스왑합니다. 이유는 미학이 아닙니다. TXFALayoutEngine.ComputeLayout은 재빌드가 진행되는 동안 돌고, 여러분이 공급한 MeasureText 함수를 통해 호스트 코드로 콜백하며, 위젯 한계에 닿으면 EXFAWidgetRuntimeError를 던질 수 있습니다. 런타임이 살아 있는 목록을 제자리에서 변이시킨다면 어느 경로든 호스트가 반은 옛 레이아웃이고 반은 새 레이아웃인 목록을 쥐게 되고, 곧 롤백될 문서를 향한 DataNode 포인터가 남습니다. 리플로우 수렴은 LayoutSignature가 결정합니다. 위젯 수와 모든 ID, 페이지 인덱스, 소수 넷째 자리까지 반올림한 경계 상자로 만든 문자열입니다. CommitEdit는 재빌드하고, 서명을 비교하고, 연속된 두 서명이 일치하거나 패스 예산이 소진될 때까지 반복합니다. 서명이 전혀 바뀌지 않았다면 LastReflowPasses는 0에 머무는데, 값만 바꾼 편집과 실제로 폼을 키운 편집을 그렇게 구분하며, 상호작용 상태는 각 재빌드를 위젯 ID로 건너가므로 포커스와 진행 중 편집이 행 삽입을 살아남습니다
바인딩된 필드가 왜 잘못된 레코드를 읽는가
스크립트가 데이터 컨텍스트 없이 돌았기 때문입니다. 명시적 <bind match="dataRef" ref="$record.actual"/>를 지닌 필드와 같은 데이터 노드의 이름을 딴 필드는 하나의 값을 향한 서로 다른 두 위젯이고, <occur max="2"/>를 지닌 반복 서브폼은 이름을 공유하고 어느 데이터 행에 속하는지만 다른 여러 위젯을 낳습니다. 문서 루트를 향해 검증과 계산을 평가하면 그 전부가 this를 datasets 패킷 전체에서 첫 일치 노드로 해석하므로, 둘째 행이 조용히 첫째 행을 검증합니다. HotPDF는 레이아웃이 위젯 항목을 낳을 때 해석된 DataNode를 각 항목에 저장하고, 그 노드를 xfskValidate와 xfskCalculate 모두의 HPDFXFAEvaluateFieldScript 호출로 흘려보내 그것을 피합니다. 같은 컨텍스트가 계산이 아직 존재하지 않는 바인딩을 겨냥할 때 EnsureValueNode가 어느 노드를 향해 만들지 결정하고, 바인딩이 해석될 수 없으면 잘못된 행에 쓰는 대신 커밋이 XFA calculation target is not bound로 깨끗하게 실패합니다. 그 스크립트 뒤의 FormCalc 의미론은 AcroForm 포맷과 계산 스크립트에서 기술한 액션으로 AcroForm 문서가 받는 것을 되울립니다. 다만 여기서의 해석 규칙은 필드 이름 범위가 아니라 XFA 범위입니다
예산은 부작용 뒤가 아니라 앞에서 검사된다
런타임의 모든 한계는 전제조건입니다. 할당이 이미 일어난 뒤 집행되는 예산은 예산이 아니기 때문입니다. TXFAWidgetRuntimeOptions.Default는 MaxWidgets 10000, MaxValueChars 1048576, MaxCalculationPasses 16, MaxReflowPasses 4로 배송되고, 기본 TXFAFormScriptOptions는 MaxOperations 100000에 MaxElapsedMilliseconds 500을 싣습니다. 아래에서는 XFA DOM이 자기 TXFADOMLimits를 적용합니다. 압축 해제 입력과 출력에 128 MB 상한, 함께 잇는 최대 1024 패킷, 1000000 노드, 중첩 깊이 256. 숫자 자체보다 중요한 세부가 둘 있습니다. 첫째, 스크립트 예산은 스크립트별이 아니라 트랜잭션 전체입니다. CommitEdit는 하나의 잔여 연산 카운터와 하나의 단조 데드라인을 시드하고, 모든 validate와 calculate 호출이 같은 카운터에서 차감하며 남은 밀리초만 받습니다. 그래서 계산 필드 200개짜리 폼이 500 ms 전체를 200번 쓸 수 없습니다. 둘째, 데드라인은 주입 가능한 MonotonicMilliseconds 함수에서 옵니다. 경과 시간 동작이 바쁜 빌드 에이전트 위의 동전 던지기가 아니라 테스트 스위트에서 재현되는 이유가 그것입니다
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // 기본 10000
Options.MaxCalculationPasses := 8; // 기본 16
Options.MaxReflowPasses := 2; // 기본 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // 트랜잭션 전체
Options.ScriptOptions.Limits.MaxElapsedMilliseconds := 200;
Options.MeasureText :=
function(const AText: UnicodeString; const AFont: TXFAFontSpec;
AMaxWidth: Double): TXFATextExtent
begin
Result := MeasureWithHostCanvas(AText, AFont, AMaxWidth);
end;
Runtime := TXFAWidgetRuntime.Create(XDPBytes, 612, 792, Options);
try
Runtime.OnLayoutChanged :=
procedure
begin
RepaintAllPages; // 리플로우가 실제로 위젯을 움직였을 때만 발화
end;
// ... 폼을 구동한다 ...
finally
Runtime.Free;
end;
end;
런타임이 멈추는 지점, 그리고 왜 그렇게 큰 소리로 말하는가
런타임은 의도적으로 범용 XFA 스크립팅 엔진이 아닙니다. DispatchEvent는 enter와 exit 활동을 포커스 이동으로 기본 처리하고, 스크립트를 실은 다른 모든 활동에는 흉내를 내는 대신 구체적이고 안정적인 진단으로 거부합니다. addInstance, removeInstance, instanceManager를 언급하는 스크립트는 XFA runtime does not support event-driven instance mutation을 반환하고, .presence를 만지는 스크립트는 그 presence 등가물을 반환하며, 그 밖의 것은 XFA runtime does not support this event script을 반환합니다. 분기할 수 있는 예측 가능한 거부가, 샘플 파일에서는 돌고 고객 파일에서는 어긋나는 부분 에뮬레이션을 이깁니다
스레딩 모델도 똑같이 무뚝뚝합니다. 하나의 런타임 인스턴스는 하나의 스레드에 속하며 내부 잠금이 없습니다. 레이아웃 엔진이 호스트 측정 콜백으로 되돌아오는데, 그 주변의 잠금은 리페인트를 기다리는 교착 상태입니다. 필드 안의 리치 콘텐츠는 라이브러리의 다른 곳과 같은 보수적 노선을 따릅니다. exData 페이로드는 XFA exData 리치 텍스트와 하이퍼링크에서 기술한 대로 처리되고, 서명과 버튼 위젯은 ReadOnly로 돌아오며, 지원되지 않는 UI 종류는 조용히 데이터를 잃는 편집 가능한 텍스트 상자가 아니라 xwkUnsupported로 표면화합니다
합치면 델파이에서의 동적 XFA에 대한 일할 수 있는 답입니다. DOM을 살아 있게 유지하고, 각 편집을 완전히 착륙하거나 아무것도 남기지 않는 트랜잭션으로 만들고, 모든 패스에 갇히게 하고, 범위 밖인 것에 대해 명시하십시오. 클레임, 세금, 복지 워크플로용으로 평가 중이라면 XFA 런타임은 HotPDF Delphi PDF component의 일부로 배송되며, 그런 프로젝트가 보통 함께 필요하게 되는 AcroForm, 평탄화, 렌더링 경로와 나란히 옵니다