ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Excel截图jar包开发指南:用POI将区域渲染成图片

Excel截图jar包开发指南:用POI将区域渲染成图片 简介面向 Java 开发者的 Excel 图表截图辅助资源基于 Aspose.cells 实现专门解决在 Java 项目中生成 Excel 图表、将图表导出为图片以及移除默认水印的常见需求。资源包共 5 个文件、约 12MB结构比较紧凑两个 jar 依赖分别承担主功能与去水印扩展Java 演示源码展示了初始化 Workbook、读取 Sheet、渲染图表并输出图片的关键调用样例 Excel 表格可用于直接测试txt 说明则提供了将 jar 手动导入本地 Maven 仓库的操作命令省去自行搜索语法的麻烦。示例代码虽然精简但已覆盖从一个 Excel 文件到生成图片的主要环节适合作为功能原型或学习模板进行对照改造对刚接触 Aspose.cells 的后端开发者而言也可以降低官方文档的查阅成本。该资源已有 919 人学习下载如需在 Java 环境快速完成 Excel 截图这份小资源包值得参考。另请注意相关 jar 仅供个人学习使用请勿用于商业项目。1. 先搞清楚 excel截图jar.zip 到底是干什么的第一眼看到excel截图jar.zip这个压缩包名字很容易把它和“截屏工具”混为一谈其实它是 Java 生态里一类很具体的工具包读取 Excel 文件里指定区域比如A1:F20把单元格内容、样式、边框、底色渲染成一张 PNG/JPEG 图片。交付形态是一个可执行的 fat jar外面套一层 zip 压缩包方便分发。这类工具在报表系统、邮件自动发送、ERP 单据预览场景里特别常用——你不想为了给领导发个表格截图就装 Office也不愿意让客户直接下载 xlsx 源码输出一张图是最省事的方式。这类包适合谁两类人一是正在做 Java 报表、定时任务推送的开发想把 Excel 转成图片直接贴进邮件或钉钉通知二是手里拿到这样一个小工具包、想搞明白它怎么用、能不能改、打包成什么结构的开发者。这篇文章就把从原理、代码实现、打包到踩坑完整讲一遍按这套路子你自己也能搓一个出来。2. 用 POI 自己实现 Excel 截图核心流程和最小可运行代码2.1 为什么不用 Robot 截屏而是直接渲染很多第一次做这个需求的人第一反应是用java.awt.Robot把屏幕上打开的 Excel 窗口截下来。这个方案在本地偶尔可以用但放到服务器上立刻废掉Linux 服务器没有显示器Excel 也不一定装了窗口被遮挡、屏幕分辨率变化都会让截图翻车。而且 Robot 截到的是“屏幕上的像素”不是“单元格区域”尺寸、清晰度完全不可控。所以常见做法是绕开 Excel 本体直接用 Apache POI 读 xlsx 文件的结构拿到单元格的值、样式、合并区域信息然后用Graphics2D把整个区域“画”成一张图片。这样做的好处有三个不依赖 Office 环境jar 包在 Linux/Win/Mac 都能跑像素尺寸可以根据 DPI 自由控制区域内每个单元格的边框、字体、背景、对齐方式都能精确还原。有人会问POI 本身不是只解析数据吗确实POI 不直接提供“导出图片”API所以中间最关键的一步是“尺寸映射”——把 Excel 的列宽单位、行高单位换算成屏幕像素再决定每个单元格画多大、文字放在哪。这一步换算错了表格比例就会失真这也是网上很多自写方案做不好的地方。2.2 列宽行高到像素的换算公式Excel 里列宽单位比较绕POI 的getColumnWidth(i)返回的是一个整数单位是“1/256 个字符宽度”默认字符宽度是 Calibri 11 号字体下数字“0”的宽度。常见换算经验式是// columnWidth 来自 sheet.getColumnWidth(colIndex) double px Math.round(columnWidth / 256f * 7f 5);7是默认字号下单个字符的像素宽5是单元格左右 padding。这个公式是靠经验凑出来的不是官方标准但实测 96 DPI 下误差在 2px 以内足够视觉对齐。如果想让不同缩放下都精确正确做法是拿sheet.getColumnWidthInPixels()——POI 从 4.1 版本开始提供这个方法内部帮你做了字体度量建议优先用它。行高换算则清晰得多row.getHeightInPoints()返回的是磅值point1 磅 1/72 英寸屏幕 96 DPI 下 1 磅等于 4/3 像素float px row.getHeightInPoints() * 96f / 72f;注意一个细节如果某一行没有被显式设置过行高getHeightInPoints()返回 -1这时候要自己给一个默认值比如 20 磅否则图片里该行高度变 0内容全挤在一起。列宽同理没有设置过列宽的列也要给默认值。2.3 读取区域并渲染成 BufferedImage 的代码下面的代码实现了最核心的能力输入 xlsx 路径、sheet 名、区域范围如A1:F20、输出图片路径然后把区域内容画成一张 PNG。这是整个excel截图jar.zip里最值得抄的部分。import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; public class ExcelImageExporter { public static void export(String xlsxPath, String sheetName, String range, String pngPath, int dpi) throws Exception { try (Workbook wb new XSSFWorkbook(new FileInputStream(xlsxPath))) { Sheet sheet wb.getSheet(sheetName); if (sheet null) throw new IllegalArgumentException(找不到 sheet: sheetName); // 解析区域例如 A1:F20 String[] parts range.split(:); int firstCol CellReference.convertColStringToIndex(parts[0].replaceAll(\\d, )); int firstRow Integer.parseInt(parts[0].replaceAll([A-Za-z], )) - 1; int lastCol CellReference.convertColStringToIndex(parts[1].replaceAll(\\d, )); int lastRow Integer.parseInt(parts[1].replaceAll([A-Za-z], )) - 1; // 计算像素尺寸 int width 0; for (int c firstCol; c lastCol; c) { width calcColumnWidthPx(sheet, c); } int height 0; for (int r firstRow; r lastRow; r) { height calcRowHeightPx(sheet, r); } float scale dpi / 96f; BufferedImage image new BufferedImage((int)(width * scale), (int)(height * scale), BufferedImage.TYPE_INT_RGB); Graphics2D g image.createGraphics(); g.scale(scale, scale); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setColor(Color.WHITE); g.fillRect(0, 0, width, height); // 按行画每个单元格 int currentRowY 0; for (int r firstRow; r lastRow; r) { int rowH calcRowHeightPx(sheet, r); int currentColX 0; Row row sheet.getRow(r); for (int c firstCol; c lastCol; c) { int colW calcColumnWidthPx(sheet, c); Cell cell row null ? null : row.getCell(c); // 合并单元格的左上角才绘制内容 drawCell(g, sheet, cell, r, c, currentColX, currentRowY, colW, rowH); currentColX colW; } currentRowY rowH; } g.dispose(); ImageIO.write(image, png, new File(pngPath)); } } private static int calcColumnWidthPx(Sheet sheet, int col) { int widthUnits sheet.getColumnWidth(col); // 1/256 字符宽度 return Math.max(18, (int)Math.round(widthUnits / 256f * 7f 5)); } private static int calcRowHeightPx(Sheet sheet, int row) { Row r sheet.getRow(row); if (r null) return 20; short h r.getHeightInPoints(); // 未设置时返回 -1POI 会自动用默认行高 return h 0 ? 28 : Math.round(h * 96f / 72f); } private static void drawCell(Graphics2D g, Sheet sheet, Cell cell, int row, int col, int x, int y, int w, int h) { // 被合并且不是左上角的单元直接跳过 for (CellRangeAddress merged : sheet.getMergedRegions()) { if (merged.containsRow(row) merged.containsColumn(col)) { if (merged.getFirstRow() ! row || merged.getFirstColumn() ! col) return; } } // 画背景、边框、文本 CellStyle style cell null ? null : cell.getCellStyle(); g.setColor(getBgColor(style)); g.fillRect(x, y, w, h); g.setColor(getBorderColor(style)); g.drawRect(x, y, w, h); if (cell ! null) { drawCellText(g, cell, x 2, y 2, w - 4, h - 4); } } // drawCellText 中处理字符串、数字、日期按样式对齐画出文本 }这段代码的逻辑线是先把区域解析成行列号然后按“列宽累加 图片宽、行高累加 图片高”算出画布尺寸再逐行逐列画矩形。画每个单元格时先判断它是不是合并区域的左上角不是的话直接跳过避免重复覆盖。scale参数实现对 DPI 的适配——设置 192 就输出两倍尺寸的图适合放到 PPT 或高清屏展示。参数说明里最值得注意的两个dpi越大图片越清晰但体积也越大打印场景设 300屏幕展示设 96~150 足够col宽度的最小保护值设为 18px防止隐藏列或零宽列把整图压缩变形后面避坑章节会再讲这个点。2.4 给工具包加一个命令行入口光有导出的核心类还不够工具包要能交给同事或者放到服务器上用最好提供一个标准命令行入口。我一般会加一个Main类参数设计成键值对形式比位置参数好扩展现有字段public class Main { public static void main(String[] args) throws Exception { String xlsx getArg(args, --xlsx); String sheet getArg(args, --sheet, Sheet1); String range getArg(args, --range, A1:F20); String out getArg(args, --out, out.png); int dpi Integer.parseInt(getArg(args, --dpi, 96)); ExcelImageExporter.export(xlsx, sheet, range, out, dpi); System.out.println(导出完成: out); } }--xlsx是必传项其余都给了默认值这样第一次跑只需要一行命令java -jar excel-screenshot.jar --xlsx 销售报表.xlsx --sheet 汇总 --range A1:H30 --out 报表.png --dpi 150。这个入口同时也是后续做批量导出、定时调度的基础。3. 把工具类打包成 jar 再压成 zip工程结构与依赖取舍3.1 工程结构和依赖选型打开一个已经打包好的excel截图jar.zip解压后里面通常是一个xxx.jar、可能附带一个lib目录、一个README.txt或者启动脚本。这里有个最常见的认知差——很多人以为 jar 里只包含自己写的代码依赖的 POI 库是外置的运行时再通过 classpath 引入但交付给非开发人员时外置 lib 目录容易漏拷、路径写错所以实际打包时我推荐用 fat jar 方案也就是把 POI 等依赖全数打进去交付物就一个 jar。jar包内部结构并不神秘它就是一个 zip 压缩包META-INF/MANIFEST.MF里指定了Main-ClassBOOT-INF/libSpring Boot 风格或根目录Maven shade 插件风格放着依赖类。遇到想研究别人打包的 jar直接改后缀为 zip 解压再用javap或者反编译工具例如 CFR 看类逻辑即可这也是热词里“反编译 jar”“jar包的lib中是什么”对应的场景。Maven 工程最小结构如下excel-screenshot/ ├── pom.xml └── src/main/java/ ├── Main.java └── ExcelImageExporter.java依赖只需要两个核心别多加dependencies dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.5/version /dependency dependency groupIdorg.apache.poi/groupId artifactIdpoi/artifactId version5.2.5/version /dependency /dependenciespoi-ooxml负责 xlsx 格式解析poi是基础包包含ss.usermodel这套统一 API两个都要引。POI 5.x 依赖了commons-compress、xmlbeans等传递依赖fat jar 打包时都会一并带进去。值得提醒的是不要引 3.x 老版本 POI 去兼容 xls——如果确实要支持老格式.xls用HSSFWorkbook单独走一套逻辑别混在一起。3.2 用 maven-shade-plugin 打成可执行 fat jarSpring Boot 的工程常用spring-boot-maven-plugin但这里不需要 Spring 容器shade 插件更轻打出来的 jar 结构也干净build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.1/version executions execution phasepackage/phase goalsgoalshade/goal/goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ServicesResourceTransformer/ /transformers finalNameexcel-screenshot/finalName /configuration /execution /executions /plugin /plugins /build注意finalName会把产物命名为excel-screenshot.jar而不是excel-screenshot-1.0.jar命令行好敲。ServicesResourceTransformer这行容易被忽略但 POI 内部用ServiceLoader加载一些扩展类不打这个 transformer 有些环境下会报NoClassDefFoundError或者 XML 解析器找不到实现的怪问题。打包命令mvn clean package -DskipTests ls target/ # excel-screenshot.jar target/original-excel-screenshot.jaroriginal-前缀的那个是没有打入依赖的瘦 jar平时用不到。3.3 压成 zip 并处理启动脚本jar 本身已经是 zip 格式为什么外面还要包一层 zip两个原因一是 fat jar 通常 20~50MB传输过程中某些文件系统或邮件网关会把它当二进制附件拦截zip 分卷或换名可以绕过二是可以顺便塞一个 Windows 启动脚本run.bat和 Linux 启动脚本run.sh使用者不需要敲java -jar一长串。一个实用的run.bat长这样echo off cd /d %~dp0 java -Xmx512m -jar excel-screenshot.jar --xlsx %1 --sheet %2 --range %3 --out %4 pause在 Windows 上直接把 xlsx 拖到 bat 图标上就能出图不用打开命令行。打包 zip 用系统自带压缩或者命令行均可注意别把外层目录建得太深zip 内层结构建议保持run.bat和excel-screenshot.jar在根目录下解压即用。这也是热词里“zip解压”“win10 右键压缩”对应到的具体坑——压缩时多层目录嵌套会把人搞疯。4. 参数怎么调DPI、字体、合并单元格、换行四块最容易失真的地方4.1 DPI 与图片尺寸的关系命令行里--dpi参数直接影响成图尺寸和清晰度它背后的逻辑是POI 拿到的行高列宽都基于 96 DPI 屏幕字体度量计算scale dpi / 96f所有像素尺寸整体放大。比如 96 时A1:F20区域渲染成 800x600192 时就变成 1600x1200边距、边框线宽也同步放大。场景dpi 建议说明网页/邮件内嵌展示96体积小加载快微信/钉钉传阅96~150手机屏幕 2~3 倍缩放时仍可读打印/PDF 附件200~300边框和文字边缘不发虚大屏数据展示150兼顾体积与观感还有一些实现细节Graphics2D的线宽默认是 1pxDPI 放大后边框会明显变细想要打印时边框和 Excel 里一样粗得按scale去乘线宽。这个在drawCell里设置g.setStroke(new BasicStroke(1 * scale))即可。别小看这个细节打印出来边框若隐若现是最常见的“看起来不对”原因。4.2 字体映射和中文乱码Excel 文件里的字体名是中文的比如“宋体”“微软雅黑”Java 在 Linux 服务器上找不到这些字体名画出来就是方框或者乱码。这是做这个工具包最典型的翻车现场。解决思路是建立一张字体映射表读取单元格样式里的字体名先查映射查不到就回退到系统默认字体并且优先用逻辑字体名Serif/SansSerif。private static Font resolveFont(String fontName, int style, int fontSize) { MapString, String map new HashMap(); map.put(宋体, Serif); map.put(微软雅黑, SansSerif); map.put(黑体, SansSerif); map.put(SimSun, Serif); map.put(Microsoft YaHei, SansSerif); String logical map.getOrDefault(fontName, SansSerif); return new Font(logical, style, fontSize); }系统里是否真有所映射的字体是另一回事。Linux 上至少要装fonts-dejavu-core和文泉驿否则Serif也会退化。判断字体是否可用的代码GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()启动时打印一次字体列表排查乱码会非常快。字体大小的换算也要注意Excel 里的字号单位是磅Graphics2D画字用的也是磅但行高换算后是像素画文字时Font构造器直接传磅值即可不用乘 DPI——因为g.scale(scale, scale)已经把坐标系整体放大了。如果漏了 scale 导致文字和格子尺寸不成比例看起来就是字悬在半空或者被截断。4.3 合并单元格和自动换行的渲染规则合并单元格是自写截图工具里最“玄学”的一部分。处理规则要做到左上角单元格负责绘制整个合并区域的内容其余被合并的格子只画边框不填充内容。如果每个格子都画内容合并区域里的文字会被画好几层黑成一团。画合并区域时要重新计算宽高。假设A1:B3合并内容绘制区域宽度是A列宽 B列宽高度是第1行高 第2行高 第3行高不重新算的话文字就会被限制在单个格子里// 检测当前 cell 是否为合并区域左上角 for (CellRangeAddress merged : sheet.getMergedRegions()) { if (merged.isInRange(row, col)) { if (merged.getFirstRow() row merged.getFirstColumn() col) { int spanW 0, spanH 0; for (int c merged.getFirstColumn(); c merged.getLastColumn(); c) { spanW calcColumnWidthPx(sheet, c); } for (int r merged.getFirstRow(); r merged.getLastRow(); r) { spanH calcRowHeightPx(sheet, r); } // 用 spanW/spanH 替代原格子的 w/h 来绘制 } return; // 非左上角直接不画 } }自动换行wrapText是另一个易失真的点。POI 能读到CellStyle.getWrapText()但从样式里读不到内容该在哪个位置换行需要自己做文本换行测量。简单可靠的逻辑是按中文字符宽度估算一个中文字符宽度约为fontSizePx像素用FontMetrics实测每行能塞下多少个字符然后以\n和“超宽截断”两个维度切分字符串逐行drawString。英文单词要在空格处断行中文可以直接按字符断混排文本以空格优先、字符回退。4.4 边框样式和背景色的取舍POI 里边框有thin、medium、dashed、dotted等十几种Graphics2D的BasicStroke也支持实线、虚线、点线。做一个最小可用的映射表thin - 1px solid、medium - 2px solid、dashed - 虚线其余类型统一回退到实线。背景色注意要读CellStyle.getFillForegroundColorColor()只读getFillBackgroundColorColor()会拿到默认的白色导致整张图白花花一片。5. 常见问题与避坑五个真实踩坑记录5.1 中文全部变成方框现象Windows 本地跑出来的图片文字正常部署到 Linux 服务器后所有中文变成“□□□□”。原因服务器没装中文字体Java 的 logical font 找不到对应字形时直接画方块。我用fc-list :langzh查看系统字体清单时发现只有 DejaVu没有中文字体。解决安装字体并刷新字体缓存apt-get install -y fontconfig xfonts-wqy ttf-mscorefonts-installer fc-cache -fv程序启动时加一行GraphicsEnvironment.getLocalGraphicsEnvironment()主动加载字体目录某些容器里字体是后挂载的不初始化拿不到。5.2 图片里突然出现一整条空白列/行现象截图区域包含隐藏列或者某列宽为 0输出图片里这一块出现一条纯白竖条把表格顶开。原因getColumnWidth在隐藏列时返回 0换算出来的像素宽度 0累加时没有跳过。解决取宽度时加保护宽度为 0 的列直接跳过而非绘制 0 宽格子。同时建议在命令行加一个参数--skip-hidden默认 true解析区域时先检查sheet.isColumnHidden(col)和row.getZeroHeight()跳过隐藏的行列。不要在解析区域字符串阶段就过滤否则行列号错乱。5.3 合并单元格里的文字只显示左上角一格现象A1:C1合并的标题画出来文字挤在第一格里右侧两列空白。原因绘制宽度没有按合并范围扩展只用了A1本身的列宽。解决在drawCellText前先查合并区域命中时把绘制宽度改为合并宽度的总和字体居中时还要把起始 x 坐标改成整个合并区域的中点偏移。测试时专门做一个包含“跨列合并 跨行合并 多级合并”的测试表一遍能把三种情况全覆盖。5.4 解压 zip 提示输入密码现象从同事那里拿到excel截图jar.zip双击解压弹出“输入密码”对话框但是对方说没设置密码。原因这是 zip 伪加密。zip 的 local file header 里通用位标记的第 0 位是加密标志位bit 0正常情况下是00 00如果这个位被置为01 00解压软件就认为文件加密了但实际上压缩数据没有加密。网上很多打包工具、老的压缩软件在制作 zip 时会把标记位置错导致这个老大难问题。解决用 ZipFile 读取时手动把这个 bit 清掉再流式解码或者直接用 7-Zip 打开选“不输入密码直接解压”。如果是自己打包分发建议用zip -r -X命令而不是第三方 GUI 工具-X表示不附加额外属性能避开伪加密生成的路径。5.5 jar 包在服务器上双击或直接运行没反应现象Windows 上java -jar xxx.jar有窗口闪一下就没了查任务管理器也没有 Java 进程。原因程序抛了未捕获异常但控制台窗口关闭太快看不到堆栈。更隐蔽的一种是MANIFEST.MF里Main-Class写错或者 fat jar 打包时覆盖了META-INF/services下的文件导致 POI 初始化失败。解决先改成java -jar xxx.jar 21 | tee run.log拿错误日志再用unzip -p xxx.jar META-INF/MANIFEST.MF检查 Main-Class 行最后用jar tf xxx.jar | grep -i services看 ServiceLoader 配置是否被合并。这三步能定位 90% 的启动失败问题。6. 进阶用法批量导出、定时推送以及怎么验证截图没画错把单区域截图跑通之后下一步通常就是批量导出。常见做法是配一个 Excel 配置表第一列写 sheet 名、第二列写区域、第三列写输出文件名程序循环读配置逐条生成try (Workbook configWb new XSSFWorkbook(new FileInputStream(config.xlsx))) { Sheet cfg configWb.getSheet(tasks); for (Row row : cfg) { String sheetName row.getCell(0).getStringCellValue(); String range row.getCell(1).getStringCellValue(); String out row.getCell(2).getStringCellValue(); ExcelImageExporter.export(report.xlsx, sheetName, range, out, dpi); } }再进一步可以把这个工具类嵌到 Spring Boot 服务里对外暴露一个 HTTP 接口前端传sheet/range/dpi三个参数就能拿到图片流省去命令行交互。定时任务建议配合spring-boot-starter-quartz或者直接用Scheduled每天早上生成前一天的报表截图推送到企业微信群机器人或者邮件附件。注意并发导出时Workbook不是线程安全的每个线程要各持一个Workbook实例别用静态变量共享。验证截图是否画对我的习惯是把生成的图片和 Excel 里手动“复制为图片”的结果放到一起目测重点看三点表格外框是否闭合、合并单元格标题是否居中、长文本是否出现截断或覆盖到下一行格子。自动化验证可以用 Python 做像素级比对简单方案是两张图缩放同尺寸后算均方误差误差超过阈值就告警。对于有公式、条件格式这类复杂样式的单元格Excel 里的显示值和 POI 读取值可能不一致建议在命令里加一个--formula-value开关默认取缓存值cell.getCachedFormulaResultType()避免公式重算导致结果与打开 Excel 看到的不一样。最后说一个我自己养成的习惯每次交付这类工具包zip 里一定会带一个test.xlsx里面放好合并单元格、中英文混排、隐藏行、边框样式这些“刁钻”用例拿到包的人第一件事跑run.bat test.xlsx五分钟就能确认环境里字体、DPI、jar 完整性都没问题。这个习惯帮我挡掉了大量“我这跑出来是乱码”的售后问题。做这种工具代码本身不难难的是把数据从 Excel 的格子映射到像素时不出差错记住一点先让测试表过再上真实数据希望帮到你。本文还有配套的精品资源点击获取
返回列表