기술 문서

Delphi로 로드된 PDF의 폼 필드 값 설정하기

거의 모든 PDF 뷰어의 필드 목록에서 독일어 청구 폼의 street 필드는 Straße로 읽힙니다. 그 /T 항목 뒤의 바이트들은 다른 이야기를 합니다: <FEFF005300740072006100DF0065>, 앞에 바이트 오더 마크를 붙인 16진 UTF-16BE 문자열입니다. 필드 이름을 그 날 바이트들과 대조하는 코드는 필드가 없다고 보고하는데, 사용자는 그것을 곧장 클릭해 들어갈 수 있음에도 불구하고요. 같은 폼이 체크박스의 on 상태를 Yes 대신 2로, 국가 목록을 [export display] 쌍으로 저장하고 있을 수도 있는데, 이렇게 여러분이 쓰는 값과 뷰어가 그리는 값이 어긋나는 길이 세 개 생깁니다

세 변주 모두 같은 곳에서 옵니다. 몇 년 전 고객이 가지고 있던 무슨 오토링 도구로 만들어진 폼이고, 여러분의 Delphi 애플리케이션이 이제 데이터베이스에서 값을 채워 모든 리더에서 같게 그려지는 파일로 돌려줘야 합니다. 폼을 다시 만들 수는 없으니, 값을 채우는 코드는 폼이 쓰였을 때의 규칙을 따라야 합니다. ISO 32000-1에는 세 경우 각각의 규칙이 있습니다. 텍스트 문자열은 §7.9.2.2, 버튼 외형 상태는 §12.7.4.2.3, 선택 필드의 선택은 §12.7.4.4의 Table 231입니다. 아직 없는 필드를 만드는 것은 별도의 일이며, Delphi에서 로드된 PDF에 AcroForm 필드 추가하기가 다룹니다

필드 찾기: HotPDF는 이름을 대조하기 전에 /T를 디코딩합니다

THotPDF.SetFormFieldValue에는 두 오버로드가 있습니다. 0 기준 필드 인덱스를 받는 것과 완전 수식 이름을 받는 것이고, 둘 다 컴포넌트가 첫 사용 시 AcroForm 트리에서 조립하는 이름 테이블로 풉니다. 테이블이 조립되는 동안 모든 부분 이름은 디코더를 통과합니다. 16진 /T 문자열은 바이트로 되돌아가고, 그 바이트들이 FE FF로 시작하며 길이가 짝수면 나머지는 UTF-16BE로 읽혀 UTF-8로 재인코딩됩니다. 디코딩된 각 부분 이름은 §12.7.3.1이 묘사하듯 마침표로 부모와 합쳐지므로, Address라는 부모 아래의 City 자식 필드는 Address.City로 등록됩니다. 디코딩되는 것은 조회 키뿐입니다. 파일 안의 /T 오브젝트는 16진 표기를 유지하고, 문서 저장은 값만 채운 필드의 신원을 건드리지 않습니다

키는 테이블에 들어가기 전에 소문자로 만들어지므로, 'address.city'가 Address.City를 찾습니다. 이것은 필드 이름을 대소문자 구분으로 다루는 표준을 넘어서는 것으로, 대소문자만 다른 두 필드는 조회에서 충돌한다는 뜻이기도 합니다. 누군가 손으로 친 설정 파일에서 필드 이름을 읽는 코드에게 잘못 찍은 대문자 하나가, 같은 폼 안에 대소문자만 다른 두 필드보다 훨씬 흔한 실패입니다

HotPDF가 비 ASCII AcroForm 이름을 풀어 내는 방식: 16진 /T 문자열이 바이트로 되돌아가고, FE FF 바이트 오더 마크는 UTF-16BE로 디코딩돼 UTF-8로 재인코딩되며, 수식된 이름이 부모와 합쳐져 Applicant.FullName과 Straße라는 이름의 필드가 모두 조회 테이블에 자리 잡습니다
바뀌는 것은 캐시 키뿐입니다. 필드 사전은 16진 인코딩을 유지하고, 조회는 표준을 넘어선 편의로 소문자로 정규화되며, 문서 저장은 값만 채운 필드의 신원을 결코 다시 쓰지 않습니다

디코딩된 이름은 AnsiString에 실린 UTF-8 바이트들이고, 바로 여기서 Delphi 자체의 문자열 변환이 방해합니다. 'Applicant.Straße' 같은 리터럴을 AnsiString 파라미터로 넘기면 시스템 ANSI 코드 페이지를 거쳐 변환되므로, ß는 UTF-8 짝 C3 9F가 아니라 한 바이트 DF로 도착하고 조회는 빗나갑니다. ASCII 이름은 영향이 없습니다. 그 외의 이름이라면 필드를 한 번 열거하고, 각 이름을 UTF8ToString으로 디코딩하고, 인덱스 오버로드로 쓰세요:

// FindFieldIndex는 직접 추가하는 헬퍼입니다: 디코딩된 UTF-8 이름을
// 유니코드 문자열과 비교하므로, 리터럴과 조회 사이에
// ANSI 코드 페이지가 끼어들지 않습니다
function FindFieldIndex(Pdf: THotPDF; const Name: string): Integer;
var
  I: Integer;
begin
  for I := 0 to Pdf.GetFormFieldCount - 1 do
    if SameText(UTF8ToString(Pdf.GetFormFieldName(I)), Name) then
      Exit(I);
  Result := -1;
end;

var
  Pdf: THotPDF;
  Street: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // ASCII 이름은 이름 오버로드로 곧장 갈 수 있습니다
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');

    Street := FindFieldIndex(Pdf, 'Applicant.Straße');
    // 값도 AnsiString이라 리터럴 'ß'는 시스템 코드 페이지에
    // 의존합니다; FEFF 헥스(Hauptstraße 12)는 그것을 피합니다
    if Street >= 0 then
      Pdf.SetFormFieldValue(Street,
        'FEFF00480061007500700074007300740072006100DF0065002000310032');

    // FEFF 접두 UTF-16BE 헥스는 이름 오버로드에서도 동작합니다
    // (이것은 München); 헥스 문자열로 쓰입니다
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

SetFormFieldValue가 쓰는 것들, 단계별로

두 오버로드는 필드를 찾은 뒤 같은 순서를 밟습니다. /V를 쓰고, 선택 인덱스를 다시 조립하고, 필드를 변경 표시하고, 버튼 외형 상태를 갈아엮고, 텍스트나 선택 외형을 재생성하고, 필드를 dirty 집합에 기록합니다. 마지막 단계가 인자 없는 RecalculateLoadedFormFieldsIncremental을 먹입니다. 이것은 변경된 필드를 읽는 계산 스크립트만, 품목·소계·합계 같은 체인을 통해 간접적으로 읽는 것까지 포함해 다시 돌립니다

새 /V의 오브젝트 타입은 필드에 달려 있습니다. 체크박스나 라디오 버튼에서 SetFormFieldValue는 언제나 이름 오브젝트를 씁니다. §12.7.4.2.3이 선택된 상태를 이름으로 저장하고 PDF 이름은 텍스트 문자열 인코딩 없는 순수 바이트이기 때문입니다. 그 외의 모든 필드는 문자열 오브젝트를 받고, 여러분이 넘긴 값은 먼저 검사됩니다. FEFF로 시작하고 길이가 짝수이며 16진 숫자만 담은 문자열은 §7.9.2.2의 UTF-16BE 형태로 간주되어 <FEFF...>로 직렬화됩니다. 위의 City 줄이 의지하는 것이 바로 그것입니다. 그 외의 것은 여러분이 공급한 바이트 그대로의 리터럴 문자열로 저장되므로, 평범한 라틴 텍스트는 평문으로 들어갑니다

체크박스는 값을 바꾼 뒤에도 왜 옛 체크 표시를 유지할까요?

