기술 문서

PDFium Component를 사용한 Delphi에서의 AcroForm JavaScript 실행

Acrobat에서는 합계 금액이 자동으로 재계산되는데 자체 개발한 Delphi 뷰어에서는 먹통인 경우, 이는 렌더링 버그가 아닐 가능성이 매우 큽니다. PDFium은 호스트가 JS 플랫폼을 명시적으로 연결하기 전에는 문서 내 모든 JavaScript 작동을 비활성화하며, PDFium Component는 버전 2.13.2부터 이 플랫폼을 자동으로 구성하여 V8 엔진이 켜진 DLL이 로드될 때마다 AcroForm JavaScript, 문서 수준 스크립트 및 필드 계산식을 실행합니다. 개발 팀은 스크립트가 시도하려는 모든 동작을 표시, 리다이렉트 또는 거부(veto)할 수 있는 이벤트 세트를 통해 통제권을 계속 유지할 수 있습니다

이 설명은 그동안 접했던 여러 지원 문의 사항들을 단번에 해결해 줍니다: "Acrobat에서는 주문서의 합계가 계산되는데 프로그램에서는 작동하지 않습니다", "app.alert이 호출되지 않습니다", "날짜 필드의 서식이 자동으로 적용되지 않습니다" 등입니다. 이들 증상 모두 동일한 규정에서 비롯된 것이며, 이 원칙을 파악해 두는 것은 위험한 악성 문서로부터 시스템을 격리하는 보안 경계선이 되므로 10분 정도 시간을 내어 읽어볼 가치가 충분합니다

PDFium에서 PDF 양식 계산식이 작동하지 않는 이유는 무엇입니까?

PDFium은 문서 JavaScript를 철저하게 옵트인(opt-in) 방식으로 다룹니다: 즉 양식 입력 환경의 m_pJsPlatform 멤버 변수가 NULL인 경우 문서 내의 모든 스크립트는 오류나 로그 한 줄 남기지 않고 자동으로 생략됩니다. 계산 대상 필드들은 마지막 저장된 고정 값을 고수하고, app.alert 호출은 유실되며, 서식 지정 스크립트도 구동되지 않습니다. 자체 JavaScript 엔진을 기본 탑재하고 있는 Acrobat은 이 파일들을 아무 문제 없이 구동하므로, 이는 내부 라이브러리의 설계 의도에 따른 기본 사양임에도 개발자가 자신의 코드에 버그가 있는 것으로 잘못 추측하는 원인이 됩니다

PDFium Component에는 과거 버전의 이력과 연관된 구조도 얽혀 있습니다. 버전 2.13.1까지 바인딩 모듈은 XFA 초기화 분기 내에서만 m_pJsPlatform을 장착하도록 제한하여, 스크립트가 사용된 PDF의 대다수를 차지하는 일반 AcroForm 문서는 V8 사용 가능 DLL이 로드된 상태에서도 JavaScript 플랫폼을 전혀 구성 받지 못했습니다. 버전 2.13.2부터 XFA 판별 조건과 관계없이 이 연결 처리를 수행하도록 수정되었습니다: 이제 InitializeFormFill은 대상 문서의 XFA 여부와 무관하게 V8FeaturesAvailable 결과가 True인 경우 항상 IPDF_JsPlatform 테이블을 작성합니다. 결과적으로 문서 수준 스크립트, 필드 계산 체인 및 app.alert 등이 일반 AcroForm 문서 환경에서도 원활하게 실행되도록 개선되었습니다

Delphi에서 AcroForm JavaScript 실행 시 어떤 V8 DLL이 필요합니까?

AcroForm JavaScript는 V8 엔진이 통합 빌드된 PDFium 바이너리를 요구하며, 이는 라이브러리가 로드되기 전에 글로벌 플래그인 EnableV8Engine이 True로 설정되어 있으면 PDFium Component가 pdfium.v8.dll 파일 명칭으로 로드해 구동합니다. 여기서 주의할 한 가지 함정이 있습니다: 컴포넌트는 XFA 문서 여부를 사전에 파싱 검사하여 EnableV8Engine 플래그를 자동으로 켜주지만, 스크립트가 사용된 일반 AcroForm 문서는 XFA 마커를 갖고 있지 않으므로 자동 감지가 수행되지 않습니다. 따라서 뷰어가 양식 스크립트를 정상 구동해야 한다면 첫 문서가 로드되기 전에 개발자가 직접 이 플래그를 수동으로 켜주어야 합니다. 일반 pdfium.dll이 이미 프로세스 메모리에 로드된 후에는 런타임에 엔진을 교체할 수 없습니다

uses PDFium;

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  // Must run before the singleton PDFium library first loads;
  // once plain pdfium.dll is in the process it cannot be swapped
  EnableV8Engine := True;
end;

procedure TViewerForm.OpenDocument(const FileName: string);
begin
  FPdf.LoadDocument(FileName);
  if not V8FeaturesAvailable then
    StatusBar.SimpleText :=
      'JavaScript disabled: pdfium.v8.dll not deployed';
