기술 문서

FDF와 XFDF 왕복에서의 멀티셀렉트 PDF 필드 (Delphi)

Deep, 줄바꿈, Blue로 이뤄진 리스트 박스 export 값은 완전히 합법적인 PDF이고 Red라는 두 번째 옵션도 그렇습니다. 둘을 골라 선택을 LF로 구분된 하나의 문자열로 눌러 담아 교환 파일에 넣으면 저쪽에서는 Deep\nBlue\nRed를 받습니다. 선택이 둘인지 셋인지 바이트 어디에도 힌트가 없습니다. 멀티셀렉트 값이 FDF나 XFDF를 거쳐 온전하게 돌아오려면 PDF를 떠나는 순간부터 다시 기록될 때까지 끝내 배열인 채로 있어야 합니다

줄바꿈 조인이 멀티셀렉트 값을 절대 운반할 수 없는 이유

붙인 문자열은 값 사이의 경계를 잃고, 값에 구분자가 들어가도 되는 이상 어떤 importer도 그 경계를 되살릴 수 없습니다. ISO 32000-1 §12.7.4.4는 choice 필드의 /V 항목이 단일 텍스트 문자열이나 텍스트 문자열 배열을 가질 수 있게 하며, MultiSelect 플래그(/Ff의 비트 22)가 켜진 리스트 박스는 옵션을 두 개 이상 고르는 순간 배열 형태를 씁니다. 같은 절은 0부터 시작하는 옵션 인덱스의 오름차순 배열인 /I도 정의하는데, 뷰어가 같은 export 값을 공유하는 두 옵션을 구분할 때 이것을 씁니다. 어느 구조도 문자열 타입 API로는 통과할 수 없습니다. HotPDF의 GetFormFieldValue는 AnsiString을 돌려주고 /V의 문자열 형태를 읽으므로, 그 위에 세운 어떤 export도 필드마다 스칼라 하나만 기록할 수 있습니다

import 쪽은 거울상 문제를 겪습니다. XFDF는 각 선택을 자체 <value> 요소로 기록하고, 이를 스칼라 setter로 넘기는 importer는 그것들을 먼저 이어 붙여야 하는데 보통 LF로 붙입니다. 앞의 Deep/Blue 옵션에서는 그 접착제가 데이터와 구분되지 않습니다. 그래서 HotPDF는 왕복의 중간에서 스칼라를 쫓아냅니다. ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF, ExportLoadedFormToXFDF는 배열을 필드에서 곧장 읽고, 짝이 되는 import 메서드들은 붙인 문자열을 한 번도 거치지 않고 새 배열을 만듭니다

스칼라 GetFormFieldValue 경로가 내장 줄바꿈을 포함한 두 개의 선택된 리스트 박스 옵션을 Deep, LF, Blue, LF, Red라는 단일 문자열로 눌러 담아 하류 리더가 이를 두 선택으로도 셋 선택으로도 해석할 수 있게 되는 순진한 멀티셀렉트 왕복
멀티셀렉트 값을 하나의 문자열로 붙이면 값 경계가 파괴되고 줄바꿈을 품은 export 값은 눌러 담은 형태를 모호하게 만든다

내보낸 FDF와 XFDF 파일에 담기는 것

디스크 위에서는 경계가 살아 있습니다. FDF에서 멀티셀렉트 필드는 타입이 붙은 배열이 되고 모든 항목은 원본 PDF에서의 표기를 유지합니다. 16진수 문자열은 hex로, 리터럴 문자열은 리터럴로 나가며 CR과 LF는 \r과 \n으로 이스케이프됩니다. ExportLoadedFormToFDF가 기록한 파일에서는 리치 텍스트 /RV나 필드의 0이 아닌 자체 /Ff가 같은 필드 딕셔너리에 함께 실립니다. XFDF에서는 루트 요소가 xml:space="preserve"를 선언하는데, ISO 19444-1이 요구하는 사항으로 이로 인해 텍스트 요소 안의 모든 공백 문자가 데이터가 됩니다. 예쁘게 출력하려다 큰일 나는 지점입니다. HotPDF는 각 <value>의 시작 태그, 이스케이프된 텍스트, 끝 태그를 한 줄에 기록하고 들여쓰기는 요소 밖에 두며, CR, LF, TAB은 문자 참조로 인코딩해 줄바꿈 정규화를 적용하는 XML 파서가 CR LF 쌍을 LF 하나로 다시 쓰지 못하게 막습니다