버튼 필드에서는 값이 무엇이 그려질지 결정하지 않습니다. 체크박스 위젯은 /AP /N 안의 어느 스트림이 현재 보이는지 이름 짓는 /AS 항목을 실고, 뷰어는 /V가 아니라 /AS에서 그립니다. /V를 Yes로 바꾸는 동안 /AS가 여전히 Off를 말하면 파일은 스스로 모순됩니다. 플래트너는 체크 안 된 박스를 페이지에 굽는 동안 폼 데이터는 체크됐다고 우깁니다. 그래서 SetFormFieldValue는 버튼 필드와 /Kids 배열의 모든 항목을 방문해, 각 위젯의 /AP /N에서 on 상태 이름을 읽고, 필드 값과 맞으면 /AS를 그 이름으로, 맞지 않으면 Off로 설정합니다

/V만 바뀔 때 HotPDF 체크박스가 옛 체크를 유지하는 이유: 뷰어는 /AS 외형 상태에서 /AP /N으로 그리므로, SetFormFieldValue는 필드와 모든 kid를 방문해 on 상태 이름을 Off가 아닌 첫 키로 읽고, 맞으면 /AS를 다시 쓰거나 아니면 Off로 씁니다
라디오 그룹은 각 kid를 /Parent 체인을 통해 부모 필드에서 상속된 값과 비교하므로, 그룹을 하나의 export 값으로 설정하면 정확히 그 위젯만 켜지고 모든 형제는 꺼집니다

실전 폼은 이 규칙을 두 가지로 휩니다. off 외형은 표준에서 Off라는 이름이지만, 오토링 도구는 그 스트림을 아예 빼먹고 뷰어가 아무것도 그리지 않게 두는 경우가 흔해, 항목 하나짜리 일반 외형 사전은 완전히 합법입니다. on 상태 이름은 저자가 고른 대로입니다. Yes, On, 2, 폼 자체 언어의 한 단어. 그래서 on 상태는 /AP /N에서 Off가 아닌 첫 키로 삼고, 여러분의 값과의 비교는 대소문자를 무시합니다. 라디오 버튼은 §12.7.4.2.4의 배치를 더합니다. 선택된 값은 부모 필드의 /V에 살고, 각 kid 위젯은 보통 자기 /V가 없습니다. 각 kid는 /Parent 체인으로 상속된 값과 비교됩니다

// 체크박스: 값은 /AP /N의 on 상태 키와 같아야 합니다
// (여기서는 'Yes'; 폼 디자이너는 '2'나 'On'을 골랐을 수도 있습니다)
Pdf.SetFormFieldValue('Consent', 'Yes');

// 라디오 그룹: /V는 부모에 이름으로 기록되고, 모든 kid 위젯은
// /AS가 자기 on 상태 이름이나 Off로 설정됩니다
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// 체크박스 비우기: 어떤 on 상태와도 맞지 않는 값은 /AS Off를 냅니다
Pdf.SetFormFieldValue('Newsletter', 'Off');

뒤집어 말하면 오타는 예외를 내지 않습니다. 뒤따르는 공백이 있는 'yes '는 아무것도 맞지 않아 박스는 Off로 끝나고 파일은 여전히 완벽히 유효합니다. 값이 데이터베이스 컬럼에서 온다면 호출 전에 trim하세요

선택 필드: /I를 /V와 맞춰 유지하기

콤보 박스나 리스트 박스는 선택을 두 곳에 기록합니다. /V 옆에서, Table 231은 /I를 /Opt로의 0 기준 인덱스 배열로 정의하는데, /I가 옵션 0을 가리키는 동안 /V가 옵션 3의 이름을 말하면 뷰어는 잘못된 행을 하이라이트할 수 있습니다. SetFormFieldValue는 선택 필드에 대한 호출마다 /I를 다시 조립합니다. 무엇보다 먼저 필드 자신의 /I 항목을 지우고 절대 옛 배열을 제자리에서 편집하지 않습니다. 간접 배열은 다른 필드와 공유될 수 있고, 그것을 고치면 그 필드의 선택까지 바뀝니다. 그다음 옵션이 상속될 수 있으므로 /Opt를 /Parent 체인으로 풀고 항목들을 걷습니다. 맨 문자열 옵션은 직접 비교되고, [export display] 쌍은 export 요소에서 비교되며, 요소가 둘 미만인 쌍은 건너뜁니다

