기술 문서

Delphi에서 PDF 양식 병합하기: 중복 필드 규칙

PDF Library for Delphi는 이름이 같은 필드에 대한 명시적인 정책을 가지고 두 AcroForm 문서를 병합합니다. MergeDocumentEx는 원본 문서 식별자와 세 가지 전략 중 하나를 받습니다. dfsReject는 병합을 거부하고, dfsMerge는 공유된 이름을 유지하며 값을 동기화하고, dfsAutoNumber는 들어오는 필드의 이름을 결정론적으로 바꿉니다. 이름 스캔은 객체 번호가 이동하기 전에 일어나므로, 거부된 병합은 두 문서 모두를 완전히 그대로 사용할 수 있는 상태로 남겨둡니다

PDF 애플리케이션 묶음을 조립해 본 사람이라면 누구나 이 문제를 겪어봤을 것입니다. 각자 SignatureDateTotal이라는 이름의 필드를 가진 양식 세 개를 파일 하나로 병합합니다. AcroForm에서 완전히 정규화된 필드 이름은 그 필드의 정체성이므로, 같은 이름을 가진 두 필드는 애초에 두 개의 필드가 아닙니다. 하나를 채우면 다른 하나도 채워지고, 하나에 적용된 서명이 아무도 의도하지 않은 범위를 뒤덮게 됩니다

이름 충돌은 왜 병합 전에 결정될까?

이전의 MergeDocument는 두 AcroForm 루트 필드 배열을 이어붙일 뿐 선택지를 제공하지 않았습니다. 게다가 결과물을 쓸 수 없다는 사실은 객체 번호가 다시 매겨지고 페이지 트리가 이어붙여진 뒤에야 드러났고, 이는 호출자에게 어느 원본 상태도 아닌 문서를 남겨두었습니다

MergeDocumentEx는 이 순서를 뒤집습니다. 두 문서에서 최상위 필드 이름을 모으고, 비교한 다음, 아무것도 옮기기 전에 전략을 적용합니다. 그래서 거부는 깔끔한 무동작입니다. 대상 문서는 그대로이고 원본 문서도 그대로이며, 둘 다 열려 있는 채로 사용할 수 있습니다. 병합 테스트는 거부된 병합 이후 원본에서 필드 값을 다시 읽어 이를 검증합니다

비교는 정렬된, 대소문자를 구분하는 이름 집합을 사용하므로, 비용은 두 개수의 곱이 아니라 전체 필드 수에 로그 계수를 곱한 값에 비례합니다. 대소문자를 구분하는 것이 여기서 옳은 선택인데, PDF 필드 이름은 대소문자를 구분하기 때문입니다. 이를 접으면 명세가 서로 다르다고 취급하는 필드들이 합쳐지고 맙니다

세 가지 전략, 그리고 각각이 옳은 상황

dfsReject는 모호한 문서를 만들어서는 안 되는 자동화 파이프라인을 위한 전략입니다. 병합은 0을 반환하고 LastErrorCode는 705를 보고하는데, 이는 중복된 이름을 다른 모든 병합 실패와 구별해 특정한 해결책, 대개는 상류에서의 필드 이름 변경으로 연결할 수 있도록 만든 전용 코드입니다

dfsMerge는 공유된 이름을 의도적으로 유지하면서 대상 값과 기본값을 원본 필드에 동기화하므로, 규격을 준수하는 뷰어는 여러 위젯을 하나의 논리적으로 이름 붙은 필드로 취급하는데, 이는 여러 위젯 주석을 가진 필드에 대한 표준 AcroForm 동작입니다. 하지 않는 것은 서로 다른 필드 딕셔너리를 하나의 객체로 접어 넣는 것입니다. 각 필드는 자신의 페이지 연관, 외형, 액션을 그대로 유지하는데, 이를 합치면 들어오는 문서에 속한 서식과 동작이 조용히 버려지기 때문입니다

dfsAutoNumber는 들어오는 중복 항목의 이름 끝에 _2부터 시작하는 숫자 접미사를 붙여 처음으로 비어 있는 값을 취합니다. 그 결과는 재현 가능합니다. 오직 존재하는 이름에만 의존하며 필드 객체 번호에는 결코 의존하지 않으므로, 같은 문서 쌍을 두 번 병합해도 두 번 다 같은 이름이 나옵니다. 이 속성은 하류 코드나 FDF 임포트, 데이터베이스 매핑이 필드를 이름으로 참조할 때 중요합니다

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  TargetDoc, SourceDoc: Integer;
begin
  Lib := TPDFlib.Create;
  try
    TargetDoc := Lib.SelectedDocument;
    Lib.LoadFromFile('application-part1.pdf', '');

    SourceDoc := Lib.NewDocument;
    Lib.LoadFromFile('application-part2.pdf', '');

    Lib.SelectDocument(TargetDoc);
    if Lib.MergeDocumentEx(SourceDoc, dfsReject) = 0 then
    begin
      if Lib.LastErrorCode = 705 then
      begin
        // Both documents are still intact - retry with a policy
        Log('duplicate field names; retrying with auto-numbering');
        Lib.MergeDocumentEx(SourceDoc, dfsAutoNumber);
      end;
    end;

    Lib.SaveToFile('application-complete.pdf');
  finally
    Lib.Free;
  end;
end;

이 코드에 담긴 2단계 패턴에 주목하십시오. 이는 오직 거부가 파괴적이지 않기 때문에 가능합니다. 먼저 엄격한 정책을 시도하고, 오류를 확인한 다음, 결정하십시오. 병합이 중간에 실패하는 방식이었다면 대체 경로는 두 파일을 다시 로드하는 데서 처음부터 다시 시작해야 했을 것입니다

병합된 양식은 이후 어떤 모습일까

dfsMerge 아래에서, "Target value"를 담은 Shared라는 이름의 대상 필드와 같은 이름을 가진 원본 필드는 둘 다 Shared라는 이름을 가지고 둘 다 대상 값을 보고하는 필드 두 개를 만들어내는데, 대상 값과 기본값이 들어오는 필드에 동기화되기 때문입니다. 그것이 공유된 이름에 대해 의도된 시맨틱입니다. 하나의 논리적 필드, 여러 위젯, 하나의 값입니다

dfsAutoNumber 아래에서는 같은 입력이 SharedShared_2를 독립적인 값을 가진 별개의 필드로 만들어냅니다. 둘 중 어느 것을 선택할지는 질문 하나로 정하십시오. 하나를 채우면 다른 것도 채워져야 하는가? 묶음의 모든 파트에 반복되는 서명자 이름이라면 그렇습니다, dfsMerge가 옳습니다. 양식마다 의미가 다른 합계라면 아닙니다, 자동 번호 매기기가 옳습니다

// After a merge, enumerate what you actually got
for I := 1 to Lib.FormFieldCount do
  Log(Format('%d: %s = %s',
    [I, Lib.GetFormFieldTitle(I), Lib.GetFormFieldValue(I)]));

양식 묶음 조립을 위한 실무 참고 사항

성공한 병합은 원본 문서를 소모합니다. 라이브러리의 문서 목록에서 제거되므로, DocumentCount가 2에서 1로 줄어드는 이유가 바로 이것입니다. 그 이후에는 원본 식별자를 계속 쓰지 마십시오. 문서 버전은 둘 중 더 높은 쪽으로 올라가므로, PDF 2.0 양식을 1.7 문서에 병합하면 2.0 파일이 나옵니다

이름에는 순서가 중요합니다. A를 B에 병합하는 것과 B를 A에 병합하는 것은 서로 다른 자동 번호 결과를 만드는데, 병합을 수행하는 문서가 자신의 이름을 그대로 유지하기 때문입니다. 묶음에 정본으로 삼을 주 양식이 있다면 그것을 대상으로 삼으십시오

서명 필드는 별도로 고려할 가치가 있습니다. 병합 전에 적용된 서명은 자신이 서명한 리비전만 다루므로, 병합은 파일이 서명 이후 바뀌었다는 실무적인 의미에서 그 서명을 무효화합니다. 서명된 파트를 병합하는 대신, 먼저 조립한 다음 조립된 문서에 서명하십시오. 페이지 콘텐츠에 관한 병합이라면, 바이트 참조 이동을 이용한 빠른 PDF 병합에서 설명하는 더 빠른 경로가 더 나은 도구입니다

마지막으로, 묶음의 데이터 쪽도 병합과 함께 계획하십시오. 필드 값이 외부 시스템에서 온다면, 자동 번호 매기기를 선택하기 전에 그 시스템이 필드를 이름으로 주소 지정하는지 확인하십시오. Shared_2Shared를 기대하는 매핑과 맞아떨어지지 않기 때문입니다. 임포트와 익스포트 형식은 FDF, XFDF, XFA 양식 데이터 교환에서, 이름 변경으로 함께 영향받을 수 있는 필드 수준 스크립팅 동작은 대화형 양식 액션과 JavaScript에서 다룹니다

양식 병합, 데이터 교환, 서명은 Delphi, C++Builder, Free Pascal용 같은 라이브러리 안에서 실행됩니다. 전체 기능 목록은 Delphi용 PDF Library 페이지에 있습니다