PDF 페이지 크기는 페이지가 생성될 때 고정되므로 이미지를 크기 조정하는 것처럼 제자리에서 콘텐츠의 비율을 간단히 조정할 수 없습니다. 축소를 실용적으로 만드는 라이브러리 모델은 캡처 및 다시 그리기(capture-and-redraw)입니다: 각 페이지의 내용을 문서에서 핸들로 추출하고, 원래 미디어 크기로 새로운 빈 페이지를 만든 다음, 캡처된 내용을 축소된 경계 상자에 다시 그립니다. 주변의 공백은 여백이 됩니다. 예를 들어 A4 페이지를 70% 비율로 축소하면 너비의 15%가 양쪽에, 상하에도 같은 비율이 배분되며 이는 바로 아래의 테두리 산술식(border arithmetic)이 만들어내는 결과입니다
CapturePage 작동 방식
CapturePage는 페이지 번호를 가져와 해당 페이지의 콘텐츠를 인메모리 캡처 객체로 승격시키고 문서의 페이지 트리에서 페이지를 제거합니다. 이 제거는 의도적인 것이며, 이것이 반복(iteration) 인덱스에 관계없이 루프가 항상 1페이지를 선택하는 이유입니다: 1페이지가 캡처되고 삭제되면 원래 2페이지였던 것이 새로운 1페이지가 되는 식입니다. 루프 카운터와 함께 페이지 선택기를 증가시키면 한 페이지 건너뛰어 결과적으로 예상 출력의 절반만 얻게 됩니다
CapturePage가 반환하는 캡처 핸들은 페이지 참조가 아니라 콘텐츠 스냅샷에 가깝습니다. 이것은 DrawCapturedPage를 호출하거나 명시적으로 해제할 때까지 유효하게 유지됩니다. DrawCapturedPage는 이 핸들과 함께 왼쪽 오프셋, 아래쪽 오프셋, 너비, 높이(모두 포인트 단위)로 주어진 대상 사각형(destination rectangle)을 받습니다. 라이브러리는 캡처된 콘텐츠를 이 사각형에 정확히 맞게 크기 조정하며, 대상 사각형이 원래 비율과 일치할 때만 가로세로 비율(aspect ratio)을 유지합니다. 일관된 크기 조정을 위해서는 사각형이 원래 크기에 배율 계수를 곱한 크기여야 하며 페이지 중앙에 배치되어야 합니다
중앙 정렬 계산
70% 축소 비율을 적용하면 각 치수의 남은 30%가 양쪽에 똑같이 나누어집니다. 따라서 가로 삽입 여백(inset)은 pageWidth * (1.0 - 0.70) / 2가 되어 너비의 15%이며, 세로 여백은 페이지 높이를 사용하여 동일한 공식을 따릅니다. 그런 다음 DrawCapturedPage의 대상 사각형은 (horizBorder, vertBorder)에서 시작하여 너비 pageWidth - 2 * horizBorder, 높이 pageHeight - 2 * vertBorder에 걸쳐지게 됩니다. 이 산술식은 특정 라이브러리에 국한된 것이 아닙니다. 단순히 큰 사각형 안의 중앙에 작은 사각형을 대칭적으로 맞추는 기하학입니다
한 가지 주목할 점: SetOrigin(1)은 좌표 원점을 왼쪽 아래가 아닌 왼쪽 위로 설정합니다. DrawCapturedPage에 전달하는 테두리 값은 설정한 원점에서부터 측정되므로, 로드와 그리기 사이에 원점 모드를 변경하면 중앙 정렬이 어긋납니다
C# 예제
다음 코드는 Pages.pdf의 모든 페이지를 캡처 및 다시 그리기 주기를 통해 처리하고 결과를 newpages.pdf에 기록합니다. PDFL은 PDFlibDLL64.dll에서 프로젝트에 추가된 ActiveX/COM 래퍼 객체입니다
private void ScalePages_Click(object sender, EventArgs e)
{
File.Delete("newpages.pdf");
double pageWidth, pageHeight, horizBorder, vertBorder;
double scaleFactor = 0.70;
int capturedPageId, ret;
PDFL.LoadFromFile("Pages.pdf", "");
PDFL.SetOrigin(1);
int numPages = PDFL.PageCount();
for (int i = 1; i <= numPages; i++)
{
// 항상 1페이지 선택: CapturePage는 페이지를 제거하므로 다음
// 반복에서 2페이지가 1페이지가 됩니다.
PDFL.SelectPage(1);
pageWidth = PDFL.PageWidth();
pageHeight = PDFL.PageHeight();
horizBorder = pageWidth * (1.0 - scaleFactor) / 2;
vertBorder = pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId = PDFL.CapturePage(1);
PDFL.NewPage();
PDFL.SetPageDimensions(pageWidth, pageHeight);
ret = PDFL.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
}
PDFL.SaveToFile("newpages.pdf");
}
Delphi 예제
Delphi 버전은 COM 계층을 통하지 않고 TPDFlib를 직접 사용하지만, 호출 순서는 동일합니다. 한 가지 실질적인 차이점은 출력 파일에 대한 방어 코드(guard)입니다: 뷰어에서 이전 실행의 결과가 열려 있어 대상이 잠겨 있으면 SaveToFile이 실패하므로, File.Delete 대신 FileExists와 DeleteFile을 함께 사용합니다
procedure TForm1.ScalePagesClick(Sender: TObject);
var
PDFLib: TPDFlib;
pageWidth, pageHeight, horizBorder, vertBorder: Double;
scaleFactor: Double;
capturedPageId, ret, numPages, i: Integer;
begin
if FileExists('newpages.pdf') then
DeleteFile('newpages.pdf');
scaleFactor := 0.70;
PDFLib := TPDFlib.Create;
try
PDFLib.LoadFromFile('Pages.pdf', '');
PDFLib.SetOrigin(1);
numPages := PDFLib.PageCount();
for i := 1 to numPages do
begin
PDFLib.SelectPage(1);
pageWidth := PDFLib.PageWidth();
pageHeight := PDFLib.PageHeight();
horizBorder := pageWidth * (1.0 - scaleFactor) / 2;
vertBorder := pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId := PDFLib.CapturePage(1);
PDFLib.NewPage();
PDFLib.SetPageDimensions(pageWidth, pageHeight);
ret := PDFLib.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
end;
PDFLib.SaveToFile('newpages.pdf');
finally
PDFLib.Free;
end;
end;
비율 값이 실제로 제어하는 것
여기서 0.70이라는 값은 렌더링된 콘텐츠가 각 페이지 치수의 70%를 차지한다는 뜻이지, 파일 크기가 원래 바이트 크기의 70%가 된다는 뜻은 아닙니다. 이 작업 후의 파일 크기는 원본 콘텐츠의 복잡성에 따라 다릅니다. 큰 이미지가 포함된 페이지는 픽셀 데이터가 더 작은 영역에 동일한 해상도로 다시 그려지기 때문에 비례적으로 축소되지 않습니다. 바이트 수준의 압축이 목표라면, 올바른 접근 방식은 기하학적 축소가 아니라 LinearizeFile을 사용하거나 스트림 압축(stream compression)을 사용하여 다시 저장하는 것입니다
또한 70%라는 수치는 엄격한 한계가 아닙니다. 0.0에서 1.0 사이의 모든 값이 작동하며, 1.0을 초과하는 값은 콘텐츠를 원래 페이지 경계 너머로 확대시키며, 페이지 크기도 늘리지 않으면 미디어 상자 가장자리에서 잘립니다. 혼합된 크기의 문서는 PageWidth와 PageHeight가 테두리 계산 전에 페이지별로 조회되기 때문에 자연스럽게 처리됩니다. 따라서 홀수 페이지는 A4이고 짝수 페이지는 A3인 문서는 특별한 조건(special casing) 없이도 각 페이지 크기에서 올바르게 중앙 정렬된 출력을 생성합니다
문제가 발생할 수 있는 상황
실제 사용 시 두 가지 실패 모드가 나타납니다. 첫 번째는 이전 실행에서 남은 출력 파일이 PDF 뷰어에 열려 있는 경우입니다: 플랫폼에 따라 SaveToFile이 실패하거나 0바이트를 쓰게 되며 새 출력은 저장되지 않습니다. 함수 맨 위에 있는 파일 삭제 보호 코드는 개발 중에는 이 문제를 처리하지만, 프로덕션 파이프라인에서는 임시 경로에 기록하고 성공 시 이름 바꾸는 것이 더 안전합니다
두 번째는 페이지 수 불일치입니다. CapturePage는 처리 과정에서 문서에서 페이지를 제거하기 때문에, 루프 전에 PageCount()에서 읽은 수는 순회할 올바른 한계(bound)가 됩니다. 루프 내에서 PageCount()를 호출하면 매 단계마다 감소하는 숫자가 반환되어 일찍 종료되고 마지막 페이지는 처리되지 않은 채로 남게 됩니다. 예제에서의 루프 변수는 남은 반복 횟수를 나타내는 역할만 할 뿐, 앞서 설명한 이유로 선택할 페이지는 항상 1이기 때문에 페이지를 선택하는 데는 절대 사용되지 않습니다
CapturePage, DrawCapturedPage, SetPageDimensions를 포함한 여기에 표시된 페이지 조작 호출은 Delphi, C#, VB.NET 및 C++용 losLab PDF 라이브러리의 일부입니다