Java 富文本导出至word

转载自 https://www.cnblogs.com/liaofeifight/p/5484891.html

最近需要将一些信息导出至word,最开始的解决方案是采用了 https://github.com/opensagres/xdocreport 这个来导出word,对文本,表格支持的都很好,但是后面发现需要支持富文本片段,转了一圈没发现它支持这个特性,无奈之下只好 寻求其他的解决办法(没理解需求就动刀一般都是这个下场),最后发现上面的文章可以解决这个问题。

实现的步骤很简单

  • 修改word文件、将需要填充的内容利用模板引擎的语法替代
  • 将word文件导出至mht格式的文件
  • 检查mht格式文件,有时候因为换行的原因,{alias}会变成{ali= \r\n as}
  • 利用模板引擎解析mht文件,然后另存为doc格式(没有测试过docx)

其中有几个需要注意的地方:

  • 中文需要转化成ASCII编码,上面的文章中对ascii的转化不是很全面, https://www.cnblogs.com/jinc/archive/2013/02/26/2933766.html 这篇文章中提出了一个‘完美’的方法。
  • 整体内容需要编码,查看mht文件的时候可以发现,它的标签属性都是3D打头的,了解了下后发现这是 quoted-printable 编码,可以采用apache 的 commons codec中的QuotedPrintableCodec来编码,也可以采用jdk自带的MimeUtility, 具体请见 https://stackoverflow.com/questions/21574745/java-encode-string-in-quoted-printable
  • 富文本也许需要 unescape ,出于有些原因,一些富文本在后台会被escape,这时候需要unescape后才可以用于输出,如果项目中采用了spring,它的HtmlUtils可以提供这个方法。
  • 整体内容编码顺序 :unescape->转为 ascii -> quoted-printable编码,因为quoted-printable会将中文编码成=3D…这类的内容

关于mht文件的维护: 导出的mht文件一般有相当的行数,如果页面一多,维护整个mht文件都会非常的困难,如果因为某个部分需要修改而要重做整个mht文件很可能导致情绪爆炸,我这里利用mht文件中的 class=3DSection 样式来分离每一个Section,这样就有一个 包含样式的主要的mht文件,内容如下(${page x}这个代表着某个Section文件渲染内容、它后面跟着的b、span标签姑且认为是换页吧):

 <html>
 	<head>//非常多的样式</head>
    <body lang=3DZH-CN style=3D'tab-interval:21.0pt;text-justify-trim:punctuati=on'>
		${page1}
		<b style=3D'mso-bidi-font-weight:normal'><span lang=3DEN-US style=3D'font-s=
		ize:15.0pt;mso-bidi-font-size:10.0pt;font-family:\4EFF\5B8B;mso-hansi-font-family:"Times New Roman";
		mso-bidi-font-family:"Times New Roman";mso-ansi-language:EN-US;mso-fareast-language:ZH-CN;mso-bidi-language:AR-SA;layout-grid-mode:line'>
        <br clear=3Dall style=3D'page-break-before:always;mso-break-type:section-break'>
	</span></b>
    ${page2}
 </html>

和其他的section文件,这样维护的时候可以具体到某个section。

2017.06.22

针对 中文需要转化成ASCII编码这个说法,其实不只是中文,某些字符,例如‘①’同样需要转化,看了一圈后将string2Ascii方法调整如下:

public static String string2Ascii(String source) {
		if (StringUtils.isEmpty(source)) {
			return "";
		}

		CharsetEncoder encoder = Charset.forName("US-ASCII").newEncoder();
		StringBuilder sb = new StringBuilder();
		char[] c = source.toCharArray();
		for (char item : c) {
			if (!encoder.canEncode(item)) {
				String itemascii = "&#" + (item & 0xffff) + ";";
				sb.append(itemascii);
			} else {
            	sb.append(item);
            }
		}

		return sb.toString();
	}

最后附上一个具体的导出工具类:



import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.net.QuotedPrintableCodec;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
import org.springframework.web.util.HtmlUtils;

import freemarker.cache.NullCacheStorage;
import freemarker.cache.TemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;

public final class DocHelper {

	private DocHelper() {
		super();
	}

	private static final Configuration cfg = new Configuration(Configuration.VERSION_2_3_25);

	static {
		cfg.setTemplateLoader(new DocTemplateLoader());
		cfg.setDefaultEncoding("UTF-8");
		cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
		cfg.setLogTemplateExceptions(false);
		// 不缓存模板
		cfg.setCacheStorage(NullCacheStorage.INSTANCE);
	}

	// THREAD-SAFE
	private static final QuotedPrintableCodec qcodec = new QuotedPrintableCodec();

	/**
	 * 模板存放文件夹
	 */
	private static final String TEMPLATE_DIR_PATH = "template/";

