All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.dahuatech.hutool.core.img.Img Maven / Gradle / Ivy

package com.dahuatech.hutool.core.img;

import com.dahuatech.hutool.core.io.FileUtil;
import com.dahuatech.hutool.core.io.IORuntimeException;
import com.dahuatech.hutool.core.io.IoUtil;
import com.dahuatech.hutool.core.io.resource.Resource;
import com.dahuatech.hutool.core.lang.Assert;
import com.dahuatech.hutool.core.util.NumberUtil;
import com.dahuatech.hutool.core.util.ObjectUtil;
import com.dahuatech.hutool.core.util.StrUtil;

import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.*;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URL;
import java.nio.file.Path;

/**
 * 图像编辑器
 *
 * @author looly
 * @since 4.1.5
 */
public class Img implements Serializable {
  private static final long serialVersionUID = 1L;

  private BufferedImage srcImage;
  private Image targetImage;
  /** 目标图片文件格式,用于写出 */
  private String targetImageType = ImgUtil.IMAGE_TYPE_JPG;
  /** 计算x,y坐标的时候是否从中心做为原始坐标开始计算 */
  private boolean positionBaseCentre = true;
  /** 图片输出质量,用于压缩 */
  private float quality = -1;

  /**
   * 构造
   *
   * @param srcImage 来源图片
   */
  public Img(BufferedImage srcImage) {
    this.srcImage = srcImage;
  }

  /**
   * 从Path读取图片并开始处理
   *
   * @param imagePath 图片文件路径
   * @return {@link Img}
   */
  public static Img from(Path imagePath) {
    return from(imagePath.toFile());
  }

  /**
   * 从文件读取图片并开始处理
   *
   * @param imageFile 图片文件
   * @return {@link Img}
   */
  public static Img from(File imageFile) {
    return new Img(ImgUtil.read(imageFile));
  }

  /**
   * 从资源对象中读取图片并开始处理
   *
   * @param resource 图片资源对象
   * @return {@link Img}
   * @since 4.4.1
   */
  public static Img from(Resource resource) {
    return from(resource.getStream());
  }

  /**
   * 从流读取图片并开始处理
   *
   * @param in 图片流
   * @return {@link Img}
   */
  public static Img from(InputStream in) {
    return new Img(ImgUtil.read(in));
  }

  /**
   * 从ImageInputStream取图片并开始处理
   *
   * @param imageStream 图片流
   * @return {@link Img}
   */
  public static Img from(ImageInputStream imageStream) {
    return new Img(ImgUtil.read(imageStream));
  }

  /**
   * 从URL取图片并开始处理
   *
   * @param imageUrl 图片URL
   * @return {@link Img}
   */
  public static Img from(URL imageUrl) {
    return new Img(ImgUtil.read(imageUrl));
  }

  /**
   * 从Image取图片并开始处理
   *
   * @param image 图片
   * @return {@link Img}
   */
  public static Img from(Image image) {
    return new Img(ImgUtil.toBufferedImage(image));
  }

  /**
   * 将图片绘制在背景上
   *
   * @param backgroundImg 背景图片
   * @param img 要绘制的图片
   * @param rectangle 矩形对象,表示矩形区域的x,y,width,height,x,y从背景图片中心计算
   * @return 绘制后的背景
   */
  private static BufferedImage draw(
      BufferedImage backgroundImg, Image img, Rectangle rectangle, float alpha) {
    final Graphics2D g = backgroundImg.createGraphics();
    g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
    g.drawImage(img, rectangle.x, rectangle.y, rectangle.width, rectangle.height, null); // 绘制切割后的图
    g.dispose();
    return backgroundImg;
  }

