需求:将Excel转化为HTML,支持图片,支持多Sheet。

要求:1. 第三方插件免费开源;2. 支持跨平台

设计方案:

1. 采用Apache POI;

2. 多个Sheet时,下个Sheet内容向下对齐HTML。

3. 图片以文件的形式单独存储,或者以二进制字符串的形式保存在HTML文件中;

开发难点:

1. HTML中图片位置应该和Excel保持一致(图片向下对齐比较简单,但用户体验不好)。

遗留问题(后续改进中):

1. HTML图片左边距和Excel接近,但是有些情况下上边距和Excel有明显区别,还需优化。

代码逻辑:

1. 使用POI将Excel文本转化为HTML字符串(包含样式,但是没有图片)。

2. 使用POI获取Excel中所有图片的信息,包括图片所在Sheet索引、行号、列号、单元格内的上边距、单元格内的左边距、二进制流等。

3. 根据图片信息,生成图片对应的HTML字符串(一张图片独占一个table);过程中需要计算图片在HTML中的左边距和上边距。

4. 解析文本HTML字符串,将图片HTML信息放在body的尾部,组合成完整的HTML字符串。

5. 生成HTML文件。

Excel(含2个Sheet):

java excel图片换行 excel转图片 java_java excel图片换行

Excel转化后的HTML:

java excel图片换行 excel转图片 java_HTML_02

maven(部分):

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-io</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.9</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.0</version>
</dependency>

代码 - 以下代码针对excel2003(xls)的操作, excel2007(xlsx)需要单独处理。

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.hssf.converter.ExcelToHtmlConverter;
import org.apache.poi.hssf.usermodel.HSSFClientAnchor;
import org.apache.poi.hssf.usermodel.HSSFPatriarch;
import org.apache.poi.hssf.usermodel.HSSFPicture;
import org.apache.poi.hssf.usermodel.HSSFPictureData;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFShape;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.w3c.dom.Document;

/**
 * POI将Excel2003转化为HTML, 支持图片 zyj 2019-10-08
 */
public class Xls2Html {
	/**
	 * POI将Excel转化为HTML(包含图片,图片以文件形式保存)
	 * 
	 * @param excel excel全路径
	 * @param html  html全路径
	 */
	public static void genHtml(String excel, String html) throws Exception {
		// 创建excel ExcelToHtmlConverter对象
		ExcelToHtmlConverter convert = new ExcelToHtmlConverter(DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument());
		// 列名不显示
		convert.setOutputColumnHeaders(false);
		// 行号不显示
		convert.setOutputRowNumbers(false);
		// 创建POI工作薄对象
		InputStream input = new FileInputStream(excel);
		HSSFWorkbook wb = new HSSFWorkbook(input);
		// 去除EXCEL每个sheet的名称
		for (int i = 0; i < wb.getNumberOfSheets(); i++) {
			if (wb.getSheetAt(i) != null) {
				String sheetName = StringUtils.leftPad(" ", i + 1, " ");
				wb.setSheetName(i, sheetName);
			}
		}
		convert.processWorkbook(wb);

		// 创建HTML的内容(不包含图片)
		Document htmlDocument = convert.getDocument();
		ByteArrayOutputStream outStream = new ByteArrayOutputStream();
		DOMSource domSource = new DOMSource(htmlDocument);
		StreamResult streamResult = new StreamResult(outStream);
		TransformerFactory tfFactory = TransformerFactory.newInstance();
		Transformer tf = tfFactory.newTransformer();
		tf.setOutputProperty(OutputKeys.ENCODING, "utf-8");
		tf.setOutputProperty(OutputKeys.INDENT, "yes");
		tf.setOutputProperty(OutputKeys.METHOD, "html");
		tf.transform(domSource, streamResult);
		outStream.close();
		String content = new String(outStream.toByteArray());

		// 图片处理
		// 以文件方式存储图片
		String htmlImg = getImgHtml(wb, html);
		// 以二进制字符串方式保存在html文件中
		// String htmlImg = getImgHtml_Base64(wb, html);
		if (!"".equals(htmlImg)) {
			int bodyIndex = content.lastIndexOf("</body>");
			String tbodyA = content.substring(0, bodyIndex);
			String tbodyB = content.substring(bodyIndex, content.length() - 1);
			StringBuilder sb = new StringBuilder();
			sb.append(tbodyA);
			sb.append(htmlImg);
			sb.append(tbodyB);
			content = sb.toString();
		}
		// 生成HTML文件
		File file = new File(html);
		String htmlFolder = file.getParent();
		String htmlName = file.getName();
		FileUtils.writeStringToFile(new File(htmlFolder, htmlName), content, "utf-8");
	}

