Java, Apache PDFBox를 이용하여 PDF 다루기
Apache PDFBox 라이브러리를 이용하여 Java로 PDF 파일을 수정, 병합, 생성하는 API 가이드 포스팅을 시작 하겠습니다.
먼저 Maven 또는 Gradle 의존성을 추가 해 줍니다. 필자가 사용한 버전은 2.0.31 입니다. 3.x 버전의 경우, 2.x버전 때의 사용법이 다르게 변경되어 다른 부분이 많습니다. 이점 참고 부탁드리겠습니다!
Maven
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.31</version>
</dependency>
Gradle
implementation group: 'org.apache.pdfbox', name: 'pdfbox', version: '2.0.31'
설정이 완료되었다면, 먼저 PDF 생성부터 알아보도록 하겠습니다. 기본적으로 PDFBox는 PDF파일을 직접 그릴 때 위에서 아래로 그리게 되어 있습니다. 즉 아래 좌표를 먼저 그렸다가, 위의 좌표를 그리면 의도치 않은 결과와 다양한 에러가 발생 할 수 있습니다.
PDDocument doc = new PDDocument();
PDDocument class를 선언하게 되면 해당 PDF 문서를 메모리에 할당합니다. 꼭 작업 완료시 doc.close() 종료 메소드를 호출 해야 합니다.
InputStream fontStream = new FileInputStream(new File(ROOT_FONT_PATH));
PDFont font = PDType0Font.load(doc, fontStream);
한글 입력이나, 추가적인 폰트 추가시 InputStream class를 이용하여 처리합니다. PDFont의 매개변수로는 글꼴 TTF를 type0 글꼴로 로드, 처리 할 PDF문서 객체를 보내줍니다. load(doc, 나눔스퀘어.ttf)
PDPage page = new PDPage();
신규 PDF 페이지를 추가할 때 PDPage class를 이용하여 처리합니다. PDPage class는 PDF 파일의 n장 E 또는 n번째 라고 생각하시면 됩니다. 만약 해당 PDF page에 content를 넣고 싶다면 아래와 같이 진행하면 됩니다.
PDPageContentStream contentStream = new PDPageContentStream(doc, page);
contentStream.beginText();
contentStream.setFont(font, 12);
contentStream.newLineAtOffset(25, 500);
contentStream.showText("신규 PDF 입니다.");
contentStream.endText();
contentStream.close();
doc.addPage(page);
PDPageContentStream class에 content를 추가할 PDF 문서와 해당 page를 매개변수로 할당 해 줍니다.
beginText()를 호출하여 text를 입력할 것이다라는 알리고 작업이 완료되었다면 contentStream.endText(); 호출하여 text 입력을 종료 합니다. 그 후 스트림을 닫고, content를 추가한 생성한 페이지를 PDF문서에 추가합니다. newLineAtOffset() 메소드는 x, y 좌표를 표현합니다. showText() 메소드는 입력한 text를 표현합니다.
※ 만약 PDF 문서에 이미지를 추가하고 싶을 때는
PDImageXObject pdfImage = PDImageXObject.createFromFile("imagePath.png", doc);
PDPageContentStream contentStream = new PDPageContentStream(doc, page);
contentStream.drawImage(pdImage, x, y);
PDImageXObject class를 이용하여 처리하면 됩니다. contentStream.drawImage(pdImage, x, y); x좌표와 y좌표에 이미지를 그립니다.
doc.save(ROOT_PATH+"test.pdf");
doc.close();
모든 PDF 생성 작업이 완료 되었다면 PDF 문서를 저장합니다.
연결된 코드는 다음과 같습니다.
public int pdfCreate() throws Exception{
// PDF를 생성한다.
PDDocument doc = new PDDocument();
// 폰트 설정
InputStream fontStream = new FileInputStream(new File(ROOT_FONT_PATH));
PDFont font = PDType0Font.load(doc, fontStream);
// 페이지 생성
PDPage page = new PDPage();
// 페이지 그리기 (content) pdf와 page를 매개변수로 넘겨준다.
PDPageContentStream contentStream = new PDPageContentStream(doc, page);
// text 작성 시작
contentStream.beginText();
// 해당 text font 설정
contentStream.setFont(font, 12);
// x, y 좌표로 시작
contentStream.newLineAtOffset(25, 500);
contentStream.showText("신규 PDF 입니다.");
// text 작성 종료
contentStream.endText();
// content 스트림 종료
contentStream.close();
// PDF를 doc에 Add 한다.
doc.addPage(page);
// 해당 경로에 PDF 파일을 저장한다.
doc.save(ROOT_PATH+"test.pdf");
// 파일 스트림 닫기
doc.close();
return 1;
}
테스트 결과는 다음과 같습니다.

