资讯动态

Java原生实现图片水印与跨平台操作系统路径获取

发布时间:2026/10/14 10:23:29 来源:尧图企业网站定制
在开发中碰到“给图片加水印”和“拿到操作系统路径”这两个需求大概率不是同一天遇到的但如果你用 Java 写过图片处理工具或者桌面级小应用就会发现这两件事老是结伴出现一个是图像输入输出的落地位置一个是文件写到哪、从哪里读的问题。把这两个点放在一起讲不是强行凑主题而是它们背后的实现思路非常一致——用 JDK 自带的能力去解决高频场景不引依赖、不折腾环境拿起来就能写。这套代码我已经在几个内部工具项目里实际用过包括给上传头像加用户名水印、给导出图片打版权标识、在跨平台脚本里定位用户目录和桌面路径。整个过程没有引入任何第三方图片库也不需要额外的 native 组件纯 Java 就能跑。适合后端开发、工具类脚本爱好者以及刚刚接触 Java 图像处理的人参考核心代码可以直接抄走改参数。1. 整体设计与方案选型为什么用 Java 原生 API 做这件事1.1 需求拆解两个看似不相关的功能怎么走到一起“加水印”和“获取操作系统路径”从表面看属于两个方向一个是图像处理一个是文件系统访问。但在实际工程项目里这两个需求往往是一整套逻辑的上下游。举个例子系统需要给用户上传的图片加上登录名水印然后保存到用户专属目录。这时候你就需要两条能力——一条是图像绘制能力用来把文字或 Logo 画到原图上另一条是路径定位能力用来确定“用户主目录在哪”、“桌面路径在哪”、“当前工作目录是哪”。如果只做加水印而不考虑输出位置那处理完的图片就不知道该往哪写如果只做路径识别而不处理图像那这个目录拿到了也派不上用场。更常见的场景是批量工具遍历某个目录下的所有图片统一打水印再输出到另一个目录。这时路径获取不仅包括操作系统级目录还包括遍历子目录、拼接输出路径、处理跨平台分隔符等一系列问题。所以把两个功能放在一起做一个工具类是很自然的选型。另外很多做小型内部系统的开发者会在这类需求上卡住不是因为不会写代码而是因为不知道 Java 标准库本身就能画图不需要接 OpenCV 或 ImageMagick不清楚跨平台路径差异写出来的代码在本机能跑、换一台电脑就报错对 BufferedImage 和 Graphics2D 的坐标系、透明度合成不熟悉导致水印效果不理想。这篇内容就是要把这些坑逐个填平。1.2 技术选型不引第三方库的底气在哪里如果你去搜索引擎查“Java 给图片加水印”很容易被带偏到“用 Thumbnailator”“用 Apache Commons Imaging”这类第三方方案。这些库确实好用但对于“只是画一行文字”这种需求引入一个完整库属于杀鸡用牛刀。Java 自带的java.awt包里有BufferedImage和Graphics2D两个核心类。BufferedImage负责在内存中表示一张图像Graphics2D负责在上面绘制文字、线条、图片。整个流程无非是用ImageIO.read()把图片文件读成BufferedImage拿到BufferedImage.createGraphics()得到Graphics2D对象调用drawString()或drawImage()绘制水印用ImageIO.write()写回文件。全程不涉及任何第三方依赖也不需要额外安装图像处理软件。这在某些内网离线环境里是巨大优势——你没法临时下载 jar 包但 JDK 标准库是必然存在的。获取操作系统路径的选型也类似。System.getProperty()提供user.home、user.dir、os.name等关键系统属性FileSystemView则提供了跨平台的桌面目录、主目录视图。这两套 API 足够覆盖绝大多数“我需要知道当前系统把用户文件放在哪”的场景。注意Java 的标准图像接口都是基于 AWT 的而 AWT 在纯服务端环境无显示器下可能报 headless 异常。解决办法是启动参数或代码里设置System.setProperty(java.awt.headless, true)这个问题后面排查章节会细说。2. 图片水印核心实现从零到能上线的完整过程2.1 基础实现读取图片、绘制文字、写出文件先看一个最简版本给单张图片加上文字水印import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; public class SimpleWatermark { public static void main(String[] args) throws IOException { File source new File(input.jpg); File target new File(output.jpg); String watermarkText 内部资料; // 读取原图 BufferedImage image ImageIO.read(source); int width image.getWidth(); int height image.getHeight(); // 创建 Graphics2D Graphics2D g2d image.createGraphics(); // 设置绘制参数 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setFont(new Font(SansSerif, Font.BOLD, 32)); g2d.setColor(new Color(255, 255, 255, 180)); // 计算文字绘制起点 FontMetrics metrics g2d.getFontMetrics(); int textWidth metrics.stringWidth(watermarkText); int x width - textWidth - 20; int y height - 20; // 绘制文字 g2d.drawString(watermarkText, x, y); g2d.dispose(); // 写出文件 ImageIO.write(image, jpg, target); } }这段代码有什么需要注意的有。第一drawString()的坐标参数指的是文字左下角基线位置不是左上角。很多新手会因此把文字画到图片外面去或者在底部被截断。上面代码里用height - 20作为基线能保证文字离底边有 20 像素的余量。第二FontMetrics.stringWidth()可以在绘制前精确测量文字宽度这样你就能根据图片宽度动态调整水印字号避免文字过长直接超出图片边界。如果图片比较窄建议把字号改成基于width / 20计算出来的动态值。第三ImageIO.read()支持 JPEG、PNG、BMP、GIF 等常见格式但 JPEG 不支持透明通道。如果你把水印颜色设成半透明的new Color(255,255,255,180)写到 JPEG 里会丢失透明度变回不透明填充。这种情况下有两个处理思路一是 waterMark 颜色直接设成不透明但偏浅的颜色二是输出成 PNG 保留透明通道。第四JPEG 图片在写入时如果原图是 CMYK 色彩空间某些 JDK 版本会读取失败甚至抛IIOException。这个比较少见但遇到了要知道原因是什么后面问题排查会有对应的处理建议。2.2 进阶细节透明度、水印位置、旋转角度基础版本能跑但离“能上线”还有距离。真实场景里你通常需要在整张图上铺满水印让盗图者难以裁剪或者把水印放在最不容易被截掉的地方。这需要几个进阶操作。先说透明度。Java 里的透明度合成通过AlphaComposite实现g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f));第二个参数0.3f表示 30% 不透明度。取值范围 0.0f 到 1.0f值越大越不透明。一般版权水印建议 0.2 到 0.4既要能看清又不能喧宾夺主。再说平铺水印。原理很简单计算图片能放下几行几列然后循环画多次。int intervalX 200; int intervalY 100; g2d.setFont(new Font(SansSerif, Font.BOLD, 20)); for (int y 50; y height; y intervalY) { for (int x 50; x width; x intervalX) { g2d.drawString(watermarkText, x, y); } }平铺水印特别适合批量导出场景。不过我建议平铺时把旋转加进去因为纯横排的水印很容易被针对性裁掉稍微旋转 30 度左右水印覆盖感会强很多。旋转水印需要用到AffineTransform。这里最容易写错的点是旋转中心如果直接g2d.rotate(Math.toRadians(-30))所有绘制内容都会围绕画布左上角旋转水印会跑到图片外面去。正确做法是先平移到某个位置再旋转或者用rotate(angle, x, y)指定中心点// 围绕图片中心旋转 30 度 g2d.rotate(Math.toRadians(-30), width / 2.0, height / 2.0); // 在旋转后的坐标系里平铺 for (int y -height; y height * 2; y 120) { for (int x -width; x width * 2; x 300) { g2d.drawString(watermarkText, x, y); } }这里我把循环范围扩大到了图片宽高的两倍左右避免旋转后出现空白区域。你实际跑一遍就会发现旋转 30 度后原本的角落区域可能盖不到必须把绘制范围外扩。还有个小细节g2d.setFont()设置中文字体时SansSerif在大多数系统上能正常渲染中文但如果部署在精简的 Linux 镜像里没有中文字体水印会变成一个个方框。这个我在后面问题排查里会专门讲。2.3 图片水印把 Logo 贴上去的完整思路有时候水印不是文字而是公司 Logo 或用户头像。这时候流程会变成读取 Logo 图片缩放然后drawImage()画到原图上。BufferedImage logo ImageIO.read(new File(logo.png)); // 缩放 Logo 到合适大小 int logoWidth 80; int logoHeight logo.getHeight() * logoWidth / logo.getWidth(); Image scaledLogo logo.getScaledInstance(logoWidth, logoHeight, Image.SCALE_SMOOTH); // 绘制到右下角 g2d.drawImage(scaledLogo, width - logoWidth - 20, height - logoHeight - 20, null);这里有个性能问题getScaledInstance()在高频批量处理时比较慢因为每次都会重新采样。如果只是处理几张图完全无所谓但如果一个定时任务每天处理上千张图建议用Graphics2D.drawImage()配合AffineTransformOp做一次缩放或者干脆把 Logo 预缩放成几个固定尺寸。图片水印的透明度设置与文字相同仍然通过AlphaComposite。只是注意如果 Logo 本身是 PNG 带透明通道再叠加透明度时会出现“双重透明”效果可能比预期更淡。这种情况可以先用临时 BufferedImage 把 Logo 画成不透明底图再做整体合成。位置选择上图文水印常用的位置有四个左上角、右上角、左下角、右下角。实际业务中用户头像类水印放在右下角最不干扰内容版权声明类水印建议在底部居中或左上角。如果要防止截图盗用平铺是最好的选择。3. 获取操作系统路径跨平台路径处理的正确姿势3.1 核心 API系统属性与文件系统视图Java 获取系统相关路径主要靠两套东西System.getProperty()和FileSystemView。前者拿到的是一组标准属性后者提供面向图形界面的目录视图。最常用的几个属性如下表属性名Windows 示例macOS 示例Linux 示例含义user.homeC:\Users\用户名/Users/用户名/home/用户名用户主目录user.dirD:\project\demo/Users/用户名/project/demo/home/用户名/project/demo当前工作目录os.nameWindows 11Mac OS XLinux操作系统名称file.separator\//文件路径分隔符看到区别没有Windows 的主目录是盘符加路径macOS 和 Linux 都是斜杠开头。路径分隔符方面Windows 反斜杠、Unix 系斜杠。如果你的代码里到处硬编码\\或/换平台必炸。FileSystemView则是另一个思路import javax.swing.filechooser.FileSystemView; FileSystemView view FileSystemView.getFileSystemView(); File homeDir view.getHomeDirectory(); File desktopDir new File(homeDir.getAbsolutePath() File.separator Desktop);FileSystemView.getHomeDirectory()在 Windows 上返回用户主目录在 macOS 上通常也是用户主目录在 Linux 桌面版上同样是/home/用户名。但注意这是一种“视图”可能依赖桌面环境。在真正的服务器环境里调用它会非常慢甚至返回不太期望的结果因此我一般只在 GUI 工具里用FileSystemView在服务端代码里只用System.getProperty。这里有一个非常容易踩的坑macOS 的桌面路径并非所有系统都叫Desktop。如果系统语言设置了中文Finder 里显示“桌面”但底层目录名仍然是Desktop不会影响代码可如果你基于本地化做了字符串匹配就会出问题。最好的做法是不管显示层直接通过FileSystemView获取桌面目录或者拼路径时用File.separator而不是写死斜杠。3.2 路径拼接与分隔符陷阱跨平台代码怎么写才不出错路径拼接是跨平台开发里最容易被忽视的细节。常见的错误写法是String path userHome \\AppData\\Local\\temp;这段代码在 Windows 上没问题到了 Linux 上就会拼出\home\user\AppData...这种完全无效的路径。正确姿势是使用File或Paths// 使用 File File baseDir new File(System.getProperty(user.home), AppData/Local/temp); // 或者使用 Paths Path targetPath Paths.get(System.getProperty(user.home), AppData, Local, temp);File(File parent, String child)构造器会自动处理分隔符Paths.get()也接受多个字符串参数并按平台规则拼接。尽管 Java 在 Windows 上也能识别正斜杠/但代码里尽量别混用尤其在需要把路径传给第三方程序时分隔符一致性可以少很多坑。另一个问题是中文路径与空格路径。Windows 用户目录经常包含中文用户名比如C:\Users\张三。很多旧代码处理这种路径时会因为编码问题乱码。JDK 8 以后默认 UTF-8 处理文件路径一般问题不大但如果你在 JVM 启动参数里指定了-Dfile.encodingGBK记得确认读取路径时不要重复转码。关于空格Linux 路径里经常有My Documents这种带空格的目录代码里拼接时不要用字符串相加也尽量不要手动给特殊字符加转义。让File和Path对象去处理它们比字符串可靠得多。再有就是安全校验。拿到一个系统路径后不要盲目去创建文件和写入。标准做法是File dir new File(System.getProperty(user.home), watermark-output); if (!dir.exists()) { boolean created dir.mkdirs(); if (!created) { throw new IOException(无法创建目录: dir); } } if (!dir.canWrite()) { throw new IOException(目录不可写: dir); }尤其是 Linux 服务器环境用户主目录的权限可能受限。我曾见过某个定时任务因为没检查可写性运行了三天才发现一张图都没写出去日志里全是权限异常但由于业务代码吞掉了异常监控完全没发现。3.3 桌面路径与常用目录获取的实际差异如果你在做的是桌面小工具用户希望“水印后的图片存到桌面”那么你就要拿到桌面路径。获取桌面路径有两种方式// 方式一基于 FileSystemView File desktop FileSystemView.getFileSystemView().getHomeDirectory(); // 方式二基于 user.home 拼接Linux 下更可控 String desktopPath System.getProperty(user.home) File.separator Desktop;方式一在 Windows 和 macOS 上更准确因为系统可能把桌面重定向到 OneDrive 目录或 iCloud 目录这时候实际桌面路径不再等于user.home/Desktop。对于重定向过的情况FileSystemView能拿到真实路径方式二就会找错地方。方式二在 Linux 服务器上更安全因为最小化安装的 Linux 可能连桌面环境都没有根本没有 Desktop 目录。这时候FileSystemView调用可能返回空值或异常。我建议做成工具方法把两种方式结合起来public static File getDesktopDirectory() { File home new File(System.getProperty(user.home)); File directDesktop new File(home, Desktop); if (directDesktop.exists() directDesktop.isDirectory()) { return directDesktop; } try { File viewDesktop FileSystemView.getFileSystemView().getHomeDirectory(); // 大概率返回 home并不等于 desktop File maybeDesktop new File(viewDesktop, Desktop); if (maybeDesktop.exists()) { return maybeDesktop; } return home; } catch (Exception ignored) { return home; } }这样兜底至少不会返回 null也不会因为环境差异直接崩溃。4. 完整实操一套可直接运行的示例代码4.1 工具类设计与核心方法说明把前面两大部分合并成一个工具类包含水印处理与路径定位两个维度是实际项目里最常用的形态。设计上分两个类WatermarkUtils负责图片水印操作PlatformPaths负责系统路径解析然后再用一个入口方法演示如何协作。先看PlatformPathsimport java.io.File; import javax.swing.filechooser.FileSystemView; public class PlatformPaths { public static String userHome() { return System.getProperty(user.home); } public static String currentDir() { return System.getProperty(user.dir); } public static String osName() { return System.getProperty(os.name); } public static File getUserDataDir(String appName) { File base; String os osName().toLowerCase(); if (os.contains(win)) { // Windows: 使用 AppData/Roaming base new File(System.getenv(APPDATA) ! null ? System.getenv(APPDATA) : new File(userHome(), AppData/Roaming).getAbsolutePath()); } else if (os.contains(mac)) { // macOS: 使用 Library/Application Support base new File(userHome(), Library/Application Support); } else { // Linux: 使用 .config base new File(userHome(), .config); } File dir new File(base, appName); if (!dir.exists()) { dir.mkdirs(); } return dir; } public static File getDesktopDirectory() { File home new File(userHome()); File directDesktop new File(home, Desktop); if (directDesktop.exists() directDesktop.isDirectory()) { return directDesktop; } try { File viewHome FileSystemView.getFileSystemView().getHomeDirectory(); File maybeDesktop new File(viewHome, Desktop); if (maybeDesktop.exists()) { return maybeDesktop; } } catch (Exception ignored) { // headless 环境可能异常直接回退到主目录 } return home; } }getUserDataDir()是按平台规范存放应用数据的核心方法。Windows 走APPDATA环境变量macOS 走Library/Application SupportLinux 走~/.config。这样应用日志和配置文件不会散落在用户目录也符合各平台约定。再看WatermarkUtilsimport javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; public class WatermarkUtils { public static void addTextWatermark(File source, File target, String text, float alpha) throws IOException { BufferedImage image ImageIO.read(source); Graphics2D g2d image.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); g2d.setColor(new Color(255, 255, 255, 200)); g2d.setFont(new Font(SansSerif, Font.BOLD, Math.max(16, image.getWidth() / 30))); FontMetrics metrics g2d.getFontMetrics(); int textWidth metrics.stringWidth(text); int x image.getWidth() - textWidth - 20; int y image.getHeight() - 20; g2d.drawString(text, x, y); g2d.dispose(); ImageIO.write(image, png, target); } public static void addTiledTextWatermark(File source, File target, String text, float alpha) throws IOException { BufferedImage image ImageIO.read(source); Graphics2D g2d image.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); g2d.setColor(new Color(255, 255, 255, 160)); g2d.setFont(new Font(SansSerif, Font.BOLD, Math.max(14, image.getWidth() / 40))); g2d.rotate(Math.toRadians(-30), image.getWidth() / 2.0, image.getHeight() / 2.0); int intervalX 180; int intervalY 90; for (int y -image.getHeight(); y image.getHeight() * 2; y intervalY) { for (int x -image.getWidth(); x image.getWidth() * 2; x intervalX) { g2d.drawString(text, x, y); } } g2d.dispose(); ImageIO.write(image, png, target); } }最后写一个入口演示两者配合public class WatermarkDemo { public static void main(String[] args) throws IOException { // 获取桌面路径 File desktop PlatformPaths.getDesktopDirectory(); // 源图目录 / 输出目录 File sourceDir new File(desktop, source-images); File outputDir new File(desktop, watermark-output); if (!outputDir.exists()) { outputDir.mkdirs(); } File[] images sourceDir.listFiles((dir, name) - name.endsWith(.jpg) || name.endsWith(.png) || name.endsWith(.bmp)); if (images null) { return; } for (File image : images) { File target new File(outputDir, wm_ image.getName().replaceAll(\\.(jpg|png|bmp)$, .png)); WatermarkUtils.addTiledTextWatermark(image, target, 示例水印, 0.3f); } System.out.println(处理完成输出目录: outputDir.getAbsolutePath()); } }listFiles()用法里有一个细节过滤器返回 true 才保留文件而name.endsWith(.jpg)默认区分大小写。如果你要处理.JPG后缀记得改成name.toLowerCase().endsWith(.jpg)否则会漏文件。4.2 关键参数的选择逻辑与计算过程有几个参数看起来只是随意填的其实背后有选择逻辑。首先是字号。代码里我用了image.getWidth() / 30作为字号下限并保证不小于 16。为什么是 30如果一张图宽度 1200 像素1200 / 30 4040 像素的文字在 1200 宽图片上大约占 3%清晰又不抢内容。如果图宽只有 300300 / 30 10低于 16 就取 16否则小图上的文字会看不清。其次是透明度。平铺水印我用了 0.3f单角水印用了 0.5f 左右。0.3 的透明度叠加 30 度倾斜文字后视觉上比较柔和不影响原图主体0.5 适合需要强制可见的版权标识。如果你处理的是用户上传的头像推荐用 0.25f 以下否则头像本身会被水印盖住太多。再次是输出格式。代码统一输出为 PNG因为 PNG 支持透明通道水印的半透明效果能保留。如果你必须输出 JPG记得先给图片填充一个白底否则透明区域会变成黑色。填充白底的标准写法是在绘制前先画一个白色矩形覆盖整个画布。还有遍历图片时的过滤逻辑。listFiles()返回的文件顺序是不可靠的如果你需要按文件名排序必须手动Arrays.sort()。批量任务里文件顺序会直接影响“先处理哪个”的日志输出以及如果任务中途失败哪些文件已经处理完了。建议养成排序的习惯。5. 常见问题与排查技巧实录5.1 典型问题与排查记录实际使用中会遇到各种幺蛾子我把典型问题整理成一个速查表方便直接对照问题可能原因解决方案中文水印显示为方框系统缺少中文字体指定支持中文的字体或随应用打包一个开源中文字体输出 JPG 后水印区域有黑底JPG 不支持透明通道透明色被解析为黑输出 PNG或先绘制白色背景再输出 JPG水印文字超出图片边界drawString坐标用错未计算文字宽度用FontMetrics.stringWidth()预测量宽度基线坐标不要直接做中心定位读取ImageIO.read()时图片为 null文件格式损坏或 CMYK 色彩空间用其他工具确认图片是否可读对 CMYK 图先转为 RGB 色彩空间批量处理时内存溢出BufferedImage同时加载太多图片逐张读取处理及时释放引用必要时调大 JVM 内存Linux 服务端代码调用FileSystemView很慢或异常headless 环境没有图形界面支持改用System.getProperty(user.home)拼接路径不用FileSystemViewWindows 下获取桌面路径失败桌面被重定向到 OneDrive 等目录使用FileSystemView.getHomeDirectory()获取重定向后的真实路径处理大量图片时 CPU 飚高getScaledInstance()频繁缩放导致高消耗预先生成缩放好的 Logo 副本或使用AffineTransformOp批量缩放再说一些不太常见但真实遇到过的情况。有一次在批量处理时发现.jpg文件的扩展名是大写的.JPG过滤器没匹配上结果一批图“静默”跳过日志里完全看不出来。后来把所有后缀判断都改成toLowerCase()这才解决。还有一次在 Linux 服务器上跑定时任务发现水印文字全部变成方框排查后发现系统里只装了英文语言包没有中文字体。临时方案是改成 IPA 字体正规方案是代码里先检测字体是否可用不可用就自动切换到系统自带的中文字体路径。字体检测代码大致是这样的public static Font getChineseFont() { Font[] fonts GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts(); for (Font font : fonts) { if (font.canDisplay(中)) { return font; } } return new Font(SansSerif, Font.BOLD, 16); }这个方法在 headless 下需要注意GraphicsEnvironment#getAllFonts()可能触发图形环境初始化如果服务端环境比较严格可以在启动参数里带上-Djava.awt.headlesstrue。5.2 实操心得几次真实场景调试得到的经验我把做过的几个项目和踩坑经历浓缩成几条建议都是常规文档里不会写的东西。第一个心得图片处理任务一定要先做“输入校验”。我在某个批量项目里吃过亏源目录里混入了一个 0 字节的损坏文件ImageIO.read()返回 null后续代码拿到 null 直接抛NullPointerException整个批处理中断。后来所有入口都加了这样的校验BufferedImage image ImageIO.read(source); if (image null) { throw new IOException(无法识别图片文件: source.getAbsolutePath()); }第二个心得水印文字不要直接用业务名要加时间戳或随机因子。内部资料如果只写“某某系统”用户直接把那行字裁掉就完事了。把日期、账号、IP 这类可以追踪来源的信息加进去威慑力完全不同。虽然这部分属于业务策略但代码层预留参数就行。第三个心得输出文件命名尽量避免覆盖原图。我的习惯是统一在前面加wm_前缀或者把输出目录强制设为另一个文件夹。单独一个output文件夹是最安全的方式因为即使程序写错原图也不会被破坏。尤其要注意ImageIO.write()的目标文件如果和源文件是同一个File对象底层操作可能因为流未关闭而产生不一致的文件内容尽量不要在同一路径上覆盖写。第四个心得把System.getProperty(os.name)判断集中到一个方法里不要在业务代码里到处写。比如public static boolean isWindows() { return osName().toLowerCase().contains(win); }这样后续如果要支持新系统只改一个地方就够了。说到最后我个人在实际操作中的体会是这类工具类代码花半天时间写好能省掉未来无数个“换个环境就报错”的加班夜。Java 的图像处理和路径处理能力比大多数人印象里要完善得多问题往往出在对坐标系、透明度合成和跨平台路径差异的理解上。如果你准备在公司内部做一个图片处理小工具把这段代码当成起点根据业务需要去调整字号、透明度、平铺间隔、存储位置基本能覆盖九成场景。还有一个小技巧写完后用几组不同尺寸、不同格式、不同中文环境的图片各跑一遍比在单张图上调参数更有价值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