기술 문서

HotPDF 헤드리스 렌더러의 위젯 필드 상속과 모양 합성

필드 값은 있는데 모양 스트림이 없는 PDF 양식은 Linux 렌더 작업에서 빈 상자나 잘못된 글꼴과 정렬, 또는 "Widget field requires a name field type"이라는 EHPDFHeadlessRenderError로 나올 수 있습니다. HotPDF의 헤드리스 렌더러는 텍스트와 버튼, 선택 위젯에 임시 모양을 그리며, 필드 트리를 통해 /FT와 /V, /DA, /Q를 상속합니다

위젯이 필드 설정을 지니는 자리

골칫거리의 대부분은 필드와 그 위젯의 분리에서 시작합니다. 필드 사전은 양식 컨트롤의 논리적 부분을 쥡니다. 타입과 값, 플래그, 기본 모양입니다. 위젯 주석은 페이지가 필요로 하는 것을 쥡니다. /Rect와 테두리, 자기 페이지를 향한 포인터입니다. 위젯 하나를 지닌 필드는 보통 하나의 사전으로 합쳐집니다. 같은 값이 여러 페이지에 나타나야 할 때는 위젯들이 필드의 자식이 되고 종종 형상과 /Parent 참조 외에는 아무것도 실지 않습니다. ISO 32000-1 §12.7.3.1은 /FT와 /Ff, /V, /DV를 그 사슬을 따라 상속 가능하게 만들고(표 220), §12.7.3.3은 표 222의 상속 집합에 가변 텍스트 속성 두 개만 더합니다. 필수이지만 조상에게서 올 수 있는 /DA와 /Q입니다. 대화형 양식 사전(표 218)은 /DA와 /Q의 문서 전역 기본값과, /DA 글꼴 이름이 조회되는 기본 자원 사전인 /DR을 공급합니다. /DR은 표 222에 없습니다. 양식 전체에 속한 것입니다. 위젯 사전만 읽는 렌더러는 아래 파일에서 타입과 값, /DA를 찾지 못하고 제 기본 글꼴로 물러섭니다

1 0 obj  % 카탈로그
<< /Type /Catalog /Pages 2 0 R
   /AcroForm << /Fields [10 0 R] /DA (/Helv 0 Tf 0 g) /Q 0
                /DR << /Font << /Helv 20 0 R /Cour 21 0 R >> >> >> >>
endobj
10 0 obj  % 말단 필드: 타입과 값, 모양 기본값이 여기 삽니다
<< /FT /Tx /T (invoice_no) /V (INV-2026-0417)
   /DA (/Cour 10 Tf 0 0 0.6 rg) /Q 2
   /Kids [11 0 R 12 0 R] >>
endobj
11 0 obj  % 페이지 1의 위젯: 형상뿐, 꼭짓점은 오른쪽 위부터
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
   /Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj  % 페이지 2의 위젯: 형상뿐
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
   /Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj

HotPDF는 각 필드 항목을 위젯에서 찾고, 없으면 /Parent를 한 사전씩 올라갑니다. 가장 가까운 값이 이기고, 사슬이 다하면 /DA와 /Q만 AcroForm 사전으로 물러섭니다. 위 파일의 기대: 두 위젯 모두 INV-2026-0417을 10포인트 Courier로, 진한 파란색, 오른쪽 정렬로 보여 줍니다. 객체 10이 제 /DA를 지니므로 문서 전역 /Helv 0 Tf는 끝까지 무대에 오르지 않습니다. 필드가 대신 12포인트 검은색 Helvetica로 나온다면 눈에 띄는 용의자는 Linux 박스에 없는 Courier 글꼴입니다. 크기가 결정적 증거입니다. 위젯에도 조상 어디에도 AcroForm 사전에도 /DA가 나오지 않을 때 HotPDF가 그리는 것이 바로 그것입니다. 그러니 위젯을 덤프하고 /Parent를 손으로 따라가 프로듀서가 /DA를 어디에 두었는지 찾으십시오. 위젯이 절대 되돌아 링크하지 않는 필드 사전에 두었을 수도 있습니다

Delphi에서 HotPDF 헤드리스 렌더링을 위한 AcroForm 객체 그래프: 형상뿐인 위젯들이 /FT와 /V, /DA, /Q를 쥔 필드로 /Parent를 오릅니다
/Parent 사슬에서 가장 가까운 값이 이기고, 사슬이 다하면 /DA와 /Q만 AcroForm 사전으로 물러섭니다

/DA의 글꼴 이름은 위젯이나 부모 필드의 /DR, AcroForm /DR, 페이지 글꼴 자원 순서로 해석됩니다. 필드 수준 /DR은 표 222 밖이지만 존재하면 모호함이 없으므로 HotPDF는 그것을 받아들입니다. 올라가기에는 한계가 있습니다. 트리의 루프는 "Widget field inheritance contains a cycle"으로 멈추고, 사전이 아닌 /Parent는 "Widget Parent requires a dictionary"로, 렌더 옵션의 MaxDepth(기본 32)보다 깊은 사슬은 "Widget field inheritance depth budget exceeded"으로 멈춥니다. VCL 컴포넌트는 상속된 AcroForm 값을 읽고 필드를 리셋할 때 같은 상속을 적용합니다

헤드리스 렌더러는 무엇을 그릴지 어떻게 정하는가?

HPDFRenderHeadlessPagePNG과 그 파일 변형은 /AP가 없거나 null인 보이는 위젯에만 폴백을 돌립니다. ISO 32000-1 §7.3.7은 null 사전 값을 없는 항목과 같다고 말하고, 렌더러는 /AP null을 그렇게 다룹니다. 존재하는 보통 모양은 /NeedAppearances가 무엇을 말하든 언제나 이기므로, 앞선 채움의 낡은 모양은 무언가가 다시 만들어 낼 때까지 계속 보입니다. 적재된 문서는 결코 수정되지 않습니다. 무엇이 그려지는지는 해석된 필드 타입과 플래그에 달려 있습니다:

필드합성된 모양
한 줄 텍스트, /FT /Tx문자열 /V를 해석된 /DA와 /Q로 /Rect 안에 배치
체크박스, /FT /Btn/Border에서 온 모서리 반경의 사각 테두리, /MK /BG 채움, 켜져 있으면 선으로 그린 벡터 체크
라디오 버튼, bit 16을 세트한 Btn켜져 있으면 채운 점이 있는 원형 테두리
푸시버튼, bit 17을 세트한 Btn/MK /CA 캡션을 가운데 놓은 사각 테두리
콤보 상자, bit 18을 세트한 /FT /Ch선택된 /Opt 항목의 표시 텍스트를 가운데 캡션으로
목록 상자, bit 18이 클리어된 Ch/TI부터 옵션마다 한 행, 선택된 모든 행 뒤에 하이라이트 띠

체크 표시와 라디오 점은 많은 생성기가 그리는 방식인 ZapfDingbats 글리프가 아니라 벡터 경로입니다. 얇은 Linux 컨테이너에는 ZapfDingbats가 설치되어 있지 않을 수 있기 때문입니다. 푸시버튼과 콤보 상자의 면은 HotPDF의 VCL 모양 생성기가 같은 필드에 그리는 것과 일치하므로, 어느 쪽이 이미지를 만들었든 양식은 똑같이 보입니다. 캡션은 글꼴과 크기, 색을 /DA에서 가져오는데, 캡션 배치는 자동 맞춤이 아니므로 /DA 크기 0은 상자 높이의 70퍼센트가 되고 12포인트를 상한으로 둡니다. 체크박스와 라디오 상태는 위젯 자신의 /AS에서 오고 상속된 /V로 물러서며, Off 이외의 상태는 표시를 그립니다. 라디오 자식마다 제 /AS를 주십시오. 부모의 /V는 그룹 전체의 선택된 상태를 명명할 뿐 자식을 서로 구별하지 못합니다

Delphi용 HotPDF 헤드리스 렌더러의 판단 흐름: 존재하는 /AP가 언제나 이기고, 그렇지 않으면 해석된 필드 타입이 합성 면을 고릅니다
임시 모양은 /AP가 없거나 null일 때만 그려지고, 적재된 문서 자체는 결코 수정되지 않습니다
uses
  SysUtils, HPDFHeadlessDocument, HPDFHeadlessCairoDevice;

function RenderFormPage(const FileName, PngName: string; PageIndex: Integer;
  out Diagnostic: string): Boolean;
var
  Doc: THPDFHeadlessDocument;
  Options: THPDFHeadlessRenderOptions;
  Info: THPDFHeadlessRenderResult;
begin
  Result := False;
  Diagnostic := '';
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(FileName);
    Options := THPDFHeadlessRenderOptions.Default;  // RenderAnnotations = True, MaxDepth = 32
    Options.Usage := hruPrint;  // 뷰어의 인쇄처럼: Print 플래그 없는 위젯은 건너뜁니다
    try
      // PageIndex는 0 기반입니다
      Info := HPDFRenderHeadlessPagePNGFile(Doc.GetRenderAccess(PageIndex),
        PngName, Options);
      Diagnostic := Format('%d appearances drawn', [Info.Appearances]);
      Result := True;
    except
      on E: EHPDFHeadlessRenderError do
        // 예: Widget field requires a name field type 등. 값이 채워진 양식이라면
        // RenderAnnotations := False로 재시도하지 마십시오, 위젯과 값이 모두 떨어져 나갑니다
        Diagnostic := E.Message;
    end;
  finally
    Doc.Free;
  end;
