기술 문서

Delphi PDF에서의 페이지 스티칭 및 워터마크 템플릿

페이지 스티칭(stitching)은 기존 페이지들의 *콘텐츠*를 하나의 공유 캔버스 위에 합성하여 구성하는 기술로, 여러 파일을 하나로 물리적 병합하는 merge 작업과는 다른 성격을 띱니다. losLab PDF Library는 Delphi를 위해 Form XObject 기반 페이지 스티칭 및 조합형 워터마크 템플릿 기능을 구현해 제공합니다: 즉 StitchPageOverlay, StitchPagesSideBySide, 및 StitchPagesVertically는 캡처된 페이지를 완전한 벡터 정밀도와 자유로운 크기 비율로 지면에 배치하며, TPDFlibCompositeWatermark는 단 한 번의 호출로 특정 페이지 범위 전체에 텍스트 및 이미지 워터마크를 다중으로 얹어줍니다. 여러 문서의 전체 페이지들을 단순히 하나의 파일 뒤에 덧붙여 병합해야 한다면, 이는 초고속 바이트 레벨 PDF 병합 기사에서 다루는 merge 기능군에 해당합니다; 본 기사에서는 동일한 하나의 좌표계 공간 내에서 다중 페이지 콘텐츠를 조율하고 병합하는 기술을 주로 다룹니다

이 기능을 요구하는 상용 실무 시나리오는 매우 명확하고 흔합니다. 예를 들어 학교에서 시험지 인쇄 용지를 절반으로 아끼기 위해 가로 형태의 종이 한 장에 두 페이지 분량의 시험 내용을 좌우 배치(2-up)하려는 경우, 다국어 번역 검수자가 원본 영문 페이지와 국문 번역 페이지를 한 화면 상에 가로로 나란히 놓고 스크롤하며 대조하려는 경우, 법무 팀에서 초안 문서의 1~3페이지에만 로고 이미지(우측 상단), 반투명 대각선 글자 'CONFIDENTIAL'(중앙), 소형 안내 문구(하단 중앙) 등 3가지 워크마크 레이어를 동시에 찍어 배포하려는 경우 등입니다. 이 세 가지 요구 사항 모두 이미 조판이 완료된 페이지 콘텐츠를 비트맵 이미지로 화질을 열화시키지 않고, 원하는 위치와 임의의 배율 크기로 안전하게 재배치해야 하는 동일한 구조적 문제 해결법을 요구합니다

PDF 콘텐츠 스트림을 단순히 합치면 오작동하는 이유는 무엇입니까?

단순히 원본 페이지의 /Contents 스트림 내용을 대상 페이지로 복사해 합치는 소박한 시도는 다음의 3가지 구조적 원인에 의해 실패합니다. 첫째, 좌표계의 충돌입니다: 두 콘텐츠 스트림 모두 각자의 MediaBox 좌측 하단 모퉁이를 고유 원점(0,0)으로 가정하고 작성되었으므로, 붙여넣은 콘텐츠가 원본 지면 데이터 바로 위에 비대칭으로 엉뚱하게 겹쳐 렌더링됩니다. 둘째, 리소스 이름의 충돌입니다: 각 페이지는 /F1 Tf/Im0 Do와 같은 드로잉 연산자들을 자체 /Resources 사전 구조에 맵핑해 해석하므로, 두 페이지가 우연히 동일한 /F1 기호를 서로 다른 폰트 정보에 연결해 둔 상태라면 병합 시 한쪽 텍스트가 엉뚱한 폰트로 오 드로잉됩니다. 셋째, 단순 스트림 복사로는 배율 조절(scale) 처리가 불가능하며, 고작해야 위치 이동(translate)만 수행할 수 있기 때문입니다

