news 2026/10/21 16:17:37

SpringBoot + POI-TL:模板化生成Word报表的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot + POI-TL:模板化生成Word报表的实战指南

如果你做过企业级项目,大概率被 Word 报表折磨过。一个月一张业务报表,模板改版一次,代码重写一次;格式稍微复杂点,原生 POI 写createParagraph、createTableCell能写到怀疑人生。后来我在某项目里接触到 POI-TL,一下子把整个流程盘活了——模板交给业务方维护,Java 代码只负责丢数据进去渲染,十几处数据的文档几分钟就能生成完。这篇文章就围绕 SpringBoot + POI-TL 这套组合,把快速生成 Word 报表的关键步骤、核心语法和踩过的坑一次讲透。

这个方案的本质是“模板即代码”:用 Word 直接打开一个 docx,在里面写{{name}}这种占位符,代码里把数据填充进去,就能输出一份带格式的正式文档。适合做合同、报表、通知、简历、审批表这类批量文档生成的场景。如果你正在用 SpringBoot 做 To B 系统,或者接手过“导出一份 Word 报表”的需求,这篇文章可以直接拿来当参考手册用。

1. 为什么是 POI-TL:原生 POI 与模板渲染的本质差异

1.1 原生 POI 操作 Word 的痛点到底在哪

Apache POI 的XWPFDocument能操作 docx,但它是完全程序化的操作方式。你要生成一个带标题、表格、图片的文档,必须手动创建段落、设置 run 属性、建立表格并逐行逐格填值。代码长还是其次,最麻烦的是样式控制:行距、缩进、字体、表格边框宽度,每一项都需要写对应的 POI 枚举或属性类。更头疼的是,这类代码和业务耦合极深,一旦模板格式变化,Java 侧往往要跟着改一大片。

我见过最典型的一个场景:某公司要导出每月经营分析报告,里面有十几段文字、三张统计表格和两个图表。用原生 POI 写下来,核心方法差不多有三四百行,每次需求变更都得在代码里找“哪个段落是哪个 run”的映射。后来业务方只微调了一句模板文案,开发也要重新定位代码、修改文本、重新测试,效率非常低。

1.2 POI-TL 的核心思路:把“样式”和“内容”彻底分离

POI-TL 的文档生成思路和原生 POI 完全不一样。它不再由代码创建段落和表格,而是让 Word 本身承担样式和排版职责——你先在 Word 里手动画出最终效果,把需要变化的位置写成占位符,然后 POI-TL 负责把这些占位符替换为真实数据。

这里的关键词是“模板语言”(Template Language)。它不是简单地做字符串替换,而是提供了一套可编程机制:{{变量}}表示文本,{%tr?集合}表示对集合做循环渲染,{{@图片变量}}表示插入图片,[~list]表示列表循环。正是因为这样,你可以做到一份模板三处复用:一份季度利润表模板,喂不同月份的数据,就能生成不同月份的正式报表。

POI-TL 底层仍然是基于 Apache POI 来读写 docx 文件,所以你不需要担心兼容 Office 的问题。它只是把 POI 的复杂操作封装成了模板标签,再把渲染结果写回 docx。这种“样式留在 Word、数据来自 Java”的方式,等于把文档生成的成本从“写代码”降级为“写模板”。

1.3 同类型方案的横向对比

在 Java 生态里,除了 POI-TL,还有几种常见做法。这里放一张对比表,方便你按实际场景取舍。

方案模板维护成本动态表格支持图片处理学习门槛适合场景
原生 Apache POI高,样式内嵌代码,改样式即改代码强,但代码量大一般,需手动创建 Drawing中高完全不需要模板的极简场景
Freemarker + Word XML 模板中,需要把文档另存为 XML 改模板弱,循环标签难处理弱,图片处理繁琐中对格式要求不高的简单文本替换
POI-TL低,直接用 Word 编辑模板强,循环块与表格绑定强,支持本地文件、流、URL 等低企业报表、合同、证书、公文等重格式文档
手动拼接 HTML 然后转 Word低强一般低快速交付原型,但转换后格式可能偏移