end;

텍스트 필드 폴백은 문자열 값을 지닌 한 줄 필드를 그립니다. 더 풍부한 텍스트 배치가 필요하면 Windows 쪽에서 채움 시점에 모양을 만들어 내십시오. EnsureLoadedFieldAppearanceStream이 파일이 평탄화되거나 발송되기 전에 없는 /AP 스트림을 지어 줍니다. 그러면 Linux 작업은 진짜 모양 스트림을 받고, 헤드리스 렌더러는 아직 하나가 필요한 필드를 만나면 이름을 불러 줍니다. 다만 Linux 출력은 Linux 렌더러로 판정하십시오. RenderLoadedPageToBitmap은 Windows GDI 백엔드를 통해 그리는데 이 폴백은 결코 돌지 않으므로, 거기서 멀쩡해 보이는 페이지가 같은 파일에서 Cairo 장치가 무엇을 내놓을지 아무것도 말해 주지 않습니다

필드 플래그 비트는 1부터 매겨집니다

ISO 32000-1 §12.7.3.1은 /Ff 워드의 비트 위치가 낮은 쪽 끝의 1부터 32까지 간다고 밝히고, 표 226과 228, 230이 모두 그 번호를 씁니다. Delphi에서 비트 N은 따라서 1 shl (N - 1)입니다. 비트 번호 자체로 시프트하면 모든 검사가 한 자리 위로 옮겨 갑니다. "radio, bit 16"이라고 쓴 1 shl 16은 실제로는 푸시버튼 플래그를 검사하고, "combo, bit 18"이라고 쓴 1 shl 18은 Edit을 검사합니다. 두 잘못된 마스크 모두 진짜 플래그입니다. 코드는 컴파일되고 돌고, 필드를 잘못된 종류로 분류합니다

Delphi에서 PDF 필드 플래그는 비트를 1부터 매깁니다: 눈금자가 라디오가 1 shl 15인 이유와 1 shl 16이 푸시버튼에 닿는 이유를 보여 줍니다
/Ff 워드의 비트 N은 Delphi 마스크 1 shl N 빼기 1을 취하고, 하나 어긋난 모든 마스크는 이웃 비트의 진짜 플래그입니다
플래그표비트Delphi 마스크
Multiline228 (텍스트)131 shl 12 = 4096
Password228 (텍스트)141 shl 13 = 8192
Comb228 (텍스트)251 shl 24 = 16777216
RichText228 (텍스트)261 shl 25 = 33554432
Radio226 (버튼)161 shl 15 = 32768
Pushbutton226 (버튼)171 shl 16 = 65536
Combo230 (선택)181 shl 17 = 131072
uses
  Classes, SysUtils, HPDFHeadlessDocument;

const
  // ISO 32000-1은 Ff 비트를 1(낮은 쪽)부터 매기므로 비트 N은 1 shl (N - 1)입니다
  TxMultiline   = 1 shl 12;  // bit 13
  TxPassword    = 1 shl 13;  // bit 14
  TxFileSelect  = 1 shl 20;  // bit 21
  TxDoNotScroll = 1 shl 23;  // bit 24
  TxComb        = 1 shl 24;  // bit 25
  TxRichText    = 1 shl 25;  // bit 26
  TxProducerLayout = TxMultiline or TxPassword or TxFileSelect or
    TxDoNotScroll or TxComb or TxRichText;

procedure ListTextFieldsForProducerAppearance(const FileName: string;
  Report: TStrings);
var
  Doc: THPDFHeadlessDocument;
  Field: THPDFHeadlessFormField;
  I: Integer;
begin
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(FileName);
    for I := 0 to Doc.FormFieldCount - 1 do
    begin
      // FieldType과 Flags, MaxLength는 조상에게서 상속된 채 도착합니다
      Field := Doc.GetFormField(I);
      if Field.FieldType <> 'Tx' then
        Continue;
      if (Field.Flags and TxProducerLayout) <> 0 then
        Report.Add(Format('%s: flags $%.8x, needs a producer /AP',
          [Field.Name, Field.Flags]))
      else if Field.MaxLength > 0 then
        Report.Add(Format('%s: /MaxLen %d, needs a producer /AP',
          [Field.Name, Field.MaxLength]))
      else if Field.Value = '' then
        Report.Add(Format('%s: blank, fill it or give it an /AP', [Field.Name]));
    end;
  finally
    Doc.Free;
  end;