Form XObject 기술은 이 세 가지 난제를 동시에 해결해 주며, 이것이 losLab PDF Library가 페이지 스티칭 구현에 이를 기본 채택한 이유입니다. ISO 32000-1 §8.10은 Form XObject를 자체 /Resources 사전 및 고유 좌표 공간을 내포하고 있으며 현재 변환 매트릭스(CTM) 상에서 이름 호출 방식으로 배치해 렌더링하는 격리된 독립형 콘텐츠 스트림으로 규정합니다. CapturePage는 기존 페이지 전체(콘텐츠와 리소스 포함)를 /Subtype /Form XObject로 패키징 캡처하며, 스티칭 함수들은 이후 이를 cm 행렬 연산자(배율, 이동, 회전 등)를 통해 원하는 곳에 대입합니다. 원본의 벡터 데이터 성질이 그대로 계존되므로 확대 시에도 텍스트와 벡터 라인이 픽셀 깨짐 없이 매끄럽게 유지되며, 이는 벡터 그래픽 및 셰이딩 패턴이 해상도 독립성을 갖는 것과 동일한 원리입니다. 어떠한 래스터화(rasterization) 처리도 유발되지 않으며, 완전히 격리된 리소스 사전을 취하므로 캡처된 페이지가 타겟 지면 데이터와 /F1과 같은 식별자 이름을 두고 리소스 충돌을 일으키지 않습니다

두 개의 PDF 페이지를 어떻게 한 페이지에 결합합니까?

StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) 호출이 직관적인 해결책입니다: 두 원본 페이지를 캡처하여 더 넓은 폭의 새로운 대상 페이지를 생성한 후, 두 캡처본을 좌우로 나열해 가운데에 여백(gutter)을 두고 배치하며 각각 TargetHeight 크기에 맞춰 비율을 자동 조절합니다. 이 함수는 성공 시 1부터 시작하는 새 페이지 번호를 반환하며 실패 시 0을 반환합니다. 원본이 되었던 두 페이지는 캡처 후 문서 뷰에서 보이지 않도록 숨김 처리됩니다. 시험지 좌우 병합(2-up) 처리는 단 3줄의 연산으로 종결됩니다

var
  Lib: TPDFlib;
  NewPage: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('exam.pdf', '');
    // Pages 1 and 2 on one new wider page with an 18pt gutter,
    // both scaled to a 595pt output height (A4 landscape)
    NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
    if NewPage > 0 then
      Lib.SaveToFile('exam-2up.pdf');
  finally
    Lib.Free;
  end;
end;

지면을 상하 수직으로 병합하려면 StitchPagesVertically(PageRanges, Gap, TargetWidth) 호출을 통해 지정한 페이지 범위 전체를 세로로 긴 단일 포스터형 페이지로 병합할 수 있습니다. PageRanges 인수에는 '1-3,5'와 같은 표준 범위 표기 문자열을 대입합니다. 각 원본 페이지들은 가로폭이 TargetWidth 크기에 일치하도록 자동 비례 조절되며, 최종 출력되는 합산 페이지 세로 크기는 비례 조절된 각 페이지별 세로 길이의 합계에 지정한 간격(Gap) 수치를 더해 자동으로 산출됩니다. 이 기능은 연속 스크롤 형태의 검수용 지면을 구성하거나 표준 레터지 규격으로 잘린 수많은 데이터 지면들을 하나의 세로로 긴 영수증 형태로 묶어 출력할 때 요긴하게 쓰입니다

한 페이지를 다른 페이지 위에 어떻게 오버레이합니까?

StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity)는 새 페이지를 생성하지 않고, 기존에 존재하던 특정 페이지 데이터를 다른 대상 페이지 영역 위에 직접 드로잉해 칠해 줍니다. 원본 페이지는 Form XObject로 패키징 캡처되어 대상 페이지의 지정 좌표 사각형 영역 상에 투명도(Opacity는 0부터 1까지 설정 가능, 1은 불투명)가 연동되어 배치됩니다. 가로폭과 세로폭은 임의 지정이 가능하므로 원본 종횡비와 상이하게 지정하면 이미지가 늘어날 수 있습니다; 찌그러짐 없는 썸네일 형태의 깔끔한 배치 처리를 희망한다면 비례 관계에 맞는 크기 수치를 대입하십시오. 결재선 서명 정보가 담긴 페이지 영역만 따로 발라내 문서 표지 영역 등에 도장 형태로 얹어 처리할 때 흔히 사용됩니다

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('report.pdf', '');
    // Page 5 holds the approval block; place it on page 1
    // in a 260 x 180 box at 45% opacity. Page 5 is consumed:
    // it is captured and hidden, so pages 6..n become 5..n-1
    if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
      Lib.SaveToFile('report-approved.pdf');
  finally
    Lib.Free;
  end;
