문서의 모든 페이지에 워터마크나 로고를 스탬핑하는 것은 결과물을 파일 크기 검사기에서 열어보기 전까지는 5분이면 끝나는 작업처럼 보입니다. 가장 확실한 방법은 페이지를 탐색하면서 각 페이지에 동일한 텍스트나 이미지 객체를 다시 빌드하는 것입니다. 시각적으로는 작동하지만 기하급수적으로 낭비가 심합니다. 100페이지 분량의 보고서에 직접 그려진 대각선 "DRAFT" 워터마크는 콘텐츠 스트림에 있는 100개의 동일한 경로 및 텍스트 데이터 복사본이며, 저장된 파일에는 그 모든 복사본이 포함됩니다
Form XObject는 바로 이러한 상황을 방지하기 위해 PDF에서 제공하는 구조입니다. 재사용 가능한 콘텐츠(전체 페이지 또는 작은 템플릿)를 여러 위치에 여러 번 칠할 수 있는 단일 명명된 객체로 포장합니다. 콘텐츠는 파일에 한 번만 존재합니다. 스탬프를 원하는 각 페이지에는 "여기에 이 변환으로 XObject N을 칠하라"는 짧은 명령이 포함됩니다. 그러면 100페이지 워터마크는 100개가 아닌 파일에 하나의 콘텐츠 개체를 추가하며, 이것이 페이지 수에 따라 선형적으로 증가하는 문서와 그렇지 않은 문서의 차이점입니다. 워터마크, 로고 스탬프, 페이지 번호 템플릿 및 인장은 모두 같은 형태의 문제이며, Form XObject는 이들 모두에 적합한 도구입니다
하나의 저장된 객체가 100번의 다시 그리기보다 나은 이유
저축은 외관상의 것이 아니라 구조적인 것입니다. PDF 페이지는 드로잉 연산자의 시퀀스인 콘텐츠 스트림을 실행하여 렌더링됩니다. 페이지당 스탬프를 다시 그리면 해당 스탬프에 대한 전체 연산자 시퀀스를 모든 페이지의 스트림에 추가하는 것이며 페이지 수만큼 바이트가 중복됩니다. Form XObject는 해당 연산자를 문서에 한 번 저장된 하나의 스트림으로 이동시킵니다. 개별 페이지가 유지하는 참조는 작습니다: 변환 행렬을 푸시하고 XObject를 호출한 다음 상태를 복원합니다. 이제 아트워크 비용은 페이지 수에 곱해지지 않습니다
이것은 스탬프가 무거울 때 가장 중요합니다. 수백 개의 경로 세그먼트가 있는 벡터 씰이나 로고 비트맵은 저장하는 데 비용이 많이 듭니다. 한 번 저장되고 참조되면 무거운 부분의 비용은 단 한 번 지불되며 페이지당 오버헤드는 호출을 위한 몇 바이트입니다. 페이지의 시각적 결과는 직접 다시 그리는 것과 동일하며, 이것이 핵심입니다. 독자는 차이를 구별할 수 없지만, 파일 크기는 큰 차이를 보입니다
페이지를 XObject로 캡처하기
PDFium은 기존 페이지에서 재사용 가능한 개체를 빌드합니다. 소스는 열어 놓은 문서의 페이지, 워터마크 아트워크만 포함된 작은 한 페이지짜리 PDF, 또는 더 큰 파일의 특정 페이지입니다. CreateXObjectFromPage는 해당 원본 페이지의 콘텐츠를 스탬핑 대상 문서에 속하는 재사용 가능한 핸들로 캡처합니다
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := 'Report.pdf';
Dest.Active := True;
Stamp.FileName := 'Watermark.pdf'; // one page of artwork
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be Active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
시그니처는 CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject입니다. 원본 문서가 Active 상태가 아니면 이 메서드는 예외를 발생시키며, PDFium이 객체를 빌드할 수 없을 때는 예외를 발생시키는 대신 nil을 반환하므로 위의 명시적 확인은 선택 사항이 아닙니다. 반환되는 핸들은 여러분이 소유하는 TPdfXObject이며, 여기에 연결된 두 가지 수명 제약 조건은 사람들이 자주 실수하는 부분이므로 아래에 별도 섹션으로 마련했습니다
페이지에 스탬프 배치하기
캡처된 XObject는 자체적으로 아무 작업도 수행하지 않습니다. 문서가 나타나게 하려면 InsertFormObjectFromXObject를 사용하여 1 기반 PageNumber 속성으로 선택된 문서의 현재 페이지에 복사본을 삽입합니다. 이 호출은 기본 페이지 객체인 FPDF_PAGEOBJECT를 반환하며, 반환된 핸들은 배치를 설정하는 방법입니다. 변환이 없으면 스탬프는 원본 페이지 자체 좌표의 원점에 놓이게 되는데, 보통 원하는 위치가 아닙니다
InsertFormObjectFromXObject는 호출당 하나의 복사본을 삽입하고 매번 새 페이지 객체를 반환하기 때문에, 한 페이지에 각기 다른 변환으로 동일한 XObject를 여러 번 칠할 수 있으며, 파일에 저장된 내용은 여전히 한 번만 계산됩니다. 모서리 로고와 희미한 전체 페이지 워터마크는 동일한 캡처된 객체에서 가져올 수 있습니다
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
RawM := M.Handle;
if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
raise Exception.Create('Cannot assign the stamp matrix');
finally
M.Free;
end;
Dest.UpdatePage; // commit this page's edits to its content stream
// if not Dest.SaveAs(...) then ... when every page is done.
end;
두 가지 하우스키핑 세부 사항으로 인해 이것이 안전해집니다. 첫째, 페이지 객체는 한 번 삽입되면 XObject가 아닌 페이지에 속합니다. 나중에 XObject를 해제해도 이미 만든 배치가 무효화되지는 않습니다. 이것이 생성-배치-해제 순서가 작동하도록 하는 요소입니다. 둘째, 삽입 및 위치 지정은 메모리에 있는 페이지의 객체 목록만 변경합니다; UpdatePage는 해당 목록을 페이지의 콘텐츠 스트림으로 다시 직렬화하는 역할을 하므로 이 호출 없이 편집한 페이지는 스탬프가 한 번도 배치되지 않은 것처럼 저장됩니다
사람들을 당황하게 만드는 핸들 수명 규칙
XObject 핸들을 제어하는 두 가지 제약 조건이 있으며, 둘 중 하나를 무시하면 원인과 무관해 보이는 오류가 발생합니다. 첫째, CreateXObjectFromPage를 호출하는 시점에 소스 문서가 활성 상태여야 합니다. 이 캡처는 라이브 원본 문서에서 원본 페이지의 콘텐츠를 읽어오기 때문에 핸들이 빌드될 때 해당 문서와 페이지가 열려 있고 유효해야 합니다. 둘째, 이것이 사람들을 놀라게 하는 부분인데, 원본 페이지를 닫기 전, 그리고 실제로는 원본 문서를 닫거나 해제하기 전에 핸들을 해제해야 합니다
그 이유는 XObject가 원본 문서가 계속 소유하고 있는 구조에 대한 참조이기 때문입니다. 소스가 사라진 후 휴대할 수 있는 분리되고 독립적인 복사본이 아닙니다. 원본을 먼저 닫으면 핸들이 이미 해제된 콘텐츠를 가리키게 되므로, 나중에 해제하거나 사용하는 등의 작업은 더 이상 유효하지 않은 메모리에서 작동합니다. 증상은 매달려 있는(dangling) 핸들의 전형적인 현상입니다: 종료 시 액세스 위반이나 할당 순서에 따라 이동하는 간헐적 손상 등이 나타나며, 스택은 문제를 일으킨 줄 대신 정리 코드를 가리킵니다. 해결책은 방어적인 코딩이 아니라 순서에 있습니다. XObject를 빌드하고 이를 필요로 하는 모든 페이지에 삽입한 다음 XObject를 해제하고 그 이후에만 원본 문서를 닫습니다. TPdfXObject 소멸자는 기본 PDFium 핸들을 대신 해제해 주므로 제때 래퍼를 해제하는 것만이 사용자의 책임입니다
행렬과 여섯 개 숫자의 의미
배치는 PDF가 모든 위치 지정 콘텐츠에 사용하는 것과 동일한 2D 아핀(affine) 변환입니다(ISO 32000-1, 8.3.4절). 이는 a, b, c, d, e, f로 표기되는 6개의 숫자이며, PDFium은 이를 FS_MATRIX 레코드로 노출합니다. 이들은 객체의 자체 공간에서 페이지 공간으로 점을 매핑합니다:
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
이 여섯 가지 값을 직접 채울 수 있지만, 손으로 구성하는 것은 회전이 잘못되는 부분입니다. 회전은 a, b, c, d 네 가지 모두를 혼합하기 때문입니다. FPdfMatrix 유닛의 TPdfMatrix 래퍼는 흔한 연산을 조합하고 진행하면서 사후 곱셈을 수행하므로 Translate, Scale, Rotate는 호출한 순서대로 체인됩니다. 대각선 워터마크는 회전 후 중앙으로 오게 하는 변환입니다; 모서리 로고는 스케일 후 변환입니다. 매트릭스가 준비되면 FS_MATRIX 타입의 Handle 속성이라는 원시 값을 로컬 변수에 복사하여 FPDFPageObj_SetMatrix에 전달하세요; 가져오기에서는 행렬을 var 매개변수로 선언하므로 속성을 직접 전달할 수 없으며 실패 시 결과는 0입니다. 6개의 값을 더블(doubles)로 직접 취하는 하위 수준의 FPDFPageObj_Transform은 래퍼를 작성하는 대신 숫자를 직접 전달할 때 사용할 수 있습니다
올바른 순서로 모든 페이지 스탬프 찍기
전체 패턴은 수명 규칙이 요구하는 순서에 따라 조각들을 조합합니다. 두 문서 모두를 열고, 스탬프를 한 번 캡처하고, 차례로 1 기반 PageNumber를 설정하여 대상 페이지를 이동하고 사본을 삽입하고 배치한 다음, 각 페이지를 UpdatePage로 커밋하고, 그런 다음 XObject를 해제하고, SaveAs로 저장한 후 소스 문서를 가장 마지막에 닫도록 합니다
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
I: Integer;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := ASource;
Dest.Active := True;
Stamp.FileName := AStamp;
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// 1. Capture the artwork once. Stamp is Active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest. PageNumber is 1-based.
for I := 1 to Dest.PageCount do
begin
Dest.PageNumber := I; // make page I current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
RawM := M.Handle;
FPDFPageObj_SetMatrix(PageObj, RawM);
finally
M.Free;
end;
Dest.UpdatePage; // commit this page's edits
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
if not Dest.SaveAs(AOutput) then
raise Exception.Create('Could not save ' + AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
try 블록의 형태가 실제 작업을 수행하고 있습니다. 내부의 finally는 제어 권한이 Stamp를 해제하는 외부의 finally에 닿기 전에 XObject를 해제하므로, 중간 루프에서 예외가 발생하더라도 해당 소스가 여전히 활성 상태인 동안 핸들이 항상 해제됩니다. 이 중첩을 올바르게 하면 수명 규칙은 저절로 처리됩니다
스탬프 찍기는 페이지 콘텐츠를 구성하고 편집하는 더 큰 툴킷의 한 부분입니다. 스탬프 자체가 캡처된 페이지가 아니라 이미지인 경우 PDFium으로 이미지를 PDF 문서로 변환에서 해당 비트맵을 문서에 먼저 넣는 방법을 다룹니다. 표시되는 스탬프와 함께 전달하고 싶은 것이 페이지의 잉크가 아니라 파일인 경우 델파이에서 PDF 첨부 파일로 작업하기에서 삽입된 파일 측면을 보여줍니다. 이 모든 것은 다른 블로그 포스트에서 다루는 렌더링, 편집 및 문서 API와 함께 Delphi 및 C++Builder용 PDFium Component와 함께 제공됩니다