在 Java 中使用 Apache POI 为 Word 文档添加水印
在 Java 中使用 Apache POI 为 Word 文档添加水印
在日常办公中,我们经常需要给 Word 文档添加水印,以标明文件的机密性或归属权。本文将介绍如何使用 Apache POI 库在 Java 中给 Word 文档添加水印。
技术栈
- Apache POI:用于操作 Word 文档(
.docx) - VML(矢量标记语言):用于绘制水印文本
实现思路
- 读取 Word 文档
- 在页眉中插入水印
- 处理水印样式,如字体、颜色、透明度等
- 生成新文件并保存
代码实现
以下是一个完整的 WordWatermarkUtils 工具类,它支持向 .docx 文件中添加水印。
public class WordWatermarkUtils {private String customText; // 水印文字private String fontName = "微软雅黑"; // 字体private int fontSize = 14; // 字体大小private String fontColor = "#616161"; // 字体颜色private String styleRotation = "0"; // 旋转角度public WordWatermarkUtils(String customText) {this.customText = customText;}public void addWatermarkToDoc(XWPFDocument doc) {XWPFHeader header = doc.createHeader(HeaderFooterType.DEFAULT);CTP ctp = header.createParagraph().getCTP();CTR ctr = ctp.addNewR();CTGroup group = CTGroup.Factory.newInstance();CTShape shape = group.addNewShape();shape.setStyle(getShapeStyle());shape.setFillcolor(fontColor);shape.addNewTextpath().setString(customText);ctr.addNewPict().set(group);}private String getShapeStyle() {return "position:absolute;width:300pt;height:50pt;rotation:" + styleRotation + ";fill-opacity:0.3";}
}
如何使用
try (FileInputStream fis = new FileInputStream("input.docx");FileOutputStream fos = new FileOutputStream("output.docx")) {XWPFDocument doc = new XWPFDocument(fis);WordWatermarkUtils watermark = new WordWatermarkUtils("机密文件");watermark.addWatermarkToDoc(doc);doc.write(fos);
}
完整代码
package com.demo;import com.microsoft.schemas.office.office.CTLock;
import com.microsoft.schemas.vml.*;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.poi.wp.usermodel.HeaderFooterType;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFHeader;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import weaver.general.BaseBean;import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.UUID;/*** 微软office Word 水印机.*/
public class WordWatermarkUtils {private String customText; // 水印文字private String fontName = "微软雅黑"; // word字体private int fontSize = 14; // 字体大小private String fontColor = "#616161"; // 字体颜色private int widthPerWord = 3; // 一个字平均长度,单位pt,用于:计算文本占用的长度(文本总个数*单字长度)private String styleTop = "10pt"; // 与顶部的间距private String styleRotation = "0"; // 文本旋转角度,如果不需要旋转水印,保持为 0private String source; // 源文件路径private String dest = "D:\\WEAVER\\doc_tmp"; // 临时文件存储路径BaseBean bean = new BaseBean();public WordWatermarkUtils(String customText, String sourcePath) {customText = customText + repeatString(" ", 8); // 水印文字之间使用8个空格分隔this.customText = repeatString(customText, 1); // 一行水印重复水印文字次数this.source = sourcePath;}/*** 【核心方法】将输入流中的docx文档加载添加水印后输出到输出流中.** @param inputStream docx文档输入流* @param //outputStream 添加水印后docx文档的输出流*/public String makeSlopeWaterMark(InputStream inputStream, String filename) throws IOException {// 创建临时文件Path tempFile = createTempFile(inputStream);if (tempFile == null || !Files.exists(tempFile) || Files.size(tempFile) == 0) {throw new RuntimeException("-------- 输入文件为空或者无法被创建");}Path sourcePath = Paths.get(this.source); // 获取源文件路径Path destinationDir = Paths.get(this.dest); // 将原文件复制到临时目标文件夹路径// 确保目标文件夹存在if (!Files.exists(destinationDir)) {try {Files.createDirectories(destinationDir);} catch (IOException e) {throw new RuntimeException("-------- 无法创建目标文件夹: " + e);}}String unescapedFilenameString = StringEscapeUtils.unescapeHtml4(filename);// 转义字符复原String tmpFileName = unescapedFilenameString.substring(0, unescapedFilenameString.lastIndexOf(".")) + "_watermark.docx";Path tmpPath = destinationDir.resolve(tmpFileName); // 临时文件路径Files.copy(sourcePath, tmpPath, StandardCopyOption.REPLACE_EXISTING);bean.writeLog("-------- 通用合同右上角水印原文件复制成功并加上后缀 .docx: " + tmpPath);try (BufferedInputStream buffIn = new BufferedInputStream(Files.newInputStream(tempFile));FileOutputStream fileOutputStream = new FileOutputStream(tmpPath.toFile())) {// 加载文档XWPFDocument doc = loadDocXDocument(buffIn, fileOutputStream);if (doc == null) {throw new RuntimeException("-------- 加载文档失败");}// 添加水印try {// 遍历文档,添加水印for (int lineIndex = -10; lineIndex < 10; lineIndex++) {styleTop = 200 * lineIndex + "pt";waterMarkDocXDocument(doc);}// 写出添加水印后的文档doc.write(fileOutputStream);// 关闭doc.close();buffIn.close();fileOutputStream.close();return tmpPath.toString();} catch (Exception e) {throw new RuntimeException("-------- 水印操作失败: " + e);}} catch (Exception e) {throw new RuntimeException("-------- 操作失败!" + e);} finally {// 删除原始文件// deleteFile(sourcePath);// 重命名临时文件为原文件名// try {// Files.move(tmpPath, sourcePath, StandardCopyOption.REPLACE_EXISTING);// } catch (IOException e) {// throw new RuntimeException("-------- 重命名失败: " + e);// }// 删除临时文件deleteFile(tempFile);}}/*** 为文档添加水印<br />* 实现参考了{@link org.apache.poi.xwpf.model.XWPFHeaderFooterPolicy# getWatermarkParagraph(String, int)}** @param doc 需要被处理的docx文档对象*/private void waterMarkDocXDocument(XWPFDocument doc) {int size = doc.getHeaderList().size();if (size == 0) {addWatermarkToHeader(doc);} else {// 遍历所有的节(Section)for (Object header : doc.getHeaderList()) {// 保留原有内容for (Object paragraph : ((XWPFHeader) header).getParagraphs()) {// 保持原有段落内容((XWPFParagraph)paragraph).getText();}// 在页眉上添加水印(具体代码见上文)addWatermarkToHeader((XWPFHeader)header);}}}/*** 页眉存在其他内容* @param header*/private void addWatermarkToHeader(XWPFHeader header) {int size = header.getParagraphs().size();if (size == 0) {header.createParagraph();}CTP ctp = header.getParagraphArray(0).getCTP();byte[] rsidr = ctp.getRsidR();byte[] rsidrdefault = ctp.getRsidRDefault();ctp.setRsidP(rsidr);ctp.setRsidRDefault(rsidrdefault);CTPPr ppr = ctp.addNewPPr();ppr.addNewPStyle().setVal("Header");// 添加水印CTR ctr = ctp.addNewR();CTRPr ctrpr = ctr.addNewRPr();ctrpr.addNewNoProof();CTGroup group = CTGroup.Factory.newInstance();CTShapetype shapetype = group.addNewShapetype();CTTextPath shapeTypeTextPath = shapetype.addNewTextpath();shapeTypeTextPath.setOn(STTrueFalse.T);shapeTypeTextPath.setFitshape(STTrueFalse.T);CTLock lock = shapetype.addNewLock();lock.setExt(STExt.VIEW);CTShape shape = group.addNewShape();shape.setId("PowerPlusWaterMarkObject");shape.setSpid("_x0000_s102");shape.setType("#_x0000_t136");shape.setStyle(getShapeStyle()); // 设置形状样式(旋转,位置,相对路径等参数)shape.setFillcolor(fontColor);shape.setStroked(STTrueFalse.FALSE); // 字体设置为实心CTTextPath shapeTextPath = shape.addNewTextpath(); // 绘制文本的路径shapeTextPath.setStyle("font-family:" + fontName + ";font-size:" + fontSize + "pt"); // 设置文本字体与大小shapeTextPath.setString(customText);CTPicture pict = ctr.addNewPict();pict.set(group);}/*** 页眉不存在其他内容* @param doc*/private void addWatermarkToHeader(XWPFDocument doc) {XWPFHeader header = doc.createHeader(HeaderFooterType.DEFAULT); // 如果之前已经创建过 DEFAULT 的Header,将会复用之int size = header.getParagraphs().size();if (size == 0) {header.createParagraph();}CTP ctp = header.getParagraphArray(0).getCTP();byte[] rsidr = doc.getDocument().getBody().getPArray(0).getRsidR();byte[] rsidrdefault = doc.getDocument().getBody().getPArray(0).getRsidRDefault();ctp.setRsidP(rsidr);ctp.setRsidRDefault(rsidrdefault);CTPPr ppr = ctp.addNewPPr();ppr.addNewPStyle().setVal("Header");// 开始加水印CTR ctr = ctp.addNewR();CTRPr ctrpr = ctr.addNewRPr();ctrpr.addNewNoProof();CTGroup group = CTGroup.Factory.newInstance();CTShapetype shapetype = group.addNewShapetype();CTTextPath shapeTypeTextPath = shapetype.addNewTextpath();shapeTypeTextPath.setOn(STTrueFalse.T);shapeTypeTextPath.setFitshape(STTrueFalse.T);CTLock lock = shapetype.addNewLock();lock.setExt(STExt.VIEW);CTShape shape = group.addNewShape();shape.setId("PowerPlusWaterMarkObject");shape.setSpid("_x0000_s102");shape.setType("#_x0000_t136");shape.setStyle(getShapeStyle()); // 设置形状样式(旋转,位置,相对路径等参数)shape.setFillcolor(fontColor);shape.setStroked(STTrueFalse.FALSE); // 字体设置为实心CTTextPath shapeTextPath = shape.addNewTextpath(); // 绘制文本的路径shapeTextPath.setStyle("font-family:" + fontName + ";font-size:" + fontSize + "pt"); // 设置文本字体与大小shapeTextPath.setString(customText);CTPicture pict = ctr.addNewPict();pict.set(group);}/*** 构建Shape的样式参数.** @return*/private String getShapeStyle() {StringBuilder sb = new StringBuilder();sb.append("position: ").append("absolute"); // 文本path绘制的定位方式sb.append(";width: ").append(customText.length() * widthPerWord).append("pt"); // 计算文本占用的长度(文本总个数*单字长度)sb.append(";height: ").append(fontSize).append("pt"); // 字体高度sb.append(";z-index: ").append("-251654144");// 控制层级sb.append(";mso-wrap-edited: ").append("f");sb.append(";margin-top: ").append(30);// 距离页面顶部10ptsb.append(";left: ").append("auto"); // 防止左对齐sb.append(";margin-right: ").append(30); // 距离页面右侧10ptsb.append(";mso-position-horizontal-relative: ").append("page");// 水平基准为页面sb.append(";mso-position-vertical-relative: ").append("page");// 垂直基准为页面sb.append(";mso-position-horizontal: ").append("right"); // // 水平居右//sb.append(";mso-position-vertical: ").append("bottom"); // 设置水印在底部sb.append(";mso-position-vertical: ").append("other"); // 垂直居上sb.append(";rotation: ").append(styleRotation);// 设置文本旋转角度sb.append(";fill-opacity: ").append(0.6); // 设置水印的透明度为 60%return sb.toString();}/*** 加载docx格式的word文档.** @param inputStream* @param outputStream* @return*/private XWPFDocument loadDocXDocument(InputStream inputStream, OutputStream outputStream) {XWPFDocument doc;try {doc = new XWPFDocument(inputStream);} catch (Exception e) {throw new RuntimeException("文档加载失败!!");}return doc;}/*** 创建临时文件.** @param inputStream docx文档输入流*/private Path createTempFile(InputStream inputStream) {Path tempFilePath = null;inputStream = (inputStream == null) ? new ByteArrayInputStream(new byte[0]) : inputStream; // 如果传入了null输入流,转换成空数组流BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);bufferedInputStream.mark(0); // 输入流头部打上Mark(方便重读)// 创建临时文件try {if (inputStream == null || inputStream.available() == 0) {throw new RuntimeException("输入流为空或为 null.");}String uuid = UUID.randomUUID().toString();tempFilePath = Files.createTempFile("dapeng_" + uuid, ".docx");// 向临时文件写入数据try (OutputStream tempout = Files.newOutputStream(tempFilePath)) {IOUtils.copy(inputStream, tempout);} catch (Exception e) { // 如果拷贝异常,删除临时文件deleteFile(tempFilePath);throw new RuntimeException("写入临时文件时出错.", e);}inputStream.close();} catch (Exception e) {// 这里表示创建临时文件失败tempFilePath = null;}return tempFilePath;}/*** 删除指定path的文件.** @param path*/private void deleteFile(Path path) {if (path != null && Files.exists(path)) {try {Files.deleteIfExists(path);} catch (IOException e) {bean.writeLog("-------- 删除文件失败: " + path + " " + e);}}}/*** 将指定字符串重复多次 (适配Java 1.8).*/private String repeatString(String pattern, int repeats) {StringBuilder buffer = new StringBuilder(pattern.length() * repeats);for (int i = 0; i < repeats; i++) {buffer.append(pattern);}return buffer.toString();}
}
总结
使用 Apache POI 和 VML,我们可以轻松地在 Word 文档中添加水印。该方法适用于各种办公场景,如合同文件、内部文档等。如果你有更复杂的需求,如图片水印或动态水印,也可以扩展此方法。
希望本文对你有所帮助,欢迎交流讨论!
相关文章:
在 Java 中使用 Apache POI 为 Word 文档添加水印
在 Java 中使用 Apache POI 为 Word 文档添加水印 在日常办公中,我们经常需要给 Word 文档添加水印,以标明文件的机密性或归属权。本文将介绍如何使用 Apache POI 库在 Java 中给 Word 文档添加水印。 技术栈 Apache POI:用于操作 Word 文…...
贪心算法二
> 作者:დ旧言~ > 座右铭:松树千年终是朽,槿花一日自为荣。 > 目标:了解什么是贪心算法,并且掌握贪心算法。 > 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安! >…...
【大模型安全】大模型的技术风险
【大模型安全】大模型的技术风险 1.DDoS攻击2.常见的传统网络攻击方式3.恶意意图的识别4.AI生成虚假信息传播5.利用AI进行黑客攻击6.模型对抗攻击7.后门攻击8.Prompt攻击9.数据投毒攻击10.模型窃取攻击11.数据窃取攻击 1.DDoS攻击 2023年11月9日凌晨,OpenAI在官网公…...
Java 线程池中 shutdown 与 shutdownNow 的区别是什么?
Java 线程池中 shutdown 与 shutdownNow 的区别 核心行为差异 | 方法 | 行为描述 | |----------------|----------------------------------------------------------------------------| | shutdown | 平缓关闭线程池:1. 停止接受新任务。2. 已提交的任务ÿ…...
基于Spring Boot的共享学习经验系统的设计与实现
目录 摘 要 第1章 绪论 1.1研究背景与意义 1.2国内外现状 1.3研究目标 第2章 需求分析 2.1业务需求 2.1.1业务概述 2.1.2业务流程 2.2.1用例概述 2.2.2用例描述 2.3非功能性需求 第3章 系统设计 3.1技术路线 3.2系统功能模块设计 3.3系统架构 3.4数据库设计 3.4.1概念结构设…...
【简单的C++围棋游戏开发示例】
C围棋游戏开发简单示例(控制台版) 核心代码实现 #include <iostream> #include <vector> #include <queue> using namespace std;const int SIZE 9; // 简化棋盘为9x9:ml-citation{ref"1" data"citationList&…...
单片机中的基础外设GPIO的知识和应用—(6)
GPIO(通用输入输出)是单片机与外部世界交互的重要接口。单片机的GPIO引脚可以灵活配置为输入、输出、中断或复用功能,广泛应用于LED控制、按键读取、传感器通信等场景。下文以STM32F103C8T6的GPIO为例。有些51单片机IO功能有的稍微有不同&…...
10-Agent循环分析新闻并输出总结报告
目录 关键词 摘要 速览 自动新闻总结与行业分析报告生成流程 创建深度行业分析报告的工作流 测试用例执行与调试 业务逻辑与循环处理任务 演示如何在循环体中添加链接读取工具 使用大模型处理和分析新闻信息 构建循环分析新闻并生成综合报告的流程 分析和优化慢速循…...
十二、Redis Cluster(集群)详解:原理、搭建、数据分片与读写分离
Redis Cluster(集群)详解:原理、搭建、数据分片与读写分离 Redis Cluster 是 Redis 官方提供的分布式存储方案,通过数据分片(Sharding)实现 水平扩展(scalability),并提供 高可用性(HA) 和 故障自动转移(failover) 能力,解决了单机 Redis 内存受限、主从复制故障…...
贪心算法解题框架+经典反例分析,效率提升300%
贪心算法是一种在每一步选择中都采取当前状态下的最优决策,从而希望最终达到全局最优解的算法策略。以下从其定义、特点、一般步骤、应用场景及实例等方面进行讲解: 定义与基本思想 • 贪心算法在对问题求解时,总是做出在当前看来是最好的选…...
策略设计模式-下单
1、定义一个下单context类 通过这类来判断具体使用哪个实现类,可以通过一些枚举或者条件来判断 import com.alibaba.fastjson.JSON; import com.tc.common.exception.BusinessException; import com.tc.common.user.YjkUserDetails; import com.tc.institution.cons…...
Go加spy++隐藏窗口
最近发现有些软件的窗口就像狗皮膏药一样,关也关不掉,一点就要登录,属实是有点不爽了。 窗口的进程不能杀死,但是窗口我不想要。思路很简单,用 spy 找到要隐藏的窗口的句柄,然后调用 Windows 的 ShowWindo…...
React基础之tsx语法
tsx在jsx的基础上添加了新的类型,除此之外没有任何区别 事件绑定 function App() { const handleClick()>{ console.log(button被点击了); } return( <div className"App"> <button onClick{handleClick}>click me</button> </di…...
一体机:DeepSeek性能的“隐形枷锁”!
一体机是DeepSeek交付的最佳方式吗? 恰恰相反,一体机是阻碍DeepSeek提升推理性能的最大绊脚石。 为啥? 只因DeepSeek这个模型有点特殊,它是个高稀疏度的MoE模型。 MoE这种混合专家模型,设计的初衷是通过“激活一堆专…...
ALBEF的动量蒸馏(Momentum distillation)
简单记录学习~ 一、传统 ITC Loss 的局限性 One-Hot Label 的缺陷 传统对比学习依赖严格对齐的图文对,通过交叉熵损失(如 softmax 归一化的相似度矩阵)强制模型将匹配的图文对相似度拉高,非匹配对相似度压低11。但 one…...
浏览器WEB播放RTSP
注意:浏览器不能直接播放RTSP,必须转换后都能播放。这一点所有的播放都是如此。 参考 https://github.com/kyriesent/node-rtsp-stream GitHub - phoboslab/jsmpeg: MPEG1 Video Decoder in JavaScript 相关文件方便下载 https://download.csdn.net…...
将PDF转为Word的在线工具
参考视频:外文翻译 文章目录 一、迅捷PDF转换器二、Smallpdf 一、迅捷PDF转换器 二、Smallpdf...
03. 对象的创建,存储和访问原理
文章目录 01. 对象创建1.1 创建过程概览1.2 类加载检查1.3 为对象分配内存1.4 将内存空间初始化为零值1.5 设置对象的必要信息1.6 总结 02. 对象的内存布局2.1 对象头区域2.2 实例数据区域2.3 对齐填充区域2.4 总结 03. 对象的访问定位其他介绍01.关于我的博客 注:读…...
机器学习-GBDT算法
目录 一. GBDT 核心思想 二. GBDT 工作原理 **(1) 损失函数优化** **(2) 负梯度拟合** **(3) 模型更新** 三. GBDT 的关键步骤 四. GBDT 的核心优势 **(1) 高精度与鲁棒性** **(2) 处理缺失值** **(3) 特征重要性分析** 五. GBDT 的缺点 **(1) 训练…...
redis基础结构
title: redis基础结构 date: 2025-03-04 08:39:12 tags: redis categories: redis笔记 Redis入门 (NoSQL, Not Only SQL) 非关系型数据库 关系型数据库:以 表格 的形式存在,以 行和列 的形式存取数据,一系列的行和列被…...
Python实战:构建个人古诗知识库,从古诗文网高效采集与存储
1. 为什么你需要一个古诗知识库? 作为一个诗词爱好者,我经常遇到这样的困扰:读到一首好诗想收藏,结果过几天就忘了出处;想查找某个主题的诗句,却记不清具体内容;看到喜欢的诗人作品,…...
Spring Security实战:Bcrypt加密算法在用户密码存储中的正确使用姿势(附完整代码)
Spring Security实战:Bcrypt加密算法在用户密码存储中的正确使用姿势(附完整代码) 在当今数字化时代,用户密码安全已成为系统开发中最基础也最关键的一环。作为开发者,我们经常面临一个核心问题:如何在数据…...
Wan2.2-I2V-A14B镜像免配置:SSH直连后cd /workspace即可执行全部命令
Wan2.2-I2V-A14B镜像免配置:SSH直连后cd /workspace即可执行全部命令 1. 镜像概述与核心优势 Wan2.2-I2V-A14B私有部署镜像是一款专为文生视频模型定制的开箱即用解决方案。这个镜像最大的特点就是"免配置"——通过SSH连接后,只需进入/works…...
SDXL 1.0插件开发:Photoshop脚本自动化集成
SDXL 1.0插件开发:Photoshop脚本自动化集成 1. 为什么需要Photoshop与SDXL 1.0的深度协作 设计师每天面对的不是单一工具,而是一整套工作流。当AI生成图像成为创意起点,问题就来了:生成的图片如何快速进入专业设计环节ÿ…...
【西瓜带你学设计模式 | 第四期 - 抽象工厂模式】抽象工厂模式 —— 定义、核心结构、实战示例、优缺点与适用场景及模式区别
文章目录前言1. 抽象工厂模式是什么?2. 解决什么问题?2.1 有多个“产品维度”,并且需要成套切换2.2 变化点分散导致代码难维护3. 核心结构4. 示例4.1 抽象产品:Slice(切片)4.2 抽象产品:Pulp&am…...
像素皇城·灵蛇贺岁实战案例:高校AI课程中像素春联生成器教学项目设计
像素皇城灵蛇贺岁实战案例:高校AI课程中像素春联生成器教学项目设计 1. 项目背景与教学价值 在高校AI课程教学中,如何将传统文化与现代技术相结合,设计出既有教育意义又富有趣味性的实践项目,一直是教学设计的难点。"像素皇…...
人肉区块链:用群体记忆对抗AI篡改
当测试数据面临AI篡改危机在生成式AI全面渗透软件开发生命周期的今天,软件测试从业者正面临前所未有的挑战。AI工具在提升测试用例生成、缺陷预测和日志分析效率的同时,也带来了隐蔽而致命的风险:AI驱动的数据篡改。自动化测试结果被注入虚假…...
Typora与AI结合:使用万象熔炉·丹青幻境为Markdown文档自动配图
Typora与AI结合:使用万象熔炉丹青幻境为Markdown文档自动配图 不知道你有没有过这样的体验:在Typora里写完一篇技术博客或项目文档,内容详实,逻辑清晰,但通篇下来全是文字,总觉得少了点什么。想配几张图吧…...
【Python并发革命】:GIL解除后首个生产级无锁插件生态正式开放下载(限时72小时)
第一章:Python并发革命的里程碑意义 Python 并发模型的演进并非渐进式改良,而是一场深刻重塑编程范式的革命。从早期依赖线程与锁的阻塞式模型,到 asyncio 的异步 I/O 抽象、async/await 语法糖的引入,再到结构化并发(…...
Qwen3-14B芯片设计辅助:Verilog注释生成、RTL代码解释、DFT建议
Qwen3-14B芯片设计辅助:Verilog注释生成、RTL代码解释、DFT建议 1. 镜像概述与硬件适配 Qwen3-14B私有部署镜像是专为芯片设计工程师打造的AI辅助工具,基于通义千问大语言模型优化定制。该镜像完美适配RTX 4090D 24GB显存配置,预装了完整的…...
