기술 문서

losLab PDF 라이브러리를 사용하여 PDF 페이지를 70%로 축소하기

PDF 페이지 크기는 페이지가 생성될 때 고정되므로 이미지를 크기 조정하는 것처럼 제자리에서 콘텐츠의 비율을 간단히 조정할 수 없습니다. 축소를 실용적으로 만드는 라이브러리 모델은 캡처 및 다시 그리기(capture-and-redraw)입니다: 각 페이지의 내용을 문서에서 핸들로 추출하고, 원래 미디어 크기로 새로운 빈 페이지를 만든 다음, 캡처된 내용을 축소된 경계 상자에 다시 그립니다. 주변의 공백은 여백이 됩니다. 예를 들어 A4 페이지를 70% 비율로 축소하면 너비의 15%가 양쪽에, 상하에도 같은 비율이 배분되며 이는 바로 아래의 테두리 산술식(border arithmetic)이 만들어내는 결과입니다

PDF 캡처 후 재그리기 파이프라인: 각 PDF 페이지가 핸들로 캡처되고, 원본 미디어 크기를 유지한 새 빈 페이지 위에 축소되어 그려진다
캡처 후 재그리기는 각 페이지의 콘텐츠를 핸들로 들어 올리고, 원본 크기의 새 페이지 위에 축소된 크기로 다시 그립니다

CapturePage 작동 방식

CapturePage는 페이지 번호를 가져와 해당 페이지의 콘텐츠를 인메모리 캡처 객체로 승격시키고 문서의 페이지 트리에서 페이지를 제거합니다. 이 제거는 의도적인 것이며, 이것이 반복(iteration) 인덱스에 관계없이 루프가 항상 1페이지를 선택하는 이유입니다: 1페이지가 캡처되고 삭제되면 원래 2페이지였던 것이 새로운 1페이지가 되는 식입니다. 루프 카운터와 함께 페이지 선택기를 증가시키면 한 페이지 건너뛰어 결과적으로 예상 출력의 절반만 얻게 됩니다

CapturePage가 돌려주는 캡처 핸들은 페이지 참조가 아니라 콘텐츠 스냅샷에 가깝습니다. 그리기 후에도 유효하게 남아 있으므로 같은 캡처를 ReleaseCapturedPage로 해제하거나 문서가 사라지기 전까지 여러 번 놓을 수 있습니다. DrawCapturedPage는 그 핸들과 함께 왼쪽 가장자리, 위쪽 가장자리, 너비, 높이로 주어진 대상 사각형을 받으며, 기본값은 전부 포인트입니다. 라이브러리는 캡처된 콘텐츠를 그 사각형에 정확히 맞추고, 사각형이 우연히 원본 비율과 일치할 때만 종횡비를 유지합니다. 균등 스케일을 원한다면 사각형은 원본 크기에 스케일 계수를 곱하고 페이지 중앙에 놓인 것이어야 합니다

중앙 정렬 계산

70% 축소 비율을 적용하면 각 치수의 남은 30%가 양쪽에 똑같이 나누어집니다. 따라서 가로 삽입 여백(inset)은 pageWidth * (1.0 - 0.70) / 2가 되어 너비의 15%이며, 세로 여백은 페이지 높이를 사용하여 동일한 공식을 따릅니다. 그런 다음 DrawCapturedPage의 대상 사각형은 (horizBorder, vertBorder)에서 시작하여 너비 pageWidth - 2 * horizBorder, 높이 pageHeight - 2 * vertBorder에 걸쳐지게 됩니다. 이 산술식은 특정 라이브러리에 국한된 것이 아닙니다. 단순히 큰 사각형 안의 중앙에 작은 사각형을 대칭적으로 맞추는 기하학입니다

70% 스케일에서 대상 사각형이 원본 페이지 중앙에 놓이고 모든 변마다 15% 여백이 계산되는 모습
70%로 축소하면 변마다 15% 여백이 남고, DrawCapturedPage는 그 중앙 정렬된 사각형을 포인트로 받습니다

한 가지 짚을 것이 있습니다: SetOrigin(1)은 좌표 원점을 좌하단이 아니라 좌상단에 놓습니다. DrawCapturedPage에 넘기는 두 번째 값은 설정한 원점 어느 쪽에서든 측정한 사각형의 위쪽 가장자리입니다. 예제들이 먼저 SetOrigin(1)을 부르는 이유가 이것입니다. 기본 좌하단 원점에서는 같은 vertBorder가 위쪽 가장자리를 페이지 아래쪽 근처에 놓아 대부분의 콘텐츠를 페이지 밖으로 밀어 냅니다

C# 예제

다음 코드는 Pages.pdf의 모든 페이지를 캡처 후 재그리기 주기로 처리해 결과를 newpages.pdf에 씁니다. PDFL은 DLL 에디션이 함께 제공하는 PDFlibrary 임포트 클래스의 인스턴스로, new PDFlibrary("PDFlibDLL64.dll")로 만듭니다. ActiveX 에디션은 같은 메서드를 PDFlibAX.PDFlibrary로 노출합니다

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);
        // 0 = 캡처된 것이 없음. 예컨대 한 페이지 소스: 유일한 페이지는 제거할 수 없다
        if (capturedPageId == 0)
            throw new InvalidOperationException("CapturePage failed on source page " + i);

        PDFL.NewPage();
        PDFL.SetPageDimensions(pageWidth, pageHeight);

        ret = PDFL.DrawCapturedPage(
            capturedPageId,
            horizBorder, vertBorder,
            pageWidth  - 2 * horizBorder,
            pageHeight - 2 * vertBorder);
        if (ret != 1)
            throw new InvalidOperationException("DrawCapturedPage failed on source page " + i);
    }

    PDFL.SaveToFile("newpages.pdf");
}

