PDFium Component는 이제 초기화하는 모든 form-fill 환경에서 FPDF_FORMFILLINFO.version을 2로 설정합니다. 네이티브 PDFium 빌드가 받아들이는 버전은 여는 문서가 아니라 그 빌드의 속성이기 때문입니다. XFA가 켜진 pdfium.v8.dll은 버전 1을 즉시 거부하므로, 그것을 통해 연 평범한 AcroForm PDF는 XFA가 어디에도 없는데도 FPDFDOC_InitFormFillEnvironment에서 실패하곤 했습니다. v3.116.0의 수정은 작지만 그 뒤의 실수는 일반적인 것이고 이름을 붙일 가치가 있습니다. 프로토콜 버전 필드는 상대가 기대하는 메모리 레이아웃을 기술하며, 그 레이아웃이 지닌 기능이 당장 필요한지에서 도출되어서는 결코 안 됩니다
pdfium.v8.dll에서 평범한 PDF에 FPDFDOC_InitFormFillEnvironment가 실패하는 이유
XFA가 켜진 PDFium 빌드가 다른 무엇보다 먼저 version 필드를 검증하는데, 예전 래퍼 로직은 현재 문서가 XFA 서식이 아니면 그것에 1을 넘겼기 때문입니다. Delphi 호스트에서 증상은 TPdf.InitializeFormFill에서 Cannot initialize form fill environment 메시지와 함께 EPdfError가 올라오는 것이고, AcroForm 텍스트 필드밖에 없는 평범한 청구서나 세금 서식을 여는 도중에 발생합니다. 같은 파일이 일반 pdfium.dll에서는 잘 열립니다. 같은 DLL이 진짜 XFA 문서도 잘 엽니다. V8 빌드와 비 XFA 문서의 조합만 깨지는데, 이는 호스트가 AcroForm JavaScript를 쓰려고 EnableV8Engine을 켠 뒤에, 또는 앞선 XFA 파일 때문에 LoadDocument의 자동 선택이 프로세스를 이미 pdfium.v8.dll로 확정한 뒤에 정확히 놓이는 조합입니다. 그 확정은 프로세스 전역입니다. EnableV8Engine은 첫 LoadLibrary 전에 읽히고, XFA 빌드가 한 번 로드되면 이후의 모든 평범한 PDF가 같은 바이너리에 대해 같은 환경 설정을 거칩니다. 호스트는 잘못한 것이 없고, 래퍼가 레코드를 채울 때 잘못된 질문을 했을 뿐입니다. 어떤 바이너리를 배포할지 아직 정하는 중이라면 PDFium DLL 배포와 로드 실패 진단 노트가 일반 빌드와 V8 선택을 다루며, 이 글은 V8 빌드가 이미 프로세스에 있다고 가정합니다
FPDF_FORMFILLINFO의 version 필드가 실제로 약속하는 것
FPDF_FORMFILLINFO.version은 PDFium에 그 레코드의 어떤 필드를 읽어도 되는지 알려 주며, 공개 헤더 fpdf_formfill.h는 허용 값을 문서가 아니라 라이브러리가 컴파일된 방식에 묶습니다. 요약하면 계약은 세 부분입니다. 버전 1은 FFI_Invalidate부터 FFI_DoGoToAction까지의 안정된 콜백과 m_pJsPlatform 포인터를 포함합니다. XFA 모듈이 없는 빌드는 1과 2를 모두 받아들이고, 2이면 추가 실험 콜백도 호출합니다. XFA 모듈이 있는 빌드는 2를 요구하며 예외가 없고, 헤더는 사람들이 놓칠 것을 예상한 듯 그 요구를 두 번 반복합니다. 계약 어디에도 문서는 언급되지 않습니다. 버전은 당신이 할당한 레코드에 대한 진술입니다. 2를 주면 m_pJsPlatform 뒤의 메모리가 존재하고 유효한 함수 포인터나 NULL을 담고 있다고 약속하는 것입니다
버전 2 영역에 XFA 기계장치가 전부 있습니다. 버전 2 아래에서는 무시되고 XFA 모듈이 컴파일되어 있을 때만 의미가 있다고 헤더가 설명하는 FPDF_BOOL인 xfa_disabled로 시작해서, FFI_DisplayCaret부터 FFI_DoURIActionWithKeyboardModifier까지 열일곱 개 함수 포인터로 이어집니다. 각각은 XFA에 필요하고 그 밖의 경우 NULL로 두라고 문서화되어 있습니다. 그 표현이 이 수정 전체의 열쇠입니다. NULL은 그 슬롯의 오류 상태가 아니라 XFA를 구동하지 않는 호스트의 문서화된 상태입니다. FillChar로 비운 뒤 버전 2로 표시한 레코드는 비 XFA 빌드에서 버전 1 레코드와 똑같이 계약을 충족하며, XFA 빌드가 받아들이는 유일한 레코드입니다
예전 선택은 ABI를 문서에 묶었다
결함은 단독으로 보면 합리적으로 보이는 조건문 하나였습니다. TPdf.InitializeFormFill은 세 가지 사실에서 RuntimeReady 플래그를 계산합니다. 문서가 TPdf.XFA로 XFA 서식 타입을 보고하는지, XFA 문자열 헬퍼가 XfaFeaturesAvailable로 해석되었는지, V8 익스포트가 V8FeaturesAvailable로 해석되었는지입니다. v3.116.0 이전에는 그 같은 플래그가 버전도 골랐습니다
// v3.115.0 이전: ABI 버전이 문서를 따라감
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
FFormFillInfo.Info.version := 2
else
FFormFillInfo.Info.version := 1;
// ... 런타임 누락 분기에서도 다시 고정
else if XFA then
begin
FFormFillInfo.Info.version := 1;
FFormFillInfo.Info.xfa_disabled := 1;
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
헤더를 옆에 두고 읽으면 실패가 자명합니다. RuntimeReady는 모든 평범한 AcroForm 문서에서 거짓이므로 모든 평범한 문서가 버전 1을 알렸습니다. pdfium.dll에서는 괜찮습니다. XFA가 켜진 빌드인 pdfium.v8.dll에서는 PDFium이 그 필드를 보고 요구되는 2보다 낮음을 확인한 뒤 null FPDF_FORMHANDLE을 반환하고, CheckPdf가 그것을 위의 예외로 바꿉니다. 예전 코드의 의도는 방어적이었습니다. XFA 빌드가 할당되지 않은 버전 2 슬롯을 읽지 않도록 버전 1을 유지하는 것입니다. 헤더가 이미 배제한 문제를 막았으면서 헤더가 분명히 경고하는 문제를 새로 만들었습니다. 바로잡은 코드는 레코드가 물리적으로 무엇인지에서 버전을 처음에 한 번 결정합니다
procedure TPdf.InitializeFormFill;
var
RuntimeReady: Boolean;
begin
FXfaRuntimeUsable := False;
FXfaPageCountOverride := -1; // 센티널: 정적 페이지 트리 사용
if not FormFill then
Exit;
FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
FFormFillInfo.Pdf := Self;
// 완전한 버전 2 레코드가 위에서 할당되고 비워진다. PDFium은
// XFA 없이도 버전 2를 받아들이고, XFA가 켜진 모든 빌드에서
// 이 문서에 XFA 서식이 없을 때도 버전 2를 요구한다.
FFormFillInfo.Info.version := 2;
FFormFillInfo.Info.xfa_disabled := 1;
// RuntimeReady는 XFA 콜백과 xfa_disabled만 가르며 버전은 가르지 않음
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
...
RuntimeReady가 여전히 속하는 곳: 콜백과 xfa_disabled
RuntimeReady는 XFA 동작을 가르는 역할을 유지하며, 다만 더 이상 레코드 레이아웃을 건드리지 않습니다. 버전 1 콜백인 FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction과 그 블록의 나머지는 AcroForm과 XFA가 모두 그것에 의존하므로 무조건 연결됩니다. 버전 2 포인터 열일곱 개는 xfa_disabled := 0과 함께 RuntimeReady 분기 안에서만 할당됩니다. 문서가 XFA인데 런타임이 없으면 레코드는 버전 2로 남고 xfa_disabled가 1이며 버전 2 슬롯은 NULL로 남고, 래퍼는 OnXfaRuntimeMissing을 올려 호스트가 pdfium.v8.dll로 재시작하도록 제안할 수 있게 합니다. 환경이 생긴 뒤 FPDF_LoadXFA는 RuntimeReady가 참이었을 때만 호출되고, 참을 반환할 때만 FXfaRuntimeUsable이 설정되며, 그것이 TPdf.XfaRuntimeAvailable이 보고하는 값입니다
if RuntimeReady then
begin
FFormFillInfo.Info.xfa_disabled := 0; // 0 = XFA 활성
FFormFillInfo.Info.FFI_DisplayCaret := FormFillDisplayCaret;
FFormFillInfo.Info.FFI_GetCurrentPageIndex := FormFillGetCurrentPageIndex;
FFormFillInfo.Info.FFI_SetCurrentPage := FormFillSetCurrentPage;
FFormFillInfo.Info.FFI_GotoURL := FormFillGotoURL;
FFormFillInfo.Info.FFI_GetPageViewRect := FormFillGetPageViewRect;
FFormFillInfo.Info.FFI_PageEvent := FormFillPageEvent;
FFormFillInfo.Info.FFI_PopupMenu := FormFillPopupMenu;
FFormFillInfo.Info.FFI_OpenFile := FormFillOpenFile;
FFormFillInfo.Info.FFI_EmailTo := FormFillEmailTo;
// ... FFI_UploadTo부터 FFI_DoURIActionWithKeyboardModifier까지
end
else if XFA then
begin
// 런타임 없음: 버전 2를 유지하고 XFA를 끈 채 호스트에 알림
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
FFormHandle := FPDFDOC_InitFormFillEnvironment(FDocument, FFormFillInfo.Info);
CheckPdf(FFormHandle <> nil, 'Cannot initialize form fill environment');
if RuntimeReady then
FXfaRuntimeUsable := FPDF_LoadXFA(FDocument) <> 0;
직접 바인딩을 작성할 때 그 블록에서 틀리기 쉬운 세부가 두 가지입니다. FXfaPageCountOverride는 다른 무엇보다 먼저 센티널 -1로 재설정되므로 FFI_PageEvent가 재쪽 나눔을 보고할 때까지 PageCount가 정적 페이지 트리로 폴백합니다. 여기에 0이 들어가면 빈 문서라고 조용히 주장하게 됩니다. 그리고 버전 2 콜백 각각은 레코드에서 소유 TPdf를 복원하고 PDFium으로 돌아가기 전에 모든 Pascal 예외를 삼키는 정적 cdecl 루틴이며, Delphi에서 PDFium ABI를 강건하게 만드는 노트가 FFI_OpenFile에 대해 명시한 규율이 바로 그것입니다. 버전 변경으로 두 규칙 중 어느 것도 완화되지 않습니다
DLL에 XFA 모듈이 없을 때 버전 2는 안전한가
안전하며, 그 이유는 라이브러리의 약속이 아니라 레코드에 있습니다. 비 XFA 빌드에서 헤더는 버전 2이면 실험 콜백도 호출된다고 하므로, 문제는 PDFium이 볼 때 무엇을 찾는가입니다. TPdfFormFillInfo는 Info 멤버가 모든 버전 2 필드를 포함한 완전한 FPDF_FORMFILLINFO인 packed 레코드이고, InitializeFormFill은 한 바이트도 건드리기 전에 FillChar로 전체를 비웁니다. 그래서 평범한 pdfium.dll에 평범한 문서를 조합하면 라이브러리는 버전 2, 설정된 xfa_disabled, 모든 실험 슬롯의 NULL을 봅니다. 이는 XFA를 구현하지 않는 호스트에 대해 헤더가 규정한 바로 그 상태입니다. 라이브러리가 넘어서 읽을 잘린 레코드는 없습니다. 레코드가 애초에 버전 2보다 짧은 적이 없기 때문입니다. 예전 로직은 Pascal 선언이 이미 없앤 레이아웃 불일치를 막고 있었습니다
정직하게 밝힐 경계는 레코드가 감당할 수 없는 것입니다. 평범한 문서에서 버전 2가 JavaScript나 XFA 스크립팅, 또는 그 콜백 뒤의 어떤 호스트 이벤트도 켜지 않습니다. m_pJsPlatform은 V8FeaturesAvailable이 참일 때만 붙고, XFA는 RuntimeReady가 참이었을 때만 활성화되며, TPdf.XFA는 환경이 무엇을 협상했든 FPDF_GetFormType의 서식 타입을 계속 보고합니다. 동적 XFA가 실제로 렌더링될지 알고 싶은 호스트는 XFA 서식 감지와 XFA 패킷 추출 노트가 권하듯 Active가 참이 된 뒤에도 XfaRuntimeAvailable을 계속 읽어야 하고, 버전 필드에서 무엇을 추론해서는 안 됩니다
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// 문서가 XFA인데 로드된 pdfium.dll이 엔진을 실행할 수 없을 때
// InitializeFormFill에서 발생한다. 버전 2가 어느 쪽이든
// 전달되므로 form 환경은 열리고, XFA 런타임만 꺼진다.
StatusBar.SimpleText :=
'XFA form detected; restart with pdfium.v8.dll to enable dynamic rendering';
end;
procedure TMainForm.OpenDocument(const FileName: string);
begin
Pdf.Active := False;
Pdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
Pdf.FormFill := True;
Pdf.FileName := FileName;
Pdf.Active := True; // 이제 pdfium.v8.dll에서 평범한 PDF에도 예외를 던지지 않음
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaWarning;
end;
프로토콜 버전과 기능 가용성은 서로 다른 두 축
이 수정에서 나오는 일반 규칙은, 콜백 구조의 버전 필드는 이 레코드가 얼마나 크고 여기서 무엇을 읽어도 되는가에 답하고 기능 감지는 그 슬롯 중 어느 것이 실제로 유용한 일을 하는가에 답한다는 것입니다. 첫째는 네이티브 바이너리와 컴파일 대상 Pascal 선언에 의해 고정됩니다. 둘째는 문서마다, DLL 익스포트 테이블마다, 호스트 구성마다 달라집니다. XFA 사례가 우연히 둘 다 필요로 하기 때문에 둘을 하나의 불리언으로 접는 유혹이 생기지만, 빌드가 최소 버전을 강제하는 순간 그 접기는 그 기능이 필요 없는 모든 문서에서 깨집니다. XFA 서식은 여기서 기능이고, ISO 32000-1 §12.7.8이 AcroForm 딕셔너리 옆에 사는 XML 페이로드로 설명하는 것입니다. 레코드 레이아웃은 프로토콜이며, PDFium은 파일을 보기도 전에 그 레이아웃을 요구할 자격이 있습니다. 같은 모양은 C 라이브러리가 구조체에 버전을 매기는 어디에나 나타납니다. 뷰어 정보 블록, 렌더 옵션 레코드, 플랫폼 콜백 테이블입니다. 안전한 패턴은 바로잡은 InitializeFormFill이 따르는 것입니다. 이해하는 가장 새로운 레이아웃을 선언하고, 완전히 비우고, 그 레이아웃에 맞게 버전을 무조건 설정한 뒤, 능력 검사가 어떤 슬롯을 채울지 결정하게 하십시오. 미래의 PDFium 헤더가 버전 3을 추가한다면 변경은 선언과 그 한 번의 대입이며, 아무도 테스트하지 않은 조합에서 틀릴 문서 의존 분기가 아닙니다
바로잡힌 form-fill 초기화는 Delphi와 Lazarus, C++Builder용 PDFium Component에 실려 있으며, 두 빌드가 같은 레코드 선언을 공유하므로 Win32와 Win64에 똑같이 적용됩니다. 애플리케이션이 이미 JavaScript 기반 AcroForm을 위해 pdfium.v8.dll을 선택한다면, 이 변경이 form 환경을 특별 취급하지 않고도 같은 바이너리로 나머지 PDF 아카이브를 열게 해 줍니다