手把手教你如何通过Java给图片添加文字和图片水印(一)

简介: 手把手教你如何通过Java给图片添加文字和图片水印(一)

前言


最近工作上有个需求,动态生成一张图片,具体来说就是基于模版图片动态添加文字和图片(文字内容不同,图片数目不同),其中文字大小不全一样,且对位置有所要求。

本文将剖析多个技术方案来实现水印生成,并最终抉择出最优方案。


技术分析


基于模版图片动态添加文字和图片,需要先调研一下有哪些技术方案,可能添加文字和图片的技术不同。


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);
  }
}
复制代码


执行效果如下:


1.jpg


上述代码中的 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();
      }
    }
  }
复制代码


执行效果如下:


2.jpg


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);
  }
}
复制代码


执行效果如下:


3.jpg


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
复制代码


结果如下,可以正常输出中文水印。


4.jpg


接下来我们试试在代码中使用 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 属性,值范围如下:


5.jpg


测试一下


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
复制代码


生成的图片如下所示:


6.jpg


我们最后还是用代码来试一下效果:


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);
}
复制代码


执行效果如下:


7.jpg


目录
相关文章
|
2月前
|
Java
java通过commons-fileupload实现多张图片的上传(servlet)
java通过commons-fileupload实现多张图片的上传(servlet)
25 2
|
2月前
|
存储 Java 计算机视觉
Java代码居然可以做出如此高级的图片编辑系统
Java代码居然可以做出如此高级的图片编辑系统
43 0
|
2月前
|
前端开发 Java
java通过commons-fileupload实现多张图片的上传(jsp页面)
java通过commons-fileupload实现多张图片的上传(jsp页面)
17 2
|
2月前
|
Java
java中复制图片
java中复制图片
10 2
|
2月前
|
文字识别 Java Maven
java调用tess4j识别图像中的文字
java调用tess4j识别图像中的文字
34 3
|
2月前
|
存储 Java Apache
java 读取图片成二进制流
【2月更文挑战第10天】
|
前端开发 安全 Java
Java实现把图片上传到图片服务器(nginx+vsftp)
在我另一篇笔记中已经记载了如何用nginx + vsftp搭建图片服务器(请参考nginx + vsftp搭建图片服务器),并且用vsftp的客户端工具filezilla测试过已经可用。但是在开发中应该是把用户在前端页面提交的图片保存到图片服务器中,接下来就来实现这个功能。
 Java实现把图片上传到图片服务器(nginx+vsftp)
|
2天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第12天】 在现代软件开发中,多线程编程是提升应用程序性能和响应能力的关键手段之一。特别是在Java语言中,由于其内置的跨平台线程支持,开发者可以轻松地创建和管理线程。然而,随之而来的并发问题也不容小觑。本文将探讨Java并发编程的核心概念,包括线程安全策略、锁机制以及性能优化技巧。通过实例分析与性能比较,我们旨在为读者提供一套既确保线程安全又兼顾性能的编程指导。
|
2天前
|
Java
Java中的多线程编程:基础知识与实践
【5月更文挑战第13天】在计算机科学中,多线程是一种使得程序可以同时执行多个任务的技术。在Java语言中,多线程的实现主要依赖于java.lang.Thread类和java.lang.Runnable接口。本文将深入探讨Java中的多线程编程,包括其基本概念、实现方法以及一些常见的问题和解决方案。
|
2天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第13天】 在Java开发中,并发编程是一个复杂且重要的领域。它不仅关系到程序的线程安全性,也直接影响到系统的性能表现。本文将探讨Java并发编程的核心概念,包括线程同步机制、锁优化技术以及如何平衡线程安全和性能。通过分析具体案例,我们将提供实用的编程技巧和最佳实践,帮助开发者在确保线程安全的同时,提升应用性能。
10 1