end;

캡처 후의 페이지 인덱스(번호) 변화 분석

CapturePage 연산은 원본 페이지를 소비(consume)합니다: 즉 원본 콘텐츠가 XObject 내부로 완전히 이동되고 껍데기만 남은 원래 페이지는 문서 뷰 상에서 숨김(hide) 처리되므로, 캡처된 원본 페이지 번호보다 뒤쪽에 정렬되어 있던 모든 페이지들의 번호 인덱스가 즉시 하나씩 당겨집니다(shift down). 이는 스티칭 API 다중 호출 시 가장 주의해야 할 연동 원칙입니다. losLab PDF Library는 각 스티칭 함수 내부에서 이 인덱스 변화를 연동해 보정해 주며, StitchPageOverlay 호출 시 대상 타겟 인덱스가 원본 인덱스보다 높은 번호인 경우 보정을 가하고, StitchPagesSideBySide는 인덱스가 뒤틀리지 않도록 더 큰 인덱스 번호를 가진 페이지부터 역순으로 내부 캡처를 수행하지만, 다중 호출 코드를 직접 짜는 경우에는 개발자가 이 인덱스 변화 추이를 감안해야 합니다. 오류 없는 다중 캡처 작성을 위한 2가지 조언이 있습니다. 첫째, 캡처 시작 *전*에 활용해야 할 모든 페이지들의 크기 수치나 인덱스 번호 리스트를 사전에 변수에 따로 담아 보존해 활용하십시오. 둘째, 다중 페이지에 대해 루프를 돌며 수동 캡처를 수행할 때는 항상 인덱스 역순(descending)으로 루프를 처리하십시오. 그래야 뒤쪽 페이지가 은닉되더라도 아직 캡처되지 않은 앞쪽 페이지 인덱스 순번이 영향받지 않고 온전히 보존됩니다

지정 페이지 범위에 다중 레이어 워터마크를 적용하는 방법

PDFlibWatermark.pas 유닛에 설계된 워터마크 템플릿 레이어는 "한 번 정의해서 여러 지면에 범용 배치하기"를 객체화해 줍니다. TPDFlibWatermarkBase는 모든 워터마크 개체들이 공유하는 기본 속성을 다룹니다: 즉 9가지 기준점 위치인 Position(wpTopLeft부터 wpBottomRight까지, 수동 좌표 입력용인 wpCustom 포함), 회전각인 Angle, 투명도인 Opacity(0부터 1까지), 그리고 적용할 페이지 범위 정보인 PageRange(공백 시 전 페이지 기본 할당)입니다. TPDFlibTextWatermark는 텍스트 본문, 폰트 ID(기본값은 Helvetica), 글자 크기 및 RGB 색상 값을 추가 제어합니다; TPDFlibImageWatermark는 이전에 연동해 등록해 둔 이미지 ID 및 배치할 크기(너비/높이) 정보를 제어합니다. 복합 구조인 TPDFlibCompositeWatermark는 하위 워터마크들을 리스트로 소유해 재귀식으로 일괄 연동 적용하며, TPDFlib.ApplyWatermark는 지정 템플릿의 지면 제어 범위를 분석해 도장을 찍고 하이라이트가 처리된 최종 처리 페이지 수를 반환해 줍니다. 앞서 예시된 법무용 3단 워터마크 구성 예제는 다음과 같습니다

