기술 문서

PDFium Component를 사용한 Delphi에서의 PDF 첨부 파일: 읽기, 추가, 삭제

PDF 첨부 파일은 문서의 내장 파일 트리 구조에 저장되며, 대부분의 PDF 뷰어 프로그램들은 이를 클립 아이콘 패널이나 첨부 파일 사이드바 형태로 외부에 표출합니다. Delphi 코드 상에서 PDFium Component는 TPdf의 인덱스 속성 세트를 통해 이 구조를 노출합니다: 즉 정수 인덱스를 사용해 데이터를 순회하고, 파일 이름과 바이트 페이로드를 읽어 들이며, 새로운 첨부 공간을 할당하거나 기존 공간을 삭제할 수 있습니다. 제공되는 API 구성은 비교적 단순하지만, 상용 코드를 작성하기 전에 숙지해 두어야 할 몇 가지 순서 제약 조건과 경로 정규화 규칙이 있습니다

열린 문서로부터 첨부 파일 데이터 읽기

AttachmentCount 속성은 문서에 선언된 내장 첨부 파일들의 총 개수를 반환합니다. PDFium 라이브러리의 연동 함수 호출 결과를 직접 읽어오므로 PDF에 내장된 실제 데이터를 충실히 대변합니다. 여기서 AttachmentName[Index]는 화면에 표시할 파일 이름을 WString 형식으로 반환하고, Attachment[Index]는 가공되지 않은 바이트 데이터를 TBytes 배열 형태로 전달합니다. 두 인덱스 모두 0부터 시작합니다. 이 속성들을 조회하기 전에 반드시 문서가 열린 상태(Pdf.Active = True)여야 합니다. 닫힌 문서에서 이들을 조회하면 예외 발생 없이 그냥 0 또는 빈 결과값만 반환됩니다

한 가지 주의할 점: Attachment[Index]는 호출될 때마다 메모리에 첨부 파일 데이터 전체를 실시간 할당해 반환한다는 사실입니다. 대용량 첨부 파일이 여러 개 엮인 문서의 경우, 화면에 파일 목록을 단순 표시하기 위해 모든 첨부 파일 데이터를 순회하며 매번 로드하면 메모리가 낭비됩니다. 화면 목록 구성 등에는 AttachmentName 속성을 먼저 읽어 활용하고, 바이트 데이터 로드는 사용자가 해당 파일을 실제로 더블클릭하거나 다운로드 요청했을 때 지연 호출되도록 배제하십시오

procedure ListAttachments(Pdf: TPdf);
var
  I: Integer;
  Data: TBytes;
begin
  if not Pdf.Active then
    Exit;

  for I := 0 to Pdf.AttachmentCount - 1 do
  begin
    Data := Pdf.Attachment[I];
    Writeln(Format('%d: %s (%d bytes)',
      [I, Pdf.AttachmentName[I], Length(Data)]));
  end;
end;

첨부 파일을 디스크 파일로 저장하기

첨부 파일을 곧바로 파일로 저장해 주는 SaveAttachment 형태의 도우미 함수는 제공되지 않습니다. 바이트 데이터를 직접 읽어 들여 수동으로 디스크 파일에 기록해야 하므로, 경로 구성 및 경로 정규화(sanitization) 작업은 개발자의 코드 단에서 직접 처리해야 합니다. 특히 외부에서 유입되어 신뢰성이 검증되지 않은 문서를 가공할 때 이 부분이 대단히 중요합니다. PDF 첨부 파일명은 임의로 작성되어 파일 내부에 저장되는 문자열이므로, 경로 구분 기호나 특수 유니코드 문자 등 TFileStream.Create 호출에 넘겼을 때 비정상적인 동작을 초래할 수 있는 데이터가 섞여 있을 수 있습니다. 출력 경로를 구성하기 전에 항상 ExtractFileName 등을 거쳐 순수 파일명만 발라내 정규화하고, 점(.) 기호로 시작하거나 시스템 규격을 초과하는 무관한 특수문자가 섞인 파일 이름은 거부하는 안전장치를 구현해 두십시오

Attachment[Index]가 반환하는 바이트 배열은 호출자 소유 메모리입니다. 이를 TFileStream 등을 거쳐 파일로 기록해 저장할 수 있으며, 선언된 임의의 파일 확장자명을 맹신하기보다는 저장 전에 바이트 헤더의 첫 몇 바이트를 대조해 실제 파일 포맷 사양이 부합하는지 교차 체크해 볼 수도 있습니다

procedure ExtractAttachment(Pdf: TPdf; Index: Integer; const OutputDir: string);
var
  SafeName: string;
  OutPath: string;
  Data: TBytes;
  FS: TFileStream;
begin
  SafeName := ExtractFileName(Pdf.AttachmentName[Index]);
  if SafeName = '' then
    SafeName := Format('attachment_%d', [Index]);

  OutPath := IncludeTrailingPathDelimiter(OutputDir) + SafeName;
  Data := Pdf.Attachment[Index];

  FS := TFileStream.Create(OutPath, fmCreate);
  try
    if Length(Data) > 0 then
      FS.WriteBuffer(Data[0], Length(Data));
  finally
    FS.Free;
  end;
end;

첨부 파일 추가 및 2단계 연동 구조

첨부 파일을 추가하려면 한 번이 아닌 두 번의 단계를 거쳐야 합니다. 우선 CreateAttachment(Name)을 호출하여 내장 파일 트리에 새 항목 영역을 생성하며, 성공 시 True가 반환됩니다. 이렇게 새로 생성된 파일 공간은 비어 있는 상태입니다. 이후 방금 생성된 타겟 공간을 가리키는 Attachment[AttachmentCount - 1] 인덱스 경로 상에 실제 파일 바이트 데이터를 대입해 채워 넣습니다. 만약 CreateAttachment 결과가 False인데도 데이터를 대입하면 기존의 엉뚱한 마지막 첨부 파일 데이터가 덮어씌워져 유실되는 사고가 나므로 예외 처리를 연동해야 합니다

첨부 파일 목록을 수정한 후 변경된 데이터는 임시 메모리 상에만 보존됩니다. 내장 파일 트리가 온전히 업데이트된 새 PDF 파일로 출력하려면 SaveAs를 최종 호출해 주어야 합니다. PDFium Component는 연동 엔진이 소스 파일에 대한 읽기 파일 핸들을 계속 물고 있으므로, 현재 열려 있는 원본 파일 경로로 덮어쓰기 저장하는 동작을 지원하지 않습니다. 동일 파일 경로로 수정 사항을 갱신하는 일반적인 디자인 패턴은 다음과 같습니다: 즉 데이터를 임시(temp) 경로 파일로 저장한 다음, 문서를 닫고(Active := False), 원본 파일을 삭제하거나 이름을 바꾼 뒤, 임시 파일을 원본 파일 이름으로 갱신(rename)하여 문서 구조를 다시 로드하는 과정을 거치는 것입니다

procedure AddFileAttachment(Pdf: TPdf; const FilePath: string);
var
  FS: TFileStream;
  Data: TBytes;
  AttachName: string;
begin
  if not Pdf.Active then
    Exit;

  FS := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);
  try
    SetLength(Data, FS.Size);
    if FS.Size > 0 then
      FS.ReadBuffer(Data[0], FS.Size);
  finally
    FS.Free;
  end;

  AttachName := ExtractFileName(FilePath);
  if Pdf.CreateAttachment(AttachName) then
    Pdf.Attachment[Pdf.AttachmentCount - 1] := Data;
end;

첨부 파일 형식(MIME Type) 정보

이름 및 바이트 페이로드와 더불어, AttachmentType[Index]는 문서 작성 시점에 함께 기입된 MIME 타입 문자열 정보를 파일 사전 구조로부터 읽어 반환합니다. 많은 PDF 생성기들이 이 필드를 누락시키거나 범용적인 application/octet-stream 형태로 대충 적기 때문에, 상용 가공 파이프라인 단에서 이 속성만으로 파일 형식을 판단하는 것은 위험합니다. 정확한 검증을 위해 바이트 페이로드의 첫 몇 바이트를 직접 파싱해 시그니처를 체크하십시오: 예를 들어 중첩 PDF의 경우 %PDF를, 오피스 문서의 경우 ZIP 압축 시그니처인 PK\x03\x04를, 레거시 바이너리 복합 문서의 경우 \xD0\xCF\x11\xE0을 확인해 보는 형태입니다. 타입 정보 속성은 UI 상의 보조 표기 용도로만 활용하고, 실제 가공 연산 시에는 추출된 실 바이트 정합성 체크를 병행하십시오

