iTextPDF와 Commons-Net으로 PDF 생성 및 FTP 서버 업로드 기능 구현하기
iTextPDF 버전변경 및 확장된 기능(JPEG 변환, 압축, 다운로드)
위 2개의 포스트를 통해 서버에 들어온 요청 데이터를 이용해 pdf 파일을 만들어 FTP 서버에 저장한 후 다운로드를 통해 클라이언트에 내려주고 있었습니다.
요구 사항 변경 : FTP 서버에 저장 시키는 기능은 빼고 그냥 서버에서 파일 만들어서 클라이언트에서 바로 볼 수 있도록 내려주세요.
예예...해드려야죠....
어떻게 접근하면 좋을까요?
필자는 일단 구현되어있는 로직을 최대한 사용하며 최소한의 수정을 거쳐
서버에 들어온 요청 데이터를 이용해 pdf 파일을 만들어 클라이언트에 내려준다. 라고 생각하고 작업을 진행했습니다.
구글링 해보니 응답받은 pdf 파일을 클라이언트(브라우저)에 렌더링 하면 툴바가 있는 창이 나타나 보여지는 것 같아 (뭔가 깔끔해보이지 않았다.)
일단 png 이미지 파일로 변환 후 내려보기로 했습니다.
@PostMapping("/contract")
public ResponseEntity<Object> contract(@RequestBody Map<String, Object> body) throws IOException, IllegalAccessException {
byte[] pdfData = contractSaveService.saveEndFindViewer(body);
if (pdfData != null) {
ByteArrayResource resource = new ByteArrayResource(pdfData);
return ResponseEntity.ok()
// .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"contract.pdf\"") // 다운로드
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"contract.pdf\"") // 바로보기
.contentType(MediaType.APPLICATION_PDF)
.contentLength(pdfData.length)
.body(resource);
}
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("파일을 찾지 못했습니다.");
}
contractSaveService.saveEndFindViewer() 메서드를 통해 요청 데이터를 이용해 pdf 파일을 만든 후 byte 배열로 내려주는 걸 확인 할 수 있습니다.
Service 쪽에 요청 데이터를 이용해 png 파일을 만들어 byte 배열을 리턴해주는 메서드를 생성하면 될 것 같습니다만?
서비스 쪽으로 가봅시다.
public byte[] saveEndFindViewer(Map<String, Object> body) throws IOException, IllegalAccessException {
// 파일명을 정해서 넘겨줘야함 (yyyyMMdd_고객번호)
String fileName = body.get("contractDate") + "_" + body.get("clientCode");
// ftpFileSender.upload(fileName + ".jpeg", pdfMaker.createPdf(body));
return ftpFileSender.upload(fileName + ".pdf", pdfMaker.createPdf(body)) ?
ftpFileSender.download(fileName + ".pdf") : null;
}
서비스쪽에는 위와같이 메서드가 하나뿐인데요.
사용되는 메서드를 보니...pdfMaker.createPdf()
메서드를 사용해 pdf 파일을 만든 후 ftpFileSender.upload()
메서드를 통해 FTP 서버로 업로드시킨 후 정상적으로 업로드가 되었다면 ftpFileSender.download()
메서드를 사용해 가져오고 있습니다.
나중에 요구사항이 또 어떻게 변할지 모르니 위 메서드는 주석처리하고...
pdfMaker
객체의 png 파일을 리턴해주는 메서드를 추가해주면 될 것 같습니다.
public byte[] createPdf(Map<String, Object> data) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
PdfWriter pdfWriter = new PdfWriter(out);
// pdf 파일 압축 수준 설정 (1~9)
pdfWriter.setCompressionLevel(BEST_COMPRESSION); // 9
Document document = new Document(new PdfDocument(pdfWriter), PageSize.A4);
document.add(contractMaker(data));
document.close();
return out.toByteArray();
}
}
여기가 실질적으로 pdf 에 들어갈 내용을 만들고 pdf파일을 만들어 내는 객체 입니다.
리턴해주는 out.toByteArray() 가 byte배열로 변환된 pdf 파일입니다.
이 객체를 png 형식에 맞게 변경 후 리턴해주면 될 것 같은데....
메서드를 하나 만들어 보았습니다.
private byte[] pdfTOpng(byte[] bytes) throws IOException {
try (PDDocument pdDocument = PDDocument.load(bytes); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
// pdf 의 페이지를 이미지로 렌더링
PDFRenderer renderer = new PDFRenderer(pdDocument);
// 반복문을 사용하여 PDF 문서의 모든 페이지를 순회하고, 각 페이지를 300 DPI의 해상도로 이미지로 변환
for (int i = 0; i < pdDocument.getNumberOfPages(); i++) {
BufferedImage image = renderer.renderImageWithDPI(i, 300);
// 이미지 리사이징 : 변환된 이미지 크기를 조정하고 새로운 BufferedImage 객체에 그려 넣습니다.
int width = image.getWidth();
int height = image.getHeight();
BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = resizedImage.createGraphics();
g.drawImage(image, 0, 0, width, height, null);
g.dispose();
// 변환된 이미지를 png 형식으로 ByteArrayOutputStream 에 저장
try (ImageOutputStream ios = ImageIO.createImageOutputStream(outputStream);
) {
ImageWriter writer = obtainImageWriter("PNG");
writer.setOutput(ios);
ImageWriteParam param = writer.getDefaultWriteParam();
}
}
return outputStream.toByteArray();
}
} // end method
public byte[] createPdf(Map<String, Object> data) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
PdfWriter pdfWriter = new PdfWriter(out);
pdfWriter.setCompressionLevel(9);
Document document = new Document(new PdfDocument(pdfWriter), PageSize.A6);
document.add(contractMaker(data));
document.close();
return this.pdfTOpng(out.toByteArray());
}
}
해당 객체에 새로만든 메서드를 리턴해주도록 변경했습니다.
public byte[] getPNG(Map<String, Object> body) throws IOException {
return pdfMaker.createPdf(body);
}
이제 pdfMaker.createPdf()
메서드는 이전과는 다른 동작을 하게 될겁니다. 왜? 위에서 변경했으니깐~
@PostMapping("/contract")
public ResponseEntity<Object> contract(@RequestBody Map<String, Object> body) throws IOException, IllegalAccessException {
byte[] pngData = contractSaveService.getPNG(body);
if (pngData == null) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("계약서를 로드하는 중 오류");
}
String base64 = Base64.getEncoder().encodeToString(pngData);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"contract.png\"")
}
byte배열로 변환된 이미지를 base64 로 인코딩해 내려줍니다.
byte배열로 변환 된 파일을 클라이언트에서 그냥 사용만하면 되는 줄 알았습니다만, 웹페이지에 바로 렌더링해서 보여주고 싶은 경우
Base64 로 인코딩해서 내려줘야 했습니다.
클라이언트쪽에서는 아래 예제 처럼 받은 byte 배열을 blob 으로 변환 후 URL을 생성하여 DOM에서 참조 할 수 있도록 하면 되지않을까? 생각했습니다.
const imageBlob = new Blob(array데이터, [options])
const blobURL = window.URL.createObjectURL(imageBlob);
document.getElementById('id').src = blobURL;
window.URL.revokeObjectURL(blobURL);
하지만 아무리 해봐도 이미지가 렌더링 되지않아 열심히 구글링을 해 알아본 결과
html 에 직접 삽입하는 경우는 Base64 로 인코딩해야 클라이언트측에서 데이터 URI 를 적용해 인라인으로 삽입할 수 있다는 사실을 알게되었습니다.
? 데이터 URI
data:
스킴이 접두어로 붙은 URL은 컨텐츠 작성자가 작은 파일을 문서 내에 인라인으로 삽입할 수 있도록 해줍니다.
구문 : data:[<mediatype>][;base64],<data>
그래서!
const imageUrl = "data:image/png;base64," + response.data;
document.getElementById('id').src = imageUrl;
문자열로 내려온 데이터 앞에 "data:image/png;base64" 를 붙여 넣어본 결과 렌더링이 잘 되었다고 합니다~~
위 문제로 2일이나 머리를 싸맸습니다...다른 분들은 부디 제가 낭비한 시간을 따라오지 마시길....