需求:将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):
Excel转化后的HTML:
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;
}
}