델파이, C++Builder, Lazarus용 PDFium 기반 VCL/LCL 컴포넌트인 PDFium Component에서 폼 필드 인덱스는 주석 인덱스가 아닙니다. 페이지는 위젯 옆에 Link, Text, Ink 주석을 함께 담을 수 있으므로, 필드 열거는 FPDFAnnot_GetSubtype으로 필터링해야 하며 0 기반의 논리적 인덱스를 노출하되 실제 주석 위치로의 매핑은 오직 네이티브 호출 시점에만 이루어져야 합니다
이를 드러내는 버그는 한 번 보면 틀림없이 알아볼 수 있습니다. 테스터가 작성이 끝난 청구서 폼에서 Tab을 누르면 캐럿이 사라지는데, 포커스가 바닥글의 하이퍼링크로 갔기 때문입니다. 더 나쁜 경우는 아예 아무 일도 일어나지 않는 것입니다: 여러분의 코드는 필드 3이 포커스되었다고 기록하고, UI 패널이 업데이트되지만, FORM_SetFocusedAnnot은 그 내내 조용히 false를 반환하고 있었습니다. 두 증상 모두 같은 설계 실수에서 나오며, 그중 하나는 그 아래에 숨은 두 번째 근본 원인을 갖고 있습니다
PDFium이 건네주는 두 개의 인덱스 공간
PDFium은 같은 페이지 위에 두 개의 번호 매김 체계를 노출하며, 이들은 우연히 폼 위젯 외에 아무것도 담고 있지 않은 문서에서만 일치합니다. 첫 번째는 주석 인덱스입니다: 페이지 /Annots 배열 안의 위치이며, FPDFPage_GetAnnotCount가 세는 것이자 FPDFPage_GetAnnot이 받는 것입니다(ISO 32000-1 §12.5.2). 두 번째는 애플리케이션 수준 API가 제공해야 하는 논리적 필드 인덱스이며, 사용자가 실제로 도달할 수 있는 대화형 필드에 대해 0부터 진행됩니다. ISO 32000-1 §12.5.6.19는 위젯 주석을 대화형 폼 필드의 시각적 표현으로 정의하며, §12.7은 폼 자체를 정의합니다. 페이지 위의 다른 모든 것은 서로 다른 의미론을 가진 다른 서브타입입니다: Link 주석은 목적지를 가지고, Ink 주석은 스트로크 목록을 가지며, Text 주석은 스티키 노트입니다. 이들 중 어느 것도 필드 개수에 속하지 않으며, 어느 것도 폼 포커스를 받을 수 없습니다. 그럼에도 /Annots 배열 안에서는 생성 애플리케이션이 그것들을 작성한 순서대로 위젯과 뒤섞여 있으며, 그 순서는 문서에 관한 다른 무언가가 시사하는 순서와 자주 다릅니다
Tab이 다음 필드 대신 하이퍼링크에 착지하는 이유는 무엇인가
필드 개수가 실제로는 주석 개수였기 때문입니다. 원래 구현은 FormFieldCount에서 FPDFPage_GetAnnotCount를 직접 반환했으며, 필드 정보 접근자, 탭 순서 헬퍼, 포커스 헬퍼는 모두 그 같은 정수를 위젯 위치로 취급했습니다. 위젯 여섯 개만 있고 다른 것은 없는 깨끗한 AcroForm 페이지에서는 6이 6과 같고 모든 테스트가 통과합니다. 바닥글에 하이퍼링크를, 여백에 리뷰어 코멘트를 하나 추가하면, 개수는 여덟 개 필드를 보고하고, 인덱스 6과 7은 폼이 아닌 객체로 해석되며, Tab은 그것들 안으로 곧장 걸어 들어갑니다
열거 쪽에서의 수정은 주석이 아니라 서브타입을 세는 것입니다. 각 주석을 열고, 그 서브타입을 물어보고, 위젯을 남긴 다음, finally 블록에서 핸들을 닫습니다. FPDFPage_GetAnnot은 FPDFPage_CloseAnnot을 통해 반드시 반환되어야 하는 소유된 핸들을 돌려주기 때문입니다
function WidgetCountForPage(Page: FPDF_PAGE): Integer;
var
Count, I: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := 0;
if Page = nil then
Exit;
Count := FPDFPage_GetAnnotCount(Page); // every annotation, not just fields
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
Inc(Result);
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
이것이 의도적으로 하지 않는 것에 주목하십시오. 폼 필 환경에게 아무것도 묻지 않으며, 폼 핸들을 필요로 하지도 않습니다. 서브타입은 주석 딕셔너리 안에 살며 페이지만으로도 읽을 수 있기 때문입니다. 이는 순서 문제에서 중요합니다: 문서가 애초에 폼 필 환경을 가질 자격이 있는지 결정하기도 전에 그 개수를 사용할 수 있으며, 이는 편의가 아니라 보안 결정으로서 AcroForm JavaScript와 호스트 이벤트에 관한 글에서 다룹니다
네이티브 경계에서 논리적 인덱스 되돌려 매핑하기
두 공간이 서로 새어 들어가지 않도록 지키는 규칙은 단순합니다: 논리적 인덱스만이 공개 API를 넘어가는 유일한 숫자이며, 그것은 네이티브 호출 직전의 마지막 함수에서 주석 인덱스로 변환됩니다. 필드 정보, 포커스, 플래그 세터, 탭 순서가 모두 똑같이 사용하는 매핑 헬퍼 하나가 그 규칙을 강제 가능하게 만듭니다
function AnnotationIndexForField(Page: FPDF_PAGE;
FieldIndex: Integer): Integer;
var
Count, I, Current: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := -1;
if (Page = nil) or (FieldIndex < 0) then
Exit;
Count := FPDFPage_GetAnnotCount(Page);
Current := 0;
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
begin
if Current = FieldIndex then
Exit(I); // real /Annots position: native calls only
Inc(Current);
end;
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
이 헬퍼의 두 가지 속성은 분명히 짚어둘 가치가 있습니다. 이것은 선형 스캔이므로, 모든 필드에 대한 순진한 루프는 수백 개의 위젯이 있는 페이지에서 이차 개수의 주석 열기 비용이 듭니다. 페이지 전체를 열거하는 경우라면, 필드마다 매퍼를 호출하는 대신 주석을 한 번 순회하며 위젯 핸들을 수집하십시오. 그리고 이것은 예외를 던지는 대신 -1을 반환하는데, 이는 호출자가 오래된 인덱스를 예외 값어치의 프로그래밍 오류로 취급할지 무시할 가치가 있는 경쟁으로 취급할지 결정할 수 있게 해줍니다. 예를 들어 캐시된 UI 목록이 여전히 참조하는 주석을 편집이 제거한 뒤가 그런 경우입니다
FORM_SetFocusedAnnot이 헤드리스 페이지에서 실패하는 이유는 무엇인가
PDFium은 페이지 뷰가 한 번도 유효하다고 표시된 적 없는 위젯에 포커스를 주는 것을 거부하기 때문입니다. FORM_SetFocusedAnnot은 폼 필 환경 안에서 주석을 페이지 뷰로 해석하며, 그 페이지 뷰가 존재하지 않으면 아무 진단도 없이 false를 반환합니다. 그래서 인덱스 매핑만 고치면 Tab이 하이퍼링크에 착지하는 것은 고쳐지지만 두 번째 증상은 손대지 않은 채 남습니다: 여러분의 논리적 포커스 기록은 필드 3이라고 말하지만, 네이티브 포커스된 위젯은 여전히 아무것도 아니며, 네이티브 포커스, 포커스된 텍스트, 포커스된 값, 선택 상태에 대해 세워진 모든 접근자는 계속 빈 값을 반환합니다. 페이지 뷰는 FORM_OnAfterLoadPage가 생성하고 FORM_OnBeforeClosePage가 파괴합니다. 시각적 컨트롤을 중심으로 만들어진 뷰어에서는 이 호출들이 페이지를 표시하는 일부로 일어나며, 이것이 바로 이 실패가 헤드리스 전용 버그처럼 보이는 경우가 그토록 많은 이유입니다: GUI 데모에서는 작동하는 같은 코드가 배치 도구에서는 실패합니다. 생명주기는 뷰어가 아니라 문서 객체에 속하므로, PDFium Component는 이제 폼 핸들이 존재하는 상태에서 페이지가 로드되거나 언로드될 때마다 두 호출을 모두 발생시킵니다. C 시그니처는 페이지를 먼저, 폼 핸들을 두 번째로 받는데, 이는 바인딩을 손으로 작성할 때 뒤집기 쉬운 부분입니다
procedure ReportFirstField(const FileName: string);
var
Pdf: TPdf;
Idx: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FormFill := True; // form-fill environment, before Active
Pdf.FileName := FileName;
Pdf.Active := True;
Pdf.PageNumber := 1; // page load also runs FORM_OnAfterLoadPage
Idx := Pdf.FocusNextFormField; // logical index, 0-based over widgets
if Idx < 0 then
Exit; // page holds no widget annotations
Writeln(string(Pdf.FormFieldInfo[Idx].Name), ' = ',
string(Pdf.FocusedFormFieldValue)); // reads the native focused widget
finally
Pdf.Free; // page unload runs FORM_OnBeforeClosePage
end;
end;
수정을 증명하는 검사는 양쪽을 비교하는 검사입니다. 논리적 인덱스로 FocusFormField를 호출한 다음, 여러분 자신의 기록이 아니라 네이티브 포커스된 위젯을 거치는 접근자, 예를 들어 FocusedFormFieldValue나 FocusedFormOptionSelected를 통해 값을 읽으십시오. 논리적 인덱스는 왕복하는데 네이티브 접근자가 빈 값으로 돌아온다면, 빠진 것은 매핑이 아니라 페이지 뷰입니다
논리적 필드 인덱스가 약속하지 않는 것
0 기반 필드 인덱스는 편의이지 의미론적 정체성이 아니며, 거기서 네 가지 한계가 따라나옵니다. 그것은 페이지별이지 문서별이 아니므로, 페이지 2의 인덱스 0은 페이지 1의 인덱스 0과 다른 위젯이고 그것들을 비교하는 것은 무의미합니다. 그것은 위치 기반이므로, 주석을 삽입하거나 삭제하면 그 변경 위쪽의 모든 캐시된 인덱스가 무효화됩니다. 저장된 인덱스는 페이지가 로드된 채로 편집되지 않은 동안에만 유효한 것으로 취급하십시오
세 번째 한계는 필드 목록을 검토하는 사람들을 놀라게 하는 것입니다. 이 인덱스는 필드가 아니라 위젯을 열거합니다. 라디오 그룹은 여러 위젯 자식을 가진 하나의 필드이므로, 세 개짜리 버튼 그룹은 모두 같은 Name을 보고하는 세 개의 연속된 인덱스를 만들어냅니다. TPdfFormFieldInfo 레코드는 정확히 이 경우를 위해 GroupCount와 GroupIndex를 갖고 있으며, 이를 무시하는 목록 UI는 같은 필드를 세 번 보여줍니다. 네 번째 한계는 순회 순서에 관한 것입니다: 여기서 노출되는 탭 순서는 위젯 열거 순서이며, /Annots 배열을 따르는 것이지 페이지 /Tabs 항목(ISO 32000-1 §7.7.3.3)이나 AcroForm 필드 트리를 따르는 것이 아닙니다. 대부분의 생산자에서는 이들이 일치합니다. 오른쪽 열을 먼저 방출한 생성기가 두 열로 배치한 폼에서는 일치하지 않으며, 폼 필드 탐색 글에서 설명하는 키보드 경로는 모든 인덱스가 올바른데도 잘못된 것처럼 느껴질 것입니다. 고객 파일이 이상하게 동작할 때는, 이론을 세우기 전에 두 인덱스 공간을 나란히 덤프해 보십시오: 같은 페이지의 주석 뷰와 필드 뷰를 함께 인쇄하면 보통 한눈에 원인이 명백해집니다
procedure DumpIndexSpaces(Pdf: TPdf);
var
I: Integer;
Info: TPdfFormFieldInfo;
begin
for I := 0 to Pdf.AnnotationCount - 1 do
Writeln('annot ', I, ': subtype ', Ord(Pdf.Annotation[I].Subtype));
for I := 0 to Pdf.FormFieldCount - 1 do
begin
Info := Pdf.FormFieldInfo[I];
Writeln('field ', I, ': ', string(Info.Name),
' widget ', Info.GroupIndex, ' of ', Info.GroupCount);
end;
end;
필드 개수를 훨씬 웃도는 주석 개수는 페이지가 서브타입을 섞어 쓰고 있다는 뜻이며, 이는 검토된 문서에서는 정상이고 정확히 이 매핑이 존재하는 이유가 되는 상황입니다. 주석 검토 워크플로 글은 같은 페이지를 마크업 쪽에서 다룹니다. 반면 모든 테스트 파일에서 개수가 같다는 것은 여러분의 픽스처가 이 부류의 버그를 전혀 감지할 수 없다는 뜻이며, 정직한 대응은 링크와 스티키 노트를 담은 폼 픽스처를 추가하는 것입니다
여기서 설명한 필드 열거, 포커스, 주석 API는 델파이, C++Builder, Lazarus용 PDFium Component와 함께 제공되며, 그 제품 페이지에는 필드 정보 레코드와 포커스 접근자를 포함한 전체 폼 필드 레퍼런스가 있습니다