HotPDF가 선택 필드의 일관성을 지키는 방식: SetFormFieldValue는 만지기 전에 로컬 /I 배열을 지우고, /Opt를 /Parent 체인으로 풀며, 각 옵션의 export 절반을 디코딩된 텍스트로 비교해, 첫 짝에 한 요소짜리 /I를 쓰고 편집 가능한 콤보 값에 인덱스가 없으면 아무것도 쓰지 않습니다
맨 문자열 옵션은 직접 비교되고 export display 쌍은 export 요소에서 비교되며, /Opt 밖의 값은 인덱스를 남기지 않는 것이 옳습니다 — 잘못된 행을 가리키는 낡은 /I는 없는 것보다 나쁩니다

비교는 디코딩된 텍스트에서 돌므로, PDFDocEncoding으로 저장된 옵션이 UTF-16BE 헥스로 쓰인 값과 맞고 그 반도 마찬가지입니다. 스칼라 값은 최대 하나의 인덱스를 내고, 필드가 갖고 있던 멀티 선택 무엇이든 대체됩니다. 아무것도 맞지 않으면 /I는 아예 쓰이지 않습니다. 편집 가능한 콤보 박스에서는 그것이 올바른 결과입니다. §12.7.4.4가 사용자가 목록 밖의 값을 치게 허용하고 그런 값은 가리킬 인덱스가 없기 때문입니다

콤보 박스에 대한 가장 흔한 불만의 뒤에도 같은 규칙이 있습니다. 호출은 성공하고 /V는 새 텍스트를 담지만 뷰어는 아무것도 선택된 것으로 보여 주지 않습니다. 대개 호출자가 [CA Canada] 쌍으로 만들어진 목록에 표시 라벨(Canada)을 넘겼습니다. 컴포넌트는 편집 가능한 콤보를 깨뜨리지 않고 그것을 오류로 다룰 수 없으므로, 검사는 여러분 코드의 몫입니다. GetFormFieldOptionValue는 각 옵션의 export 요소를 돌려주고, 가드를 짧게 만들어 줍니다:

// SetChoiceByExport은 컴포넌트 일부가 아니라 직접 만드는 가드입니다:
// /Opt의 어떤 export 요소와도 맞지 않는 값을 거부합니다
procedure SetChoiceByExport(Pdf: THotPDF; const FieldName,
  ExportValue: AnsiString);
var
  I: Integer;
begin
  for I := 0 to Pdf.GetFormFieldOptionCount(FieldName) - 1 do
    if Pdf.GetFormFieldOptionValue(FieldName, I) = ExportValue then
    begin
      // /Opt는 [[US United States] [CA Canada] [MX Mexico]]입니다:
      // 'CA'는 export 요소와 맞고 /I는 [1]이 됩니다
      Pdf.SetFormFieldValue(FieldName, ExportValue);
      Exit;
    end;
  raise Exception.CreateFmt('%s is not an export value of %s',
    [string(ExportValue), string(FieldName)]);
end;

// 자유 텍스트의 편집 가능 콤보: 직접 씁니다. /V는 설정되고,
// /I는 제거되며 인덱스는 지어내지 않습니다
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

/NeedAppearances 없이도 새 값이 보이나요?

