본문 바로가기
Programming/etc

[JAVA] iText API를 이용해 html을 PDF로 변환 후 다운로드하는 예제

by prinha 2020. 12. 29.
반응형

반응형

 

itextpdf.com/en/resources/api-documentation

 

API documentation

Currently we have lists of application programming interfaces (APIs) directories for iText 5, iText 7 and several add-ons.

itextpdf.com


예제를 보기전에 꼭 확인 해주세요!

초보개발자가 저처럼 삽질하지않길 바라면서 포스팅을 시작합니다..

구현해놓고보면 전혀 어렵지않은데 자잘자잘하게 너무 번거롭고 귀찮은 작업이에요ㅠㅠ(인코딩, 서버의 차이 등등..)

 

윈도우 서버에서는 html파일을 만들어 pdf생성이 가능했으나,

리눅스 서버에서는 The document has no pages라는 에러가 발생했다..

도큐먼트 페이지가 없어 open과 close를 할 수 없는 에러였는데 별짓을 다해봐도 해결을 못해서

결국 html코드를 직접 JAVA단에 삽입(막노동+무식한 방법..)하여 해결했다..

혹시 이 에러를 해결하신 분은 댓글로 남겨주세요..저도 너무 궁금합니다..

윈도우에서는 실행이 가능했는데 리눅스에서 불가하다는게 서버환경이나 버전에 따라 다를 수 있으므로 꼭 확인하세요ㅠㅠ

 

ireport로 jrxml파일을 만들어서 jasperreport로 구현하는 방법도 있습니다..

오히려 그 방법이 더 간단할 수도 있어요..참고하세요.


iText 기본 라이브러리를 설치했다는 가정하의 코드 진행입니다. 

저는 라이브러리를 다운받아 사용했는데 pom.xml에 의존성 주입하는 방법도 있습니다.

 

0. html 템플릿을 만든다. (handlebars와 비슷한 원리?라고 보시면 될 듯 합니다.)

html템플릿을 만들 때 주의해야 할 점이 있습니다..

  • 한글 출력이 안되서 저도 무척이나 애먹었는데, 인코딩+폰트가 중요합니다. iTextAsian.jar파일을 라이브러리폴더에 복사한 후 , 폰트는 HYGoThic-Medium, HYSMyeongJo-Medium, HYSMyeongJoStd 중 한가지를 사용하며, 인코딩은 UniKS-UCS2-H와 UniKS-UCS2-V 중 하나를 선택합니다. 한글을 안쓰시면 안하셔도 무관하나.. 폰트와 인코딩설정을 안해주시면 출력자체가 안되니 주의하세요.
  • <table><tr><td> 안에 텍스트나 el태그를 넣어주셔야 출력이 됩니다. 테스트해보면 아시겠지만 웬만한 다른 태그들은 안먹어요..
  • 스타일시트는 거의(?) 안먹는다고 보시면 됩니다. 이 것 때문에 고생하신 분들이 많이 보이는데 저도 시간 많이 들였습니다..기본적인 정렬, 폰트, 컬러 등만 먹는다고 생각하세요.
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body face="HYGoThic-Medium" encoding="UniKS-UCS2-H">

	<table width="100%" bgcolor="#2862A7">
		<tr><td><br></td></tr>
		<tr><td align="center" color="white" style="font-zize:20pt;font-weight:bold;">
        ID : ${img1}</td></tr>
		<tr><td> </td></tr>
	</table>
	<table><tr><td><br></td></tr></table>
    <table width="100%" border="1">
    	<tr>
    		<td style="font-size:8pt" bgcolor="#ebecf0" align="center">img2</td>
    		<td style="font-size:8pt" bgcolor="#ebecf0" align="center">img3</td>
    		<td style="font-size:8pt" bgcolor="#ebecf0" align="center">img4</td>
    	</tr>
    	<tr>
    		<td style="font-size:6.5pt" align="center">${img2}</td>
    		<td style="font-size:6.5pt" align="center">${img3}</td>
    		<td style="font-size:6.5pt" align="center">${img4}</td>
    	</tr>
	</table>
</body>
</html>

 

1. script단에서 html에 삽입할 데이터를 controller에 비동기 방식으로 보낸다.

(DB에서 데이터를 가져와 pdf를 만드는 경우도 있는데, 이번 경우는 현재 페이지가 가지고있는 데이터를 가지고 pdf를 생성한다.)

// jsp에서 button클릭시 PdfDownload()함수 실행
function PdfDownload(){
	var imagingVO = {
		img1:img1,
        	img2:img2,
        	img3:img3,
        	img4:img4
	};

    $.ajax({
       url : GLOBAL_CONTEXT_PATH + '/imagingInfo/pdfCreate.do',
       type:"POST",
       data:imagingVO,
       success:function(data){
           alert("성공");
        },
        error:function(data){
           alert("다운로드에 실패하였습니다.");        
        }
    });
}

 

2. Controller

DB에서 데이터를 보내는 경우 비즈니스로직과 DB연동을 해주는 클래스가 필요하나,

이번 예제에서는 스크립트로부터 데이터를 파라미터로 받기 때문에 Controller만 필요합니다.

"tempDir"은 app_resource.properties에 미리 명시해놓은 임시폴더입니다.저는 tempDir=/tmp/로 했습니다. (c드라이브에 tmp폴더 자동 생성됩니다.)

pdf를 다운로드 받은 후 임시폴더에 있는 pdf파일을 삭제하는 코드도 작성되어있습니다.


private String getToDay() { // 현재날짜와시간출력
	SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
	return sdf.format(Calendar.getInstance().getTime());
}

@RequestMapping(value = "/imagingInfo/pdfCreate.do", method = RequestMethod.POST)
public ModelAndView pdfCreate(@ModelAttribute("imagingVO") ImagingVO imagingVO,
		ModelMap model, HttpServletRequest request) throws Exception {

	String img1 = imagingVO.getImg1();

	// PDF파일 저장 위치 및 파일명 생성(파일 출력 및 PDF생성시 사용)
	String pdfUpFilePath = ResourceBundleUtil.getResourceBundle(img1, "tempDir", ResourceBundleUtil.RBU_UPLOAD);
	String fileName = "img_" + img1 + "_" + getToDay() + ".pdf";

	String fileRealPath = pdfUpFilePath + fileName;
    
    //임시폴더의 PDF파일 삭제 -> pdf다운로드 후 임시폴더에 있을 필요가 없기때문에 삭제
	File FileList = new File(filePath);
	//해당 폴더의 전체 파일리스트 조회
	String fileList[] = FileList.list();
		
	//전체파일
	for(int i=0; i<fileList.length; i++){
	//파일명 조회
	String FileName = fileList[i];
			
	//파일명에 pdf 값이 존재하는지 체크
	if(FileName.contains(".pdf")){
			//존재하면 파일삭제
			File deleteFile = new File(filePath+FileName);
			deleteFile.delete();
		}
	}
		
	PdfPage pdfPage = null; // 실질적으로 Pdf를 생성하는 클래스
	try {
		Map<String, Object> params = BeanUtils.describe(imagingVO);

		pdfPage = new PdfPage("/html파일경로와 파일명.html",params);
        
		// 여기서 pdf생성, 저장
		pdfPage.runFileWriteProcesser(fileRealPath);
			
	} catch (BizException e) {
			e.printStackTrace();
	}

	// 파일 출력시 사용할 파일 경로
	String filePath = ResourceBundleUtil.getResourceBundle(img1, "tempDir",ResourceBundleUtil.RBU_DOWNLOAD);
	File download = new File(filePath, fileName);
	model.addAttribute("download",download);
    model.addAttribute("fileName",fileName);
    
	return new ModelAndView("download", "download", model);
}

 

3. DownloadView

context-web.xml에 커스텀 뷰 클래스를 사용하는 beanNameViewResolver 설정을 먼저 해줍니다.

<bean id="beanNameViewResolver"
	class="org.springframework.web.servlet.view.BeanNameViewResolver">
	<property name="order" value="0" />
</bean>
<!-- bean id는 controller에서 쓸 뷰 네임 -->
<bean id="download" class="파일경로.파일명" />

 

AbstractView를 상속받는 뷰 클래스를 작성해줍니다.

이 클래스는 생성된 PDF를 OutputStream을 이용해 파일다운로드 기능을 담당합니다.

public class DownloadView extends AbstractView {

	public DownloadView() {
		setContentType("application/octet-stream");
	}

	@Override
	protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		Assert.notNull(model, "model is null");

		Object o = model.get("download");
		Assert.notNull(o, "download file is empty");

		response.setContentType(getContentType());
		response.setHeader("Content-Transfer-Encoding", "binary");

		OutputStream out = response.getOutputStream();

		String fileName = null;
		InputStream is = null;
		File file = null;

	
		@SuppressWarnings("unchecked")
		HashMap<String, Object> map = (HashMap<String, Object>) model.get("download");
		file = (File) map.get("download");
		is = new FileInputStream(file);
		response.setContentLength((int) file.length());
		fileName = (String) model.get("fileName");
	
		response.setHeader("Content-Disposition", "attachment; fileName=\"" + fileName + "\";");

