Technical Article

Delphi에서 XFA 서식 있는 텍스트 하이퍼링크를 PDF 링크로 플래튼하기

XML Forms Architecture(XFA)는 더 이상 권장되지(deprecated) 않습니다. ISO 32000-1은 §12.7에서 XFA가 PDF 2.0부터 제거되었음을 언급하고 있으며, 최신 PDF 뷰어들은 하나씩 XFA 엔진 지원을 중단하고 있습니다. 하지만 그렇다고 기존 아카이브 파일들이 다 사라진 것은 아닙니다. 지난 20여 년간 정부 제출 서식, 보험 신청서, 은행 명세서 등이 XFA 형식으로 작성되었으며, 해당 파일들은 여전히 이메일 수신함과 기업 문서 처리 라인에 유입되고 있습니다. 이를 렌더링하던 이전 뷰어에서 파일을 열지 못하게 되면, 폼은 '다른 리더기에서 열어주십시오'라는 안내 문구만 있는 빈 페이지로 표시됩니다. 영구적인 해결책은 모든 리더기에서 정상적으로 렌더링할 수 있는 정적 PDF 콘텐츠로 XFA를 플래튼(flatten)하는 것입니다.

플래튼 작업에서 어려운 부분은 입력 필드가 아닙니다. 텍스트 박스나 체크 박스는 AcroForm 위젯으로 쉽게 매핑할 수 있습니다. 진짜 난관은 XFA가 그리기 요소 내의 <exData contentType="text/html"> 블록에 저장하는 서식 있는 텍스트(rich text)입니다. 이 블록은 인라인 스타일과 링크 앵커(anchor)가 포함된 HTML 하위 집합입니다. 이를 정적 페이지에 담으려면 스타일이 적용된 텍스트와 라이브 하이퍼링크를 모두 재현해야 하는데, 많은 라이브러리가 하이퍼링크 변환 단계에서 처리에 실패하곤 합니다.

실제 XFA 서식 있는 텍스트의 구조

exData 본문은 XHTML의 작은 파편입니다. 단락은 <p>이며, 스타일이 가미된 텍스트 영역은 굵기, 모양, 색상 및 크기에 대한 자체 인라인 CSS를 포함하는 <span>으로 표현되고, 하이퍼링크는 텍스트를 감싸는 <a href="...">입니다. 단일 라인에 여러 개의 span이 연속으로 나타나 각각 다른 스타일을 띨 수 있고, 그중 하나가 앵커 링크일 수도 있습니다. 이러한 스타일 정보는 누락시킬 수 없는 중요한 데이터입니다. 법적 경고 문구여서 굵은 빨간색으로 표시된 텍스트가 플래튼 후 일반 텍스트로 보인다면 원본 문서를 훼손하는 결과가 되기 때문입니다.

따라서 플래튼 엔진은 이 블록을 단순한 하나의 문자열로 취급할 수 없습니다. 인라인 구조를 분석하고, 그리기 요소의 기본 글꼴에 span의 인라인 CSS를 적용하여 각 텍스트 구간(run)의 유효 스타일을 결정하며, 행 전체에 걸쳐 이 구간들을 차례로 배치해야 합니다. HotPDF는 이렇게 정렬되는 조각들을 내부적으로 TXFARichRun 레코드로 취급합니다. 레코드는 구간의 텍스트, 적용된 스타일, 측정된 영역 상자(measured box) 및 앵커 링크인 경우 가리키는 Href 주소를 포함합니다.

왼쪽에서 오른쪽으로 런 레이아웃 배치

위치 정렬 단계를 거치면서 서식 있는 텍스트 처리는 텍스트 분석이 아닌 식자(typesetting) 프로세스로 전환됩니다. 각 구간들이 동일한 행을 공유하므로 새로운 구간은 이전 구간이 끝난 바로 그 지점에서 시작해야 합니다. 마크업에는 이러한 위치가 직접 기록되어 있지 않으므로 엔진이 이를 측정해야 합니다. 엔진의 내부 LayoutRichText 루틴은 나중에 텍스트를 그릴 때 사용할 글꼴 메트릭(font metrics)과 동일한 데이터를 기준으로 각 구간을 측정하고, 이전 구간들의 너비 합계를 계산하여 다음 구간의 시작 가로 오프셋을 설정합니다. 첫 번째 구간은 그리기 영역의 시작점에서 출발하고, 두 번째 구간은 첫 번째 구간 너비만큼 지난 곳에서 시작하며, 세 번째 구간은 이전 두 구간의 합산 너비 지점에서 시작하여 행의 정렬을 완성합니다.

