민서네집

[poi] .docx 문서의 paragraph를 다른 문서로 copy 하기. 본문

Java

[poi] .docx 문서의 paragraph를 다른 문서로 copy 하기.

브라이언7 2014. 8. 19. 14:24

How to copy a paragraph of .docx to another docx with Java and retain the style


http://stackoverflow.com/questions/10208097/how-to-copy-a-paragraph-of-docx-to-another-docx-withjava-and-retain-the-style/25375809#25375809


stackoverflow.com 에 처음으로 답변을 달았다.


comment 다는 것도 reputation 이 50 이나 필요해서 답변도 못 달줄 알았는데, 답변은 달 수 있었다.


내 reputation 이 지금 1 이다. ^^;;


http://stackoverflow.com/users/1986574/heeseok




Style 을 Copy 하려면


// Copy Styles of Table and Paragraph.
private static void copyStyle(XWPFDocument srcDoc, XWPFDocument destDoc, XWPFStyle style) {

	if( destDoc == null || style == null ) return;

	if( destDoc.getStyles() == null ) {
		destDoc.createStyles();
	}

	List usedStyleList = srcDoc.getStyles().getUsedStyleList(style);
	for (XWPFStyle xwpfStyle : usedStyleList) {
		destDoc.getStyles().addStyle(xwpfStyle);
	}
}





원본 .docx 파일의 Page Layout 을 Copy 하고 싶다면, CTPageMar 객체에 Setting 해야 한다.


[참고] http://stackoverflow.com/questions/17787176/spacing-and-margin-settings-in-word-document-using-apache-poi-docx


아래 코드의 주석에도 달아놨지만 Page Layout 객체를 가져오기 위해 CTPageMar 객체를 꺼낼때 이클립스에서 에러가 난다.


The type org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar cannot be resolved. It is indirectly referenced from required .class files


구글링해서 찾아보니, jar 파일 용량을 줄이기 위해 잘 쓰지 않는 클래스를 뺀 것이다.

완전한 클래스 파일을 얻기 위해 ooxml-schemas-1.1.jar 파일을 다운로드 받아서 클래스 패스에 추가해야 한다.


[참조] http://poi.apache.org/faq.html#faq-N10025

[참조] http://poi.apache.org/overview.html#components


// Page Layout Copy 하기.

//[Error] The type org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar cannot be resolved. It is indirectly referenced from required .class files

// poi-ooxml-schemas-3.10-FINAL-20140208.jar 파일에서 CTPageMar 클래스가 없어서 에러가 남.

// [참조] http://poi.apache.org/faq.html#faq-N10025
// [참조] http://poi.apache.org/overview.html#components

// poi-ooxml-schemas-3.10-FINAL-20140208.jar 파일은 많이 쓰는 클래스만 따로 모아놓은 것이라고 함. 용량이 4,830 kbyte

// < ooxml-schemas 1.1 download >
// http://repo.maven.apache.org/maven2/org/apache/poi/ooxml-schemas/1.1/

private static void copyLayout(XWPFDocument srcDoc, CustomXWPFDocument destDoc)
{
	CTPageMar pgMar = srcDoc.getDocument().getBody().getSectPr().getPgMar();

	BigInteger bottom = pgMar.getBottom();
	BigInteger footer = pgMar.getFooter();
	BigInteger gutter = pgMar.getGutter();
	BigInteger header = pgMar.getHeader();
	BigInteger left = pgMar.getLeft();
	BigInteger right = pgMar.getRight();
	BigInteger top = pgMar.getTop();

	destDoc.getDocument().getBody().addNewSectPr().addNewPgMar();

	CTPageMar pgDstMar = destDoc.getDocument().getBody().getSectPr().getPgMar();

	pgDstMar.setBottom(bottom);
	pgDstMar.setFooter(footer);
	pgDstMar.setGutter(gutter);
	pgDstMar.setHeader(header);
	pgDstMar.setLeft(left);
	pgDstMar.setRight(right);
	pgDstMar.setTop(top);
}




image를 docx 문서 안에 저장하려고 poi-src-3.10-FINAL-20140208.zip 파일 안에 든 


org.apache.poi.xwpf.usermodel.SimpleImages 라는 클래스를 실행해 봤더니 Image 파일을 로딩해서 문서를 만들어야 하는데, 생성된 docx 파일이 오류가 있다고 Microsoft Word에서 열리지 않는다.