	/**
	 * Excel中图片转化为HTML,图片以文件方式存储
	 * 
	 * @param wb   Excel的工作簿
	 * @param html html文件的全路径
	 * @return 关于图片的html
	 * @throws IOException
	 */
	public static String getImgHtml(HSSFWorkbook wb, String html) throws IOException {
		// 获取Excel所有的图片
		Map<String, HSSFPictureData> pics = getPictrues(wb);
		if (pics == null || pics.size() == 0) {
			return "";
		}
		// 图片文件夹 = html的文件夹 + html的名称
		File file = new File(html);
		String htmlFolder = file.getParent();
		String htmlName = file.getName();
		File imgFolder = new File(htmlFolder + "/" + htmlName.substring(0, htmlName.lastIndexOf(".")));
		// 判断图片文件夹是否存在, 不存在则创建
		if (!imgFolder.exists() && !imgFolder.isDirectory()) {
			imgFolder.mkdirs();
		}
		StringBuilder sb = new StringBuilder();
		for (Map.Entry<String, HSSFPictureData> entry : pics.entrySet()) {
			HSSFPictureData pic = entry.getValue();
			// 保存图片, 图片路径 = HTML路径/HTML名称/sheet索引_行号_列号_单元格内的上边距_单元格内的左边距_uuid.后缀
			String ext = pic.suggestFileExtension();
			String imgName = entry.getKey() + "." + ext;
			byte[] data = pic.getData();
			// 创建图片
			FileOutputStream out = new FileOutputStream(imgFolder + "/" + imgName);
			out.write(data);
			out.close();
			// 图片在Excel中的坐标,sheet索引_行号_列号_单元格内的上边距_单元格内的左边距_uuid
			String[] arr = StringUtils.split(entry.getKey(), "_");
			// 图片的上边距和左边距
			float top = getTop(wb, Integer.parseInt(arr[0]), Integer.parseInt(arr[1]), Integer.parseInt(arr[3]));
			float left = getLeft(wb.getSheetAt(Integer.parseInt(arr[0])), Integer.parseInt(arr[2]), Integer.parseInt(arr[4]));
			// margin设置为8,以保持和body的内边距一致
			String htmlImg = "<table style=\"position: absolute; margin:8; left: " + left + "; top: " + top + "pt;\">\n<tbody>\n<tr>\n<td>\n";
			htmlImg += "<image src=\"" + htmlName.substring(0, htmlName.lastIndexOf(".")) + "/" + imgName + "\" />";
			htmlImg += "\n</td>\n</tr>\n</tbody>\n</table>\n";
			sb.append(htmlImg);
		}
		return sb.toString();
	}

	/**
	 * Excel中图片转化为HTML,图片以二进制字符串方式存储在html文件中
	 * 
	 * @param wb   Excel的工作簿
	 * @param html html文件的全路径
	 * @return 关于图片的html
	 * @throws IOException
	 */
	public static String getImgHtml_Base64(HSSFWorkbook wb, String html) throws IOException {
		// 获取Excel所有的图片
		Map<String, HSSFPictureData> pics = getPictrues(wb);
		if (pics == null || pics.size() == 0) {
			return "";
		}
		StringBuilder sb = new StringBuilder();
		for (Map.Entry<String, HSSFPictureData> entry : pics.entrySet()) {
			HSSFPictureData pic = entry.getValue();
			byte[] data = pic.getData();
			String baseStr = Base64.getEncoder().encodeToString(data);
			// 图片在Excel中的坐标,sheet索引_行号_列号_单元格内的上边距_单元格内的左边距_uuid
			String[] arr = StringUtils.split(entry.getKey(), "_");
			float top = getTop(wb, Integer.parseInt(arr[0]), Integer.parseInt(arr[1]), Integer.parseInt(arr[3]));
			float left = getLeft(wb.getSheetAt(Integer.parseInt(arr[0])), Integer.parseInt(arr[2]), Integer.parseInt(arr[4]));
			// margin设置为8,以保持和body的内边距一致
			String htmlImg = "<table style=\"position: absolute; margin:8; left: " + left + "; top: " + top + "pt;\">\n<tbody>\n<tr>\n<td>\n";
			htmlImg += "<image src=\"data:image/png;base64," + baseStr + "\" />";
			htmlImg += "\n</td>\n</tr>\n</tbody>\n</table>\n";
			sb.append(htmlImg);
		}
		return sb.toString();
	}

