前言
最近工作上有个需求,动态生成一张图片,具体来说就是基于模版图片动态添加文字和图片(文字内容不同,图片数目不同),其中文字大小不全一样,且对位置有所要求。
本文将剖析多个技术方案来实现水印生成,并最终抉择出最优方案。
技术分析
基于模版图片动态添加文字和图片,需要先调研一下有哪些技术方案,可能添加文字和图片的技术不同。
Graphics2D
利用 JDK 自带的 Graphics2D ,该类扩展 Graphics 类,以提供对几何形状、坐标转换、颜色管理和文本布局更为复杂的控制。它是用于在 Java(tm) 平台上呈现二维形状、文本和图像的基础类。
Thumbnailator
使用第三方 Jar 包 Thumbnailator:使用第三方 Jar 包还是比较简单的,在 Thumbnailator
中已有相应的API了,只需阅读官方的文档即可实现。
GraphicsMagick与Im4Java
ImageMagick 是一个免费的创建、编辑、合成图片的开源软件。它可以读取、转换、写入多种格式的图片。图片切割、颜色替换、各种效果的应用,图片的旋转、组合,文本,直线,多边形,椭圆,曲线,附加到图片伸展旋转。
ImageMagick 是个图片处理工具,可以安装在绝大多数的平台上使用,Linux、Mac、Windows 都没有问题。GraphicsMagick 是在ImageMagick 基础上的另一个项目,大大提高了图片处理的性能,在 Linux 平台上,可以使用命令行的形式处理图片。关于 ImageMagick 在不同环境的安装教程,推荐阅读这篇文章。
开源社区针对 ImageMagick 开发了两款 Java API,分别是 JMagick 和 Im4Java,两者的区别如下:
- JMagick 是一个开源API,利用 JNI(Java Native Interface)技术实现了对 ImageMagick API 的 Java 访问接口,因此也将比纯 Java 实现的图片操作函数在速度上要快。JMagick 只实现了 ImageMagicAPI 的一部分功能,它的发行遵循LGPL协议。
- Im4java 是 ImageMagick 的另一个 Java 开源接口。与 JMagick 不同之处在于 Im4java 只是生成与ImageMagick相对应的命令行,然后将生成的命令行传至选中的 ImageCommand(使用java.lang.ProcessBuilder.start()实现)来执行相应的操作。它支持大部分ImageMagick 命令,可以针对不同组的图片多次复用同一个命令行。
Im4java 支持 GraphicsMagick,GraphicsMagick 是 ImageMagick 的分支。相对 ImageMagick ,GraphicsMagick 更稳定,消耗资源更少。最重要的是不依赖 dll 环境,且性能更好,所以我们选择使用 Im4java,想要使用该 API,那么本机上就需要安装 GraphicsMagick。
本人尝试在 Mac 上安装 GraphicsMagick,推荐阅读 Mac 安装 GraphicsMagick ,这里补充一点个人安装时的经验。
Mac 可以使用 brew 命令:
brew install libpng
brew install libjpeg
#通过 brew 安装 GraphicsMagick(libpng 等依赖包会一并下载)
brew install graphicsmagick
查看 GraphicsMagick 的版本以及安装路径:
% gm -version
GraphicsMagick 1.3.38 2022-03-26 Q16 http://www.GraphicsMagick.org/
......
Configured using the command:
./configure '--prefix=/usr/local/Cellar/graphicsmagick/1.3.38_1' '--disable-dependency-tracking' '--disable-openmp' '--disable-static' '--enable-shared' '--with-modules' '--with-quantum-depth=16' '--without-lzma' '--without-x' '--without-gslib' '--with-gs-font-dir=/usr/local/share/ghostscript/fonts' '--without-wmf' 'CC=clang' 'CXX=clang++' 'PKG_CONFIG_PATH=/usr/local/opt/libpng/lib/pkgconfig:/usr/local/opt/freetype/lib/pkgconfig:/usr/local/opt/jpeg-turbo/lib/pkgconfig:/usr/local/opt/jasper/lib/pkgconfig:/usr/local/opt/libtiff/lib/pkgconfig:/usr/local/opt/little-cms2/lib/pkgconfig:/usr/local/opt/webp/lib/pkgconfig' 'PKG_CONFIG_LIBDIR=/usr/lib/pkgconfig:/usr/local/Homebrew/Library/Homebrew/os/mac/pkgconfig/11'
.....
测试效果
% gm identify /Users/xxxx/Downloads/certificate_blank.jpg
/Users/xxx/Downloads/certificate_blank.jpg JPEG 453x640+0+0 DirectClass 8-bit 64.1Ki 0.000u 0m:0.000003s
技术方案
Graphics2D
文字水印
public class Graphics2DUtil {
private static final String FONT_FAMILY = "楷体";
private static final String CERTIFICATE_BASE_PATH = "/src/main/resources/static/certificate-blank.png";
private static final String WATERMARK_IMAGE_PATH = "/src/main/resources/static/icon.png";
public static void graphics2DDrawTest(String srcImgPath, String outPath) {
try {
BufferedImage targetImg = ImageIO.read(new File(srcImgPath));
int imgWidth = targetImg.getWidth();
int imgHeight = targetImg.getHeight();
BufferedImage bufferedImage = new BufferedImage(imgWidth, imgHeight,
BufferedImage.TYPE_INT_BGR);
Graphics2D g = bufferedImage.createGraphics();
g.drawImage(targetImg, 0, 0, imgWidth, imgHeight, null);
g.setColor(Color.BLACK);
// 第一行文本字体大小为120,居中显示
Font userNameFont = new Font(FONT_FAMILY, Font.PLAIN, 120);
g.setFont(userNameFont);
String userName = "hresh";
int[] userNameSize = getContentSize(userNameFont, userName);
int userNameLeftMargin = (imgWidth - userNameSize[0]) / 2;
int userNameTopMargin = 400 + userNameSize[1];
g.drawString(userName, userNameLeftMargin, userNameTopMargin);
g.dispose();
FileOutputStream outImgStream = new FileOutputStream(outPath);
ImageIO.write(bufferedImage, "png", outImgStream);
g.dispose();
} catch (IOException e) {
e.getStackTrace();
}
}
/**
* 获取文本的长度,字体大小不同,长度也不同
*
* @param font
* @param content
* @return
*/
public static int[] getContentSize(Font font, String content) {
int[] contentSize = new int[2];
FontRenderContext frc = new FontRenderContext(new AffineTransform(), true, true);
Rectangle rec = font.getStringBounds(content, frc).getBounds();
contentSize[0] = (int) rec.getWidth();
contentSize[1] = (int) rec.getHeight();
return contentSize;
}
public static void main(String[] args) throws IOException {
String projectPath = System.getProperty("user.dir");
String srcImgPath = projectPath + CERTIFICATE_BASE_PATH;
String outPath = projectPath + "/src/main/resources/static/out/image_by_graphics2D.png";
graphics2DDrawTest(srcImgPath, outPath);
}
}
执行效果如下:
上述代码中的 getContentSize()方法,根据 Font 和文本内容获取文本的宽度和高度,进一步可以知道文本中每个字符的宽高,如果文本需要换行,离不开字符的宽高数据。除了上述获取文本宽高的实现方式,还有一种实现方式,不过不推荐使用。
FontMetrics fm = sun.font.FontDesignMetrics.getMetrics(font);
int width = fm.stringWidth(content);
int height = fm.getHeight();
因为 sun.font.FontDesignMetrics 在未来的版本可能会被删除掉,本人目前还是使用 JDK8。
图片水印
public static void graphics2DDrawTest(String srcImgPath, String waterImgPath, String outPath) {
FileOutputStream outputStream = null;
try {
BufferedImage targetImg = ImageIO.read(new File(srcImgPath));
int imgWidth = targetImg.getWidth();
int imgHeight = targetImg.getHeight();
BufferedImage bufferedImage = new BufferedImage(imgWidth, imgHeight,
BufferedImage.TYPE_INT_BGR);
Graphics2D g = bufferedImage.createGraphics();
g.drawImage(targetImg, 0, 0, imgWidth, imgHeight, null);
g.setColor(Color.BLACK);
BufferedImage icon = ImageIO.read(new File(waterImgPath));
g.drawImage(icon, 350, 600, icon.getWidth(),
icon.getHeight(), null);
FileOutputStream outImgStream = new FileOutputStream(outPath);
ImageIO.write(bufferedImage, "png", outImgStream);
g.dispose();
} catch (IOException e) {
e.getStackTrace();
} finally {
try {