PDFium Component는 PDF 분할을 위한 단일 메서드인 ImportPages를 제공합니다. 단일 페이지를 격리하든, 임의의 경계에서 자르든, 문서 자체의 책갈피 구조를 따르든 기타 모든 작업은 각 출력 파일에 들어갈 페이지 번호를 결정하는 여러 가지 방법에 지나지 않습니다. 메커니즘은 동일하게 유지됩니다. 이를 일찍 이해하면 많은 잘못된 시행착오를 줄일 수 있습니다
분할 루프의 작동 방식
소스 문서를 어떻게 나누든 패턴은 동일합니다. 새 TPdf 인스턴스를 만들고 CreateDocument를 호출하여 메모리에 빈 PDF를 초기화한 다음 ImportPages를 사용하여 원하는 페이지를 가져와 결과를 저장한 후, 다음 반복을 시작하기 전에 Active를 False로 재설정합니다. 마지막 단계는 사람들이 놓치는 부분입니다: 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를 초과하면 ImportPages는 False를 반환하지만 예외를 발생시키지 않으며 이름만으로는 감지할 수 없는 부분적인 출력 파일을 생성하지도 않습니다. 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 조건 가드가 이 상황을 조용히 처리합니다. 표면화할 가치가 있는 것은 책갈피의 페이지 번호가 문서 범위 밖에 있는 경우인데, 페이지가 삭제된 후 윤곽선이 업데이트되지 않은 잘못된 형태의 파일에서 발생합니다. StartPage와 EndPage에 대한 경계 검사는 가비지 범위를 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페이지의 조용한 실패는 쉽게 간과될 수 있습니다. 각 호출의 반환 값을 확인하고 루프가 끝났을 때 예상 총계와 비교하여 성공 횟수를 세십시오
여기에 설명된 ImportPages 및 CreateDocument 메서드는 Delphi 및 C++Builder용 PDFium Component의 일부입니다