var
  Lib: TPDFlib;
  Logo: TPDFlibImageWatermark;
  Diagonal, Footer: TPDFlibTextWatermark;
  Stack: TPDFlibCompositeWatermark;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('draft.pdf', '');
    Stack := TPDFlibCompositeWatermark.Create;
    try
      Logo := TPDFlibImageWatermark.Create;
      Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
      Logo.Position := wpTopRight;
      Logo.Width := 90;
      Logo.Height := 32;
      Stack.Add(Logo);

      Diagonal := TPDFlibTextWatermark.Create;
      Diagonal.Text := 'CONFIDENTIAL';
      Diagonal.Position := wpCenter;
      Diagonal.Angle := 45;
      Diagonal.FontSize := 60;
      Diagonal.Opacity := 0.15;
      Stack.Add(Diagonal);

      Footer := TPDFlibTextWatermark.Create;
      Footer.Text := 'Internal review copy';
      Footer.Position := wpBottomCenter;
      Footer.FontSize := 9;
      Footer.Opacity := 0.6;
      Stack.Add(Footer);

      Stack.PageRange := '1-3';   // children inherit this range
      Lib.ApplyWatermark(Stack);
      Lib.SaveToFile('draft-marked.pdf');
    finally
      Stack.Free;                 // composite frees its children
    end;
  finally
    Lib.Free;
  end;
end;

두 가지 주요 연동 세부 사항을 짚어 둡니다. 범위(Range)의 상속 처리는 단방향이며 비파괴적으로 작동합니다: 하위 개체에 PageRange가 비어 있는 경우 적용 시점에 상위 composite의 지정 범위를 상속받는 반면, 하위 개체 자체에 범위가 적혀 있으면 그 수치를 고수하므로, 바닥 푸터 정보는 전 페이지에 찍히게 놔두고 중앙 대각선 마크는 특정 draft 페이지에만 찍히도록 정교하게 배치할 수 있습니다. 또한 객체 소유권은 composite 개체에 귀속됩니다: TPDFlibCompositeWatermark.Destroy 호출 시 소유하고 있는 모든 하위 개체들의 메모리 해제를 재귀적으로 일괄 대리하므로, 개발자는 위 예제처럼 최상위 composite 개체 하나만 해제해 주면 안전하게 메모리가 관리됩니다

아키텍처 제약 사항

기술 사양의 2가지 한계를 전합니다. 첫째, 캡처 기법은 원본 페이지의 콘텐츠 드로잉 스트림 및 리소스 사양만 묶어 이송합니다. 주석(annotation), 링크 요소, 양식 필드 및 코멘트 등은 스트림이 아닌 페이지 사전에 결합된 별도의 독립 객체들이므로 XObject 내부로 딸려 들어가지 않습니다; 즉 스티칭 결과물은 원본 지면의 눈에 보이는 그림 정보만 계승할 뿐, 원본이 갖고 있던 클릭 가능한 양식이나 주석 등의 대화형(interactive) 기능 레이어는 계승하지 않습니다. 둘째, 9가지 기준점 기반 텍스트 배치는 근사치 연산입니다: 풀 레이아웃 조판 엔진 없이는 특정 폰트로 드로잉될 텍스트의 실질 픽셀 너비를 사전에 완벽히 파악할 수 없으므로, 라이브러리는 wpCenter 등 기준점 계산 시 텍스트 박스의 평균 크기를 페이지 폭의 약 40% 수준으로 가산해 비례 연산합니다. 짧은 단어를 대략 중앙에 정렬하는 데는 충분하나, 긴 문장을 픽셀 단위로 한 치의 오차 없이 정교하게 밀착 배치해야 한다면 wpCustom을 정해 좌표계를 수동 계산해 넘기십시오

페이지 캡처 및 스티칭은 원본 지면 데이터를 다시 인코딩하는 방식이 아니라 내부 객체 주소 참조 관계만 재매핑하므로, 용량 증가가 거의 유발되지 않아 실 처리 크기는 제약이 되지 않습니다; 만약 수백 메가바이트의 대용량 파일을 제어해야 한다면 스티칭 처리 전에 대용량 파일 직접 접근 병합 및 분할 가이드 기사에서 설명하는 원칙들을 적용해 보십시오. 여기에 예시된 스티칭 함수 및 워터마크 템플릿 클래스 등은 Delphi, C# 및 VB.NET용 losLab PDF Library의 기본 내장 기능으로 제공되며, 3가지 기본 스티칭 호출 함수 외에 더 복잡한 비대칭 배치가 필요한 경우 활용할 수 있는 저수준 제어자 CapturePage, DrawCapturedPage, 및 DrawCapturedPageMatrix 호출 등도 함께 제공됩니다