구글에서 검색해 봤더니 이미지를 넣어서 문서를 못 만드는 버그가 있는 것 같다.


https://issues.apache.org/bugzilla/show_bug.cgi?id=49765


이것을 해결한 웹페이지도 찾았다.


살펴보니 XWPFDocument를 상속해서 CustomXWPFDocument 클래스를 따로 만들어서 createPicture()라는 메서드를 만들었다.


http://pastebin.com/CbQ3iw8t


http://pastebin.com/2YAneYgt


위 2개의 웹페이지를 보고 클래스를 만들고, 원본 docx 파일에서 이미지를 뽑아내서 새로운 docx 파일에 이미지를 넣도록 테스트를 해 봤는데, 원본 문서의 이미지와 크기가 다르게 지정이 된다. (150% 정도 확대되서 들어간다.)


그래서 클래스를 약간 변형해서 원본 문서에서 이미지의 크기 cx, cy를 빼와서 픽셀 단위가 아니라 그대로 cx, cy를 넘겨서 xml 파일 안에 셋팅해 주도록 바꾸었다. 이렇게 하니 원본 docx 파일 안의 이미지 크기와 새로 생성한 docx 파일 안의 이미지 크기가 동일하게 보였다.


import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.List;

import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.xwpf.usermodel.BodyElementType;
import org.apache.poi.xwpf.usermodel.Document;
import org.apache.poi.xwpf.usermodel.IBodyElement;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFPicture;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFStyle;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.xmlbeans.XmlException;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STPageOrientation.Enum;

public class WordFinal {

	public static void main(String[] args) throws IOException, XmlException {

		XWPFDocument srcDoc = new XWPFDocument(new FileInputStream("Source.docx"));

		CustomXWPFDocument destDoc = new CustomXWPFDocument();

		// Copy document layout.
		copyLayout(srcDoc, destDoc);

		FileOutputStream out = new FileOutputStream("Destination.docx");

		for (IBodyElement bodyElement : srcDoc.getBodyElements()) {

			BodyElementType elementType = bodyElement.getElementType();

			if ( elementType == BodyElementType.PARAGRAPH ) {

				XWPFParagraph srcPr = (XWPFParagraph) bodyElement;

				copyStyle(srcDoc, destDoc, srcDoc.getStyles().getStyle(srcPr.getStyleID()));

				boolean hasImage = false;

				XWPFParagraph dstPr = destDoc.createParagraph();

				// Extract image from source docx file and insert into
				// destination docx file.
				for (XWPFRun srcRun : srcPr.getRuns()) {

					// 여기서 createRun() 하지 않으면 pr.removeRun(0); 할때 에러가 난다.
					dstPr.createRun();

					if (srcRun.getEmbeddedPictures().size() > 0)
						hasImage = true;

					for (XWPFPicture pic : srcRun.getEmbeddedPictures()) {

						byte[] img = pic.getPictureData().getData();

						long cx = pic.getCTPicture().getSpPr().getXfrm().getExt().getCx();
						long cy = pic.getCTPicture().getSpPr().getXfrm().getExt().getCy();

						try {
							String blipId = dstPr.getDocument().addPictureData(new ByteArrayInputStream(img),
									Document.PICTURE_TYPE_PNG);
							destDoc.createPictureCxCy(blipId, destDoc.getNextPicNameNumber(Document.PICTURE_TYPE_PNG),
									cx, cy);
						} catch (InvalidFormatException e1) {
							e1.printStackTrace();
						}
					}
				}

				if (hasImage == false)
				{
					int pos = destDoc.getParagraphs().size() - 1;
					destDoc.setParagraph(srcPr, pos);
				}

			} else if ( elementType == BodyElementType.TABLE ) {

				XWPFTable srcTable = (XWPFTable) bodyElement;

				copyStyle(srcDoc, destDoc, srcDoc.getStyles().getStyle(srcTable.getStyleID()));

				destDoc.createTable();

				int pos = destDoc.getTables().size() - 1;

				destDoc.setTable(pos, srcTable);
			}
		}

		writeAndClose(destDoc, out);
	}