	/**
	 * Excel的图片获取
	 * 
	 * @param wb Excel的工作簿
	 * @return Excel的图片,键格式:sheet索引_行号_列号_单元格内的上边距_单元格内的左边距_uuid
	 */
	public static Map<String, HSSFPictureData> getPictrues(HSSFWorkbook wb) {
		Map<String, HSSFPictureData> map = new HashMap<String, HSSFPictureData>();
		// getAllPictures方法只能获取不同的图片,如果Excel中存在相同的图片,只能得到一张图片
		List<HSSFPictureData> pics = wb.getAllPictures();
		if (pics.size() == 0) {
			return map;
		}
		for (Integer sheetIndex = 0; sheetIndex < wb.getNumberOfSheets(); sheetIndex++) {
			HSSFSheet sheet = wb.getSheetAt(sheetIndex);
			HSSFPatriarch patriarch = sheet.getDrawingPatriarch();
			if (patriarch == null) {
				continue;
			}
			for (HSSFShape shape : patriarch.getChildren()) {
				HSSFClientAnchor anchor = (HSSFClientAnchor) shape.getAnchor();
				if (shape instanceof HSSFPicture) {
					HSSFPicture pic = (HSSFPicture) shape;
					int picIndex = pic.getPictureIndex() - 1;
					HSSFPictureData picData = pics.get(picIndex);
					// 键格式:sheet索引_行号_列号_单元格内的上边距_单元格内的左边距_uuid
					String key = sheetIndex + "_" + anchor.getRow1() + "_" + anchor.getCol1() + "_" + anchor.getDy1() + "_" + anchor.getDx1() + "_" + UUID.randomUUID();
					map.put(key, picData);
				}
			}
		}
		return map;
	}

	/**
	 * Excel中单元格的上边距(HTML的上边距),支持多Sheet,递加,单位pt
	 * 
	 * @param wb         Excel的工作簿
	 * @param sheetIndex Sheet的索引
	 * @param rowIndex   单元格所在行号
	 * @param dy         单元格内的上边距
	 * @return 上边距
	 */
	public static float getTop(HSSFWorkbook wb, Integer sheetIndex, Integer rowIndex, int dy) {
		float top = 0;
		HSSFSheet sheet = null;
		// 左侧Sheet的上边距
		for (Integer i = 0; i < sheetIndex; i++) {
			sheet = wb.getSheetAt(i);
			// 获得总行数
			Integer rowNum = sheet.getLastRowNum() + 1;
			// 空sheet的总行高是0,空行的行高是0
			top += getTop(sheet, rowNum, 0);
		}
		// 当前sheet的上边距
		sheet = wb.getSheetAt(sheetIndex);
		top += getTop(sheet, rowIndex, dy);
		return top;
	}

	/**
	 * Sheet中单元格的上边距,单位pt,上边距 = SUM(上方每个单元格的高度) + 单元格内的上边距
	 * 
	 * @param sheet    Excel的Sheet
	 * @param rowIndex 单元格所在行号
	 * @param dy       单元格内的上边距
	 * @return 上边距
	 */
	public static float getTop(HSSFSheet sheet, Integer rowIndex, int dy) {
		float top = 0;
		// SUM(上方每个单元格的高度)
		for (int i = 0; i < rowIndex; i++) {
			HSSFRow row = sheet.getRow(i);
			// 排除空行(HTML转化时也被排除了)
			if (row == null) {
				continue;
			}
			top += row.getHeightInPoints();
		}
		// 单元格内的上边距单位转化为pt
		if (dy != 0) {
			top += dy / 20.00f;
		}
		return top;
	}

	/**
	 * Sheet中单元格的左边距,单位px,左边距 = SUM(左侧每个单元格的宽度) + 单元格内的左边距
	 * 
	 * @param sheet    Excel的Sheet
	 * @param colIndex 单元格所在列号,
	 * @param dx       单元格内的左边距
	 * @return 左边距
	 */
	public static float getLeft(HSSFSheet sheet, Integer colIndex, int dx) {
		float left = 0;
		// SUM(左侧每个单元格的宽度)
		for (int i = 0; i < colIndex; i++) {
			float width = sheet.getColumnWidthInPixels(i);
			left += width;
		}
		// 单元格内的左边距单位转化为px
		if (dx != 0) {
			int cw = sheet.getColumnWidth(colIndex);
			int def = sheet.getDefaultColumnWidth() * 256;
			float px = (cw == def ? 32.00f : 36.56f);
			left += dx / px;
		}
		return left;
	}

}