Delphi 예제

Delphi 버전은 DLL 임포트 클래스를 거치지 않고 TPDFlib을 직접 쓰지만 호출 순서는 동일합니다. 실질적 차이 하나는 출력 파일 가드입니다. File.Delete 대신 FileExists와 DeleteFile을 씁니다. 뷰어에 아직 열린 채 잠긴 이전 실행의 파일이 있으면 SaveToFile이 실패하기 때문입니다

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);
      // 0 = 캡처된 것이 없음. 예컨대 한 페이지 소스: 유일한 페이지는 제거할 수 없다
      if capturedPageId = 0 then
        raise Exception.CreateFmt('CapturePage failed on source page %d', [i]);

      PDFLib.NewPage();
      PDFLib.SetPageDimensions(pageWidth, pageHeight);

      ret := PDFLib.DrawCapturedPage(
        capturedPageId,
        horizBorder, vertBorder,
        pageWidth  - 2 * horizBorder,
        pageHeight - 2 * vertBorder);
      if ret <> 1 then
        raise Exception.CreateFmt('DrawCapturedPage failed on source page %d', [i]);
    end;

    PDFLib.SaveToFile('newpages.pdf');
  finally
    PDFLib.Free;
  end;
end;

비율 값이 실제로 제어하는 것

여기서 0.70은 렌더링된 콘텐츠가 각 페이지 차원의 70%를 차지한다는 뜻이지, 파일이 원본 바이트 크기의 70%가 된다는 뜻이 아닙니다. 이 연산 후의 파일 크기는 원본 콘텐츠의 복잡도에 달려 있습니다. 큰 이미지를 담은 페이지는 픽셀 데이터가 같은 해상도로 더 작은 영역에 다시 그려지므로 비례적으로 줄지 않습니다. 작은 파일이 목표라면 기하 스케일링이 아니라, 압축 없이 나갔던 페이지 콘텐츠 스트림의 CompressContent와 이미지 재압축(Delphi 에디션의 OptimizeImages)을 보십시오. LinearizeFile은 다른 도구입니다. 한 번에 한 페이지씩 로딩(Fast Web View)을 위해 파일을 재배열하며, 네트워크 위에서 뷰어가 큰 파일을 여는 데는 도움이 되지만 줄이지는 않습니다

또한 70%라는 수치는 엄격한 한계가 아닙니다. 0.0에서 1.0 사이의 모든 값이 작동하며, 1.0을 초과하는 값은 콘텐츠를 원래 페이지 경계 너머로 확대시키며, 페이지 크기도 늘리지 않으면 미디어 상자 가장자리에서 잘립니다. 혼합된 크기의 문서는 PageWidth와 PageHeight가 테두리 계산 전에 페이지별로 조회되기 때문에 자연스럽게 처리됩니다. 따라서 홀수 페이지는 A4이고 짝수 페이지는 A3인 문서는 특별한 조건(special casing) 없이도 각 페이지 크기에서 올바르게 중앙 정렬된 출력을 생성합니다

문제가 발생할 수 있는 상황

실제 사용 시 두 가지 실패 모드가 나타납니다. 첫 번째는 이전 실행에서 남은 출력 파일이 PDF 뷰어에 열려 있는 경우입니다: 플랫폼에 따라 SaveToFile이 실패하거나 0바이트를 쓰게 되며 새 출력은 저장되지 않습니다. 함수 맨 위에 있는 파일 삭제 보호 코드는 개발 중에는 이 문제를 처리하지만, 프로덕션 파이프라인에서는 임시 경로에 기록하고 성공 시 이름 바꾸는 것이 더 안전합니다

두 번째는 페이지 개수 불일치입니다. CapturePage가 처리하는 대로 문서에서 페이지를 제거하므로, 루프 전에 PageCount()에서 읽은 개수가 반복 대상으로서 올바른 상한입니다. 루프 안에서 PageCount()를 다시 불러 봤자 소용이 없습니다. 매 패스가 한 페이지를 제거하고 한 페이지를 추가하므로 개수는 그대로이며, 소스 페이지가 몇 개 남았는지 아무 것도 말해 주지 않습니다. 예제의 루프 변수는 남은 반복 횟수 카운터로만 쓰입니다. 페이지 선택에는 결코 쓰이지 않습니다. 선택할 페이지는 앞서 설명한 이유로 언제나 1이기 때문입니다

매 패스가 SelectPage(1)을 불러야 하는 이유를 보여주는 루프 다이어그램: 캡처된 페이지가 트리에서 제거되고 남은 페이지들이 앞으로 당겨진다
CapturePage가 처리된 페이지를 하나하나 제거하므로 대기열이 이름을 다시 짓고, 페이지 1만이 올바른 선택 값이 됩니다

여기서 보여 준 페이지 조작 호출 — CapturePage, DrawCapturedPage, SetPageDimensions를 포함해 — 는 Delphi, C#, VB.NET, C++용 losLab PDF Library의 일부입니다