이러한 프로세스로 인해 텍스트 측정에 사용되는 글꼴 설정이 매우 중요합니다. 레이아웃 단계에서는 진행 너비(advance)를 측정하고, 실제 렌더링 단계에서는 글리프를 그립니다. 이 두 단계에서 사용하는 글꼴 정보가 다르면 레이아웃 단계에서 계산된 상자 영역이 렌더러가 출력하는 글리프 위치와 일치하지 않게 됩니다. HotPDF는 내부 RunStyleToFontSpec 헬퍼를 통해 각 구간의 유효 스타일을 렌더러의 기본 글꼴 설정인 10포인트 Arial 글꼴 명세에 매핑하여 일치 상태를 유지합니다. 이렇게 하면 측정된 진행 너비와 그려진 텍스트가 정확히 부합하게 되어, 계산된 상자가 사용자가 화면에서 보는 텍스트 영역을 정확히 덮게 됩니다.

// Conceptual shape of one laid-out run. The engine builds an array of these
// internally; you never construct them yourself, but the fields explain how a
// link's hit box is derived from measured geometry rather than from text.
type
  TRichRunInfo = record
    Dx, Dy : Double;       // top-left, relative to the draw-box origin
    W, H   : Double;       // measured run box (width from the layout pass)
    Text   : AnsiString;   // the run's visible characters
    Href   : AnsiString;   // URI target for an <a> run, '' otherwise
  end;

앵커 런에서 PDF 링크 주석으로

완성된 PDF 내의 하이퍼링크는 페이지 콘텐츠 자체에 포함되지 않습니다. 이는 ISO 32000-1 §12.5.6.5에 명시된 별도의 객체인 Link 주석(Link annotation)입니다. 주석은 페이지에서 클릭할 수 있는 영역인 /Rect와 클릭 시 실행될 액션을 가집니다. 외부 링크의 경우 액션은 URI 액션입니다: 대상 주소를 문자열로 저장하는 /S /URI/URI를 의미합니다. 아래에 표시되는 텍스트는 일반 페이지 콘텐츠이며, 주석은 그 위에 오버레이된 보이지 않는 클릭 가능 영역(hot zone)입니다.

플래튼 경로는 이 모델을 그대로 따릅니다. 텍스트 구간에 Href 정보가 포함되어 있는 경우, HotPDF는 먼저 서식 있는 텍스트를 그린 다음 해당 구간의 영역 상자 위에 Link 주석을 구성합니다. 이 주석을 구성하는 공개 API는 페이지 메서드인 AddURILink로, 이는 /URI 액션을 포함하는 /Type /Annot /Subtype /Link 객체를 생성하고 주석 딕셔너리를 반환합니다. 영역 직사각형은 그리기 요소의 로컬 좌표계에서 페이지 좌표계로 변환된 텍스트 구간의 측정된 상자입니다. 그 결과 사용자가 보는 링크 텍스트 위에 정확하게 일치하는 클릭 영역이 생성됩니다.

// The same public API the flatten path uses for each anchor run. It produces
// an ISO 32000-1 12.5.6.5 Link annotation: /Subtype /Link with a /URI action
// over the given rectangle. The optional description fills /Contents so a
// screen reader can announce the target.
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // page-space hit box for the run
  Annot := Pdf.CurrentPage.AddURILink(LinkRect,
    'https://www.example.gov/appeal', 'File an appeal online');
end;

히트 박스가 측정된 너비에서 나와야 하는 이유

화면에 표시되는 텍스트를 기준으로 검색하여 해당 단어를 감싸는 영역을 계산하면 되지 않을까 생각하기 쉽습니다. 하지만 플래튼된 텍스트가 저장되는 구조상의 특성 때문에 이 방식은 작동하지 않습니다. 스타일이 적용된 텍스트 구간은 임베디드 서브셋 글꼴(subset font)을 사용하여 렌더링됩니다. 서브셋 글꼴은 보관하는 글리프의 번호를 재지정하므로, 페이지 콘텐츠 스트림에는 원본 문자 코드가 아닌 16진수 CID 코드가 기록됩니다. 즉, 페이지를 구성하는 바이트는 인간이 읽는 텍스트가 아니며 텍스트 검색도 불가능합니다. 스트림 내에 텍스트 자체가 리터럴 형식으로 존재하지 않기 때문에 앵커 텍스트 캡션을 검색해도 결과를 얻을 수 없습니다.

