품목들의 가격이 자동으로 합산되고 날짜가 스스로 지정된 형식으로 변환되는 인보이스나 지출 결의서 PDF를 구현하려면 해당 AcroForm에 세 가지 요소가 결합되어야 합니다. HotPDF는 Delphi에서 로드된 파일에 이 세 가지 요소를 모두 추가할 수 있습니다: ApplyLoadedFieldCalculation은 AFSimple_Calculate 합계를 위한 것이고, ApplyLoadedFieldValidation은 Acrobat AF 포맷 및 유효성 검사 헬퍼를 위한 것이며, EnsureLoadedFieldAppearanceStream은 뷰어가 실제로 그리는 값을 구워 넣는 것입니다
HotPDF는 Delphi 및 C++Builder용 네이티브 VCL PDF 컴포넌트이며, 이 세 가지 메서드는 처음부터 새로 생성하는 문서가 아닌 LoadFromFile을 통해 열어 둔 기존 문서를 대상으로 작동합니다. 각 메서드는 GetLoadedFormFieldNames가 반환하는 순서와 동일한 AcroForm /Fields 배열의 0부터 시작하는 필드 인덱스를 인수로 받아서 필드 딕셔너리에 직접 값을 씁니다. 따라서 전체 작업 흐름은 문서를 로드하고, 인덱스를 찾고, 기능을 연결한 뒤 SaveLoadedDocument를 호출하는 것입니다. 필드 세트 자체를 구성하는 것은 Delphi에서 로드된 PDF에 AcroForm 필드 추가하기에서 다루는 별도의 작업이며, 이 페이지에서는 기존 필드 세트에 산술 연산 및 포맷 지정 기능을 부여하는 방법을 설명합니다
왜 PDF 양식에 자체 필드를 합산하는 스크립트가 필요한가요?
PDF에는 스프레드시트 레이어가 존재하지 않기 때문입니다. 텍스트 필드는 수식이 아닌 문자열을 보유하므로, 변경 시 합계를 다시 계산하는 스크립트가 연결되어 있지 않다면 품목이 바뀔 때 문서 내에서 합계가 갱신되지 않습니다. 작동 방식은 트리거 이벤트와 작업을 매핑하는 필드 추가 작업 딕셔너리인 /AA (ISO 32000-1 §12.7.5)입니다: 즉, 문자가 입력될 때의 키 입력 트리거(K), 화면 표시용 포맷 트리거, 커밋 시점의 유효성 검사 트리거(V), 그리고 양식의 임의의 필드가 바뀔 때마다 실행되는 계산 트리거(C) 등이 있습니다. 각 작업은 JavaScript 작업(/S /JavaScript, ISO 32000-1 §12.6.4.14)에 해당하며, 실행되는 스크립트가 실제로 합산이나 서식 변환을 수행합니다. Acrobat은 AFSimple_Calculate, AFNumber_Format, AFDate_FormatEx 등 사전 작성된 도우미 라이브러리를 함께 제공하므로, 실제로 연결하는 스크립트는 직접 작성한 복잡한 코드가 아닌 대개 해당 라이브러리를 호출하는 한 줄의 코드입니다
다단계 양식에서 올바른 결과를 얻기 위해서는 한 가지 세부 사항이 결정적인 역할을 하는데, 그것은 바로 '계산 순서'입니다. Acrobat은 계산 작업이 포함된 모든 필드를 실행할 때 페이지 순서가 아닌 AcroForm /CO 배열에 정의된 시퀀스에 따라 구동합니다. ApplyLoadedFieldCalculation은 지정한 필드에 계산 작업을 연결합니다; 하나의 계산 필드 값이 다른 필드 값의 입력으로 사용될 때(예: 세금 라인이 읽는 소계, 그리고 둘 모두를 합산하는 최종 합계), 최종 합계가 이전 상태의 소계를 참조하여 한 단계 뒤처진 값을 표시하지 않도록 /CO 리스트에서 각 입력 필드가 이를 사용하는 결과 필드보다 앞서 나열되어 있는지 확인해야 합니다
PDF 양식이 자동으로 합계를 계산하게 하려면 어떻게 해야 하나요?
ApplyLoadedFieldCalculation은 대상 필드 인덱스, 수행할 연산 및 집계할 소스 필드 이름들을 인수로 받습니다. HotPDF는 대상 필드의 계산 작업에 AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...])를 작성하며, THPDFFieldCalcOp 열거형을 통해 연산 함수를 선택할 수 있습니다: fccSum, fccAverage, fccProduct, fccMinimum, fccMaximum이 각각 Acrobat의 SUM, AVG, PRD, MIN, MAX에 대응합니다. 소스 이름들은 GetLoadedFormFieldNames가 반환하는 것과 동일한 AcroForm 필드명 문자열이므로, 3개의 품목 라인과 배송비를 합산하는 것은 단 한 번의 호출로 끝납니다
var
Pdf: THotPDF;
Names: THPDFAnsiStringArray;
i, TotalIdx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
Names := Pdf.GetLoadedFormFieldNames;
TotalIdx := -1;
for i := 0 to High(Names) do
if Names[i] = 'grandTotal' then TotalIdx := i;
if TotalIdx >= 0 then
begin
Pdf.ApplyLoadedFieldCalculation(TotalIdx, fccSum,
['lineTotal.1', 'lineTotal.2', 'lineTotal.3', 'shipping']);
Pdf.EnsureLoadedFieldAppearanceStream(TotalIdx);
end;
Pdf.SaveLoadedDocument('invoice-live.pdf');
finally
Pdf.Free;
end;
end;
합계 필드에 대해 EnsureLoadedFieldAppearanceStream을 호출하는 것은 시각적 효과만을 위한 것이 아닙니다. 계산은 뷰어 내부에서 구동되므로, 스크립트 실행이 가능한 리더가 파일을 열기 전까지는 합계 필드에 렌더링된 자체 콘텐츠가 없습니다. 외관 스트림(Appearance stream)을 구워 두면 파일에 임시로 표시될 값을 생성해 주며, 스크립트를 실행하지 않는 인쇄 인프라나 섬네일 생성기가 이 값을 참조하게 됩니다
AF 헬퍼 제품군을 사용한 필드 포맷 지정 및 유효성 검사
ApplyLoadedFieldValidation은 단 한 번의 호출로 포맷, 키 입력 및 유효성 검사 스크립트를 한 번에 연결하며, 개발자가 전달하는 THPDFFieldValidationKind에 따라 어떤 Acrobat 헬퍼가 바인딩될지 결정됩니다. fvkCurrency를 전달하면 HotPDF는 일치하는 AFNumber_Keystroke와 함께 포맷 스크립트로 AFNumber_Format(2,0,0,0,"$ ",true)를 작성합니다; fvkNumber는 구분 기호 인수로 "."을 사용하고, fvkPercentage는 AFPercent_Format을 출력하며, fvkDate는 DateFormat 인수에 커스텀 마스크를 별도로 전달하지 않는 한 AFDate_FormatEx("dd/mm/yyyy")를 출력합니다. 특수 포맷인 fvkZipCode, fvkSSN 및 fvkPhone은 Acrobat이 각각에 대해 예약해 둔 코드 값과 함께 AFSpecial_Format에 매핑되고, fvkArbitraryMask는 기본 프리셋으로 처리할 수 없는 임의의 포맷에 대해 Mask 인수를 통해 마스크 문자열을 입력받습니다. 숫자 유형의 경우 HotPDF는 AFRange_Validate 스크립트도 함께 연결하여 지정된 범위를 벗어난 값이 입력되면 커밋 단계에서 거부되도록 합니다
// 송장 날짜를 ISO 형식으로, 금액을 통화 형식으로 포맷하고, 세금 ID에 마스크를 씌웁니다
Pdf.ApplyLoadedFieldValidation(DateIdx, fvkDate, 'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency, '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx, fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);
왜 외관 스트림(Appearance stream)을 재생성해야 하나요?
PDF에서는 필드가 갖는 값 문자열과 뷰어가 실제로 렌더링하는 픽셀 데이터가 별개로 관리되며, 한쪽을 수정한다고 해서 다른 쪽이 자동으로 갱신되지 않기 때문입니다. 필드는 그 값을 /V에 저장하지만, 리더가 화면에 그리는 내용은 필드 외관 스트림인 /AP /N에서 가져옵니다. 오브젝트 그래프를 통해 /V를 편집해도, 엄격한 규칙을 준수하는 뷰어는 무언가가 외관 스트림을 다시 빌드하기 전까지 여전히 이전 모습을 그대로 렌더링합니다. 양식 딕셔너리는 문서를 열 때 모든 외관 스트림을 다시 빌드하도록 요청하는 /NeedAppearances 플래그를 포함할 수 있고 Acrobat은 이를 준수하지만, 브라우저 내장 렌더러, 모바일 리더, 프린트 서버 및 섬네일 생성기 등은 이 플래그를 무시하고 기존 스트림을 그대로 그리거나 아무것도 그리지 않는 경우가 많습니다. EnsureLoadedFieldAppearanceStream은 필드 /DA, /V, /Rect를 직접 파싱하여 /AP /N Form XObject를 생성함으로써 이 문제를 해결하며, 뷰어의 렌더링 기능에 의존하는 대신 파일 내부에 완성된 모양을 함께 실어서 전송합니다
// 저장하기 전에 모든 필드의 외관을 구워서 값이 모든 환경에서 올바르게 렌더링되도록 합니다
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');
AF 스크립트가 작동하지 않는 경우
모든 AF* 스크립트는 뷰어에 JavaScript 엔진이 탑재되어 있을 때만 실행되며, 대부분의 뷰어에는 엔진이 내장되어 있지 않습니다. Acrobat 및 일부 데스크톱 소프트웨어만 이러한 헬퍼를 구동하며, Chrome, Edge, Firefox 내장 뷰어와 주요 모바일 플랫폼, 서버 사이드 래스터라이저 및 인쇄 파이프라인 등은 양식 내의 JavaScript를 전혀 실행하지 않습니다. 이러한 리더 환경에서는 계산이나 포맷 변환이 수행되지 않고 단지 필드가 마지막으로 저장되었던 상태의 외관 스트림을 그대로 투영하게 되며, 이것이 /AP /N을 구워 두는 작업이 핵심적인 이유입니다. AF* 레이어는 스크립트 실행이 가능한 리더를 사용하는 일부 사용자를 위한 편의 기능으로 다루어야 하며, 실행을 보장하는 수단으로 간주해서는 안 됩니다. 송장 청구 금액이나 세금 신고액처럼 서버에서 절대적으로 정확해야 하는 값은 양식이 반환된 후 백엔드에서 직접 다시 연산하여 검증해야 합니다. 양식을 작성한 리더가 스크립트를 단 한 줄이라도 실행했을 것이라고 가정해서는 위험합니다
추가로 유념해야 할 상황은 입력 문서가 XFA 포맷인 경우입니다. 동적 XFA 양식은 AF* 도우미들이 인식하지 못하는 XML 페이로드 내부에 자체 연산 엔진을 내장하므로, XFA 필드에 AFSimple_Calculate를 연결하는 것은 아무 효과가 없습니다. 먼저 Delphi에서 XFA를 AcroForm으로 플래트닝(Flattening)하기에 기재된 것처럼 XFA 양식을 단순한 AcroForm으로 납작하게 압축한 뒤, 이 페이지의 세 가지 메서드를 적용하여 결과 AcroForm 필드들을 제어해야 합니다
자동으로 계산되고 자율적으로 서식을 지정하는 AcroForm은 로드된 문서에 대해 세 가지 호출(계산 연결, 포맷 및 유효성 검사 연결, 외관 굽기)을 수행한 뒤 SaveLoadedDocument를 호출하는 과정으로 요약됩니다. 여기에 기술된 로드된 양식 API는 AcroForm의 다른 사양과 함께 상세한 THPDFFieldValidationKind 및 THPDFFieldCalcOp 열거형 설명서를 제공하는 Delphi 및 C++Builder용 HotPDF 컴포넌트의 일부입니다