		try {
			FileCopyUtils.copy(is, out);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException ex) {
				}
			}
		}
		
		out.flush();
	}
}

 

4. PdfPage

실질적으로 PdfPage를 생성, 저장하는 클래스입니다.

Controller에서 가장 먼저 runFileWriteProcesser()메소드를 호출하는 것이 시작입니다.

public class PdfPage {

	private final static String NEW_PAGE = "NEW_PAGE";

	private Map<String, Object> params = null;
	private String filePath = null;
	private String saveFilePath = null;
	private Rectangle pageSize = PageSize.A4;
	private StyleSheet styleSheet = null;
	private OutputStream pdfOutputStream = null;

	public PdfPage(String filePath, Map<String, Object> params) {
		this.filePath = filePath;
		this.params = params;
	}

	public void setPageSize(Rectangle pageSize) {
		this.pageSize = pageSize;
	}

	public void setStyleSheet(StyleSheet styleSheet) {
		this.styleSheet = styleSheet;
	}

	public void setSaveFilePath(String saveFilePath) {
		this.saveFilePath = saveFilePath;
	}

	public void runFileWriteProcesser() throws Exception {
		if (this.saveFilePath == null) {
			throw new RuntimeException("SaveFilePath 가 null 입니다.");
		}

		this.runFileWriteProcesser(saveFilePath);
	}
    
    	public void runFileWriteProcesser(String pdfFile) throws Exception {
	
		this.params.put("contextPath", contextPath);
		this.params.put("util", new VelocityUtil());

		File file = new File(contextPath + this.filePath);

		// File을 읽어서 String로 변경.
		String inString = this.readFileToString(file);

		// 벨록시티를 이용하여 값 맵핑.
		StringReader reader = new StringReader(convertingVelocity(inString, params)); // PDF(iText)로 변환

		this.pdfOutputStream = new FileOutputStream(pdfFile); // PDF(iText)로 변환된 결과를 담을 스트림 생성
        
		htmlParser(reader, pdfOutputStream, new StyleSheet());
	}
    	
	//html Data를 담은 Reader을 받아 iText의 Document로 추출하여 OutputStream에 담는다.
	public void htmlParser(Reader htmlReader, OutputStream outputStream, StyleSheet styleSheet)
			throws DocumentException, IOException, BizException {

		Document document = new Document(pageSize);
		List<Element> elementList = new ArrayList<Element>();
        
		try {
			PdfWriter.getInstance(document, outputStream);
            
			document.open();
			document.newPage();
            
			elementList = HTMLWorker.parseToList(htmlReader, new StyleSheet());
		} catch (ExceptionConverter e) {
			Object o = e.getCause();

			if (o != null && o instanceof FileNotFoundException) {
				throw new BizException("이미지를 찾을 수 없습니다.");
			}
			throw e;
		}

		for (Element element : elementList) {
			try {
				// New Page 처리
				if (element != null && element instanceof Paragraph) {
					String msg = ((Paragraph) element).getContent();
					if (msg != null && msg.trim().equals(PdfPage.NEW_PAGE)) {
						document.newPage();
					}
				} else {
					document.add(element);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		document.close();
	}
    
   
	//파일내용을 String 타입으로 변환한다.
	public String readFileToString(File file) {

		int fileSize = (int) file.length();
		char[] ch = new char[fileSize];
		InputStreamReader fis = null;
		try {
			fis = new InputStreamReader(new FileInputStream(file), "UTF-8");

			int pos = 0;
			int size = 10;
			int temp = 0;

			while ((size = fis.read(ch, pos, size)) > 0) { // read() 메서드를 통하여 읽기
				pos += size;
				temp = ch.length - pos;
				if (temp < 10) {
					size = temp;
				}
			}

		} catch (IOException e) {
			e.printStackTrace();

		} finally {
			if (fis != null) {
				try {
					fis.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

		return String.valueOf(ch);
	}

	//스트링을 Velocity엔진을 이용하여 변환한다.
	public String convertingVelocity(String inString, Map<String, Object> params) {

		try {
			Velocity.init();
		} catch (Exception e) {
			e.getStackTrace();
		}

		VelocityContext context = new VelocityContext(params);

		StringWriter writer = new StringWriter();

		String logTag = Thread.currentThread().getName();
		try {
			Velocity.evaluate(context, writer, logTag, inString);

		} catch (Exception e) {
			e.getStackTrace();
		}
		return writer.toString();
	}
}

 

이렇게하면 html파일을 파싱하여 PDF로 변환 후 저장, 다운로드까지 구현이 가능합니다!

코드가 잘못되었다거나 질문이 있으시면 댓글 남겨주세요! 

 

 

 

반응형