Java와 iText 라이브러리를 활용하여 PDF 문서에 워터마크 삽입하기
Embed Watermarks in PDF with Java and iText
워터마크는 저작권 표시, 무단 복제 방지, 문서의 출처 표시 등과 같은 중요한 역할을 합니다. iText PDF 라이브러리는 강력한 성능과 뛰어난 유연성을 자랑하는 PDF 엔진으로, Java 환경에서 문서와 데이터를 효율적으로 취급할 수 있도록 설계되어 있습니다. 이를 활용하면 워터마크를 비롯한 여러 PDF 조작 작업들을 간결하게 처리할 수 있습니다.
Prerequisite
요청 바디로 PDF 문서, 워터마크 이미지, 그리고 서명을 받아 워터마크가 삽입된 PDF 파일을 응답으로 제공하는 간결한 REST API를 구현해 보는 과정을 통해 전체적인 프로세스를 살펴보려고 합니다.
이 글은 Spring Boot 프로젝트를 기반으로 진행되며, 그 설정은 다음과 같습니다:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
}
- Java 17
- Spring Boot 3.1.4
- Gradle 8
Configuring iText PDF
먼저, PDF 문서를 조작하기 위한 iText PDF 라이브러리 의존성을 build.gradle
에 추가합니다:
dependencies {
// iText
+ implementation 'com.itextpdf:itextpdf:5.5.13.3'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
}
Handling Request
다음은 HTTP 요청을 핸들링할 수 있는 컨트롤러를 구현합니다.
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/watermarks")
public class WatermarkController {
private final WatermarkService service;
@PostMapping
public ResponseEntity<?> watermarkEmbedApi(
@RequestPart(value = "pdf") MultipartFile pdfFile,
@RequestPart(value = "watermark") MultipartFile watermarkImage,
@RequestPart(value = "signature") String signature) {
byte[] pdf = service.embed(pdfFile, watermarkImage, signature);
return ResponseEntity
.ok(pdf);
}
}
@RequestPart
: 여러 형태의 데이터를 전달 받기 위해@RequsetPart
어노테이션을 활용하였습니다.value = "pdf"
: PDF 파일을MultipartFile
타입으로 바인딩합니다.value = "watermark"
: 워터마크 이미지 파일을MultipartFile
타입으로 바인딩합니다.value = "signature"
: 서명을String
형태로 바인딩합니다.
service.embed()
: 서비스 레이어로 비즈니스 처리를 위임합니다.
Editing PDF
이제, 컨트롤러로부터 전달받은 요청을 처리하는 비즈니스 로직을 구현합니다.
public byte[] embed(MultipartFile pdf, MultipartFile watermark, String signature) {
try {
// prefaring
byte[] origin = pdf.getBytes();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PdfReader pdfReader = new PdfReader(origin);
PdfStamper pdfStamper = new PdfStamper(pdfReader, outputStream);
Image watermarkImage = Image.getInstance(watermark.getBytes());
Phrase phrase = Phrase.getInstance(signature);
// applying
int numberOfPages = pdfReader.getNumberOfPages();
for (int i = 1; i <= numberOfPages; i++) {
PdfContentByte pdfContent = pdfStamper.getOverContent(i);
Rectangle rectangle = pdfReader.getPageSize(i);
applyWatermark(pdfContent, watermarkImage, rectangle);
applySignature(pdfContent, phrase, rectangle);
}
// cleanup resources and return results
pdfStamper.close();
pdfReader.close();
return outputStream.toByteArray();
} catch (Exception e) {
log.info("embed: Failed embedding watermark {} {}", e.getMessage(), e.getCause(), e);
throw new RuntimeException(e);
}
}
위 로직은 다음과 같이 크게 세 가지 단계로 이루어져 있습니다:
- 준비 단계: 이 단계에서는 입력들을 바이트 배열 형태로 변환하고, 이를 처리하기 위한 iText 라이브러리의 전용 객체들로 변환합니다.
PdfReader
: PDF 파일을 읽는 데 사용됩니다. 제공된 입력 스트림에서 PDF를 읽거나 PDF 파일로부터 직접 읽을 수 있습니다.PdfStamper
: 이 객체는PdfReader
로 읽은 PDF 파일에 기존 내용 위에 추가 내용(보통 이미지나 텍스트 등)을 삽입 하는데 사용됩니다. 스탬핑 작업이 끝나면 결과물을 출력 스트림에 쓸 수 있습니다.Image
: 이 객체는 이미지를 나타냅니다. 이미지 파일로부터 직접 읽을 수 있으며,byte
배열로부터도 이미지를 생성할 수 있습니다. 생성된Image
객체는 이후에PdfStamper
를 이용하여 PDF 문서에 삽입할 수 있습니다.Phrase
: 이 객체는 하나 이상의Chunk
객체를 담고 있는데,Chunk
는 PDF에서 가장 작은 단위의 텍스트 블록을 나타냅니다. 따라서Phrase
는 같은 스타일과 폰트(전역적으로 설정됨)을 공유하는 하나 이상의 단어 또는 문장을 표현합니다.
- 워터마크 및 서명 적용 단계: 원본 PDF 각 페이지에 대해 워터마크 이미지와 서명을 적용합니다.
pdfReader.getNumberOfPages()
: 객체에서 PDF 페이지 수를 가져와 전체 페이지를 순회합니다. 첫번째 페이지의 인덱스는1
입니다.pdfStamper.getOverContent()
: 특정 페이지에 직접 콘텐츠를 추가하기 위한PdfContentByte
객체를 얻습니다. 객체는 PDF 내용(그래픽 또는 텍스트)을 쓸 수 있는 메서드를 제공하므로, 워터마크나 서명과 같은 추가적인 컨텐츠를 PDF에 쓸 수 있게 해줍니다.pdfReader.getPageSize()
: 특정 페이지의 크기를 나타내는Rectangle
객체를 얻습니다. 이는 워터마크 또는 서명을 적정한 위치에 배치하는 데 사용됩니다.applyWatermark()
와applySignature()
는 추출한 PDF 페이지에 워터마크와 서명을 적용하는 사용자 정의 함수로 아래에서 살펴보겠습니다.
- 파일 생성 및 반환 단계: 리소스를 정리하고 워터마크와 서명이 적용된 PDF를 생성하여 이를 바이트 배열 형태로 반환합니다.
워터마크를 적용하는 applyWatermark()
함수는 아래와 같이 구현하였습니다:
private static void applyWatermark(
PdfContentByte pdfContent,
Image watermarkImage,
Rectangle rectangle)
throws DocumentException {
PdfGState watermarkState = new PdfGState();
watermarkState.setFillOpacity(0.5f);
pdfContent.setGState(watermarkState);
watermarkImage.setRotationDegrees(0);
watermarkImage.scaleToFit(256, 256);
float positionX = (rectangle.getWidth() - watermarkImage.getScaledWidth()) / 2;
float positionY = (rectangle.getHeight() - watermarkImage.getScaledHeight()) / 2;
watermarkImage.setAbsolutePosition(positionX, positionY);
pdfContent.addImage(watermarkImage);
}
PdfGState
: 그래픽 상태를 결정하는 데 사용됩니다. 삽입하려는 이미지를 반투명하게 조절하기 위해 활용하였습니다.pdfContent.setGState()
: 설정된 그래픽 상태watermarkState
를 PDF 문서 페이지인PdfContentByte
에 적용합니다.Image
객체에서는 이미지 편집을 위한 다양한 기능을 제공하고 있습니다.setRotationDegrees(float)
: 워터마크 이미지의 회전 각도를 0으로 설정합니다.scaleToFit(float, float)
: 워터마크 이미지의 크기를 256x256 픽셀에 맞게 조정합니다.setAbsolutePosition(float, float)
: 워터마크 이미지의 절대 위치를 설정합니다. 현재Rectangle
객체에서 PDF 문서 페이지의 가로 및 세로 크기를 추출하여 워터마크 이미지가 정 중앙에 오도록 좌표를 설정하였습니다.
pdfContent.addImage(Image)
:PdfContentByte
에 워터마크 이미지를 추가합니다.
텍스트 서명을 추가하는 과정도 워터마크를 삽입하는 프로세스와 유사합니다:
private static void applySignature(
PdfContentByte pdfContent,
Phrase phrase,
Rectangle rectangle) {
PdfGState signatureState = new PdfGState();
signatureState.setFillOpacity(0.75f);
pdfContent.setGState(signatureState);
PdfTemplate template = pdfContent.createTemplate(rectangle.getWidth(), rectangle.getHeight());
float positionX = rectangle.getWidth() / 2;
float positionY = 15;
int rotation = 0;
ColumnText.showTextAligned(template, Element.ALIGN_CENTER, phrase, positionX, positionY, rotation);
pdfContent.addTemplate(template, 0, 0);
}
PdfGState
: 워터마크 이미지에 반투명도를 조절한 것과 마찬가지로 추가하려는 문구의 반투명도를 조절할 수 있습니다.pdfContent.createTemplate()
: 서명을 그리는 데 사용되는PdfTemplate
객체를 생성합니다.showTextAligned()
: 생성한 템플릿 내에 텍스트의 위치를 지정합니다. 현재 정중앙 하단에 오도록 설정하였습니다.pdfContent.addTemplate()
:PdfContentByte
에 템플릿을 추가하여 완성된 서명을 PDF에 적용합니다.
Handling Korean Encoding Issues
일부 상황에서, iText 라이브러리는 문자 인코딩 문제를 겪을 수 있습니다. 특히, 한글 문서 작업을 수행할 때 발생할 수 있는 이런 문제를 해결하기 위해, 한글 폰트를 명시적으로 지정해 주어야 합니다. 이를 위해 BaseFont
객체를 생성하고, 이를 사용하여 Font
객체를 생성할 수 있습니다.
public byte[] embed(MultipartFile pdf, MultipartFile watermark, String signature) {
try {
// prefaring
byte[] origin = pdf.getBytes();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PdfReader pdfReader = new PdfReader(origin);
PdfStamper pdfStamper = new PdfStamper(pdfReader, outputStream);
Image watermarkImage = Image.getInstance(watermark.getBytes());
+ BaseFont baseFont = BaseFont.createFont("/SpoqaHanSansNeo.otf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
+ Font font = new Font(baseFont, 9, Font.NORMAL, BaseColor.RED);
- Phrase phrase = Phrase.getInstance(signature);
+ Phrase phrase = Phrase.getInstance(0, signature, font);
...
}
}
BaseFont.createFont()
: iText 라이브러리에서 사용하는 폰트에 대한 정보를 제공하는 객체입니다. 표준적으로 사용되는 몇 가지 글꼴 외에도 사용자 지정 글꼴을 추가할 수 있도록 지원합니다.- 첫 번째 인자로 사용하고자 하는 글꼴의 경로를 지정합니다. 현재 코드에서는
resources
디렉토리에 넣어둔 SpoqaHanSansNeo 폰트 경로를 입력하였습니다. - 두 번째 인자는 사용하려는 폰트 파일의 인코딩을 지정해야 합니다. 여기에는 Java 플랫폼에 있는 폰트 인코딩을 지정할 수 있습니다.
BaseFont.IDENTITY_H
는 Unicode를 유지하면서 폰트의 표준 인코딩을 사용하도록 지정하고 있습니다.IDENTITY_H
와IDENTITY_V
는 각각 가로로 정렬된 텍스트와 세로로 정렬된 텍스트를 위한 유니코드를 지원하는 인코딩입니다. - 세 번째 인자는 임베딩 여부로, PDF 파일이 해당 글꼴을 사용해서 텍스트를 렌더링하는 데 필요한 글꼴 데이터를 포함하고 있는지 여부를 설정합니다.
- 첫 번째 인자로 사용하고자 하는 글꼴의 경로를 지정합니다. 현재 코드에서는
Phrase.getInstance()
: 사용자 지정 폰트가 설정된Font
객체를 주입하여Phrase
객체를 생성합니다.
한편, 애플리케이션을 실행하는 운영 체제의 언어 환경 설정에 따라서는 한글 문자가 제대로 인식되지 않을 수도 있습니다. 이런 경우에는 애플리케이션 실행 시 JVM(Java Virtual Machine)에 추가적인 옵션을 부여하여 해결할 수 있습니다.
예를 들어, JVM을 시작할 때 -Dfile.encoding=UTF-8
옵션을 사용하면 전체 시스템의 기본 문자열 인코딩이 UTF-8로 설정됩니다. 또는, 애플리케이션을 실행하는 동안에도 다음과 같이 특정 인코딩을 지정하는 것이 가능합니다:
$ java -Dfile.encoding=UTF-8 -jar application.jar
Playgrounds
PDF 문서와 워터마크 이미지 파일을 준비하여, 새롭게 구현한 워터마크 적용 기능을 테스트해보겠습니다. 이를 위해 API 클라이언트 도구를 활용하여 다음과 같이 요청을 전송하였습니다:
POST /watermarks HTTP/1.1
Content-Type: multipart/form-data; charset=utf-8; boundary=__X_PAW_BOUNDARY__
Host: localhost:8080
Connection: close
User-Agent: RapidAPI/4.2.0 (Macintosh; OS X/14.3.0) GCDHTTPRequest
Content-Length: 1046636
--__X_PAW_BOUNDARY__
Content-Disposition: form-data; name="pdf"; filename="original.pdf"
Content-Type: application/pdf
--__X_PAW_BOUNDARY__--
-__X_PAW_BOUNDARY__
Content-Disposition: form-data; name="watermark"; filename="catsriding.png"
Content-Type: image/png
--__X_PAW_BOUNDARY__--
--__X_PAW_BOUNDARY__
Content-Disposition: form-data; name="signature"
© 2020-present Jynn. All Rights reserved.
--__X_PAW_BOUNDARY__--
요청이 성공적으로 처리되었으며, 워터마크와 서명이 적용된 새로운 PDF 파일이 응답으로 반환되었습니다. 원본 파일과 변환된 파일을 비교해보도록 하겠습니다.
여기까지 PDF 문서에 워터마크를 삽입하는 과정에 대해 알아보았습니다. iText PDF 라이브러리를 사용하면서, PDF 편집 프로그램에서 사용하던 기능들과 유사한 다양한 기능들을 통해 객체 기반으로 PDF를 조작하는 경험을 할 수 있었습니다.
iText PDF 라이브러리는 단순히 워터마크를 삽입하는 것뿐만 아니라 데이터 추출, 문서 분할 및 병합, 화면 렌더링 등의 다양한 PDF 조작 기능을 지원하고 있어 알아두면 좋은 도구인 것 같습니다. 😎
- Java