스프링

[스프링] spring thymeleaf to pdf 이미지,한글 적용하기

ImNM 2023. 3. 6. 17:49

두둥의 정산서 pdf

두둥 프로젝트를 진행하면서

통신 판매 중개업종이므로 , 호스트에게 공연 카드결제 대금

정산 작업도 배치로 돌려야 했는데, 이때 pdf로 정산서를 보내줘야할 일이 생겼다.

 

두둥에서는 메일도 thymeleaf를 사용해서 보내고 있으므로,

pdf 도 thymeleaf 를 사용해서 보내는 방법으로 결정했다.

 

이미지와, 한글 적용을 하려면 꽤나 고생좀 해야하는데 그 방법을 공유하고자한다.


목차

1.  flying-saucer-pdf 

2. 한글 적용하기

3. 이미지 가능하게 하기


1. flying-saucer-pdf

 

 

GitHub - flyingsaucerproject/flyingsaucer: XML/XHTML and CSS 2.1 renderer in pure Java

XML/XHTML and CSS 2.1 renderer in pure Java. Contribute to flyingsaucerproject/flyingsaucer development by creating an account on GitHub.

github.com

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로 렌더링 한다.

 

 

FileOutputStream (Java Platform SE 7 )

Returns the unique FileChannel object associated with this file output stream. The initial position of the returned channel will be equal to the number of bytes written to the file so far unless this stream is in append mode, in which case it will be equal

docs.oracle.com

테스팅 할 때는 아웃풋 스트림을 파일 아웃풋 스트림으로 잡아도 상관없다.

 

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

 

GitHub - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!

모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.

github.com

 

- B64ImgReplacedElementFactory

 

GitHub - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!

모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.

github.com

 

- settlement.html

 

GitHub - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!

모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.

github.com

 

각각 소스는 위에서 참고 가능하다