두둥 프로젝트를 진행하면서
통신 판매 중개업종이므로 , 호스트에게 공연 카드결제 대금
정산 작업도 배치로 돌려야 했는데, 이때 pdf로 정산서를 보내줘야할 일이 생겼다.
두둥에서는 메일도 thymeleaf를 사용해서 보내고 있으므로,
pdf 도 thymeleaf 를 사용해서 보내는 방법으로 결정했다.
이미지와, 한글 적용을 하려면 꽤나 고생좀 해야하는데 그 방법을 공유하고자한다.
목차
1. flying-saucer-pdf
2. 한글 적용하기
3. 이미지 가능하게 하기
1. flying-saucer-pdf
baeldung 에서도 자세히는 아니지만 대충은 확인 가능하다.
https://www.baeldung.com/thymeleaf-generate-pdf
간단하다. 해당 모듈을 이용해서 thymeleaf 로 html 스트링을 만든뒤에,
render 을 통해서 pdf를 만드는것이 가능하다.
두둥 서버스에서는 outputStream 을 파일로 내려주는게 아니라
ByteStream 으로 만든뒤에,
private S3 에 올리고 나서
이메일 전송용 잡에서 private S3에서 다운로드 받아 이메일로 전송하는 구조를 취하고있다.
// 타임리프 템플릿 엔진에서 settlement.html 파일을 context와 내려준다.
String html = templateEngine.process("settlement", context);
// PdfRender 클래스
@Component
@RequiredArgsConstructor
@Slf4j
public class PdfRender {
public ByteArrayOutputStream generatePdfFromHtml(String html)
throws DocumentException, IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//renderer 설정
ITextRenderer renderer = new ITextRenderer();
renderer.getFontResolver();
renderer.setDocumentFromString(html);
renderer.layout();
// PDF 만들어준다.
renderer.createPDF(outputStream);
outputStream.close();
// outputStream 으로 리턴후 S3로 파일업로드를 stream 형태로 올린다.
return outputStream;
}
}
타임리프 템플릿 엔진으로 html으로 변환을 한후에,
pdf로 렌더링 한다.
테스팅 할 때는 아웃풋 스트림을 파일 아웃풋 스트림으로 잡아도 상관없다.
String outputFolder = System.getProperty("user.home") + File.separator + "thymeleaf.pdf";
OutputStream outputStream = new FileOutputStream(outputFolder);
// 중간 생략
renderer.createPDF(outputStream);
PdfRender 클래스를 만든후에
배치 작업간에 실행시켰다.
2. 한글 적용하기
기본 설정으로는 한글이 나오지 않는다.
따라서 한글이 되는 폰트를 리소스에 넣고 , 해당 한글 폰트를 html에 넣은뒤에
적용을 해야한다.
body {
width: 100%;
font-size: 12px;
font-weight: lighter;
color:#000;
line-height:180%;
font-family: NanumBarunGothic,serif;
}
우선은 폰트는 NanumBarunGothic 으로 정했다.
한글 되는 폰트면 다된다.!
나눔 바른 고딕 치면 다운로드 가능하다 NanumBarunGothic.ttf 로 하면 된다.
그뒤에 render 과정에서 폰트를 정해줘야한다.
renderer.getFontResolver()
//폰트를 설정한다.
.addFont(
new ClassPathResource("/templates/NanumBarunGothic.ttf")
.getURL()
.toString(),
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED);
renderer.setDocumentFromString(html);
renderer.layout();
이렇게 적용해 놓으면 폰트를 심을 수 있다.
3. 이미지 가능하게 하기
이미지를 올릴 수 있는 방법은 크게 두가지가있다.
1. 정적 이미지를 리소스에 포함해서 하는 방법
2. html src url 을 pdf 그리는 과정에서 감지 되는 스트림으로 받아와서 base64로 인코딩한후
html에 직접 포함하는 방법.
1번의 형식은 로컬에서 잘됐는데 도커환경에서 배포를 해보니 pdf 에 이미지가 제대로 안담기는 현상이 일어났다.
또한 파일로 포함해서 올리기는 .. 패키징 사이즈도 커지니 부담스러워서
2번의 방식으로 다시 적용했다.
<img
width="244"
alt="en-banner-black"
src="https://asset.dudoong.com/common/en-banner-black.png"
/>
html 템플릿 안에는 원래대로 src를 적어둔다.
@Component
public class B64ImgReplacedElementFactory implements ReplacedElementFactory {
public ReplacedElement createReplacedElement(
LayoutContext c, BlockBox box, UserAgentCallback uac, int cssWidth, int cssHeight) {
Element e = box.getElement();
if (e == null) {
return null;
}
String nodeName = e.getNodeName();
if (nodeName.equals("img")) {
String attribute = e.getAttribute("src");
FSImage fsImage;
try {
fsImage = buildImage(attribute, uac);
} catch (BadElementException e1) {
fsImage = null;
} catch (IOException e1) {
fsImage = null;
}
if (fsImage != null) {
if (cssWidth != -1 || cssHeight != -1) {
fsImage.scale(cssWidth, cssHeight);
}
return new ITextImageElement(fsImage);
}
}
return null;
}
protected FSImage buildImage(String srcAttr, UserAgentCallback uac)
throws IOException, BadElementException {
FSImage fsImage;
if (srcAttr.startsWith("data:image/")) {
String b64encoded =
srcAttr.substring(
srcAttr.indexOf("base64,") + "base64,".length(), srcAttr.length());
byte[] decodedBytes = Base64.getDecoder().decode(b64encoded);
fsImage = new ITextFSImage(Image.getInstance(decodedBytes));
} else {
fsImage = uac.getImageResource(srcAttr).getImage();
}
return fsImage;
}
public void remove(Element e) {}
public void reset() {}
@Override
public void setFormSubmissionListener(FormSubmissionListener listener) {}
}
B64ImgReplacedElementFactory 클래스만든뒤에 img 태그가 발견되면 이미지를 다운로드 받아서
base64 형식으로 바꿔준다.
public class PdfRender {
private final B64ImgReplacedElementFactory b64ImgReplacedElementFactory;
public ByteArrayOutputStream generatePdfFromHtml(String html)
throws DocumentException, IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ITextRenderer renderer = new ITextRenderer();
SharedContext sharedContext = renderer.getSharedContext();
sharedContext.setPrint(true);
sharedContext.setInteractive(false);
sharedContext.setReplacedElementFactory(b64ImgReplacedElementFactory);
sharedContext.getTextRenderer().setSmoothingThreshold(0);
renderer.getFontResolver()
.addFont(
new ClassPathResource("/templates/NanumBarunGothic.ttf")
.getURL()
.toString(),
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED);
renderer.setDocumentFromString(html);
renderer.layout();
renderer.createPDF(outputStream);
outputStream.close();
return outputStream;
}
}
B64ImgReplacedElementFactory 를 적용하면 이미지도 포함해서 pdf를 랜더링 할 수 있다.
배치잡에서 적용시킨 모습은 다음과 같다.
// Batch App 이벤트 pdf 정산 소스
String html = templateEngine.process("settlement", context);
// html
ByteArrayOutputStream outputStream =
pdfRender.generatePdfFromHtml(html);
String fileKey =
s3PrivateFileUploadService.eventSettlementPdfUpload(event.getId(), outputStream);
- pdfRender
- B64ImgReplacedElementFactory
- settlement.html
각각 소스는 위에서 참고 가능하다
'스프링' 카테고리의 다른 글
[스프링] spring feign client wiremock test (1) | 2023.03.07 |
---|---|
[스프링] spring batch 도커로 세팅하기 with jenkins (0) | 2023.03.07 |
[스프링] spring rate limit 적용히기 bucket4j (2) | 2023.03.06 |
[스프링] spring 프록시 환경에서 HttpContentCache 적용 (0) | 2023.03.06 |
[스프링] spring swagger 같은 코드 여러 에러 응답 예시 만들기 (3) | 2023.03.06 |