前言
最近工作上有个需求,动态生成一张图片,具体来说就是基于模版图片动态添加文字和图片(文字内容不同,图片数目不同),其中文字大小不全一样,且对位置有所要求。
本文将剖析多个技术方案来实现水印生成,并最终抉择出最优方案。
技术分析
基于模版图片动态添加文字和图片,需要先调研一下有哪些技术方案,可能添加文字和图片的技术不同。
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 { if (outputStream != null) { outputStream.flush(); outputStream.close(); } } catch (Exception e) { e.getStackTrace(); } } } 复制代码
执行效果如下:
Thumbnailator
图片水印
Thumbnailator 不支持文字水印,只能测试一下图片水印的效果。
public class ThumbnailsUtil { 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 addImgWaterMark(String srcImagePath, String waterImgPath, String outPath) throws IOException { // 原始图片信息 BufferedImage targetImg = ImageIO.read(new File(srcImagePath)); // 水印图片 BufferedImage watermarkImage = ImageIO.read(new File(waterImgPath)); int height = targetImg.getHeight(); int width = targetImg.getWidth(); System.out.println("width:" + width + "; height:" + height); // 可以自定义坐标位置 int x = 600; int y = 600; Coordinate coordinate = new Coordinate(x, y); Thumbnails.of(targetImg).size(width, height) // .watermark(Positions.CENTER, watermarkImage, 1f) // 0.5f表示透明度,最大值为1 .watermark(coordinate, watermarkImage, 1f) // 0.5f表示透明度,最大值为1 .outputQuality(1) // 图片质量,最大值为1 .toFile(new File(outPath)); } public static void main(String[] args) throws IOException { String projectPath = System.getProperty("user.dir"); String srcImgPath = projectPath + CERTIFICATE_BASE_PATH; String waterImgPath = projectPath + WATERMARK_IMAGE_PATH; String outPath = projectPath + "/src/main/resources/static/out/img_water_image_by_thumbnails.png"; addImgWaterMark(srcImgPath, waterImgPath, outPath); } } 复制代码
执行效果如下:
GraphicsMagick与Im4Java
文字水印
GraphicsMagick 生成文字水印的命令如下:
gm convert -font ${fontType} -fill ${color} -pointsize ${fontSize} -draw "text ${dx},${dy} '${textContent}'" ${sourceImgPath} ${distImgPath} 复制代码
参数含义如下:
- fontType:字体类型;
- color:字体颜色;
- fontSize:字体大小;
- dx:水印x轴位置
- dy:水印y轴位置
- textContent:水印内容
- sourceImgPath:源文件路径
- distImgPath:目标文件路径
我们先尝试使用命令来生成文字水印,看看能否成功。
gm convert -font /System/Library/Fonts/Supplemental/Songti.ttc -fill red -pointsize 50 -draw "text 400,500 '你好'" certificate-blank.png test.png 复制代码
结果如下,可以正常输出中文水印。
接下来我们试试在代码中使用 ImageMagick 来生成文字水印:
public class Im4JavaUtil { private static final int[] ICON_LEFT_MARGINS = new int[]{552, 467, 395}; 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"; // 是否使用 GraphicsMagick private static final boolean IS_USE_GRAPHICS_MAGICK = true; // 本机上graphicsmagick的安装位置 private static final String GRAPHICS_MAGICK_PATH = "/usr/local/Cellar/graphicsmagick/1.3.38_1/bin"; /** * 命令类型 */ private enum CommandType { convert("转换处理"), identify("图片信息"), textWaterMark("文字水印"), imageWaterMark("图片水印"); private String name; CommandType(String name) { this.name = name; } } private static ImageCommand getImageCommand(CommandType command) { ImageCommand cmd = null; switch (command) { case convert: case textWaterMark: cmd = new ConvertCmd(IS_USE_GRAPHICS_MAGICK); break; case identify: cmd = new IdentifyCmd(IS_USE_GRAPHICS_MAGICK); break; case imageWaterMark: cmd = new CompositeCmd(IS_USE_GRAPHICS_MAGICK); break; } cmd.setSearchPath(GRAPHICS_MAGICK_PATH); return cmd; } public static void addTextWatermark(String srcImagePath, String destImagePath, String content) throws Exception { GMOperation op = new GMOperation(); op.font("/System/Library/Fonts/Supplemental/Songti.ttc"); // 文字方位-居中 op.gravity("center"); op.pointsize(120).fill("#BCBFC8").draw("text 0,0 '" + content + "'").quality(90.0); // 原图 op.addImage(); // 目标 op.addImage(); ImageCommand cmd = getImageCommand(CommandType.textWaterMark); cmd.run(op, srcImagePath, destImagePath); } public static void main(String[] args) throws Exception { String projectPath = System.getProperty("user.dir"); String srcImgPath = projectPath + CERTIFICATE_BASE_PATH; String outPath = projectPath + "/src/main/resources/static/out/text_water_image_by_im4.png"; String content = "中国"; addTextWatermark(srcImgPath, outPath, content); } } 复制代码
上述代码看起来比较简单,需要注意的是 op.draw()方法中的内容,尤其是单引号,一定不能漏掉,如果不加单引号,中文文字水印会乱码。
图片水印
gm composite -gravity ${gravity} -dissolve ${dissolve} -geometry +${dx}+${dy} ${waterImgPath} ${sourceImgPath} ${distImgPath} 复制代码
参数含义如下:
- gravity:水印相对位置,
- dissolve:水印透明度
- dx:水印距离右边缘的距离
- dy:水印距离下边缘的距离
- waterImgPath:水印图片路径
- sourceImgPath:源图片路径
- distImgPath:目标图片路径
关于 gravity 属性,值范围如下:
测试一下
gm composite -gravity Center -dissolve 90 -geometry +50+$50 /Users/xxx/IdeaProjects/java_deep_learning/src/main/resources/static/icon.png /Users/xxx/IdeaProjects/java_deep_learning/src/main/resources/static/certificate-blank.png /Users/xxx/IdeaProjects/java_deep_learning/src/main/resources/static/out/img_water_image.png 复制代码
生成的图片如下所示:
我们最后还是用代码来试一下效果:
public static void addImgWatermark(String srcImagePath, String destImagePath, String waterImgPath) throws Exception { // 原始图片信息 BufferedImage targetImg = ImageIO.read(new File(srcImagePath)); // 水印图片 BufferedImage watermarkImage = ImageIO.read(new File(waterImgPath)); int w = targetImg.getWidth(); int h = targetImg.getHeight(); IMOperation op = new IMOperation(); // 水印图片位置 op.geometry(watermarkImage.getWidth(null), watermarkImage.getHeight(null), w - watermarkImage.getWidth(null) - 300, h - watermarkImage.getHeight(null) - 100); // 水印透明度 op.dissolve(90); // 水印 op.addImage(waterImgPath); // 原图 op.addImage(srcImagePath); // 目标 op.addImage(destImagePath); ImageCommand cmd = getImageCommand(CommandType.imageWaterMark); cmd.run(op); } 复制代码
执行效果如下: