기술 문서

델파이에서 PDFium 컴포넌트를 사용하여 PDF 문서를 여러 파일로 분할하기

PDFium Component는 PDF 분할을 위한 단일 메서드인 ImportPages를 제공합니다. 단일 페이지를 격리하든, 임의의 경계에서 자르든, 문서 자체의 책갈피 구조를 따르든 기타 모든 작업은 각 출력 파일에 들어갈 페이지 번호를 결정하는 여러 가지 방법에 지나지 않습니다. 메커니즘은 동일하게 유지됩니다. 이를 일찍 이해하면 많은 잘못된 시행착오를 줄일 수 있습니다

분할 루프의 작동 방식

소스 문서를 어떻게 나누든 패턴은 동일합니다. 새 TPdf 인스턴스를 만들고 CreateDocument를 호출하여 메모리에 빈 PDF를 초기화한 다음 ImportPages를 사용하여 원하는 페이지를 가져와 결과를 저장한 후, 다음 반복을 시작하기 전에 ActiveFalse로 재설정합니다. 마지막 단계는 사람들이 놓치는 부분입니다: CreateDocument는 메모리에 여전히 열려 있는 문서를 암시적으로 닫지 않으므로 출력물을 저장하고 명시적으로 Active := False를 재설정한 후에 다시 호출해야 합니다; 먼저 재설정하면 상태가 깨끗하게 정의된 상태로 유지됩니다. 외부 TPdf 인스턴스는 모든 반복에서 재사용되므로 대규모 작업에 대한 할당 압력을 낮게 유지합니다

페이지 단위 분할의 핵심은 다음과 같습니다:

procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 1 to Source.PageCount do
    begin
      PdfOut.CreateDocument;

      // Range is a 1-based page number string; insertion point 1 = first position
      if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
        raise Exception.CreateFmt('Failed to import page %d', [I]);

      OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);

      PdfOut.Active := False;   // reset before next CreateDocument
    end;
  finally
    PdfOut.Free;
  end;
end;

ImportPages에 대한 Range 매개변수는 PDFium이 내부적으로 사용하는 것과 동일한 문자열 형식입니다: 쉼표로 구분된 페이지 번호 목록 또는 하이픈으로 구분된 범위이며 모두 1 기반입니다. '3'은 페이지 3을 가져옵니다. '1-5'는 페이지 1부터 5까지 순서대로 가져옵니다. '2,5,8'은 이 세 페이지를 가져옵니다. 세 번째 매개변수는 대상 문서 내 1 기반의 삽입 위치입니다; 1을 전달하면 가져온 페이지는 항상 이전에 비어 있던 파일의 맨 앞에 배치되며, 여기서 원하는 동작입니다

페이지 범위로 분할

호출자가 1-12,13-24,25-36과 같은 목록을 제공하면 시작/끝 쌍으로 구문 분석하고 동일한 루프를 실행하여 각 쌍에서 범위 문자열을 구성합니다:

procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
  const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(RangeList) do
    begin
      PdfOut.CreateDocument;
      if not PdfOut.ImportPages(Source, RangeList[I], 1) then
        raise Exception.Create('Invalid page range: ' + RangeList[I]);
      OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);
      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

여기서 ImportPages에 도달하기 전의 유효성 검사가 중요합니다. 범위 문자열의 페이지 번호가 Source.PageCount를 초과하면 ImportPagesFalse를 반환하지만 예외를 발생시키지 않으며 이름만으로는 감지할 수 없는 부분적인 출력 파일을 생성하지도 않습니다. SaveAs의 반환 값을 확인하고 실패를 별도로 기록하세요; 빈 출력 파일을 생성하는 범위는 누군가 열어보기 전까지는 명백하게 잘못된 것이 아닙니다

책갈피 경계에서 분할

세 번째 접근 방식은 외부에서 제공한 목록 대신 문서 자체의 구조를 사용합니다. 각 최상위 책갈피에는 대상 페이지 번호가 포함됩니다. 정의된 섹션은 해당 페이지부터 다음 책갈피 페이지 이전까지 또는 마지막 항목의 경우 문서 끝까지 실행됩니다

procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
  Bm: TBookmarks;
  I, StartPage, EndPage: Integer;
  PdfOut: TPdf;
  RangeStr, OutFile, SafeTitle: string;
begin
  Bm := Source.Bookmarks;
  if Length(Bm) = 0 then
    Exit;

  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(Bm) do
    begin
      StartPage := Bm[I].PageNumber;
      if I < High(Bm) then
        EndPage := Bm[I + 1].PageNumber - 1
      else
        EndPage := Source.PageCount;

      if (StartPage < 1) or (EndPage < StartPage) then
        Continue;

      RangeStr := Format('%d-%d', [StartPage, EndPage]);

      PdfOut.CreateDocument;
      if not PdfOut.ImportPages(Source, RangeStr, 1) then
      begin
        PdfOut.Active := False;
        Continue;   // skip a malformed section instead of writing an empty file
      end;

      SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
      SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
      OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);

      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

책갈피가 없는 문서는 사용자에게 오류 조건으로 표시할 만한 가치가 없습니다. 이 분할 모드가 작업할 내용이 없다는 뜻일 뿐입니다. Length(Bm) = 0 조건 가드가 이 상황을 조용히 처리합니다. 표면화할 가치가 있는 것은 책갈피의 페이지 번호가 문서 범위 밖에 있는 경우인데, 페이지가 삭제된 후 윤곽선이 업데이트되지 않은 잘못된 형태의 파일에서 발생합니다. StartPageEndPage에 대한 경계 검사는 가비지 범위를 ImportPages에 전달하는 대신 해당 항목을 건너뜁니다

출력 파일 이름 지정 및 Active 재설정

책갈피 파생 이름에 대한 파일 이름 안전성을 명시적으로 주의해야 합니다. 책갈피 제목에는 PDF 문자열에서는 유효하지만 파일 시스템 경로에서는 사용할 수 없는 문자가 포함될 수 있습니다. 최소한 출력 경로를 작성하기 전에 슬래시, 백슬래시 및 콜론을 바꾸십시오. Windows에서는 *, ?, ", <, >, |도 금지됩니다; 정규식(regex)을 가져올 필요 없이 고정된 문자 세트에 대한 간단한 루프가 이를 처리합니다

각 반복 끝에 있는 Active := False 줄은 이 패턴에서 유일하게 명확하지 않은 요구 사항이므로 강조할 가치가 있습니다. CreateDocument는 열려 있는 것을 암시적으로 닫지 않습니다. CreateDocument가 다시 실행될 때 Active가 여전히 True인 경우 메모리에 아직 있는 문서가 제대로 닫히거나 저장된 적이 없으며, 이 상태에서는 잘 정의된 동작을 기대할 수 없으므로 다음 문서를 시작하기 전에 명시적으로 저장하고 재설정하십시오. try/finally 쌍과 같은 것으로 생각하십시오: finally 블록은 외부 개체를 해제하고, Active := False는 루프 반복 간에 내부 문서 상태를 재설정합니다

이 접근 방식을 사용하면 한 번에 하나 이상의 출력 문서를 메모리에 보관하지 않기 때문에 대규모 분할 작업 전반의 메모리 사용이 일정하게 유지됩니다. 원본 문서는 시종일관 열려 있고 읽기 전용 상태로 유지됩니다. ImportPages는 원본을 수정하지 않고 새 문서에 페이지 데이터를 복사합니다. 소스가 암호화된 경우 루프 전에 해당 암호로 열면 각 출력 파일에 복사된 페이지가 암호 해제됩니다. 이는 각기 다른 수신자에게 분배되는 분할 출력에 보통 적합한 동작입니다

SaveAs에 대해 짚고 넘어갈 점이 한 가지 더 있습니다: 이 메서드는 Boolean을 반환합니다. 존재하지 않는 출력 디렉터리, OS가 거부하는 문자가 포함된 경로, 또는 디스크가 가득 찬 상황에서는 모두 SaveAs가 예외를 발생시키지 않고 False를 반환하게 됩니다. 200페이지 분량의 문서를 200개의 단일 페이지 파일로 분할하는 일괄 작업에서 147페이지의 조용한 실패는 쉽게 간과될 수 있습니다. 각 호출의 반환 값을 확인하고 루프가 끝났을 때 예상 총계와 비교하여 성공 횟수를 세십시오

여기에 설명된 ImportPagesCreateDocument 메서드는 Delphi 및 C++Builder용 PDFium Component의 일부입니다