기술 문서

Delphi에서 정적 XFA 양식을 AcroForm 필드로 변환

PDFium Component는 FPdfDocumentXfaFlatten 유닛의 FlattenPdfXfaToAcroForm을 통해 Delphi에서 정적 XFA 양식을 평범한 AcroForm 필드로 변환합니다. 이 함수는 XFA 템플릿과 datasets 패킷을 읽고, 절대 위치로 배치된 모든 필드를 자체 appearance 스트림을 가진 AcroForm 터미널 필드로 매핑하며, 결과를 증분 리비전으로 덧붙여, 변환된 PDF가 XFA를 전혀 지원하지 않는 뷰어에서도 평범한 양식으로 열립니다

동기는 보통 로드맵 항목이 아니라 지원 티켓입니다. 정부나 보험 양식이 XFA로 저장하는 디자이너에서 만들어져 한 데스크톱 리더에서는 동작하지만, 다른 곳에서는 더 새로운 뷰어가 필요하다는 말만 있는 백지 페이지로 열립니다. XFA는 PDF 2.0(ISO 32000-2)에서 폐기 예정으로 분류됐고, 브라우저와 모바일 뷰어는 구현한 적이 없으며, PDFium 자체도 V8이 켜진 빌드에서만 XFA를 렌더합니다. 레이아웃이 설계 시점에 고정되는 정적 XFA 양식은 AcroForm 필드로 재건하기에 충분한 지오메트리를 템플릿에 갖고 있고, 이 컨버터가 다루는 것이 정확히 그 조각입니다

FlattenPdfXfaToAcroForm은 실제로 무엇을 만들어 내나

FlattenPdfXfaToAcroForm은 소스의 변하지 않은 사본에 이어붙인 바이트 수준의 AcroForm 리비전을 만들어 내며, 원본 파일을 다시 쓰는 일은 결코 없습니다. PDFium은 폼 필드를 만드는 공개 API가 없으므로, 컴포넌트는 이 일에 PDFium을 전혀 거치지 않습니다. 자체 객체 리더로 활성 리비전을 읽고, 필드 딕셔너리와 위젯 어노테이션, appearance XObject, /Helv 폰트 리소스를 raw PDF 객체로 만들어, ISO 32000-1 §7.5.6의 의미로 성긴 증분 업데이트를 직렬화합니다

새 /AcroForm 딕셔너리는 /Fields, /DA, /DR을 실고 /XFA 항목은 없습니다. 소스가 자기 AcroForm을 간접 객체로 유지하면 그 객체는 제자리에서 다시 쓰이고, 인라인 딕셔너리는 카탈로그 안에서 교체됩니다. 어느 쪽이든 활성 리비전은 더 이상 XFA 패킷을 가리키지 않으므로, 두 모델을 모두 이해하는 뷰어는 XFA 양식 배치를 시도하는 대신 AcroForm 필드를 엽니다. 원본 XFA 바이트는 이전 리비전에 남아 있습니다. 증분 업데이트라는 것이 그런 것이지만, 현재 리비전의 어떤 것도 그것들을 참조하지 않습니다

Delphi용 PDFium Component의 정적 XFA에서 AcroForm 변환 파이프라인: FlattenPdfXfaToAcroForm이 템플릿과 datasets 패킷을 읽고, 절대 위치 필드를 필드 딕셔너리와 위젯으로 매핑하며, /XFA 항목 없이 AcroForm을 성긴 증분 리비전으로 덧붙임
FlattenPdfXfaToAcroForm은 원본 바이트를 절대 건드리지 않습니다. 변환된 필드, 위젯, appearance 스트림은 /AcroForm이 단지 XFA 패킷을 가리키기를 멈추는 리비전으로 덧붙여집니다
uses
  System.SysUtils, System.Classes, System.IOUtils, FPdfDocumentXfaFlatten;