如果你只是导出几百个字符的 TXT 文本,原生 POI 也不复杂。但一旦进入“格式复杂 + 动态数据 + 批量生成”这个组合场景,POI-TL 的成本优势就非常明显了。它既保留了 Office 原生的排版精度,又让 Java 侧代码保持简洁,这也是我最终在项目里选它而不是直接手写 POI 的原因。

2. 环境准备:SpringBoot 集成 POI-TL 的完整依赖配置

2.1 先搭一个干净的基础工程

我用的是常规 SpringBoot 工程,JDK 8 或 11 都行,Maven 做依赖管理。工程内部结构很简单:

  • controller:接收前端请求,返回 Word 文档下载
  • service:负责查询业务数据、准备渲染模型
  • resources/templates/:存放 Word 模板文件
  • pojo:定义渲染用的数据模型

模板文件我习惯统一放在resources/templates/report_template.docx这种固定位置,避免散落在磁盘各处导致管理混乱。如果你对项目有私有化部署要求,也可以把模板放到 Nacos、数据库或对象存储里动态加载,POI-TL 支持直接传 InputStream,这点在后面会提到。

2.2 引入 poi-tl 依赖时最容易踩的版本坑

poi-tl的 Maven 坐标是com.deepoove:poi-tl,它内部已经依赖了 Apache POI 相关组件。我项目里使用的稳定版本是1.12.1,这个版本对 POI 5.2.x 的兼容性比较好。如果你用了更高的 POI 版本,建议先用官方文档确认兼容范围,再决定是否需要显式覆盖依赖。

下面是我在pom.xml里的关键配置:

<dependency> <groupId>com.deepoove</groupId> <artifactId>poi-tl</artifactId> <version>1.12.1</version> </dependency> <!-- 如果 poi-tl 引入的 poi-ooxml 版本跟你的工程冲突,就显式覆盖一下 --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.5</version> </dependency>

有一个很常见的坑:如果你的 SpringBoot 工程里其他模块也依赖了 POI,由于 Maven 的依赖仲裁机制,可能会把 POI 解析成旧版本,比如 4.1.2。旧版 POI 和 poi-tl 1.12.1 一起运行时,容易出现椭圆圈报错,比如找不到org.apache.poi.xwpf.usermodel.XWPFTable的某个方法。解决办法很直接:显式声明poi-ooxml版本,保证全工程统一版本。实测下来,5.2.5 和 poi-tl 1.12.1 搭配是很稳的。

2.3 第一个最小可运行示例

为了快速验证工程是否打通,我通常先写一个最简单的接口:用内存 Map 当数据模型,渲染一个仅有{{title}}占位符的模板,然后把文件输出到浏览器。