텍스트와 선택 필드라면 보입니다. 뷰어는 저장된 /AP 스트림을 그리고 /V를 보지 않으므로, AcroForm 사전이 /NeedAppearances true(§12.7.3.3)를 실고 뷰어가 그것을 존중하기로 하지 않는 한, 새 값과 오래된 외형은 오래된 텍스트를 화면과 인쇄에 남깁니다. 플래트너와 썸네일 생성기는 그 플래그를 자주 무시합니다. SetFormFieldValue는 호출의 일부로 텍스트와 선택 필드의 외형 스트림을 재생성하는데, 공개된 EnsureLoadedFieldAppearanceStream과 같은 빌더를 씁니다. 스트림은 상속된 /DA 문자열, /Q 정렬, comb 필드의 /MaxLen, 값 자체에서 만들어집니다. /DA가 이름 짓는 폰트는 AcroForm /DR 리소스로 풀리므로, Type0 폰트는 Helvetica로 폴백하는 대신 자기 글리프를 유지합니다. 버튼은 디자이너가 그린 상태 외형을 유지하고, 버튼에서는 /AS만 움직입니다

SetFormFieldValue는 프로시저라 무엇이 그려졌는지 알려 주지 않습니다. 쓸 만한 /Rect가 없는 위젯은 스트림을 받지 못하고, 고아 위젯이나 사각형 없는 위젯을 지닌 깨진 폼은 실제로 존재합니다. 렌더 결과가 중요하면 값을 설정한 뒤 EnsureLoadedFieldAppearanceStream을 직접 호출하세요. 최소 하나의 위젯이 스트림을 받으면 True를 돌려줍니다. GetFormField는 호출이 기대하는 Index 속성을 가진 THPDFLoadedFormField를 건네주는데, 그 오브젝트의 소유는 여러분에게 있고 free해야 합니다

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // False는 이 필드의 어떤 위젯도 그릴 사각형이 없었다는 뜻입니다.
    // 값은 저장되지만 보여 줄 것은 없습니다
    if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
      raise Exception.Create('No widget rectangle to paint into');
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;

/V가 바뀌었는지 확인하는 것은 사용자가 무엇을 볼지에 대해 거의 아무것도 증명하지 못합니다. 의미 있는 테스트는 페이지를 전후로 렌더해 위젯 사각형 안의 픽셀은 바뀌고 밖의 픽셀은 안 바뀜을 확인합니다. 우리가 이 검사를 유지하는 이유는 외형 바이트만으로는 이전에 속았기 때문입니다. 한 스트림은 옳은 바이트를 전부 담고 있었는데, /NeedAppearances true를 존중한 뷰어가 /V에서 표시를 다시 만들어 그중 아무것도 보여 주지 않았습니다

값 다시 읽기, 그다음에 보통 오는 것

GetFormFieldValue는 필드의 상속된 /V를 돌려줍니다. FEFF 접두 헥스로 저장된 값은 디코딩되어 UTF-8로 돌아오므로, 위의 City 필드는 München의 UTF-8 바이트로 읽히고 UTF8ToString이 그것을 Delphi 문자열로 만듭니다. 리터럴 문자열은 저장될 때의 바이트로 돌아옵니다. 왕복 검사에는 넘긴 철자가 아니라 양쪽의 디코딩된 텍스트를 비교하세요

필드마다 호출 하나씩 대신 폼과 외부 시스템 사이에서 필드 데이터를 대량으로 주고받는 것은 Delphi에서 XFDF 임포트와 익스포트의 주제입니다. 완성된 폼을 아무도 편집 못 하게 잠그는 것은 Delphi에서 AcroForm과 XFA 필드 플래트닝인데, 여기서 묘사한 바로 그 /AS 상태와 외형 스트림을 정적 페이지 콘텐츠로 바꿔 버립니다. 그 시점에 어긋나 있는 것은 영원히 어긋나 있으므로, 채우고, 검사하고, 그다음 플래트닝하세요

SetFormFieldValue, EnsureLoadedFieldAppearanceStream, 증분 재계산 API는 HotPDF Delphi Component의 일부로, Delphi나 C++Builder에서 로드한 문서의 필드 이름 바꾸기, 제거, 플래트닝 호출들과 나란히 있습니다