procedure ConvertXfaForm(const SourceFile, TargetFile: string);
var
  Source: TFileStream;
  Options: TPdfXfaFlattenOptions;
  OutputPdf: TBytes;
  Report: TPdfXfaFlattenReport;
  Fields: TPdfXfaFlattenFields;
begin
  Options := TPdfXfaFlattenOptions.Default;
  Options.MaxFields := 1024;   // 기본값은 512, 허용 범위는 1..4096

  Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyWrite);
  try
    if not FlattenPdfXfaToAcroForm(Source, Options, OutputPdf, Report, Fields) then
      raise Exception.Create('XFA conversion failed: ' + Report.ErrorText);
  finally
    Source.Free;
  end;

  TFile.WriteAllBytes(TargetFile, OutputPdf);
  Writeln(Format('%d template fields, %d bound to datasets, %d written on %d pages',
    [Report.TemplateFields, Report.DatasetBound, Report.FieldsWritten, Report.PagesWritten]));
end;

이 함수는 거부되거나 형태가 잘못된 입력에 대해 False를 반환하고 Report.ErrorText를 채우며, 나가는 길에 소스 스트림 위치를 복원합니다. 취소는 전파되는 유일한 예외 경로입니다. Options.CancellationToken이 발화하면 보고서가 채워진 뒤 EPdfOperationCancelled가 다시 던져집니다. TPdfXfaFlattenReport는 SourceSha256과 OutputSha256도 실어, 배치 작업이 어느 입력이 어느 변환 파일을 만들었는지 증명해야 할 때 요긴합니다. TPdfXfaFlattenOptions.Default는 8 MiB 패킷 예산, 512 필드, 512 MiB 출력 천장을 설정하며, 소스는 암호화되어서는 안 됩니다

템플릿과 datasets 패킷 읽기

PDFium Component는 ISO 32000-1 §12.7.8이 /XFA 항목에 허용하는 두 레이아웃을 모두 받습니다. 패킷 이름과 스트림 참조를 번갈아 놓는 배열 형태와, 하나의 스트림이 <xdp:xdp> 문서 전체를 담는 단일 스트림 XDP 형태입니다. 패킷 스트림은 압축 없이도 되고 predictor 없는 /FlateDecode여도 되며, 압축 해제는 MaxPacketBytes로 한도가 묶입니다

단일 스트림 사례는 순진한 XML 처리가 깨지는 곳입니다. 디자이너는 흔히 템플릿이 접두어 없이 앉아 있는 동안 <xfa:datasets>에 네임스페이스 접두어를 붙이므로, 리터럴 태그 이름으로 자르면 하나의 패킷은 찾고 다른 하나는 놓칩니다. 컨버터는 여는 태그와 닫는 태그가 전체 접두어 이름으로 짝을 이루는지 별도 검사한 뒤, 요소와 속성 이름을 로컬 이름으로 대조합니다. 변환 말고 패킷만 들여다봐야 한다면, PDFium VCL로 XFA 양식 감지와 패킷 추출하기 가이드가 그 읽기 전용 경로를 다룹니다

필드 값은 두 곳에서 옵니다. 템플릿의 <value> 요소가 기본값을 주고, datasets 패킷이 필드 이름으로 묶인 채워진 데이터를 줍니다. 실제 양식에서는 필드 이름이 반복되므로, 바인더는 이름 붙은 <subform> 조상들의 사슬을 데이터의 조상 경로와 비교해 가장 긴 충돌 없는 일치를 택합니다. 서로 다른 subform 아래 두 리프 필드가 이름을 공유하면 AcroForm /T는 SubformPath.Name이 되어 두 필드가 변환 후에도 구별됩니다

choiceList에는 왜 명시적 Combo 플래그가 필요할까