HotPDF가 멀티셀렉트 리스트 박스에 대해 기록하는 export 형태. FDF는 /V [(Deep 줄바꿈 Blue) (Red)]처럼 필드마다 타입 붙은 배열 하나와 hex region 값을 실고 XFDF는 xml:space preserve 아래 선택마다 value 요소 하나를 실어 공백이 데이터로 셈되게 한다
경계는 디스크 위에서 살아 있습니다. FDF는 각 선택을 자체 배열 항목으로 유지하고 XFDF는 각 선택을 별개의 value 요소에 기록하므로 어떤 importer도 추측할 필요가 없습니다
<!-- FDF: 필드마다 타입이 붙은 배열 하나 -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>

<!-- XFDF: 선택마다 <value> 하나 -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
  <fields>
    <field name="options">
      <value>Deep&#xA;Blue</value>
      <value>Red</value>
    </field>
  </fields>
</xfdf>

호출 코드를 쓸 때 두 가지 export 엣지 케이스가 중요합니다. ExportLoadedFormToFDF는 완전한 FDF 본문을 메모리에서 조립한 뒤에야 대상 파일을 만들므로, export할 수 없는 값(문자열이 아닌 것을 담은 배열 같은)은 디스크의 기존 .fdf가 온전한 상태에서 예외를 일으킵니다. 두 번째는 XFDF 자체의 진짜 빈틈입니다. 빈 문자열 export 옵션까지 제공하는 리스트 박스에서 아무것도 선택하지 않은 경우입니다. <value/>는 "아무것도 선택 안 됨"과 "빈 옵션이 선택됨" 어느 쪽이든 되고 포맷에는 세 번째 표기가 없습니다. ExportLoadedFormToXFDF는 그 상황에서 대상 파일을 열기 전에 예외를 일으킵니다. FDF에는 이런 문제가 없습니다. /V []와 /V [()]는 다른 객체이기 때문입니다. 세 exporter 모두 /T 이름이 없는 위젯 전용 말단 노드는 건너뜁니다. 그런 항목은 어떤 importer도 필드로 되돌아 매칭할 수 없습니다

var
  Pdf: THotPDF;
  Written: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    begin
      // 멀티셀렉트 리스트 박스는 /V [(...) (...)]로 기록된다
      Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
      try
        Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
      except
        on E: Exception do
          // 빈 선택과 빈 export 옵션이 겹치면 XFDF는 둘을
          // 구분하지 못하며 기존 .xfdf 파일은 그대로 남는다
          ShowMessage('XFDF export refused: ' + E.Message);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

가져온 선택을 /Opt와 대조하는 방법

HotPDF는 대상이 MultiSelect 플래그가 켜진 choice 필드이고 모든 값이 필드의 /Opt 배열(2원소 옵션 쌍이라면 첫 원소) 안의 export 값과 일치할 때만 들어온 배열을 받아들입니다. 옵션 슬롯은 한 번만 쓸 수 있습니다. 둘 다 b로 export하는 옵션을 가진 리스트는 [<62> <62>]를 서로 다른 두 선택으로 받아들이고 세 번째 b는 거부합니다. 남는 슬롯이 없기 때문입니다. 재구성된 /I는 값이 도착한 순서와 무관하게 /Opt 순서를 따르며, §12.7.4.4가 요구하는 대로 인덱스는 오름차순을 유지합니다. 새 /V와 /I는 떠 있는 객체로 만들어져 모든 값이 통과한 뒤에야 대입되므로, 거부된 값이 반쪽 배열이나 낡은 인덱스를 필드에 남기는 일은 없습니다

대입이 끝나면 필드가 새 appearance 스트림을 받습니다. 손수 만든 importer가 잊기 쉬운 단계이고 증상도 혼란스럽습니다. 뷰어는 /V가 아니라 저장된 /AP를 그리므로 데이터가 바뀌었는데도 화면과 인쇄에는 옛 선택이 남습니다. 리스트 박스는 변경된 필드 집합에도 합류하므로 인자 없는 RecalculateLoadedFormFieldsIncremental 호출이 이 필드를 읽는 계산들만 정확히 다시 돌립니다. FDF에서 16진수 표기로 도착한 값은 저장까지 hex로 유지됩니다. 스칼라 값 하나만 설정하면 된다면 로드된 PDF에서 폼 필드 값 하나 설정하기 글이 그 경로를 다루는데, 이 경로는 설계상 다중 선택을 취급하지 않습니다

HotPDF의 멀티셀렉트 값 import 검증. 대상은 /Ff에 MultiSelect가 켜진 choice 필드여야 하고 들어오는 모든 값은 /Opt export 값과 일치해야 하며 슬롯은 한 번씩만 쓰이고, /I는 /Opt 순서의 오름차순으로 재구성되며, 떠 있는 /V와 /I는 모든 값이 통과한 뒤에야 대입된다
무엇이든 기록되기 전에 들어오는 값마다 필드 옵션과 대조하므로 거부된 값이 반쪽 배열이나 낡은 선택 인덱스를 남길 일이 없습니다

특별 취급이 필요한 입력 패턴이 하나 있습니다. PDF는 평범한 ASCII export 값을 바이트 순서 표식 없는 16진수 문자열로 저장할 수 있습니다. Abr를 <416272>로 담는 식이고, PDF 문자열 본문을 그대로 복사하는 exporter는 디코딩된 텍스트 대신 생 숫자 416272를 요소 텍스트로 갖는 XFDF를 만들어 냅니다. 16진수 인코딩 export 값을 생 숫자로 실은 XFDF는 표기로는 /Opt와 맞을 수 없습니다. 엄격한 비교로는 416272라는 옵션을 찾지 못하고 import가 실패합니다. HotPDF의 XFDF importer는 그런 파일을 받으면 매칭되지 않은 값을 16진수 페이로드로 디코딩해 한 번 더 시도합니다. 이 재시도는 의도적으로 좁습니다. 정확히 일치하는 것이 전혀 없는 값에만 돌아가고, 정확히 일치하는 값은 먼저 예약되므로 구제된 표기가 나중에 정확한 표기로 도착하는 값이 필요로 하는 옵션 슬롯을 가져가는 일은 없으며, 저장되는 /V 항목은 옵션 자체의 PDF 문자열이라 /V와 /I가 같은 선택을 기술합니다. 적정량의 관용입니다. import가 원래 예외를 일으켰을 지점에서만 발동하고 이미 매칭된 값은 결코 재해석되지 않습니다. 매칭 자체는 PDFDocEncoding, 어느 쪽 바이트 순서 표식이든 있는 UTF-16, UTF-8을 아는 단일 Unicode 디코더를 통과하므로, 인코딩을 섞어 쓰는 문서에서도 같은 논리 텍스트는 같은 옵션과 맞습니다

FDF 파싱의 두 함정: hex 종결자와 남의 객체 번호

첫 함정은 어휘론적입니다. << /T (region) /V <416273>>>에서 첫 >는 hex 문자열을 닫고 다음 두 개가 딕셔너리를 닫습니다. >> 쌍만 찾는 스캐너는 딕셔너리를 한 글자 일찍 닫아 필드를 잃습니다. HotPDF의 파일 레벨 FDF importer는 /Fields 배열을 슬라이스하는 동안 hex 문자열이나 리터럴 안에 있는지 추적하고, ImportLoadedInterchangeFromFDF 뒤의 스트림 기반 리더는 파일 전체를 PDF 문법으로 토큰화하므로, 순진한 스캔이 둘로 보는 닫는 괄호를 둘 다 셋으로 봅니다

두 번째 함정은 참조입니다. FDF 파일은 자체 객체 번호를 갖는 작은 PDF 문법 문서이므로(ISO 32000-1 §12.7.7) /V [11 0 R]은 채우려는 PDF의 객체 11이 절대로 아니라 FDF 파일의 객체 11입니다. 대상 문서 기준으로 풀면 우연히 그 번호를 달고 있는 객체가 조용히 끌려 들어옵니다. Delphi 2009 이상 빌드에서 interchange 리더는 FDF를 완전한 객체 그래프로 디코딩하고, 그래프 안에서 참조를 따라가고, 결과 문자열을 인라인 배열처럼 검증하며, FDF에 객체 11이 없으면 예외를 일으킵니다. 파일 레벨 ImportLoadedFormFromFDF는 FDF의 객체 표 없이 각 필드 딕셔너리를 독자적으로 읽습니다. 참조 배열은 문자열 전용 검사에 실패하고 그 필드는 건너뛰어집니다

import 경로 고르기: 건너뛰기, 예외, 롤백

세 import 경로 모두 멀티셀렉트 값을 같은 방식으로 검증합니다. 값이 실패했을 때 무엇이 벌어지는지가 다릅니다. ImportLoadedFormFromFDF는 실패한 필드는 건너뛰고 실제 적용한 필드 수를 돌려주므로, 보낸 항목 수보다 낮은 카운트가 유일한 이상 신호입니다. ImportLoadedFormFromXFDF와 ImportLoadedInterchangeFromFDF는 첫 거부 필드에서 예외를 일으키고 각 호출은 로드된 문서 위에서 하나의 트랜잭션으로 동작합니다. 예외가 메서드를 떠나면 호출이 이미 바꿔 놓은 모든 필드와(interchange 경로라면 모든 주석도) 롤백됩니다. False를 돌려주는 interchange import도 Status와 info 레코드를 비운 채 롤백됩니다

사람이 검토해 돌려보낸 데이터라면 예외를 일으키는 경로 중 하나를 쓰세요. 반만 적용된 주문서는 거부된 것보다 나쁩니다. 저장된 PDF 어디에도 어떤 리스트 박스가 검토본에서 왔고 어떤 것이 옛 값을 유지했는지 나타나지 않기 때문입니다. ImportLoadedFormFromFDF의 건너뛰고-세기 동작은 다른 용도에 맞습니다. 폼의 살짝 다른 리비전용으로 만들어졌을지 모르는 FDF로부터의 최선 노력 프리필입니다

var
  Pdf: THotPDF;
  Source: TMemoryStream;
  Status: AnsiString;
  Info: THPDFFDFInterchangeInfo;
begin
  Pdf := THotPDF.Create(nil);
  Source := TMemoryStream.Create;
  try
    Source.LoadFromFile('order-form-reviewed.fdf');
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    try
      // 필드만 대상. /Opt 밖의 값이나 단일 선택 리스트를
      // 여러 값으로 겨냥하면 예외와 함께 import 전체가 롤백된다
      if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
      begin
        // 가져온 필드를 읽는 계산만 다시 돌린다
        Pdf.RecalculateLoadedFormFieldsIncremental;
        Pdf.SaveLoadedDocument('order-form-filled.pdf');
      end;
    except
      on E: Exception do
        ShowMessage('Import rejected, document unchanged: ' + E.Message);
    end;
  finally
    Source.Free;
    Pdf.Free;
  end;
end;

자체 데이터 저장소를 XFDF 배열 콜백에 묶기

XFDF와 콜백으로 기술할 수 있는 어떤 필드 저장소든 상호 변환하는 HPDFXFDF 유닛은 배열 지원을 별도의 레코드 THPDFXFDFArrayAccess와 HPDFXFDFExportFields, HPDFXFDFImportFields의 추가 오버로드에 실었습니다. 기존 THPDFXFDFAccess 레코드에 슬롯을 덧붙이는 편이 더 짧았겠지만 잘못된 길이었습니다. 호출자들은 흔히 그 레코드를 지역 변수로 선언하고 아는 슬롯만 채우므로, 끝에 새 함수 포인터가 붙으면 라이브러리가 콜백으로 착각할 스택 쓰레기가 그 자리를 차지합니다. 별도 레코드에서는 기존 코드가 자기 레이아웃과 오버로드를 유지하고 그 오버로드들은 내부적으로 전부 nil인 배열 레코드를 넘깁니다

두 import 오버로드는 반복되는 <value> 요소를 다르게 취급합니다. 원래 스칼라 오버로드는 기존 호출자들이 그 관례에 의존하므로 여전히 LF로 붙여 SetFormFieldValue를 호출합니다. 배열을 아는 오버로드는 절대 붙이지 않습니다. 목록을 SetFormFieldValueArray로 넘기고, 콜백이 거부한 단일 값에만 스칼라 setter로 폴백하며, 두 개 이상의 목록을 콜백이 거부하면 예외를 일으킵니다. export에서는 Default(THPDFXFDFArrayAccess)에서 출발해 여러분이 할당하지 않은 슬롯(선택적 SetFormFieldRV 포함)이 nil로 남게 하세요. 값 목록 필드라면 아무것도 선택되지 않은 경우까지 포함해 GetFormFieldValueArray가 True를 돌려주고, 스칼라 콜백이 그 필드를 맡게 하려면 False를 돌려줍니다. 빈 배열은 단일 빈 <value/>로 나가므로 빈 문자열 옵션을 제공하는 저장소는 콜백 안에서 그 사례를 거부해야 합니다. ExportLoadedFormToXFDF가 그렇게 하는 것과 같습니다

uses HPDFXFDF;

// 평범한 함수 포인터, "of object"가 아님: Context가 자체 저장소를 실어 나른다.
// TFormStore와 그 메서드는 여러분 코드이지 HotPDF의 일부가 아니다
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
  out Values: THPDFXFDFValueArray): Boolean;
begin
  Result := TFormStore(Context).IsListField(FieldIndex);
  if Result then
    Values := TFormStore(Context).Selections(FieldIndex);
end;

procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
  Access: THPDFXFDFAccess;
  ArrayAccess: THPDFXFDFArrayAccess;
begin
  Access := MakeStoreAccess(Store);             // 기존 스칼라 바인딩
  ArrayAccess := Default(THPDFXFDFArrayAccess); // 쓰지 않는 슬롯은 모두 nil
  ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
  HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;

지금까지의 이야기는 이미 존재하고 /Ff에 MultiSelect 비트가 이미 켜져 있는 리스트 박스를 가정합니다. choice 필드를 만들고 플래그 비트를 설정하는 것은 로드된 PDF에 ListBox 등 AcroForm 필드 추가하기에서, XFDF의 <annots> 부분을 통해 오가는 주석 마크업은 HotPDF에서 XFDF 주석 import와 export에서 다룹니다. 여기 보인 모든 호출의 메서드 레퍼런스와 자기 폼에 돌려볼 평가판은 HotPDF Delphi PDF 컴포넌트 페이지에 있습니다