서드파티 송장 템플릿이 있거나, 누군가가 오래전에 더는 찾을 수 없는 소프트웨어로 생성해 둔 보관용 계약서가 있고, 요구 사항은 그것을 상호작용 가능하게 만드는 것이라고 합시다: 모서리에 서명 상자를 넣고, 텍스트 필드를 몇 개 추가하고, 평면 체크리스트를 실제 체크박스로 바꾸는 것입니다. 문제는 이 PDF를 처음부터 만드는 것이 아니라는 점입니다. 이미 존재하고, 이미 페이지와 콘텐츠 스트림과 여러분이 제어할 수 없는 글꼴을 가지고 있으며, 다시 빌드하지 않고 그 객체 그래프 위에 AcroForm 위젯을 덧붙여야 합니다. 이는 새 문서에 폼을 만드는 문제와는 다르고, 사람들이 막히는 지점은 뷰어에서 결과를 열어 보기 전까지는 보이지 않습니다. 방금 쓴 필드가 페이지 어디에도 없기 때문입니다
HotPDF는 Delphi 및 C++Builder용 네이티브 VCL PDF 컴포넌트이며, v2.247.0부터는 이를 위해 전용 메서드 집합을 제공합니다: LoadFromFile. 이 글에서는 이 메서드들이 하는 일, 이들이 만드는 ISO 32000-1 딕셔너리, 그리고 이 한 가지 플래그 없이는 전체 작업이 조용히 빈 파일처럼 보인다는 점을 살펴봅니다
로드된 문서에서 필드를 만드는 작업이 별도 경로인 이유
PDF를 아무것도 없는 상태에서 만들 때는 HotPDF가 전체 객체 모델을 소유합니다. 각 페이지는 쓰기 가능한 THPDFPage 래퍼이고, 텍스트 필드를 AddTextField를 통해 추가하면 새 위젯이 페이지의 주석 객체, 페이지 객체, 폼의 필드 컬렉션에 연결되고, 이어서 문서의 글꼴 리소스에서 appearance stream을 생성합니다. appearance stream은 위젯의 보이는 표면, 즉 상자와 테두리와 기본 텍스트이며, 뷰어는 이를 PDF drawing operators로 그대로 렌더링합니다
로드된 문서는 그런 기반을 전혀 제공하지 않습니다. 페이지는 raw dictionary로 들어왔고, 위젯을 달 수 있는 쓰기 가능한 THPDFPage 래퍼도 없으며, 더 중요한 것은 appearance stream을 그릴 글꼴 리소스 파이프라인도 대기하고 있지 않다는 점입니다. 따라서 로드 경로는 다른 방식으로 동작합니다. 필드 딕셔너리를 파싱된 객체 그래프에 바로 써 넣고, 페이지 객체가 아니라 0부터 시작하는 인덱스로 페이지를 가리킵니다. 필드 유형과 플래그 비트는 처음부터 만드는 경로와 정확히 같으므로 Text field는 어느 쪽이든 Text field입니다. 달라지는 것은 아래의 연결 구조이고, 특히 위젯의 표면을 그리는 방식입니다
/NeedAppearances 플래그는 여기서 선택 사항이 아닙니다
이 한 가지가 작업 결과가 보일지 말지를 결정합니다. 로드 경로는 appearance stream을 생성하지 않으므로, 새로 추가한 위젯은 뷰어에 /AP 항목 없이 도착합니다: 설명된 표면이 없는 필드입니다. 많은 뷰어는 appearance도 없고 그것을 만들라는 지시도 없는 위젯을 렌더링하라고 하면 아무것도 그리지 않습니다. 필드는 파일 안에 있고 구조적으로는 유효하며 폼 입력 도구로 접근할 수 있지만, 사람 눈에는 완전히 보이지 않습니다
해결책은 ISO 32000-1 §12.7.3에 정의되어 있습니다: AcroForm 딕셔너리는 /NeedAppearances 불리언을 포함하고, 이것이 true이면 규격을 따르는 리더는 각 필드의 /DA (기본 appearance) 문자열과 값에서 부족한 appearance stream을 스스로 구성해야 합니다. HotPDF가 이 작업을 대신해 줍니다. 로드된 문서에 어떤 필드든 처음 추가하면 EnsureLoadedAcroForm가 실행됩니다: 카탈로그에 /AcroForm 배열이 없으면 그것을 만들고, /Fields를 강제로 /NeedAppearances true로 설정합니다. 직접 호출하지는 않지만, 그 존재를 알면 동작이 설명됩니다. 또한 다음과 같은 배포상의 주의점도 분명히 이해할 수 있습니다: 일부 최소 기능 또는 비규격 뷰어는 /NeedAppearances를 무시하고도 여전히 아무것도 렌더링하지 않습니다. 대중적인 독자에게는 이 플래그가 제 역할을 하지만, 대상이 특이한 임베디드 렌더러라면 약속하기 전에 그 환경에서 먼저 시험해 보십시오
여섯 가지 필드 유형 추가하기
모든 메서드는 같은 형식을 따릅니다. 0부터 시작하는 페이지 인덱스, PDF 사용자 공간 좌표계의 위젯 사각형 네 모서리, 필드 이름, 그리고 해당 유형이 필요로 하는 추가 인자를 전달합니다. 사각형은 X1, Y1, X2, Y2이며 PDF 원점은 페이지의 왼쪽 아래에 있으므로 Y 값이 클수록 더 위에 놓입니다. 이것은 파일 형식의 좌표 규칙이지 화면의 좌상단 기준이 아니며, 이 점을 거꾸로 이해하는 것은 플래그를 잊는 것 다음으로 흔한 실수입니다. 각 호출은 새 필드의 0부터 시작하는 인덱스를 반환하며, 페이지 인덱스가 범위를 벗어나거나 페이지 객체를 찾을 수 없으면 -1를 반환합니다
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;
// Text field: name, initial value, max length (0 = unlimited)
Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);
// CheckBox: export value, initial checked state
Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);
// Signature field: just a name and a rectangle
Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');
if Idx >= 0 then
Pdf.SaveLoadedDocument('contract-interactive.pdf');
finally
Pdf.Free;
end;
end;
텍스트 필드의 세 번째와 네 번째 문자열 인자는 필드 이름과 초기 /V 값이며, 정수 인자는 /MaxLen보다 클 때만 기록됩니다. HotPDF는 편집 가능한 각 필드에 기본 appearance 문자열로 /Helv 12 Tf 0 0 0 rg, 이것이 /NeedAppearances를 준수하는 뷰어가 값을 칠할 글꼴과 색을 결정할 때 읽는 내용입니다. 체크박스는 export value를 받는데, 박스가 선택되었을 때 폼이 제출하는 문자열입니다. 또한 초기 상태를 위한 불리언도 함께 받습니다. 내부적으로는 일치하는 /V, /AS, 그리고 /DV 이름 항목을 기록하여 on/off 상태가 파일이 열리는 순간 일관되게 유지되도록 합니다. 빈 export value는 Yes, 즉 체크박스의 일반적인 "on" 이름입니다
선택 필드와 /Ff 비트 플래그
ComboBox와 ListBox는 모두 선택 필드이며, ISO 32000-1 §12.7.4에서 필드 유형은 /Ch입니다. 드롭다운과 스크롤 목록의 차이는 필드 플래그 정수 /Ff: 비트 18, Combo 플래그, 값 $40000. HotPDF는 이 비트를 AddLoadedComboBox에 설정하고 AddLoadedListBox에는 비워 둡니다. 그렇지 않으면 두 메서드는 동일하며, 둘 다 선택 항목을 문자열들의 열린 배열로 받아 /Opt 항목에 기록합니다
// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
['United States', 'Canada', 'Mexico']);
// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
['Low', 'Normal', 'High']);
// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');
옵션 목록에 대해서는 두 가지를 덧붙이겠습니다. HotPDF는 각 /Opt 항목을 일반 문자열로 기록하는데, 내보내기 값과 표시 레이블이 같은 텍스트입니다. ISO 32000-1 §12.7.4.4는 사용자가 읽는 값과 제출값이 다를 때 사용할 수 있는 두 요소짜리 [export display] 형식도 허용합니다. 로드 생성 메서드는 더 단순한 단일 문자열 형식을 사용하므로, 내보내기 값과 표시 값을 다르게 써야 한다면 결과 딕셔너리에서 직접 설정해야 합니다. 그리고 필드의 현재 선택값으로 전달하는 값은 제공한 옵션 중 하나여야 하는데, 뷰어가 이를 목록과 대조하기 때문입니다
푸시 버튼은 또 다른 플래그 기반 사례입니다: 필드 유형 /Btn에 비트 17, PushButton 플래그, 값 $10000. 이 비트가 클릭 가능한 버튼과 체크박스를 구분하는데, 체크박스 역시 /Btn 필드이지만 이 비트는 없습니다. 전달하는 캡션은 appearance characteristics dictionary /MK의 normal caption /CA로 기록됩니다. 범위를 분명히 하자면, 버튼은 레이블과 사각형과 함께 생성되지만 로드 생성 메서드는 action을 연결하지 않으므로, 버튼 자체는 보기에는 그럴듯하지만 클릭해도 아무 일도 일어나지 않습니다. submit, reset, JavaScript actions를 연결하는 것은 별개의 문제입니다. from-scratch 작성 측면의 field-plus-action 워크플로는 Delphi에서 AcroForm 필드와 액션 만들기에서 다루며, 이것이 로드 경로가 의도적으로 빼놓은 부분을 비교하기에 적절한 기준점입니다
모든 필드가 공유하는 딕셔너리
여섯 메서드 아래에는 위젯 주석(annotation)을 만들고 두 곳에 등록하는 하나의 공통 빌더가 있습니다. 그것은 /Type /Annot 및 /Subtype /Widget, /Rect 배열을 네 좌표로 만들고, annotation 플래그 /F 4는 Print 비트를 설정해 필드가 화면뿐 아니라 인쇄물에도 나타나게 하며, field name /T, field type /FT, flags /Ff, 그리고 /P에 대한 역참조입니다. 그런 다음 새 필드를 AcroForm의 /Fields 배열과 해당 페이지의 /Annots 배열에 추가하고, 중간의 간접 참조를 풀어 실제 배열을 확장하게 하므로 위젯이 고아가 되지 않습니다
그 이중 등록이 중요한 이유는 두 목록 중 하나에만 존재하는 위젯은 미묘하게 깨지기 때문입니다. 필드가 /Fields에 있지만 페이지의 /Annots에 없으면 폼은 알 수 있지만 절대 그려지지 않습니다. 반대는 그려지지만 폼 로직은 모릅니다. HotPDF는 추가할 때마다 둘을 항상 동기화하므로, 원래라면 스펙을 보며 손으로 정확히 맞춰야 할 보관 작업을 대신해 줍니다
솔직한 한계 몇 가지
이것을 기반으로 워크플로를 만들기 전에 기대치를 분명히 하십시오. flatten-and-regenerate 동작은 뷰어가 /NeedAppearances를 존중하는지에 달려 있으며, 이는 Acrobat, 최신 브라우저 PDF 엔진, 일반적인 데스크톱 리더를 포괄하지만, 세상 모든 렌더러에서 보장되는 것은 아닙니다. 플래그를 무시하는 뷰어까지 포함해 어디서나 필드가 똑같이 렌더링되는 파일을 만들어야 한다면, 당신은 appearance-stream 영역에 있는 것이고, /AP를 그려 주는 처음부터 만드는 작성 경로가 더 적합합니다. 서명 필드 역시 마찬가지로, 서명할 준비가 된 빈 서명 위젯으로 생성될 뿐입니다; 필드를 배치하는 것과 암호학적 서명을 적용하는 것은 다른 일입니다
이미 있는 것을 바꾸는 것이지 추가하는 것이 아니라면 관련 작업은 form flattening입니다. 여기서는 상호작용 필드를 다시 정적 페이지 콘텐츠로 구워 넣어 값이 영구적이고 수정 불가능하게 되며, XFA가 들어 있는 폼을 어떻게 처리하는지도 포함한 이 round trip은 Delphi에서 XFA 및 AcroForm 필드 평탄화하기. 필드 추가와 필드 평탄화는 같은 생애주기의 양 끝입니다: 이 글은 상호작용을 없던 문서에 얹는 방법이고, 평탄화는 폼의 역할이 끝났을 때 그것을 다시 떼어내는 방법입니다
여기에 나온 로드된 문서 폼 API는 표준 HotPDF Component의 일부로 제공되며, Delphi 및 C++Builder용 필드 플래그, appearance 처리, 그리고 AcroForm 모델 전반에 대한 전체 레퍼런스와 함께 제공됩니다