	private static void writeAndClose(XWPFDocument destDoc, FileOutputStream out) throws IOException {

		// 마지막 Paragraph에 있는 <w:r><w:br w:type="page"/></w:r> 를 찾아서 제거한다.
		int size = destDoc.getBodyElements().size();
		if (size > 0) {
			IBodyElement iBodyElement = destDoc.getBodyElements().get(size - 1);

			if( iBodyElement.getElementType() == BodyElementType.PARAGRAPH ) {
				XWPFParagraph pr = (XWPFParagraph) iBodyElement;

				// page break 를 가지고 있는지 제대로 check 하지 못한다.
//				if( pr.isPageBreak() ) {
//					System.out.println("######### page break ##############");
//				}

				pr.removeRun(0); // Good. page break 를 가지고 있는 Run 이 없어진다.
			}
		}

		destDoc.write(out);
		out.close();
	}

	// Copy Styles of Table and Paragraph.
	private static void copyStyle(XWPFDocument srcDoc, XWPFDocument destDoc, XWPFStyle style) {

		if (destDoc == null || style == null)
			return;

		if (destDoc.getStyles() == null) {
			destDoc.createStyles();
		}

		List<XWPFStyle> usedStyleList = srcDoc.getStyles().getUsedStyleList(style);
		for (XWPFStyle xwpfStyle : usedStyleList) {
			destDoc.getStyles().addStyle(xwpfStyle);
		}
	}

	// Copy Page Layout.
	//
	// if next error message shows up, download "ooxml-schemas-1.1.jar" file and
	// add it to classpath.
	//
	// [Error]
	// The type org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar
	// cannot be resolved.
	// It is indirectly referenced from required .class files
	//
	// It is why there isn't CTPageMar class in
	// poi-ooxml-schemas-3.10-FINAL-20140208.jar.
	//
	// [ref.] http://poi.apache.org/faq.html#faq-N10025
	// [ref.] http://poi.apache.org/overview.html#components
	//
	// < ooxml-schemas 1.1 download >
	// http://repo.maven.apache.org/maven2/org/apache/poi/ooxml-schemas/1.1/
	//
	private static void copyLayout(XWPFDocument srcDoc, XWPFDocument destDoc) {
		CTPageMar pgMar = srcDoc.getDocument().getBody().getSectPr().getPgMar();

		BigInteger bottom = pgMar.getBottom();
		BigInteger footer = pgMar.getFooter();
		BigInteger gutter = pgMar.getGutter();
		BigInteger header = pgMar.getHeader();
		BigInteger left = pgMar.getLeft();
		BigInteger right = pgMar.getRight();
		BigInteger top = pgMar.getTop();

		CTPageMar addNewPgMar = destDoc.getDocument().getBody().addNewSectPr().addNewPgMar();

		addNewPgMar.setBottom(bottom);
		addNewPgMar.setFooter(footer);
		addNewPgMar.setGutter(gutter);
		addNewPgMar.setHeader(header);
		addNewPgMar.setLeft(left);
		addNewPgMar.setRight(right);
		addNewPgMar.setTop(top);

		CTPageSz pgSzSrc = srcDoc.getDocument().getBody().getSectPr().getPgSz();

		BigInteger code = pgSzSrc.getCode();
		BigInteger h = pgSzSrc.getH();
		Enum orient = pgSzSrc.getOrient();
		BigInteger w = pgSzSrc.getW();

		CTPageSz addNewPgSz = destDoc.getDocument().getBody().addNewSectPr().addNewPgSz();

		addNewPgSz.setCode(code);
		addNewPgSz.setH(h);
		addNewPgSz.setOrient(orient);
		addNewPgSz.setW(w);
	}
}


Test 할 수 있는 Java 파일 2개와 이미지 파일을 첨부합니다.


apache-poi-java-sample.zip


[참고] poi-bin-3.10.1-20140818.zip를 http://poi.apache.org/download.html#POI-3.10.1 에서 다운로드 받아서 다시 SimpleImages.java 를 실행해 봐도 계속 동일한 오류가 발생한다. 


How to merge docx files


http://stackoverflow.com/questions/2494549/is-there-any-java-library-maybe-poi-which-allows-to-merge-docx-files


method 1,2개로 깔끔하게 merge 할 수 있을줄 알았더니 의외로 소스 코드가 지저분해진다. 


(실제로 merge 되는지 실행해 보지도 않았다.)


POI 와 Docx4j 로 merge 하는 소스가 있는데, POI 로 merge하는 소스가 좀 더 이해하기 쉽고, 간단한 것 같다.


( 참고로 POI 는 excel 파일 조작 위주의 라이브러리이고, docx4j 는 워드 파일 조작 위주의 라이브러리라고 한다. )


[출처] http://www.esupu.com/open-source-office-document-java-api-review/


Comments