PDFium 컴포넌트의 TPdf.SetFocusedFormFieldText는 현재 포커스된 폼 필드의 살아있는 편집 버퍼에 값을 쓰며, XFA 폼의 경우 그 버퍼는 디스크에 직렬화되는 datasets 패킷에 결코 도달하지 못한다 — 그래서 사용자가 입력하고 여러분의 코드가 받아들여졌다고 확인한 값이, 파일을 다음에 열면 조용히 사라져 있다. AcroForm 필드는 이 문제를 겪지 않는다: 같은 호출이 포커스가 떠나는 순간 필드의 /V 항목에 커밋된다. XFA 입력 폼을 작성하고, 저장하고, 다시 열었더니 금액 필드가 다시 비어 있는 것을 발견한 사용자는 렌더링 결함을 마주친 것이 아니다 — 이는 PDFium 엔진 자체가 폼 데이터 쓰기를 위해 노출하는 것의 경계에 부딪힌 것이다
이는 애초에 XFA 폼을 감지하거나 그 JavaScript를 실행시키는 것보다 더 좁은 질문이다: "PDFium이 XFA를 지원하는가"도 아니고 "AcroForm 스크립트를 어떻게 실행하는가"도 아니라, 구체적으로 SetFocusedFormFieldText가 성공을 보고한 뒤 그 값에 무슨 일이 일어나는가다. 짧게 말하면, PDFium의 쓰기 경로 입장에서 AcroForm과 XFA는 같은 폼 모델의 두 방언이 아니다 — 이는 사용자가 입력하는 것과 저장이 실제로 포착하는 것 사이에 완전히 다른 관계를 가진 두 개의 폼 모델이며, 이 둘을 혼동하는 것이 한 줄짜리 API 호출을 고객의 파일럿 배포가 시작되고 3주 뒤의 지원 티켓으로 바꾸는 원인이다. AcroForm JavaScript 글은 그 한 줄짜리 호출을 보여주고 AcroForm 대 XFA 결과를 코드 주석으로 언급한다; 이 글은 같은 API에 머물면서 내부 쓰기 경로, XFA 쓰기가 결코 도달하지 못한다는 datasets 패킷 증거, 이 공백이 Delphi 바인딩이 아니라 PDFium 자체에 있는 이유, 그리고 저장을 넘어 편집이 살아남아야 하는 문서를 위한 자체 XML 패치 우회법을 살펴본다
SetFocusedFormFieldText는 필드 값을 어떻게 쓰는가?
TPdf.SetFocusedFormFieldText는 값을 문서 모델에 직접 찔러 넣는 것이 아니라 키 입력 수준의 편집을 시뮬레이션함으로써 동작한다. 내부적으로 FORM_SelectAllText를 호출해 포커스된 필드의 현재 내용을 선택한 다음, FORM_ReplaceSelection을 호출해 그 선택을 새 문자열로 덮어쓴다 — 키보드로 전체 선택 후 입력할 때 촉발되는 것과 같은 두 작업이다. 이 쓰기가 우회하지 않고 PDFium의 인터랙티브 텍스트 편집 경로를 거치기 때문에, 그 필드에 연결된 어떤 키 입력, 서식, 계산 스크립트든 사람이 입력할 때와 정확히 똑같이 발동하며, 이것이 JavaScript를 살아있게 유지하는 뷰어에서 프로그램적인 폼 채우기에 이 API를 유용하게 만드는 이유다. 읽기 쪽 짝은 FocusedFormFieldText로, FORM_GetFocusedText가 뒷받침하며, SetFocusedFormFieldText가 방금 쓴 것과 같은 살아있는 버퍼를 그대로 반영한다
if Pdf.FocusedFormFieldIndex >= 0 then
begin
if Pdf.SetFocusedFormFieldText('1284.50') then
Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
else
Log('No field is focused, or it does not accept text');
end
else
Log('Focus a field first - FocusFormField or a real click');
AcroForm은 왜 값을 유지하고 XFA는 왜 잃어버리는가?
AcroForm 텍스트와 콤보 필드가 지속되는 이유는 PDFium 자체의 폼 채우기 환경이 여러분을 대신해 편집 버퍼를 커밋하기 때문이다: 필드가 포커스를 잃는 순간, 버퍼는 표준을 준수하는 모든 PDF 리더가 필드의 저장된 값을 알기 위해 확인하는 바로 그 키인 필드의 /V 항목에 쓰인다. 내부적으로 FORM_ForceToKillFocus를 호출하는 TPdf.ClearFormFieldFocus는 그 커밋을 필요에 따라 강제하므로, 프로그램적으로 값을 설정하는 코드는 UI의 다른 곳에서 실제 마우스 클릭이 일어나기를 기다릴 필요가 없다. 그 직후 저장하면, 새 텍스트는 TPdf.SaveAs가 실행되기도 전에 문서 객체 그래프의 일부가 되어 있다. /V는 나중에 덧붙은 무언가가 아니라 실제 필드 딕셔너리 안의 실제 항목이기 때문이다
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus; // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');
// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50'); // passes
XFA 필드 편집은 실제로 어디에 사는가?
XFA 필드에는 그런 배선이 전혀 없다. 사용자가 입력하는 텍스트는 PDFium의 XFA 렌더링 및 상호작용 계층에 속하는 CPWL_Edit 버퍼에 놓이며, 그 계층에는 그 버퍼를 PDF에 저장된 datasets 패킷으로 다시 복사하는 코드 경로가 전혀 없다. TPdf.GetXfaDatasets는 이 공백을 눈에 보이게 만든다: XFA 필드 편집 전후에 이를 호출하면 돌려받는 바이트는 동일한데, 이 메서드는 방금 편집한 위젯의 살아있는 상태가 아니라 문서가 열렸을 때의 원본 패킷을 읽기 때문이다. 이는 캐싱 버그도 새로고침 타이밍 문제도 아니다 — 디스크 상의 datasets 패킷과 메모리 상의 편집 버퍼는 그저 PDFium의 공개 API가 결코 연결하지 않는 두 개의 서로 다른 상태 조각일 뿐이다
var
Before, After: TBytes;
begin
Before := Pdf.GetXfaDatasets;
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
After := Pdf.GetXfaDatasets;
// Before and After are byte-for-byte identical on an XFA document -
// the edit never touched the packet GetXfaDatasets reads from
end;
이것은 PDFium 컴포넌트의 버그인가, PDFium의 한계인가?
빠진 조각은 그 위의 Delphi 바인딩이 아니라 PDFium 자체에 있다. PDFium의 공개 API에는 갱신된 패킷을 주입할 FPDF_SetXFAPacket도, 저장 전에 XFA 엔진에게 현재 DOM을 datasets XML로 다시 직렬화하라고 요청할 FPDF_SaveAsXFA도 없다. TPdf.SaveAs를 뒷받침하는 내보내기인 FPDF_SaveAsCopy는 PDFium이 이미 가지고 있는 문서 객체 그래프를 그대로 써낸다; XFA 엔진에게 먼저 살아있는 상태를 플러시하라고 요청할 훅이 없는데, 그런 훅이 애초에 업스트림에 존재하지 않기 때문이다. PDFium 컴포넌트는 PDFium 자체가 결코 구현한 적 없는 조정 작업을 추가할 수 없으며, PDFium의 내부 XFA 상태를 추측하는 자체 개발 DOM-투-XML 직렬화기를 배포하는 것은 이 솔직한 공백보다 더 나쁠 것이다: 그것은 다음 PDFium 버전이 프로젝트 밖 누구도 볼 수 없는 무언가를 바꾸기 전까지는 작동하는 것처럼 보일 것이다
이 경계는 애초에 SetFocusedFormFieldText를 만든 것과 같은 v2.13.2 감사 도중 드러났다. FORM_ReplaceSelection은 Pascal 코드에서 한 번도 호출된 적 없이 여러 버전에 걸쳐 DLL 임포트 테이블에 바인딩되어 있었고, 마침내 그것을 사용하는 쓰기 경로를 추가한 것이 이 지속성 공백을 이론이 아니라 문서화할 만큼 구체적으로 만들었다. 같은 감사 라운드는 무관하지만 정신적으로 관련된 공백도 발견했다: AcroForm JavaScript는 v2.13.0부터 조용히 비활성화되어 있었는데, JS 플랫폼이 XFA 초기화 분기 안에서만 연결되어 있었기 때문이었다. 그래서 app.alert나 계산 필드를 가진 평범한 AcroForm 문서는 스크립트 엔진을 아예 받지 못했다. 그것은 고칠 수 있었다 — JS 플랫폼을 XFA 여부와 무관하게 모든 문서로 확장하는 것 — 그리고 같은 버전에서 배포되었다; 여기서 다룬 지속성 공백은 위의 이유로 고칠 수 없었다. JavaScript 수정과 그 주변의 호스트 거부 이벤트는 PDFium 컴포넌트로 AcroForm JavaScript 실행하기에서 다룬다
Delphi에서 이에 대해 무엇을 해야 하는가?
AcroForm 문서의 경우, 수정은 좋은 습관 이상의 것이 아니다: 값이 프로그램적으로 설정되었다면 나중의 UI 상호작용이 여러분을 대신해 커밋을 촉발할 것이라고 가정하는 대신, SaveAs 전에 ClearFormFieldFocus를 호출하거나(혹은 다른 방식으로 포커스를 옮기거나) 하라. AcroForm일 수도 XFA일 수도 있는 문서 — 범용 뷰어에서 흔한 경우 — 의 경우, 저장이 확실히 유지될 것이라고 호출자에게 약속하기 전에 FormType이나 XFA 불리언을 확인하라. 그리고 전체 탐지 방법 집합, 특히 XFA 콘텐츠가 그 외에는 평범하고 /V를 존중하는 AcroForm 위젯 위에 겹쳐진 XFAF 경우까지 포함해서는 XFA 폼 감지와 XFA 패킷 추출하기를 읽어보라
편집된 값이 저장을 넘어 살아남아야 하는 진짜 동적 XFA 폼의 경우, 인터랙티브 편집 버퍼는 애초에 올바른 도구가 아니다. 견고한 경로는 GetXfaDatasets를 결과가 아니라 기준선으로 취급하는 것이다: 문서가 열릴 때 한 번 읽고, 사용자가 필드별로 무엇을 바꿨는지 여러분 자신의 기록을 유지하며 — 이는 정확히 여러분의 UI가 이미 가지고 있는 값이다, PDFium은 나중에 그것을 여러분에게 돌려주지 않을 것이므로 — 그것들을 여러분 스스로 기준선 XML에 패치하고, 여러분 자신의 출력을 구동하라. 여러분 자신의 코드가 제어하는 XML을 거치는 쓰기는 CPWL_Edit 버퍼가 결코 해낼 수 없는 저장을 넘어서 살아남는다
function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
NewValue: string): TBytes;
var
DatasetsXml: string;
begin
// GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
// your own helper over your own XML library, nothing PDFium provides
DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;
고객이 발견하기 전에 이 공백 잡아내기
TPdf.SaveAs는 XFA 필드 값이 살아남았든 아니든 True를 반환하는데, PDFium 입장에서는 저장이 진짜로 성공했기 때문이다 — 요청받은 모든 바이트를 썼다. 이것이 정확히 스모크 테스트를 슬쩍 지나쳐 고객에게 도달하는 종류의 결함으로 만든다: 아무것도 예외를 일으키지 않고, 아무것도 로그에 남지 않으며, 파일은 잘 열리고, 오직 그 특정 값만 잘못되어 있다. 저장된 파일을 실제로 다시 열어 필드 값을 비교하는 왕복 테스트 — 혹은 앞선 예제처럼 전후의 GetXfaDatasets를 비교하는 것 — 는 사용자가 XFA 콘텐츠를 편집할 수 있게 하는 어떤 뷰어의 회귀 스위트에도 속해야 하며, 기본적으로 잘 작동하는 AcroForm 경로만이 아니다
이 중 어느 것도 PDFium 컴포넌트에 대해 제기할 결함이라기보다는 그 주위로 설계해야 할 경계에 가깝다: SetFocusedFormFieldText는 두 폼 모델 모두에 대해 그 이름이 말하는 그대로 정확히 수행하며, 결과의 차이는 AcroForm과 XFA가 각각 PDFium 쪽에서 그 버퍼를 무엇에 연결하는지로 깔끔하게 거슬러 올라간다. 여기서 언급한 API, 포커스와 저장 원시 기능, 패킷 리더는 Delphi와 C++Builder용 PDFium 컴포넌트의 일부다