end;

V8FeaturesAvailable 함수는 직관적인 런타임 검사 도구입니다: 로드된 바이너리가 V8 엔진 종속 익스포트 함수들을 실제로 해석해 제공하는지 여부를 확인해 주므로, 인스톨러가 어떤 DLL 파일을 배포했는지에 상관없이 작동 검증에 신뢰할 수 있습니다. 동일한 엔진 및 플래그 사양이 동적 XFA 렌더링 기능도 제어합니다; XFA 감지 동작과 V8 자동 선택 로직의 연동 사양에 대해서는 PDFium을 통한 XFA 양식 감지 및 XFA 패킷 추출 기사에서 상세히 설명합니다

경고, 인쇄, 메일 전송 및 양식 전송용 호스트 이벤트

스크립트가 트리거하는 눈에 보이는 모든 외부 동작은 TPdf 이벤트를 거쳐 개발자의 코드로 역라우팅되며, 이들 이벤트가 할당되지 않은 경우 기본적으로 안전하게 무시(no-op) 처리됩니다. OnJavaScriptAlertapp.alert로부터 메시지, 타이틀, 버튼 유형 및 아이콘 정보를 수신해 누른 버튼 결과를 반환할 수 있게 돕습니다; OnJavaScriptResponse는 비밀번호 마크를 포함해 app.response 입력 프롬프트 기능을 제어합니다; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail, 그리고 OnJavaScriptSubmitForm은 비프음, 인쇄 요청, 메일 및 양식 전송 요청 정보를 전체 매개변수와 함께 노출합니다. 이 이벤트들을 연결하지 않은 뷰어 프로그램도 렌더링 및 계산은 정상적으로 작동합니다; 단지 해당 문서가 다이얼로그를 열거나 인쇄를 하거나 외부로 메일 등을 전송하는 기능이 차단될 뿐입니다

procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
  const Msg, Title: WString; ButtonType, Icon: Integer;
  var Result_: Integer; var Handled: Boolean);
begin
  // Route app.alert through the VCL so it is owned by our window
  Result_ := MessageBox(Handle, PWideChar(Msg), PWideChar(Title),
    MB_OK or MB_ICONINFORMATION);
  Handled := True;
end;

procedure TViewerForm.PdfJavaScriptSubmitForm(Sender: TObject;
  const Url: WString; const Data: TBytes);
begin
  // Nothing is transmitted unless this handler chooses to send it
  LogAudit(Format('Document requested form submit to %s (%d bytes)',
    [Url, Length(Data)]));
end;

이와 같은 기본 작동 분리 방식은 보안 위협 모델링 측면에서 대단히 유용합니다. OnJavaScriptMailOnJavaScriptSubmitForm은 단순한 알림(notification) 형태로 설계되었습니다: PDFium Component는 자체적으로 네트워크 전송이나 MAPI 제어를 직접 수행하지 않으므로, 준비되지 않은 호스트 프로세스 상에서 악성 문서가 this.submitForm 또는 this.mailDoc를 호출하더라도 아무런 실제 전송이 유발되지 않습니다. 호스트 프로그램이 이벤트를 핸들링하여 전송 프로세스를 수동 구현(opt-in)해야만 비로소 외부 전송이 일어나므로, 로컬 데이터를 외부로 전송할지 여부는 항상 개발자의 코드 통제 아래 URL 및 페이로드 데이터를 면밀히 검토해 결정할 수 있습니다

악성 PDF가 임의의 URL을 호출해 로드하지 못하게 차단하는 방법

URI 실행 동작은 기존 라이브러리 엔진에서 유일하게 수동 차단이 아닌 자동 실행(active) 기본 설정을 채택했던 영역이어서 별도의 거부(veto) 이벤트가 제공되었습니다. 버전 2.13.1 전까지 FFI_DoURIActionWithKeyboardModifier 콜백은 XFA 필드가 제공한 URI를 아무런 조건 없이 ShellExecuteW로 전송해 실행하였고, 이로 인해 사용자가 클릭 시 악성 문서가 임의의 사이트를 띄울 수 있는 취약성이 있었습니다. OnXfaUriAction은 이 취약성을 해소합니다: 컴포넌트는 URI 실행 전에 이 이벤트를 참고하며, 이벤트 내에서 Handled를 True로 지정하면 ShellExecuteW 실행을 취소시킬 수 있습니다. 이 이벤트를 할당하지 않으면 하위 호환성을 위해 기존처럼 자동 실행되므로, 보안을 중시하는 뷰어 프로그램이라면 항상 이 이벤트를 장착해 확인해야 합니다

procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
  const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
  // Veto anything that is not plain https; the default would
  // otherwise ShellExecuteW the URI as-is
  if not SameText(Copy(Uri, 1, 8), 'https://') then
  begin
    LogAudit('Blocked URI action: ' + Uri);
    Handled := True;
  end;
end;