XFA와 PDF는 선택 필드의 기본값이 정반대이므로, 플래그를 하나도 복사하지 않는 컨버터는 모든 드롭다운을 목록 상자로 만듭니다. 기본 open="userControl"이나 open="onEntry"인 XFA <choiceList>는 드롭다운입니다. 플래그 없는 PDF /FT /Ch 필드는 스크롤되는 목록 상자이며, Combo 플래그만(ISO 32000-1 표 230의 비트 위치 18, 값 131072, PDFium이 FPDF_FORMFLAG_CHOICE_COMBO로 부르는 같은 비트) 그것을 콤보 박스로 만듭니다. 따라서 컨버터는 드롭다운 사례에 Combo 플래그를 설정하고, 목록을 영구히 열어 두는 open="always"나 open="multiSelect"일 때만 플래그를 비워 둡니다. 3.181.1 이전 빌드는 Combo와 Pushbutton 값을 뒤바꾸고 비표준 open="on"을 찾았으므로, 그것들로 평탄화된 양식은 다시 돌려 볼 가치가 있습니다

PDFium Component 정적 XFA에서 AcroForm 변환의 선택 필드 플래그 판정: userControl이나 onEntry의 open 속성은 Combo 플래그, 비트 18 값 131072를 설정해 드롭다운 콤보 박스를 만들고, open always나 multiSelect는 영구히 열린 목록 상자를 위해 플래그를 비워 둠
Combo 플래그가 없으면 XFA 드롭다운은 스크롤되는 목록 상자로 평탄화됩니다. 그래서 컨버터는 open 속성을 읽고 드롭다운 사례에만 비트 18을 설정합니다

UI 매핑의 나머지는 ISO 32000-1 §12.7.4의 필드 플래그 표를 따릅니다:

  • textEdit, numericEdit, dateTimeEdit는 /FT /Tx가 됩니다. 여러 줄 텍스트 필드는 Multiline 플래그(/Ff 4096, 표 228)와 줄을 나눈 appearance 스트림을 받습니다
  • passwordEdit는 Password 플래그(/Ff 8192)를 달고 UTF-16 코드 유닛이 아니라 유니코드 스칼라마다 별표 하나를 그리는 appearance를 가진 /FT /Tx가 됩니다
  • checkButton은 묶인 데이터가 1이면 값이 /Yes이고 그 외에는 /Off인 /FT /Btn 체크박스가 됩니다
  • <exclGroup>은 /V, /DV, /Opt, /Kids를 가진 하나의 /FT /Btn /Ff 32768 라디오 부모(표 226)가 되고, 멤버마다 자체 /AS와 두 상태 /AP를 가진 자식 위젯이 하나씩 따릅니다
  • imageEdit는 이미지를 칠하는 appearance를 가진 푸시버튼이 됩니다. 베이스라인 JPEG는 /DCTDecode 아래 그대로 임베드되고, PNG는 디코딩되어 PNG가 알파나 tRNS 투명을 갖고 있으면 추가 /SMask와 함께 /FlateDecode 픽셀로 재인코딩됩니다

줄바꿈을 포함해 평범한 출력 가능 ASCII가 아닌 값은 <FEFF...> UTF-16BE 문자열로 쓰이므로, 일본어 이름이나 두 줄 주소도 여행을 살아남습니다. Fields 출력은 쓰인 것을 거울처럼 보여 주므로 편리한 감사 추적이 됩니다

procedure DumpConvertedFields(const Fields: TPdfXfaFlattenFields);
const
  KindNames: array[TPdfXfaFlattenFieldKind] of string = (
    'text', 'check', 'choice', 'image', 'numeric', 'dateTime', 'password', 'radio');
var
  I: Integer;
  F: TPdfXfaFlattenField;
begin
  for I := 0 to High(Fields) do
  begin
    F := Fields[I];
    Writeln(Format('%s.%s [%s] page %d at %.1f,%.1f size %.1f x %.1f',
      [F.SubformPath, F.Name, KindNames[F.Kind], F.PageIndex + 1, F.X, F.Y, F.W, F.H]));
    case F.Kind of
      fxfkChoice:
        Writeln(Format('  %d items, list box: %s', [Length(F.Choices), BoolToStr(F.ListBox, True)]));
      fxfkRadio:
        Writeln(Format('  %d radio widgets, value "%s"', [Length(F.RadioWidgets), F.Value]));
      fxfkImage:
        Writeln(Format('  %d image bytes', [Length(F.ImageData)]));
    else
      Writeln('  value "' + F.Value + '"');
    end;
  end;
end;

지오메트리, 측정 단위, 페이지 라우팅

XFA 지오메트리는 각 필드를 자기 페이지의 MediaBox 높이에 대해 뒤집어 PDF 사용자 공간으로 변환됩니다. XFA는 페이지 위에서 아래로 재지만 PDF는 아래에서 위로 재기 때문입니다. 중첩된 <subform> 요소들의 오프셋은 템플릿을 내려가며 누적되고, 모든 필드는 양의 폭과 높이로 끝나야 하며 그렇지 않으면 변환이 거부됩니다

Delphi용 PDFium Component의 XFA에서 PDF 지오메트리 변환: XFA 템플릿에서 위쪽 모서리부터 아래로 잰 필드 사각형은 자기 페이지의 MediaBox 높이에 대해 뒤집혀 y 좌표가 아래에서 위로 자라는 PDF 사용자 공간으로 감
아래로 가는 XFA 템플릿과 위로 가는 PDF 사용자 공간은 MediaBox 높이에 대한 하나의 뺄셈에서 만나며, 이것이 변환된 모든 위젯을 설계된 위치에 놓습니다

측정은 실제 디자이너가 내보내는 단위를 받습니다. 맨 숫자와 pt, 그리고 in(72포인트), mm(72 / 25.4), cm(72 / 2.54), pc(12), px(0.75), mp(0.001, 밀리포인트)입니다. 파싱은 로캘 독립적이므로, 독일어 Windows 설치에서도 8.5in이 엉터리가 되지 않습니다. 알 수 없는 단위 접미어는 포인트로 읽히는 대신 거부되는데, 포인트로 읽히면 필드가 조용히 잘못된 자리에 놓입니다

페이지 라우팅은 <pageSet>을 따릅니다. 형제 <pageArea> 요소들은 선언 순서대로 1부터 번호가 매겨져 자기 아래 중첩된 필드들을 청구하고, <pageArea> 조상이 없는 필드는 1페이지에 남습니다. 각 페이지는 자체 /Annots 업데이트를 받습니다. PDF의 마지막 페이지를 넘어 라우팅된 필드는 1페이지에 떨어진 위젯이 아니라 오류입니다

컨버터는 어느 XFA 양식을 거부하나

컨버터는 XFA 레이아웃 엔진을 돌려야만 알 수 있는 레이아웃의 양식은 무엇이든 거부합니다. 즉 <subform>이나 <exclGroup>의 layout 속성이 position 이외의 값(동적 양식이 빚어지는 흐르는 tb, lr-tb 등), 어떤 <script> 요소, 위에 나열한 일곱 밖의 어떤 UI 종류입니다. 실행 시점에 페이지를 늘리는 동적 양식은 별개의 도구 체인 소관이며, PDFium Component의 동적 XFA 재페이지매김과 페이지 수 글이 그 라이브 레이아웃에 무엇이 걸리는지 보여 줍니다

거부는 의도적인 것입니다. 흐르는 subform에는 고정 좌표가 없으므로 어떤 "변환"이든 위치를 지어내야 하고, 스크립트 양식은 흔히 정적 AcroForm이 재현할 수 없는 총계를 계산하거나 가시성을 토글합니다. 완전해 보이지만 계산된 총계를 조용히 잃은 PDF를 만드는 것은 Report.ErrorText의 명확한 오류보다 나쁩니다. XFA 3.3 사양은 이 서브셋보다 훨씬 많은 것을 정의하며, 컨버터는 그렇지 않은 척하지 않습니다