그 다음 PDF 파일을 수정하고 싶을 때 PDF 파일을 읽어와 수정 할 수 있습니다.
PDDocument doc = PDDocument.load(new File(ROOT_PATH+"newPDF.pdf"));
PDDocument.load() static 메소드를 이용하면 해당 PDF 파일을 읽을 수 있습니다. ( 대용량인 경우 메모리 사용량 주의 )
int pageCount = doc.getNumberOfPages();
해당 PDF 파일의 page count를 얻을 수 있습니다. 이것을 활용하여 PDF 파일의 page index를 구하고 삭제하거나 추가하거나 수정할 수 있습니다.
content 추가 및 수정 메소드는 다음과 같습니다.
PDDocument doc = PDDocument.load(new File(ROOT_PATH+"newPDF.pdf"));
PDPage page = new PDPage();
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
for(int i = 0; i < 5; i++){
Map<String, Object> map = new HashMap<String, Object>();
map.put("text", "안녕하세요 " + i);
map.put("x", 30 * i);
map.put("y", 100 * i);
list.add(map);
}
pdfPageContent(doc, page, list);
doc.addPage(page);
public void pdfPageContent(PDDocument doc, PDPage page, List<Map<String, Object>> params) throws Exception{
// 폰트 설정
InputStream fontStream = new FileInputStream(new File(ROOT_FONT_PATH));
PDFont font = PDType0Font.load(doc, fontStream);
// 페이지 그리기 (content) pdf와 page를 매개변수로 넘겨준다.
PDPageContentStream contentStream = new PDPageContentStream(doc, page);
// text 작성 시작
contentStream.beginText();
// 해당 text font 설정
contentStream.setFont(font, 12);
// x, y 좌표로 시작
for(Map map : params) {
int x = (int)map.get("x");
int y = (int)map.get("y");
String text = (String)map.get("text");
contentStream.newLineAtOffset(x, y);
contentStream.showText(text);
}
// text 작성 종료
contentStream.endText();
// content 스트림 종료
contentStream.close();
}
해당 page를 삭제하는 예시는 다음과 같습니다.
// 페이지 삭제
for(int i = 0; i < pageCount; i++){
if(i == 2 && pageCount > 1){
doc.removePage(0);
doc.removePage(1);
}
}
removePage() 메소드를 활용하여 매개변수에 해당 PDF의 index 값을 넣고 호출 합니다.
PDF 파일간의 병합은 더욱 간단합니다.
public int pdfMerge() throws Exception {
File f1 = new File(ROOT_PATH+"test.pdf");
File f2 = new File(ROOT_PATH+"test2.pdf");
PDFMergerUtility merger = new PDFMergerUtility();
merger.setDestinationFileName(ROOT_PATH+"merge.pdf");
merger.addSource(f1);
merger.addSource(f2);
merger.mergeDocuments();
return 1;
}
병합할 PDF 파일 2개를 불러와서 PDFMergerUtility class를 이용하여 해당 PDF source를 병합합니다. PDF 파일은 addSource() 메소드에 순차적으로 진행 됩니다. mergeDocuments() 메소드를 호출하면 병합 작업 한 PDF 파일을 setDestinationFileName() 메소드를 호출한 파일 경로에 저장 됩니다.
결과는 다음과 같습니다.

마지막으로 실제 업무에서 PDF파일 약 500개 정도를(약 1GB ~ 3GB 용량) 병합하여 하나로 압축하는 작업을 진행했었는데, 파일이 많을 경우 파일 스트림의 오픈 시간이 길어진다. 파일 스트림을 장시간 열고 있으면 에러가 발생할 수 있으니, 전체를 한번에 병합하여 처리하는 것 보다 병합본을 계속 읽어 하나씩 병합하는 방법으로 처리하는 것이 에러가 발생하지 않고 속도도 빠르며 안전하다.