첨부 파일 삭제 처리

DeleteAttachment(Index)는 특정 지정 인덱스의 첨부 파일 항목을 파괴하며, 성공 시 True를 반환합니다. 삭제 처리 후 뒤에 있던 남은 항목 인덱스들이 하나씩 앞으로 당겨지므로(shift down), 루프를 돌며 다중 첨부 파일을 제거할 때는 항목이 누락되지 않도록 인덱스 순방향이 아닌 끝 항목부터 시작하는 역방향(downto) 루프를 돌려 삭제해 나가야 합니다. 이 변경사항도 SaveAs를 통해 최종 저장해 주어야 확정됩니다

문서 가공 파이프라인에서 보안 확보나 파일 용량 최적화 등의 목적으로 인입된 PDF 내의 모든 첨부 파일을 일괄 박리하는 처리는 매우 흔하게 쓰입니다. 다음 루프를 적용해 처리해 주십시오:

procedure StripAllAttachments(Pdf: TPdf);
var
  I: Integer;
begin
  for I := Pdf.AttachmentCount - 1 downto 0 do
    Pdf.DeleteAttachment(I);
end;

실무 환경에서의 PDF 첨부 파일 활용 사례

첨부 파일 처리 API는 PDFium이 해독 가능한 모든 PDF 규격을 완벽 수용하나, 실제 상용 실무 상에서 첨부 파일 구조가 빈번하게 활용되는 핵심 시나리오는 다음과 같습니다. 장기 보존 표준인 PDF/A-3(ISO 19005-3) 규격은 아카이빙 데이터 보존용 소스 파일을 PDF 바디 내부에 함께 묶어 보존할 수 있도록 첨부 파일 구조를 공식 허용하고 있습니다. ZUGFeRD 및 Factur-X 전자 송장 기술은 이 기능을 핵심 스펙으로 활용하여, 사용자가 육안으로 볼 수 있는 PDF 지면 디자인 레이아웃 내부에 구조화된 XML 데이터 파일을 묶어 전송합니다. 그 밖에 메일에서 연동 추출된 PDF의 경우 메일 본문의 오리지널 첨부 파일 정보들이 이 구조 상에 같이 녹아 있기도 하며, 테크니컬 디자인 설명서 등에서 원본 CAD 및 소스 리소스 데이터를 PDF 내부에 묶어 아카이빙하는 용도로도 널리 활용됩니다

외부로부터 다량의 PDF 파일을 인입받아 처리하는 자동화 가공 시스템을 설계 중이라면, 파일 진입 필터 단에서 AttachmentCount를 읽어 체크하는 방어적 구성을 적용해 볼 가치가 충분합니다. 첫째, 송장 PDF 내부의 XML 데이터처럼 자동 추출해서 DB화해야 하는 유용한 비즈니스 소스 데이터를 식별해 선별할 수 있습니다. 둘째, 검증되지 않은 첨부 파일 구조 내부에는 악의적인 실행 파일이나 맬웨어 코드 조각이 실려 있을 가능성도 배제할 수 없으므로, 해당 파일을 직접 열어 처리할 계획이 없더라도 첨부 파일 포함 유무 자체를 사전에 식별해 모니터링하기에 적합합니다. 두 조작 모두 어려운 코딩 없이 컴포넌트의 카운트 속성과 파일 이름 확인을 통해 실 바이트 가공 여부를 깔끔하게 연동해 낼 수 있습니다

여기에 소개된 첨부 파일 제어 속성 구성 요소 등은 Delphi 및 C++Builder용 PDFium Component의 표준 내장 사양으로 제공됩니다