AcroForm 리비전을 쓰고 검증할 때의 함정

라이터를 만드는 동안 문제 둘이 드러났고, 둘 다 손으로 증분 업데이트를 만드는 누구에게나 해당됩니다. 첫째, 카탈로그는 AcroForm이 다른 곳에서 다시 쓰이는 간접 객체라 바이트가 변하지 않을 때조차 항상 업데이트 집합의 일부여야 합니다. 성긴 업데이트 라이터는 자기가 실은 객체들에서 트레일러 /Root를 유도하므로, 루트 객체 없는 업데이트는 뷰어가 닻을 내릴 수 없는 리비전을 만듭니다

둘째는 검증에 관한 것입니다. PDFium 래퍼로 변환된 파일을 적재하면 문서가 /AcroForm을 갖는 순간 폼 환경 초기화가 발동되는데, 테스트 설비의 호스트 pdfium.dll에는 FORM_GetTextDirection 익스포트가 없어서, 필드 하나 검사되기 전에 초기화가 실패했습니다. 그래서 읽기 검사는 폼 층을 건드리지 않고 활성 리비전을 파싱하는 컴포넌트 객체 리더를 씁니다

uses
  System.SysUtils, System.Classes, FPdfCompress;

function HasActiveAcroForm(const Pdf: TBytes): Boolean;
var
  Stream: TBytesStream;
  Objects: TPdfActiveObjects;
  Revision: TPdfSparseRevisionInfo;
  ErrorText: string;
begin
  Stream := TBytesStream.Create(Pdf);
  try
    if not ReadPdfActiveObjects(Stream, TPdfActiveObjectReadOptions.Default,
      Objects, Revision, ErrorText) then
      raise Exception.Create(ErrorText);
    // 활성 카탈로그는 AcroForm을 참조해야 합니다. 컨버터가 쓴 인라인
    // AcroForm에는 /XFA 항목이 없습니다
    Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
  finally
    Stream.Free;
  end;
end;

appearance 스트림은 의도적으로 단순합니다. 텍스트와 선택 값에는 10포인트 Helvetica, 체크박스에는 선으로 그린 사각형, 선택된 라디오 상태에는 채워진 안쪽 사각형입니다. 변환된 양식을 어디서든 읽을 수 있게 하지만 원본 XFA 렌더링의 픽셀 사본은 아니므로, 템플릿에서 정의된 폰트와 색, 보더는 넘어오지 않습니다. 양식이 디자이너 출력과 동일해 보여야 한다면 프로덕션 파이프라인을 전환하기 전에 XFA 빌드로 한 번 렌더해 비교하십시오

FAQ

FlattenPdfXfaToAcroForm은 PDFium의 V8이나 XFA 빌드가 필요한가요?

아니요. FlattenPdfXfaToAcroForm은 PDFium Component 코드로 패킷을 파싱하고 리비전을 쓰므로, 표준 pdfium.dll로 동작하며 어떤 XFA 스크립트도 실행하지 않습니다

컨버터는 암호화된 XFA 문서를 처리할 수 있나요?

직접은 아닙니다. 암호화된 소스는 오류로 거부되므로, 문서를 먼저 복호화하고 복호화된 사본을 변환하십시오

변환된 필드는 여전히 편집 가능한가요?

네. 출력에는 값과 appearance 스트림을 가진 실제 AcroForm 텍스트, 체크박스, 라디오, 선택, 푸시버튼 필드가 들어 있으므로, 사용자는 AcroForm을 지원하는 어떤 뷰어에서든 양식 채우기를 계속할 수 있습니다

정적 XFA 변환, XFA 패킷 추출, V8이 켜진 XFA 런타임은 모두 Delphi 및 C++Builder용 PDFium Component의 일부이며, 이것은 Delphi, C++Builder, Lazarus 프로젝트를 위해 PDFium을 감쌉니다