스키마(scheme)에 대한 허용 목록(allow-list)을 구성해 검사하는 것이 최소한의 보안 조치입니다; 한 단계 더 나아가 뷰어 상에서 사용자에게 동의 팝업을 띄우거나 자체 샌드박스가 결합된 브라우저 컴포넌트를 통해 안전하게 로드하도록 구현할 수 있습니다. 동일한 예외 거부 제어 철학이 v2.13.1 이벤트 세트 전체에 적용되어 있습니다: OnXfaEmail, OnXfaHttpRequestOnXfaOpenFile은 네트워크 또는 파일 전송을 컴포넌트가 임의로 직접 수행하지 않고 개발자가 매개변수 정보를 이벤트 인수로 전달받아 검증할 수 있도록 돕습니다. 이러한 보호 장치를 포함하여 신뢰할 수 없는 문서를 안전하게 렌더링하고 격리하기 위한 종합적인 보안 방침은 Delphi에서 안전한 PDF 미리보기 화면 구축하기 기사에서 설명합니다

SetFocusedFormFieldText를 사용하여 포커스된 필드 값 제어

버전 2.13.2에 추가된 TPdf.SetFocusedFormFieldTextFocusedFormFieldText와 짝을 이루는 데이터 작성용 API입니다: FORM_SelectAllText를 통해 포커스된 필드의 기존 문자열 전체를 선택하고 FORM_ReplaceSelection을 호출해 새로운 문자열로 덮어쓰며, 이는 실제 사용자가 키보드로 값을 수동 입력하는 동작과 정확하게 매칭되어 작동합니다. 데이터가 입력 이벤트 경로를 타고 전달되므로, 필드에 할당되어 있는 키스트로크, 서식 자동 구성 및 계산 실행 스크립트가 수동 입력 시와 동일하게 정상적으로 작동합니다. 이에 따라 JavaScript가 장착된 뷰어 프로그램 상에서 프로그래밍 방식으로 셀 값을 채워 넣는 데 가장 안전하고 안정적인 도구가 됩니다. 관련 필드 탐색 및 포커스 관리에 대해서는 양식 필드 내비게이션 기사에서 설명합니다

if FPdf.FocusedFormFieldIndex >= 0 then
begin
  if not FPdf.SetFocusedFormFieldText('42.50') then
    ShowMessage('No focused field accepts text on this page');
  // AcroForm: value commits to /V when focus leaves the field.
  // XFA: the write stays in the in-memory edit buffer only and
  // will not survive a save
end;

위의 주석에 명시된 데이터 보존 조건의 차이는 단순히 참고하라는 설명이 아닌 명확한 기술 제약 사항입니다. AcroForm 텍스트 및 콤보 박스 필드의 경우 수정된 값은 포커스를 잃을 때 필드의 /V 엔트리에 반영되어 문서 저장 시에도 영구 보존됩니다. 반면 XFA 텍스트 필드의 경우 수정된 값은 메모리 상의 편집 버퍼 내에만 임시 보존되는데, PDFium 엔진이 위젯 수정 값을 내부 XFA 데이터셋 패킷으로 동기화해 덮어씌우는 공식 API를 노출하지 않기 때문입니다; 저장 시 데이터가 영구 보존되지 않습니다. XFA 데이터 세트를 영구 수정하려면 위젯 대신 데이터셋 XML 내부의 노드를 직접 조작해야 합니다

배포 전 알아두어야 할 제약 조건

두 가지 기술적 한계를 밝혀 둡니다. 첫째, PDFium이 구현하는 JavaScript API는 Acrobat 개체 모델 중 필드 접근, 계산 및 서식 구성 이벤트, app.alert, app.response, 인쇄, 메일 및 전송 요청 등 양식 스크립팅 핵심 영역에 특화된 하위 집합입니다. Acrobat 전용 고유 개체에 심하게 의존해 작성된 문서들은 오작동할 수 있으며 대개 에러를 출력하지 않고 조용히 생략되므로, 호환성을 과신하기보다는 실제 양식 파일들을 사용해 검증해야 합니다. 둘째, V8 엔진을 탑재하려면 훨씬 대용량의 pdfium.v8.dll을 함께 배포해야 합니다; 스크립트 실행이나 XFA 기능이 전혀 필요 없는 간단한 뷰어 프로그램이라면 상대적으로 콤팩트한 일반 버전 DLL 파일을 배포하고 m_pJsPlatform을 장착하지 않는 독립적 보안 구성을 고수하는 편이 더 유리할 수 있습니다

권장되는 프로그래밍 구성 방식은 매우 직관적입니다: 프로그램 시작 시 EnableV8Engine을 켜고, 다이얼로그 창이 이질감 없이 출력되도록 alert 및 response 이벤트를 연결하며, OnXfaUriAction 이벤트를 등록하고 메일 및 양식 전송 이벤트에 감사 로그 기록을 연동한 뒤, 추가 요건이 식별되기 전까지는 나머지를 기본 상태로 두고 운영하는 것입니다. 전체 이벤트 명세와 양식 제어 API 레퍼런스는 PDFium Component 제품 페이지에서 제공됩니다