	/**
	 * 
	 * @param 模板名称
	 * @param variables
	 *            变量
	 * @param out
	 *            输出流 <b>需要手动关闭输出流!!</b>
	 * @throws Exception
	 *             处理|输出失败
	 */
	public static void report(String templateName, Map<String, Object> variables, Writer out) throws Exception {
		Template template = cfg.getTemplate(templateName);
		Map<String, Object> root = variables;
		if (root == null) {
			root = new HashMap<String, Object>();
		}
		map2Ascii(root);
		// 查找文件夹
		Resource dir = new ClassPathResource(TEMPLATE_DIR_PATH + templateName);
		if (dir.exists()) {
			for (File file : dir.getFile().listFiles()) {
				String baseName = FilenameUtils.getBaseName(file.getName());
				root.put("page" + baseName, process(cfg.getTemplate(templateName + "/" + baseName), root));
			}
		}
		template.process(root, out);
	}

	private static String process(Template template, Map<String, Object> root) throws TemplateException, IOException {
		StringWriter sw = new StringWriter();
		template.process(root, sw);
		return sw.toString();
	}

	private static final class DocTemplateLoader implements TemplateLoader {

		@Override
		public void closeTemplateSource(Object source) throws IOException {
			//
		}

		@Override
		public Object findTemplateSource(String templateName) throws IOException {
			int index = templateName.indexOf('_');
			if (index == -1) {
				return new ClassPathResource(TEMPLATE_DIR_PATH + templateName + ".mht");
			} else {
				return new ClassPathResource(TEMPLATE_DIR_PATH + templateName.substring(0, index) + ".mht");
			}
		}

		@Override
		public long getLastModified(Object source) {
			return -1;
		}

		@Override
		public Reader getReader(Object source, String arg1) throws IOException {
			Resource resource = (Resource) source;
			InputStream is = null;
			try {
				is = resource.getInputStream();
				return new StringReader(IOUtils.toString(is));
			} finally {
				if (is != null) {
					is.close();
				}
			}
		}
	}

	private static void map2Ascii(final Map<String, Object> map) {
		for (Map.Entry<String, Object> it : map.entrySet()) {
			object2Ascii(it.getValue());
		}
	}

	/**
	 * 简单的将string属性内容进行ascii编码
	 * 
	 * @param obj
	 *            要编码的对象
	 *            <p>
	 *            <b>仅支持对象和 Collection</b>
	 *            </p>
	 * @return
	 */
	private static void object2Ascii(final Object obj) {

		if (ClassUtils.isPrimitiveOrWrapper(obj.getClass()) || ClassUtils.isPrimitiveArray(obj.getClass())
				|| ClassUtils.isPrimitiveWrapperArray(obj.getClass())) {
			return;
		}

		if (Collection.class.isAssignableFrom(obj.getClass())) {
			objects2Ascii((Collection<?>) obj);
			return;
		}

		ReflectionUtils.doWithFields(obj.getClass(), new FieldCallback() {

			@Override
			public void doWith(Field f) throws IllegalArgumentException, IllegalAccessException {
				if (f.getType() == String.class) {
					f.setAccessible(true);
					String oldValue = (String) f.get(obj);
					if (!StringUtils.isEmpty(oldValue)) {
						String v;
						if (f.isAnnotationPresent(QuotedPrintableEncode.class)) {
							v = quotedPrintableEncode(oldValue);
						} else {
							v = string2Ascii(oldValue);
						}
						f.set(obj, v);
					}
				}
				if (Collection.class.isAssignableFrom(f.getType())) {
					f.setAccessible(true);
					Object v = f.get(obj);
					if (v != null) {
						objects2Ascii((Collection<?>) v);
					}
				}
			}

		});
	}

	private static void objects2Ascii(final Collection<?> objs) {
		for (Object obj : objs) {
			object2Ascii(obj);
		}
	}

	public static String string2Ascii(String source) {
		if (StringUtils.isEmpty(source)) {
			return "";
		}

		CharsetEncoder encoder = Charset.forName("US-ASCII").newEncoder();
		StringBuilder sb = new StringBuilder();
		char[] c = source.toCharArray();
		for (char item : c) {
			if (!encoder.canEncode(item)) {
				String itemascii = "&#" + (item & 0xffff) + ";";
				sb.append(itemascii);
			} else {
            	sb.append(item);
            }
		}

		return sb.toString();
	}

	/**
	 * quoted-printable 编码输出
	 * 
	 * @param source
	 * @return
	 * @throws EncoderException
	 */
	public static String quotedPrintableEncode(String source) {
		if (StringUtils.isEmpty(source)) {
			return source;
		}
		try {
			// 先 unescape
			// 再 转为ASCII
			// 最后编码
			return qcodec.encode(string2Ascii(HtmlUtils.htmlUnescape(source)));
		} catch (EncoderException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}
}