@RestController public class ReportController { @GetMapping("/hello-word") public void helloWord(HttpServletResponse response) throws IOException { // 模板文件放在 resources/templates/test_template.docx InputStream templateStream = this.getClass() .getClassLoader() .getResourceAsStream("templates/test_template.docx"); // 数据模型,Map 即可 Map<String, Object> data = new HashMap<>(); data.put("title", "Hello POI-TL"); data.put("author", "某开发者"); data.put("date", "2024-12-01"); XWPFTemplate template = XWPFTemplate.compile(templateStream) .render(data); // 输出到 HTTP 响应流 response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); response.setHeader("Content-Disposition", "attachment; filename=\"hello.docx\""); template.write(response.getOutputStream()); template.close(); } }

这段代码里XWPFTemplate.compile负责解析模板,render负责将数据填充进模板,write负责把结果写到输出流。整个过程可以类比成“先读入 Word 模板,像套 MAILMERGE 一样套数据,再导出新文档”,但 POI-TL 支持的能力比 MAILMERGE 强得多,后面细讲。

3. POI-TL 核心语法:从文本占位符到图片表格的完整解析

3.1 文本占位符与对象导航

POI-TL 的最基础能力是文本替换。你在 Word 中写入{{name}},然后在数据模型里放一个 key 为name,值为字符串或对象的字段,渲染时它就会被替换成实际值。

对象导航也很自然。如果数据模型里有一个user对象,其中包含name和age字段,那么在模板里可以写{{user.name}}。这个语法和 Freemarker 等模板引擎是一致的,学习成本几乎为零。

需要注意的是:如果数据模型里的值是null,POI-TL 默认会把它视为空字符串,而不是抛出异常。这一点很实用,比如用户没有填写备注时,渲染出来的文档里该位置就是空白,不会因此中断整个导出。但如果你期望 null 时展示一个默认值,比如“—”,就需要在 Java 侧提前对数据做格式化处理。

3.2 循环块:让表格行数随数据动态变化

报表场景里最核心的需求就是动态表格。比如一份员工名单,人数不固定,模板不能写死行数。POI-TL 用{%行?变量名}这种“循环行标签”来处理。

具体写法分两步:第一步,在 Word 表格中选中要循环的那一行,把单元格里需要变化的内容写成{{name}}、{{department}}这类占位符;第二步,在该行的最前面(或表格行起始位置)加上{%?users},其中users是 Java 侧传来的List<Map>或List<对象>的字段名。渲染时,POI-TL 会对该行反复复制,有多少个元素就生成多少行,同时把行内的变量自动替换。

一套完整的表格循环写法长这样:

List<Map<String, Object>> users = new ArrayList<>(); for (int i = 1; i <= 3; i++) { Map<String, Object> user = new HashMap<>(); user.put("name", "员工" + i); user.put("department", "技术部"); user.put("job", "开发工程师"); users.add(user); } Map<String, Object> data = new HashMap<>(); data.put("users", users);

模板里表格行写法如下(用 Word 打开模板,在表格行任意单元格输入即可):

{%?users} {{name}} | {{department}} | {{job}}

渲染后的效果就是三行员工数据,每行自动带上了你在表格里预设的字体、边框和缩进样式。这对批量报表尤其有用,数据库查出多少条记录,模板就自动生成多少行,完全不用代码参与表格排版。

3.3 图片填充与图片渲染参数

图片处理是 Word 导出里很多人头痛的部分,POI-TL 支持得相当完整。你只需要在模板里写{{@photo}},数据模型里给photo传入一个图片对象即可。

POI-TL 支持的图片数据格式很多,最常用的是byte[]、BufferedImage、文件路径或 URL 字符串。我平时最习惯用的是byte[],因为业务系统里用户头像、签名图片经常已经存在数据库或文件服务里,直接查出来就是字节数组,不用再转文件。

如果你需要控制图片尺寸,可以使用Pictures工具类指定宽高:

byte[] avatarBytes = getAvatarFromDatabase(); Map<String, Object> data = new HashMap<>(); data.put("photo", Pictures.ofBytes(avatarBytes) .size(100, 100) // 单位:像素 .create());

还有一种情况:某张图片可能不存在,但报表标题上又留了图片位。POI-TL 提供默认图片的概念,使用Pictures.ofBytes(byte[])时可以设置默认值。如果最终数据没有该图片,模板会渲染默认图,避免整篇文档出现破图。具体用法是Pictures.ofBytes(new byte[0]).setDefaultPicture(...),不过要注意,如果你的模板里图片是必填项,还是建议在 Java 侧提前做空值校验,比靠默认图更可靠。

3.4 列表标签与其他实用标签

文本换行之外,POI-TL 还支持无序列表[~listName]。用法是先在 Word 中创建一个列表段落,在列表项前写上[~skills],数据模型里传入字符串数组即可自动生成项目符号列表。适合存技能标签、证书清单、附件清单这类非表格数据。

POI-TL 还有两个高频标签值得提一下:{{?condition}}用于条件判断,能根据布尔值决定内容是否显示;{{field.newline?}}用于设置换行格式。不过从我实际经验来看,条件标签用得相对少,因为大多数业务需求要么拼接数据,要么动态生成表格,很少需要在 Word 里做复杂的条件渲染。如果你以后遇到“只有某些条件下展示某个段落”的需求,再去翻官方文档也不迟,核心语法掌握上面三块基本就能覆盖九成场景。

4. 实战:SpringBoot 下快速生成人员信息 Word 报表

4.1 需求场景与模板设计思路

我以一个常见的“人员信息登记表”为例,拆解整个实战过程。假设需求是这样的:系统里有一批员工记录,需要按部门批量导出 Word 版登记表,包含姓名、工号、岗位、入职时间、技能列表、一张个人照片,以及一个技能等级汇总表。用户可以直接从浏览器点击下载,稍加修改就能作为正式档案使用。

这个需求本质上是典型的“一张模板 + 多份数据”的模式。我的设计思路是:先做一份标准 Word 模板,模板里包含个人信息区(标题、文本占位符)、技能列表区(列表标签)、照片区(图片占位符)和表格汇总区(循环表格标签)。然后写一个接口,接收部门编号,从数据库查出该部门所有员工数据,循环渲染同一个模板文件,生成一份“部门员工信息汇总文档”。

模板设计有两个细节值得注意。第一,照片占位符所在的单元格尽量不要和其他文字放在同一行,否则 Word 的换行计算和 POI-TL 的表格直接替换配合起来容易产生布局偏移。第二,汇总表的第一行表头不要写在循环标签内,只把数据行放在{%?skills}里面,否则每次渲染都会多出一行表头。

4.2 核心代码与数据模型实现

工程里的数据模型我按 POJO 来定义,方便服务层复用。这里给出关键类:

public class EmployeeReport { private String name; private String empNo; private String position; private String hireDate; private List<String> skills; private byte[] photo; private List<SkillLevel> skillLevels; // getter / setter 省略 } public class SkillLevel { private String skillName; private String level; private String remark; }

在 Service 层,组装数据的逻辑大概是这样的:

public void exportEmployeeReport(String deptId, HttpServletResponse response) throws IOException { List<EmployeeReport> employees = employeeMapper.selectByDept(deptId); // 组装合并模型 Map<String, Object> data = new HashMap<>(); List<Map<String, Object>> employeeRows = new ArrayList<>(); for (EmployeeReport emp : employees) { Map<String, Object> row = new HashMap<>(); row.put("name", emp.getName()); row.put("empNo", emp.getEmpNo()); row.put("position", emp.getPosition()); row.put("hireDate", emp.getHireDate()); row.put("skills", emp.getSkills()); row.put("photo", Pictures.ofBytes(emp.getPhoto()).size(80, 100).create()); row.put("skillLevels", emp.getSkillLevels()); employeeRows.add(row); } data.put("employeeRows", employeeRows); data.put("deptName", "技术部"); data.put("exportTime", LocalDate.now().toString()); // 模板渲染 try (InputStream templateStream = this.getClass() .getClassLoader() .getResourceAsStream("templates/employee_report_template.docx")) { XWPFTemplate template = XWPFTemplate.compile(templateStream).render(data); response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); String fileName = URLEncoder.encode("员工信息登记表", "UTF-8"); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + ".docx\""); template.write(response.getOutputStream()); template.close(); } }

注意两个细节:第一,photo字段是字节数组时,最好用Pictures.ofBytes()包一层并设置尺寸,否则图片会按原始分辨率拉伸,排版会很难看;第二,导出文件名里带中文时,直接用filename=拼接会出现乱码,需要用URLEncoder.encode转义,浏览器才能正确识别。

4.3 模板文件的关键约定与最终效果

模板文件employee_report_template.docx中,我按以下格式组织内容:

  • 标题段落:{{deptName}}员工信息登记表
  • 汇总信息:导出时间{{exportTime}}
  • 每个员工的小节占位:
    • 姓名:{{name}}
    • 工号:{{empNo}}
    • 岗位:{{position}}
    • 入职时间:{{hireDate}}
    • 技能列表:[~skills]
    • 照片:{{@photo}}
  • 技能等级汇总表:
    • 表头行固定写“技能名称、等级、备注”
    • 数据行写成{%?skillLevels},行内单元格为{{skillName}}、{{level}}、{{remark}}

最终渲染完成的 docx 打开后,表格行数会跟随员工数量和技能数量动态变化,每人照片按 80x100 像素固定显示,整个文档格式和我们手工在 Word 里排版的一模一样。说实话,第一次跑通时我也觉得挺有意思,因为开发侧几乎没有碰任何表格绘制代码。

5. 常见问题与排查技巧实录

5.1 占位符没被替换,渲染出来还是 "{{name}}"

这是我被问得最多的一个问题。排查思路其实很简单,按下面顺序来:

  1. 确认模板里的占位符是不是{{和}}英文大括号。有人从网页或 PDF 复制文本到 Word,会复制成全角花括号,显示上几乎一样但就是替换不了。
  2. 确认 key 拼写完全一致,大小写也敏感。数据模型里放的是name,模板里写Name就不行。
  3. 确认模板文件是.docx而不是.doc。POI-TL 不支持老式.doc,遇到.doc需要先另存为.docx再使用。
  4. 如果模板里有“智能引号”或自动补全括号,也会影响解析,建议先删掉占位符重新输入一遍。

有个更隐蔽的场景:有的用户把{{name}}放在了文本框(Text Box)里,POI-TL 默认不会渲染文本框内占位符,因为文本框是独立于文档正文流的 XML 结构。这个我踩过一次,后来统一要求模板里所有占位符必须写在正文或表格单元格中,避免使用文本框。

5.2 表格循环渲染报错:“Cannot find the field”

出现这个错误时,先别慌,大概率是循环块和变量名的对应关系出了偏差。{%?skillLevels}中的skillLevels必须在同一个数据模型里存在,并且必须是 List 或数组类型。如果你把List<SkillLevel>放进了employeeRows里某一行的内部,而循环块却写在了该行外面,就会导致 POI-TL 找不到对应字段。

我的建议是:先用枚举法排查数据模型层级。比如{%?employeeRows}在最外层,那么它内部的字段访问路径就是{{name}}、{{photo}}、{%?skillLevels},这些字段都来自EmployeeReport对象。如果某个字段没做好 getter/setter,渲染也会失败。用 Map 代替 POJO 可以规避 getter 问题,但可读性差一些,工程规模大了我不建议这么做。

5.3 图片渲染不出来,或图片尺寸异常

图片不显示,第一步确认模板写法是{{@photo}}而不是{{photo}},多了@才是图片标签。第二步确认数据模型里photo字段确实有值,数据库里可能存的是空byte[]或 null。POI-TL 对 null 图片有时会忽略,但控制台可能只打一条 warn 日志,不仔细看很容易漏掉。

图片尺寸异常一般是因为没调用.size()指定宽高,POI-TL 会用图片原始像素直接插入,导致一张手机拍照的照片把整个单元格撑破。经验做法是:所有图片插入前统一调用size()设置合理宽高,并且用像素值,单位不要填 cm 或英寸,避免换算误差。

5.4 中文文件名下载时乱码

这个和 POI-TL 本身无关,是 HTTP 响应头的通用问题。中文文件名要转码:

String encodedFileName = URLEncoder.encode("员工信息登记表", "UTF-8"); response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + ".docx\"");

不同浏览器对 URL 编码的表现略有差异,但实测 Chrome、Edge 和较新版本的 Firefox 都能正确显示中文文件名。如果你对内网老旧浏览器还要兼容,可以考虑同时加一个filename*参数,但一般业务系统不强制。

5.5 性能问题:大批量数据导出如何优化

一次导出几百人的报表,POI-TL 本地渲染几十毫秒到几百毫秒都很正常。但如果数据量到了几千条、模板里有大量图片,内存和耗时就会明显上升。原因在于 POI-TL 是基于 POI 的 DOM 模型工作,整个 docx 会被加载到内存中解析和重建。

我在项目里常用三种手段:第一,分批渲染,如果业务允许,把数据拆成每 500 条一批,分别渲染到不同文档再合并,或者直接生成 ZIP 包分批压缩下载;第二,模板文件尽量精简,不要在模板里塞大段无关图片和复杂样式,否则每次渲染都要解析一遍;第三,如果没有特殊要求,关闭文档的“快速加载宏”和嵌入字体,这些特性会让 docx 体积变大、渲染变慢。

6. 一些真实可复用的解放方案与维护心得

这里再多说几句我在实际项目里沉淀出来的经验。当好一个报表生成功能上线后,真正的工作难点往往不是“生成”,而是后续的模板版本更迭和维护。POI-TL 最好的特性就是模板可控性:业务人员可以直接用 Word 打开模板修改文案、调整字体、增减表格列,只要不破坏占位符格式,Java 代码一行都不用变。

基于这一点,我建议你在团队里约定几条硬性规范:占位符统一用英文大小写驼峰命名,禁止中文命名;模板文件统一放在集中位置,文件名带版本号;模板修改后必须先用最小数据做一次渲染验证,避免等到生产环境导出报错才发现问题。把模板当代码一样做版本管理,比任何技术选型都重要。

还有一个技巧想分享给第一次做类似功能的读者:先在 Word 里把最终效果完全画好,确认格式满意了,再回头把需要替换的内容改成占位符。千万不要反过来,先用占位符凑模板,再一点点调样式,那样你会有很多“样式调好了但占位符变了”的死循环。把模板视为设计的产物,而不是代码的副产品,这个思路会让后续的维护成本低很多。

最后说点个人体会。在我接触过的所有 Java 文档生成方案里,POI-TL 确实是最契合“快速生成报表”这四个字的:上手快、模板直观、渲染可控、SpringBoot 集成也几乎零成本。如果你正被原生 POI 的代码量折磨,或者还在考虑用 Freemarker 做 Word XML 模板解析,我建议直接用这个方案先做一个最小验证工程,把模板和数据模型跑通了,再决定要不要替换现有逻辑。你在实际操作中踩过的坑,也不妨反过来补充到模板命名规范和版本管理流程里,这些经验越攒越值钱。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 16:16:33

SVM分类入门实战:从玩具数据集到C与gamma调优

1. 从一个玩具数据集说起&#xff1a;为什么选择SVM做分类入门很多人学机器学习&#xff0c;第一个真正动手跑起来的分类器要么是逻辑回归&#xff0c;要么是KNN&#xff0c;SVM往往被排到后面。原因也简单——SVM听起来就"数学味"重&#xff0c;什么最大间隔、核函数…

作者头像 李华
网站建设 2026/10/21 16:16:32

C++ Primer Plus 第6版练习答案:从编译到避坑的完整指南

简介&#xff1a;这份资源是《C Primer Plus》第6版中文版编程练习答案的Word文档&#xff0c;面向正在学习C基础语法、需要对照练习结果自查的初学者与自学者。内容按章节组织&#xff0c;覆盖第二章「开始学习C」与第三章「处理数据」的多个练习&#xff0c;如姓名地址输出、…

作者头像 李华
网站建设 2026/10/21 16:16:14

构建可验证、可组合、可进化的个人技能操作系统

1. 项目概述&#xff1a;当“skills”不再只是简历上的单词&#xff0c;而成为可验证、可组合、可进化的个人能力操作系统“skills”这个词最近在技术社区、职业发展平台和高校教学研讨中高频出现&#xff0c;但它早已不是求职简历里那行潦草罗列的“Python/Photoshop/沟通能力…

作者头像 李华
网站建设 2026/10/21 16:16:04

硅二极管温度传感器:从PN结原理到工程选型全解析

前阵子调试某工业采集板&#xff0c;客户对温度测量这一路提出了"必须用数字传感器"的死要求。我算了一笔账&#xff1a;如果改用分立二极管测温方案&#xff0c;物料成本能压掉一大半&#xff0c;精度反而更好控。最后板子做出来&#xff0c;实测读数与标定用铂电阻…

作者头像 李华
网站建设 2026/10/21 16:13:03

lingbot-map:轻量级语义地图交互方案

1. 项目概述&#xff1a;一个名字里藏着三重线索的轻量级地理交互方案“lingbot-map”这个名称乍看像某个开源库的代号&#xff0c;但拆开来看&#xff0c;它其实是一组非常具体的信号&#xff1a;“ling”暗示语言处理或轻量级&#xff08;lightweight/lingual&#xff09;&am…

作者头像 李华
网站建设 2026/10/21 16:09:47

PPT抠图去背景全攻略:删除背景、透明色与边缘处理技巧

做PPT最烦的一件事就是图片背景去不掉&#xff0c;白底、黑底、花底&#xff0c;放在版面上怎么都别扭&#xff0c;整个页面显得特别“脏”。很多人第一反应是打开专业软件抠图&#xff0c;其实PPT里就藏着全套抠图去背景的工具&#xff0c;平时根本用不到那么复杂。这篇就来把…

作者头像 李华