手把手教你如何通过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开发实现图片URL地址检验,如何编码?
【10月更文挑战第14天】Java开发实现图片URL地址检验,如何编码?
97 4
|
2月前
|
Java
Java开发实现图片地址检验,如果无法找到资源则使用默认图片,如何编码?
【10月更文挑战第14天】Java开发实现图片地址检验,如果无法找到资源则使用默认图片,如何编码?
70 2
|
2月前
|
Java 数据安全/隐私保护
Java ffmpeg 实现视频加文字/图片水印功能
【10月更文挑战第22天】在 Java 中使用 FFmpeg 实现视频加文字或图片水印功能,需先安装 FFmpeg 并添加依赖(如 JavaCV)。通过构建 FFmpeg 命令行参数,使用 `drawtext` 滤镜添加文字水印,或使用 `overlay` 滤镜添加图片水印。示例代码展示了如何使用 JavaCV 实现文字水印。
175 1
|
2月前
|
前端开发 小程序 Java
java基础:map遍历使用;java使用 Patten 和Matches 进行正则匹配;后端传到前端展示图片三种情况,并保存到手机
这篇文章介绍了Java中Map的遍历方法、使用Pattern和matches进行正则表达式匹配,以及后端向前端传输图片并保存到手机的三种情况。
27 1
|
2月前
|
存储 算法 Java
java制作海报六:Graphics2D的RenderingHints方法参数详解,包括解决文字不清晰,抗锯齿问题
这篇文章是关于如何在Java中使用Graphics2D的RenderingHints方法来提高海报制作的图像质量和文字清晰度,包括抗锯齿和解决文字不清晰问题的技术详解。
74 0
java制作海报六:Graphics2D的RenderingHints方法参数详解,包括解决文字不清晰,抗锯齿问题
|
文字识别 Java BI
java文字识别技术(亲测,识别率很高)
java文字识别程序的关键是寻找一个可以调用的OCR引擎。tesseract-ocr就是一个这样的OCR引擎,在1985年到1995年由HP实验室开发,现在在Google。tesseract-ocr 3.0发布,支持中文。
7072 0
|
2天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
4天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
4天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
4天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
20 3