직사각형 영역을 식별할 수 있는 유일한 대안은 레이아웃 단계에서 산출한 지오메트리 정보뿐입니다. 각 구간의 오프셋과 너비는 글리프 번호 재지정이 일어나기 전에 행을 정렬하는 과정에서 계산되며, 텍스트가 화면에 실제로 위치할 좌표를 정확히 나타냅니다. HotPDF는 텍스트 검색 대신 구간의 물리적 레이아웃 상자에서 곧바로 링크 영역 정보를 가져옵니다. 렌더링 글꼴을 기준으로 측정했기 때문에 서브셋 설정 여부에 영향을 받지 않고 영역이 항상 정확합니다. 인코딩 과정에서 지오메트리는 유지되지만 텍스트 정보는 소실됩니다. 이것이 바로 너비 측정을 기반으로 좌표를 결정해야 하는 이유이며, 텍스트 검색으로 링크 영역을 끼워 맞추려는 시도가 좌표 오차나 링크 누락을 유발하는 이유입니다.

코드에서 플래튼(평탄화) 구동하기

이미 XFA 패킷을 포함하고 있는 PDF의 경우 진입점은 FlattenLoadedXFA입니다. 문서를 로드하고 이 메서드를 호출한 후 결과를 저장합니다. Editable 매개변수는 폼 필드의 처리 방식을 결정합니다: True를 전달하면 작성 가능한 AcroForm 위젯 상태를 유지하고, False를 전달하면 모든 위젯을 읽기 전용으로 설정하여 결과를 고정된 문서로 저장합니다. 어느 쪽이든 스타일이 정의된 텍스트와 링크 주석을 포함하는 서식 있는 그리기 블록이 생성됩니다. 이 함수는 생성된 위젯의 수를 반환합니다.

var
  Pdf: THotPDF;
  Emitted, i: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('xfa_appeal_form.pdf');
    // True keeps fields fillable; False freezes them read-only.
    Emitted := Pdf.FlattenLoadedXFA(True);

    // Anything the engine could not map is reported, not raised.
    for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
      Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);

    Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
    Writeln('Widgets emitted: ', Emitted);
  finally
    Pdf.Free;
  end;
end;

호출 후에는 항상 XFAFlattenWarnings를 검사해야 합니다. 이 리스트는 매 플래튼 작업이 시작될 때 초기화되며, 지원하지 않는 필드 종류, 디코딩할 수 없는 이미지, 유효한 span이 없는 exData 블록 등 엔진이 렌더링을 처리하지 못한 모든 항목에 대한 경고 메세지를 보관합니다. 예외를 발생시키지 않으므로 리스트가 비어 있다면 모든 변환이 성공적으로 완료되었음을 의미하고, 내용이 있다면 확인이 필요한 원본 요소를 식별할 수 있습니다. 로드된 PDF가 아닌 원시 XFA 데이터를 XDP 바이트로 직접 다루는 경우, 자매 메서드인 ApplyXFAAsAcroForm이 이 바이트를 직접 입력받아 동일한 경로로 처리하고 경고 사항을 기록합니다. 반대 기능을 제공하는 AddXFAPacket은 빌드 중인 문서에 XFA 패킷을 포함하는 데 사용됩니다.

리더기에서 결과 확인

Acrobat이나 사용하는 PDF 뷰어에서 플래튼된 파일을 열고 두 가지 사항을 확인해 보십시오. 첫째, 굵은 텍스트는 굵게 표시되고 지정 색상이 정확히 반영되는 등 서식 있는 텍스트가 정상 렌더링되었는지, 그리고 각 span이 겹치거나 영역을 벗어나지 않고 올바른 순서로 배열되어 있는지 확인합니다. 둘째, 하이퍼링크가 활성화 상태인지 확인합니다. 링크 영역에 마우스를 올렸을 때 상태 표시줄에 대상 주소가 표시되고, 클릭 시 URI 액션이 연동되어야 합니다. 뷰어의 주석 검사기를 사용하여 각 링크가 텍스트 위치를 감싸는 정확한 /Rect를 가지고 폼 기반 XFA가 아닌 정적 글리프 위에 올려진 온전한 /Link 주석인지 검증하십시오. 스타일이 가미된 정적 텍스트와 정확한 좌표의 실제 Link 주석이 결합되어, 더 이상 XFA 엔진을 지원하지 않는 환경에서도 플래튼된 문서를 영구적으로 사용할 수 있게 보장합니다.

서식 있는 텍스트 주변의 텍스트 박스, 체크 박스, 초이스 리스트 등 필드 자체를 플래튼하는 프로세스는 XFA 폼을 AcroForm 위젯으로 플래튼하는 방법 가이드에서 다룹니다. 변환 엔진이 자동 생성하는 링크 외에 직접 Link 주석을 빌드하고 배치하는 방법은 HotPDF에서 PDF 주석 작업하기를 참조하십시오. 두 기능 모두 Delphi 및 C++Builder용 HotPDF 컴포넌트와 함께 제공되는 동일한 주석 및 폼 모델을 기반으로 합니다.