end;

이 검색은 필드 수준 플래그를 읽는데, 프로듀서가 플래그를 두는 곳이 거기입니다. 렌더러는 별도 위젯의 /Ff도 존중하므로, 목록은 계획 보조로, 렌더 진단을 최종 판결로 대하십시오. 빈 값 검사가 중요한 것은 /V도 /AP도 없는 텍스트 필드는 합성할 것이 없기 때문입니다. 렌더는 "A text-field widget without a value has no appearance to synthesize"로 멈추고, 치료법은 파일이 렌더러에 닿기 전에 값을 주거나 모양을 주는 것입니다

"Degenerate appearance geometry"는 무슨 뜻인가?

직사각형이나 모양 상자에 면적이 없다는 뜻이며, 뒤집힌 /Rect는 원인이 아닙니다. ISO 32000-1 §7.9.5는 직사각형이 대각선 방향으로 마주 보는 어느 두 꼭짓점도 이름 지을 수 있게 하고, 그 노트는 처리 소프트웨어에게 정규화할 준비를 하라고 말합니다. 폭을 x2 - x1로 계산하는 소박한 렌더러는 [300 720 160 700]에서 음수를 받고 주석을 거절하거나 그 모양을 거울에 비춥니다. HotPDF는 어떤 크기 검사보다 먼저 좌표를 왼쪽 아래-오른쪽 위 순서로 맞바꿉니다. 합성 면이든 기존 /AP 스트림이든 마찬가지로, 그래서 위의 위젯 11은 꼭짓점이 관례적인 것처럼 그려집니다. 오류는 모양 스트림을 지닌 주석에 대해서만, 정규화 뒤 /Rect가 폭이나 높이가 0이거나 스트림의 /BBox가 제 /Matrix 아래에서 선이나 점으로 무너질 때 올라옵니다. 나타나면 위젯의 /Rect를 스트림의 /BBox와 /Matrix와 함께 덤프하십시오. 셋 중 하나가 0 크기입니다

/AP가 없고 면적 0의 /Rect를 지닌 위젯은 다른 길을 갑니다. 폴백은 그것을 건너뛰고 페이지로 계속 갑니다. ISO 32000-2 §12.5.2는 모양 요구에서 Popup과 Projection, Link 주석, 하나의 점으로 무너지는 직사각형만 면제합니다. 단순히 선인 직사각형은 PDF 2.0 아래에서 여전히 모양을 빚져 있습니다. 렌더러는 그것도 건너뛰되, 그려 낼 면적이 없기 때문입니다

필드 타입을 찾지 못할 때

"Widget field requires a name field type"는 렌더러가 위젯에서 /Parent 링크가 닿는 데까지 올라갔는데 /FT를 찾지 못했다는 뜻입니다. 렌더러는 페이지의 /Annots 배열에서 시작하므로 위로 향한 /Parent 링크만이 필드로 가는 유일한 길입니다. 적재는 다른 방향으로 걷습니다. AcroForm /Fields 배열에서 /Kids를 따라 아래로 내려오지요. 그래서 나중에 페이지가 렌더에 실패하는 파일에서도 LoadFromFile은 성공할 수 있습니다. 전형적인 원인은 필드의 /Kids에 나열되었지만 되돌아 가리키지 않는 위젯입니다. ISO 32000-1 표 188은 그에 대한 일부 덮개를 줍니다. 위젯이 여러 자식 중 하나일 때만 /Parent를 요구하고 "absent otherwise"라고 말하므로, 문자 그대로 읽는 라이터는 별개 위젯 하나를 지닌 필드의 링크를 떨궈 놓습니다. ISO 32000-2는 같은 항목을 "optional otherwise"로 늦추었습니다. 우리라면 필드와 합쳐지지 않은 모든 위젯에 어차피 /Parent를 쓰겠습니다. ISO 32000-2는 그것을 허용하고, 페이지 주도 리더가 — 우리든 남의 것이든 — 필드에 닿을 수 있는 유일한 길이기도 합니다. 프로듀서를 고칠 수 없다면 홀로 된 위젯을 그 필드 사전으로 합치는 것이 같은 결과를 줍니다

부모에게 링크하고 오르는 길 어딘가에서 /DA를 찾는 위젯은 HotPDF Delphi component의 헤드리스 렌더러에서 어떤 /AP 없이도 올바르게 그려집니다. 그래서 Linux 양식 파이프라인에서 모양 스트림보다 먼저 바로잡을 것은 필드 트리입니다