  /**
   * 计算旋转后的图片尺寸
   *
   * @param width 宽度
   * @param height 高度
   * @param degree 旋转角度
   * @return 计算后目标尺寸
   * @since 4.1.20
   */
  private static Rectangle calcRotatedSize(int width, int height, int degree) {
    if (degree < 0) {
      // 负数角度转换为正数角度
      degree += 360;
    }
    if (degree >= 90) {
      if (degree / 90 % 2 == 1) {
        int temp = height;
        height = width;
        width = temp;
      }
      degree = degree % 90;
    }
    double r = Math.sqrt(height * height + width * width) / 2;
    double len = 2 * Math.sin(Math.toRadians(degree) / 2) * r;
    double angel_alpha = (Math.PI - Math.toRadians(degree)) / 2;
    double angel_dalta_width = Math.atan((double) height / width);
    double angel_dalta_height = Math.atan((double) width / height);
    int len_dalta_width = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_width));
    int len_dalta_height = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_height));
    int des_width = width + len_dalta_width * 2;
    int des_height = height + len_dalta_height * 2;

    return new Rectangle(des_width, des_height);
  }

  /**
   * 设置目标图片文件格式,用于写出
   *
   * @param imgType 图片格式
   * @return this
   * @see ImgUtil#IMAGE_TYPE_JPG
   * @see ImgUtil#IMAGE_TYPE_PNG
   */
  public Img setTargetImageType(String imgType) {
    this.targetImageType = imgType;
    return this;
  }

  /**
   * 计算x,y坐标的时候是否从中心做为原始坐标开始计算
   *
   * @param positionBaseCentre 是否从中心做为原始坐标开始计算
   * @return this
   * @since 4.1.15
   */
  public Img setPositionBaseCentre(boolean positionBaseCentre) {
    this.positionBaseCentre = positionBaseCentre;
    return this;
  }

  /**
   * 设置图片输出质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩
   *
   * @param quality 质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩
   * @return this
   * @since 4.3.2
   */
  public Img setQuality(double quality) {
    return setQuality((float) quality);
  }

  /**
   * 设置图片输出质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩
   *
   * @param quality 质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩
   * @return this
   * @since 4.3.2
   */
  public Img setQuality(float quality) {
    if (quality > 0 && quality < 1) {
      this.quality = quality;
    } else {
      this.quality = 1;
    }
    return this;
  }

  /**
   * 缩放图像(按比例缩放)
   *
   * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小
   * @return this
   */
  public Img scale(float scale) {
    if (scale < 0) {
      // 自动修正负数
      scale = -scale;
    }

    final Image srcImg = getValidSrcImg();

    // PNG图片特殊处理
    if (ImgUtil.IMAGE_TYPE_PNG.equals(this.targetImageType)) {
      final AffineTransformOp op =
          new AffineTransformOp(AffineTransform.getScaleInstance(scale, scale), null);
      this.targetImage = op.filter(ImgUtil.toBufferedImage(srcImg), null);
    } else {
      final String scaleStr = Float.toString(scale);
      // 缩放后的图片宽
      int width = NumberUtil.mul(Integer.toString(srcImg.getWidth(null)), scaleStr).intValue();
      // 缩放后的图片高
      int height = NumberUtil.mul(Integer.toString(srcImg.getHeight(null)), scaleStr).intValue();
      scale(width, height);
    }
    return this;
  }

  /**
   * 缩放图像(按长宽缩放)
* 注意:目标长宽与原图不成比例会变形 * * @param width 目标宽度 * @param height 目标高度 * @return this */ public Img scale(int width, int height) { final Image srcImg = getValidSrcImg(); int srcHeight = srcImg.getHeight(null); int srcWidth = srcImg.getWidth(null); int scaleType; if (srcHeight == height && srcWidth == width) { // 源与目标长宽一致返回原图 this.targetImage = srcImg; return this; } else if (srcHeight < height || srcWidth < width) { // 放大图片使用平滑模式 scaleType = Image.SCALE_SMOOTH; } else { scaleType = Image.SCALE_DEFAULT; } double sx = NumberUtil.div(width, srcWidth); double sy = NumberUtil.div(height, srcHeight); if (ImgUtil.IMAGE_TYPE_PNG.equals(this.targetImageType)) { final AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance(sx, sy), null); this.targetImage = op.filter(ImgUtil.toBufferedImage(srcImg), null); } else { this.targetImage = srcImg.getScaledInstance(width, height, scaleType); } return this; } /** * 等比缩放图像,此方法按照按照给定的长宽等比缩放图片,按照长宽缩放比最多的一边等比缩放,空白部分填充背景色
* 缩放后默认为jpeg格式 * * @param width 缩放后的宽度 * @param height 缩放后的高度 * @param fixedColor 比例不对时补充的颜色,不补充为null * @return this */ public Img scale(int width, int height, Color fixedColor) { Image srcImage = getValidSrcImg(); int srcHeight = srcImage.getHeight(null); int srcWidth = srcImage.getWidth(null); double heightRatio = NumberUtil.div(height, srcHeight); double widthRatio = NumberUtil.div(width, srcWidth); if (widthRatio == heightRatio) { // 长宽都按照相同比例缩放时,返回缩放后的图片 scale(width, height); } else if (widthRatio < heightRatio) { // 宽缩放比例多就按照宽缩放 scale(width, (int) (srcHeight * widthRatio)); } else { // 否则按照高缩放 scale((int) (srcWidth * heightRatio), height); } // 获取缩放后的新的宽和高 srcImage = getValidSrcImg(); srcHeight = srcImage.getHeight(null); srcWidth = srcImage.getWidth(null); if (null == fixedColor) { // 补白 fixedColor = Color.WHITE; } final BufferedImage image = new BufferedImage(width, height, getTypeInt()); Graphics2D g = image.createGraphics(); // 设置背景 g.setBackground(fixedColor); g.clearRect(0, 0, width, height); // 在中间贴图 g.drawImage( srcImage, (width - srcWidth) / 2, (height - srcHeight) / 2, srcWidth, srcHeight, fixedColor, null); g.dispose(); this.targetImage = image; return this; } /** * 图像切割(按指定起点坐标和宽高切割) * * @param rectangle 矩形对象,表示矩形区域的x,y,width,height * @return this */ public Img cut(Rectangle rectangle) { final Image srcImage = getValidSrcImg(); fixRectangle(rectangle, srcImage.getWidth(null), srcImage.getHeight(null)); final ImageFilter cropFilter = new CropImageFilter(rectangle.x, rectangle.y, rectangle.width, rectangle.height); final Image image = Toolkit.getDefaultToolkit() .createImage(new FilteredImageSource(srcImage.getSource(), cropFilter)); this.targetImage = ImgUtil.toBufferedImage(image); return this; } /** * 图像切割为圆形(按指定起点坐标和半径切割),填充满整个图片(直径取长宽最小值) * * @param x 原图的x坐标起始位置 * @param y 原图的y坐标起始位置 * @return this * @since 4.1.15 */ public Img cut(int x, int y) { return cut(x, y, -1); } /** * 图像切割为圆形(按指定起点坐标和半径切割) * * @param x 原图的x坐标起始位置 * @param y 原图的y坐标起始位置 * @param radius 半径,小于0表示填充满整个图片(直径取长宽最小值) * @return this * @since 4.1.15 */ public Img cut(int x, int y, int radius) { final Image srcImage = getValidSrcImg(); final int width = srcImage.getWidth(null); final int height = srcImage.getHeight(null); // 计算直径 final int diameter = radius > 0 ? radius * 2 : Math.min(width, height); final BufferedImage targetImage = new BufferedImage(diameter, diameter, BufferedImage.TYPE_INT_ARGB); final Graphics2D g = targetImage.createGraphics(); g.setClip(new Ellipse2D.Double(0, 0, diameter, diameter)); if (this.positionBaseCentre) { x = x - width / 2 + diameter / 2; y = y - height / 2 + diameter / 2; } g.drawImage(srcImage, x, y, null); g.dispose(); this.targetImage = targetImage; return this; } /** * 图片圆角处理 * * @param arc 圆角弧度,0~1,为长宽占比 * @return this * @since 4.5.3 */ public Img round(double arc) { final Image srcImage = getValidSrcImg(); final int width = srcImage.getWidth(null); final int height = srcImage.getHeight(null); // 通过弧度占比计算弧度 arc = NumberUtil.mul(arc, Math.min(width, height)); final BufferedImage targetImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); final Graphics2D g2 = targetImage.createGraphics(); g2.setComposite(AlphaComposite.Src); // 抗锯齿 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.fill(new RoundRectangle2D.Double(0, 0, width, height, arc, arc)); g2.setComposite(AlphaComposite.SrcAtop); g2.drawImage(srcImage, 0, 0, null); g2.dispose(); this.targetImage = targetImage; return this; } /** * 彩色转为黑白 * * @return this */ public Img gray() { final ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); this.targetImage = op.filter(ImgUtil.toBufferedImage(getValidSrcImg()), null); return this; } /** * 彩色转为黑白二值化图片 * * @return this */ public Img binary() { this.targetImage = ImgUtil.copyImage(getValidSrcImg(), BufferedImage.TYPE_BYTE_BINARY); return this; } /** * 给图片添加文字水印
* 此方法并不关闭流 * * @param pressText 水印文字 * @param color 水印的字体颜色 * @param font {@link Font} 字体相关信息 * @param x 修正值。 默认在中间,偏移量相对于中间偏移 * @param y 修正值。 默认在中间,偏移量相对于中间偏移 * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 * @return 处理后的图像 */ public Img pressText(String pressText, Color color, Font font, int x, int y, float alpha) { final BufferedImage targetImage = ImgUtil.toBufferedImage(getValidSrcImg()); final Graphics2D g = targetImage.createGraphics(); if (null == font) { // 默认字体 font = new Font("Courier", Font.PLAIN, (int) (targetImage.getHeight() * 0.75)); } // 抗锯齿 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setColor(color); g.setFont(font); // 透明度 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); // 在指定坐标绘制水印文字 final FontMetrics metrics = g.getFontMetrics(font); final int textLength = metrics.stringWidth(pressText); final int textHeight = metrics.getAscent() - metrics.getLeading() - metrics.getDescent(); g.drawString( pressText, Math.abs(targetImage.getWidth() - textLength) / 2 + x, Math.abs(targetImage.getHeight() + textHeight) / 2 + y); g.dispose(); this.targetImage = targetImage; return this; } /** * 给图片添加图片水印 * * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件 * @param x 修正值。 默认在中间,偏移量相对于中间偏移 * @param y 修正值。 默认在中间,偏移量相对于中间偏移 * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 * @return this */ public Img pressImage(Image pressImg, int x, int y, float alpha) { final int pressImgWidth = pressImg.getWidth(null); final int pressImgHeight = pressImg.getHeight(null); return pressImage(pressImg, new Rectangle(x, y, pressImgWidth, pressImgHeight), alpha); } /** * 给图片添加图片水印 * * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件 * @param rectangle 矩形对象,表示矩形区域的x,y,width,height,x,y从背景图片中心计算 * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 * @return this * @since 4.1.14 */ public Img pressImage(Image pressImg, Rectangle rectangle, float alpha) { final Image targetImg = getValidSrcImg(); fixRectangle(rectangle, targetImg.getWidth(null), targetImg.getHeight(null)); this.targetImage = draw(ImgUtil.toBufferedImage(targetImg), pressImg, rectangle, alpha); return this; } // ----------------------------------------------------------------------------------------------------------------- Write /** * 旋转图片为指定角度
* 来自:http://blog.51cto.com/cping1982/130066 * * @param degree 旋转角度 * @return 旋转后的图片 * @since 3.2.2 */ public Img rotate(int degree) { final Image image = getValidSrcImg(); int width = image.getWidth(null); int height = image.getHeight(null); final Rectangle rectangle = calcRotatedSize(width, height, degree); final BufferedImage targetImg = new BufferedImage(rectangle.width, rectangle.height, getTypeInt()); Graphics2D graphics2d = targetImg.createGraphics(); // 抗锯齿 graphics2d.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); // 从中心旋转 graphics2d.translate((rectangle.width - width) / 2D, (rectangle.height - height) / 2D); graphics2d.rotate(Math.toRadians(degree), width / 2D, height / 2D); graphics2d.drawImage(image, 0, 0, null); graphics2d.dispose(); this.targetImage = targetImg; return this; } /** * 水平翻转图像 * * @return this */ public Img flip() { final Image image = getValidSrcImg(); int width = image.getWidth(null); int height = image.getHeight(null); final BufferedImage targetImg = new BufferedImage(width, height, getTypeInt()); Graphics2D graphics2d = targetImg.createGraphics(); graphics2d.drawImage(image, 0, 0, width, height, width, 0, 0, height, null); graphics2d.dispose(); this.targetImage = targetImg; return this; } /** * 获取处理过的图片 * * @return 处理过的图片 */ public Image getImg() { return this.targetImage; } /** * 写出图像 * * @param out 写出到的目标流 * @return 是否成功写出,如果返回false表示未找到合适的Writer * @throws IORuntimeException IO异常 */ public boolean write(OutputStream out) throws IORuntimeException { return write(ImgUtil.getImageOutputStream(out)); } // ---------------------------------------------------------------------------------------------------------------- Private method start /** * 写出图像为PNG格式 * * @param targetImageStream 写出到的目标流 * @return 是否成功写出,如果返回false表示未找到合适的Writer * @throws IORuntimeException IO异常 */ public boolean write(ImageOutputStream targetImageStream) throws IORuntimeException { Assert.notBlank(this.targetImageType, "Target image type is blank !"); Assert.notNull(targetImageStream, "Target output stream is null !"); final Image targetImage = (null == this.targetImage) ? this.srcImage : this.targetImage; Assert.notNull(targetImage, "Target image is null !"); return ImgUtil.write(targetImage, this.targetImageType, targetImageStream, this.quality); } /** * 写出图像为目标文件扩展名对应的格式 * * @param targetFile 目标文件 * @return 是否成功写出,如果返回false表示未找到合适的Writer * @throws IORuntimeException IO异常 */ public boolean write(File targetFile) throws IORuntimeException { final String formatName = FileUtil.extName(targetFile); if (StrUtil.isNotBlank(formatName)) { this.targetImageType = formatName; } if (targetFile.exists()) { targetFile.delete(); } ImageOutputStream out = null; try { out = ImgUtil.getImageOutputStream(targetFile); return write(out); } finally { IoUtil.close(out); } } /** * 获取int类型的图片类型 * * @return 图片类型 * @see BufferedImage#TYPE_INT_ARGB * @see BufferedImage#TYPE_INT_RGB */ private int getTypeInt() { switch (this.targetImageType) { case ImgUtil.IMAGE_TYPE_PNG: return BufferedImage.TYPE_INT_ARGB; default: return BufferedImage.TYPE_INT_RGB; } } /** * 获取有效的源图片,首先检查上一次处理的结果图片,如无则使用用户传入的源图片 * * @return 有效的源图片 */ private Image getValidSrcImg() { return ObjectUtil.defaultIfNull(this.targetImage, this.srcImage); } /** * 修正矩形框位置,如果{@link Img#setPositionBaseCentre(boolean)} 设为{@code true},则坐标修正为基于图形中心,否则基于左上角 * * @param rectangle 矩形 * @param baseWidth 参考宽 * @param baseHeight 参考高 * @return 修正后的{@link Rectangle} * @since 4.1.15 */ private Rectangle fixRectangle(Rectangle rectangle, int baseWidth, int baseHeight) { if (this.positionBaseCentre) { // 修正图片位置从背景的中心计算 rectangle.setLocation( // rectangle.x + (Math.abs(baseWidth - rectangle.width) / 2), // rectangle.y + (Math.abs(baseHeight - rectangle.height) / 2) // ); } return rectangle; } // ---------------------------------------------------------------------------------------------------------------- Private method end }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy