HotPDF는 필드 값을 끝까지 배열로 유지하면서 멀티셀렉트 리스트 박스 값을 FDF와 XFDF로 왕복시킵니다. v2.755.0부터 ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF, ExportLoadedFormToXFDF는 선택된 각 옵션을 별도의 FDF 문자열이나 XFDF <value> 요소로 기록하고, 짝을 이루는 import 메서드들은 무언가를 바꾸기 전에 모든 값을 필드 옵션과 대조하고 /I 선택 인덱스를 재구성합니다. 중간에 하나의 문자열로 뭉개지는 것은 없습니다
이 수정이 잡는 실패는 재현하기 쉽습니다. 제품 옵션의 멀티셀렉트 리스트 박스가 있는 주문 폼에서 사용자가 두 개를 고르게 하고, 백오피스 시스템용으로 폼 데이터를 export한 다음, 편집된 파일을 PDF로 다시 import해 보세요. 이 변경 이전에는 리스트 박스가 비어 있거나 잘못된 채로 돌아왔습니다. 이유는 export 값 하나에 줄바꿈이 들어 있었고, 옛 경로는 선택들을 줄로 구분된 단일 문자열로 평탄화했기 때문입니다. 그 문자열에서 여러 선택을 되찾아내는 건 원래 신뢰할 수 없었고, export 값 자체에 줄바꿈이 있으면 아예 동작할 수 없습니다
멀티셀렉트 값을 줄바꿈으로 합치면 왕복이 깨지는 이유는?
선택들을 하나의 문자열로 합치면 값 사이의 경계가 버려지고, 값은 그 구분자를 담을 수도 있으므로 어떤 importer도 문자열을 정확히 되돌려 나눌 수 없습니다. ISO 32000-1 §12.7.4.4는 choice 필드의 /V 엔트리로 단일 텍스트 문자열이나 텍스트 문자열 배열을 모두 허용하고, MultiSelect 플래그(/Ff의 비트 22)를 가진 리스트 박스는 옵션을 둘 이상 고르는 순간 배열 형태를 씁니다. 같은 절은 /I를 오름차순 0 기반 옵션 인덱스 배열로 정의하는데, 뷰어들은 우연히 export 값을 공유하는 두 옵션을 구분할 때 이걸 씁니다. HotPDF의 스칼라 getter GetFormFieldValue는 문자열 형태만 읽으므로, 배열을 이것에 통과시키면 export가 빈 문자열로 퇴화했고, 옛 XFDF import는 반복되는 <value> 요소를 LF로 합쳤습니다. 옵션이 Deep, 줄바꿈, Blue로 export된 걸 상상해 보세요. 합친 뒤 Deep\nBlue\nRed는 선택 둘일 수도 셋일 수도 있는데, 파일은 어느 쪽인지 알 방법을 주지 않습니다. 수정은 왕복 한가운데에서 스칼라를 쓰는 것을 아예 끝낸 것입니다
export된 FDF와 XFDF 파일에는 무엇이 들어갈까?
HotPDF는 멀티셀렉트 값을 FDF에서는 타입 배열로, XFDF에서는 선택 하나당 <value> 요소 하나로 기록하므로, 경계는 디스크 위에서 계속 보입니다. FDF에서 각 항목은 소스 PDF에서 가졌던 표기를 유지합니다. 16진수 문자열은 hex로 나가고 literal string은 CR과 LF를 \r과 \n으로 바꾸는 단일 헬퍼가 이스케이프합니다. XFDF에서 루트는 ISO 19444-1이 요구하는 대로 xml:space="preserve"를 실는데, 이는 텍스트 요소 안의 어떤 공백이든 데이터로 셉니다. 그래서 HotPDF는 각 <value>의 시작 태그, 이스케이프된 텍스트, 끝 태그를 한 덩어리로 쓰고, 들여쓰기는 요소 밖에 두며, CR, LF, TAB을 문자 참조로 인코딩해 줄 끝 정규화를 적용하는 XML 파서가 원본 바이트를 바꿀 수 없게 합니다
<!-- 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
Blue</value>
<value>Red</value>
</field>
</fields>
</xfdf>
호출 코드를 쓰기 전에 알아 둘 export 엣지 케이스 둘입니다. 첫째, ExportLoadedFormToFDF는 대상 파일을 만들기 전에 FDF body 전체를 메모리에서 조립합니다(2.755.1에서 수정). 그래서 문자열 외의 것을 담은 배열 같은 export할 수 없는 값은 기존 파일을 잘라내지 않은 채 예외를 일으킵니다. 둘째, 빈 문자열 export 값을 함께 제공하는 리스트 박스의 빈 선택은 XFDF에서 모호합니다. <value/>가 아무것도 선택되지 않음을 뜻할 수도 빈 옵션이 선택됐음을 뜻할 수도 있기 때문입니다. ExportLoadedFormToXFDF는 그 경우 추측하는 대신 예외를 일으키고, 대상 파일을 열기 전에 일으킵니다. FDF에는 그런 모호함이 없습니다. /V []와 /V [()]는 구별되니까요. 두 FDF exporter 모두 /T 이름이 없는 위젯 전용 단말도 건너뛰는데, XFDF exporter와 맞추기 위해서입니다. 어떤 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;
HotPDF는 import에서 멀티셀렉트 값을 어떻게 검증할까?
HotPDF는 대상이 MultiSelect 플래그가 켜진 choice 필드이고 배열의 모든 값이 필드의 /Opt 배열에 있는 export 값과 일치할 때만 import된 배열을 받아들입니다. 각 옵션 슬롯은 한 번 쓰일 수 있으므로, export 값 b를 공유하는 옵션 둘을 가진 리스트는 [<62> <62>]를 서로 다른 선택 둘로 받아들이고 세 번째 b는 거부합니다. 재구성된 /I는 들어온 값의 순서가 아니라 /Opt 순서를 따릅니다. §12.7.4.4가 오름차순 인덱스를 요구하기 때문입니다. HotPDF는 새 /V와 /I를 분리된 오브젝트로 만들고 모든 값이 검증을 통과한 뒤에야 대입합니다. 그래서 거부된 값이 반쯤 찬 배열이나 오래된 인덱스를 남겨 두는 일은 없습니다. 사본은 공유 조상 배열이 아니라 import되는 필드에 기록되고, FDF에서 도착한 hex 표기는 저장까지 hex로 남으며, 리스트 박스에 계산이 의존하는 필드들은 재계산 대상으로 표시됩니다. 단일 값만 설정하면 되는 경우 불러온 PDF에서 폼 필드 값 하나 설정하기가 스칼라 경로를 거치는데, 이 경로는 설계상 다중 선택을 다루지 않습니다
일부 다른 도구는 plain ASCII export 값을 BOM 없는 hex string으로 기록하기도 합니다. 예컨대 <416272> 같이요. 그리고 그 hex 숫자들을 텍스트로 써서 XFDF를 export합니다. 돌아오는 길의 엄격한 literal 비교는 실패하고 import가 중단됩니다. v2.755.1은 재시도를 하나 추가합니다. 어떤 값이 어떤 옵션과도 일치하지 않으면 HPDFHexSpellingText가 그 텍스트를 hex 페이로드로 디코딩하고 결과를 다시 비교합니다. 재시도는 원래라면 예외를 일으켰을 입력에만 적용되므로, 이미 일치한 값을 바꾸는 일은 없습니다. 같은 릴리스에서 스칼라 경로와 배열 경로도 같은 유니코드 디코더를 쓰게 됐습니다. PDFDocEncoding과 어느 쪽 BOM이든 가진 UTF-16, UTF-8을 이해하는 디코더입니다. 그 전에는 인코딩을 섞은 문서에서 하나의 논리적 값이 한 경로에서는 일치하고 다른 경로에서는 실패할 수 있었습니다
유효한 FDF 파일이 파싱 중에 필드를 잃을 수 있는 이유는?
16진수 문자열을 추적하지 않는 FDF 스캐너는 hex 값이 딕셔너리 종결자 바로 옆에서 끝날 때 필드 딕셔너리를 반으로 자를 수 있습니다. << /T (region) /V <416273>>에서 첫 번째 >는 hex 문자열을 닫지만, 순진한 스캐너는 그것과 다음 >를 함께 딕셔너리의 끝으로 읽고 필드를 조용히 버립니다. 파일 수준 FDF importer는 원래 hex 문자열 안에 있는지 추적해 왔고, 2.755.1에서 ImportLoadedInterchangeFromFDF 뒤의 배열 및 딕셔너리 스캐너도 같게 됐습니다. 두 번째 문제는 간접 참조입니다. FDF 파일은 자체 오브젝트 번호를 가진 작은 PDF 문법 문서이므로(ISO 32000-1 §12.7.7), /V [11 0 R] 같은 값은 당신이 채우는 PDF의 오브젝트 11이 아니라 FDF 파일의 오브젝트 11을 가리킵니다. HotPDF의 단순화된 FDF 파서는 파일 안의 참조를 해석하지 않으므로, 대상 문서에서 우연히 오브젝트 11이 무엇이든 읽는 대신 그런 배열을 거부합니다
파일, 스트림, XFDF import는 오류를 다르게 보고합니다
세 import 경로는 같게 검증하지만 실패를 다르게 보고하므로, 의도를 갖고 하나를 고를 가치가 있습니다. ImportLoadedFormFromFDF는 검증에 실패한 필드를 건너뛰고 실제 적용한 필드 수를 반환합니다. 기대보다 낮은 개수가 문제의 유일한 신호입니다. ImportLoadedInterchangeFromFDF와 ImportLoadedFormFromXFDF는 첫 거부된 필드에서 예외를 일으킵니다. 각 필드는 제 몫으로 커밋되므로, 예외 전에 처리된 필드들은 새 값을 유지합니다. 이중 어느 것도 교환 파일 전체에 대한 트랜잭션으로 취급하지 마세요. 전부 아니면 전무가 필요하면 예외가 발생했을 때 저장하는 대신 불러온 문서를 버리세요
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 밖의 값이나 멀티셀렉트가 아닌 대상은 예외를 일으킵니다
if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
Pdf.SaveLoadedDocument('order-form-filled.pdf');
except
on E: Exception do
ShowMessage('Import rejected, nothing saved: ' + E.Message);
end;
finally
Source.Free;
Pdf.Free;
end;
end;
기존 호출자를 깨지 않고 XFDF 콜백 확장하기
하위 수준 XFDF 유닛의 배열 지원은 별도 레코드인 THPDFXFDFArrayAccess와 HPDFXFDFExportFields, HPDFXFDFImportFields의 새 오버로드에 살며, 기존 THPDFXFDFAccess 레코드 끝에 추가된 필드에 살지 않습니다. 이유는 바이너리 호환성입니다. 지역 변수로 THPDFXFDFAccess를 채우는 코드는 아는 슬롯만 설정하고 나머지를 지우지 않는 경우가 많으므로, 그 레코드에 추가된 새 함수 포인터는 스택 찌꺼기를 담게 되고 라이브러리는 그것을 진짜 콜백으로 치게 됩니다. 별도 레코드로는 기존 호출자가 옛 레이아웃과 옛 오버로드를 유지하고, 그 오버로드는 내부적으로 전부 nil인 배열 레코드를 넘깁니다. 원래 스칼라 import 오버로드는 호환성을 위해 반복 값을 여전히 LF로 합치고, 배열을 아는 오버로드만 그것들을 갈라 놓습니다. 자기 데이터 저장소를 바인딩할 때는 Default(THPDFXFDFArrayAccess)에서 시작하세요. 목록 값 필드에는 아무것도 선택되지 않은 것까지 GetFormFieldValueArray에서 True를 반환하고, 스칼라 콜백으로 폴백하려면 False를 반환합니다
uses HPDFXFDF;
// plain function pointer, "of object" 아님: Context가 당신의 저장소를 나름
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를 보세요. 전체 API 참조와 체험판 다운로드는 HotPDF Delphi PDF component 페이지에 있습니다