import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * @Author: zengms
 * @Description:
 * @Date: 2021-11-01
 */
@Data
public class ImageDto {

    @ApiModelProperty(value = "图片的长")
    private Integer width;

    @ApiModelProperty(value = "图片的高")
    private Integer height;

    @ApiModelProperty(value = "图片信息")
    private String imageBase64;
}
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * @Author: zengms
 * @Description:
 * @Date: 2021-11-01
 */
@Data
public class ImageVerifyDto {

    @ApiModelProperty(value = "大图信息")
    private ImageDto bigImage;

    @ApiModelProperty(value = "小图信息")
    private ImageDto smallImage;

    @ApiModelProperty(value = "小图y坐标")
    private Integer yHeight;

    @ApiModelProperty(value = "小图x坐标")
    private Integer xWidth;
}
package org.code.modules.system.util;

import lombok.extern.slf4j.Slf4j;
import org.code.modules.system.model.ImageDto;
import org.code.modules.system.model.ImageVerifyDto;
import org.springframework.util.Base64Utils;
import sun.misc.BASE64Encoder;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Random;

/**
 * @Author: zengms
 * @Description:滑块拼图验证码
 * @Date: 2021-10-28
 */
@Slf4j
public class SlipImageUtil {

    private static int targetWidth = 180;//小图长(根据原图的大小设置滑动图片的长度)
    private static int targetHeight = 160;//小图宽(根据原图的大小设置滑动图片的高度)
    private static int circleR = 25;//半径(根据原图的大小设置滑半径的大小)
    private static int r1 = 20;//距离点(根据原图的大小设置距离点的大小)
    /**
     * base64 图片前缀
     */
    private static final String BASE64_PNG = "data:image/png;base64,";
    private static final String BASE64_JPG = "data:image/jpg;base64,";

    /**
     * 图片格式
     */
    private static final String IMG_FORMAT_PNG = "png";
    private static final String IMG_FORMAT_JPG = "jpg";

    /**
     * @Description: 生成小图轮廓
     */
    private static int[][] getBlockData() {
        int[][] data = new int[targetWidth][targetHeight];
        double x2 = targetWidth -circleR; //47

        //随机生成圆的位置
        double h1 = circleR + Math.random() * (targetWidth-3*circleR-r1);
        double po = Math.pow(circleR,2); //64

        double xbegin = targetWidth - circleR - r1;
        double ybegin = targetHeight- circleR - r1;

        //圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆
        //计算需要的小图轮廓,用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色
        for (int i = 0; i < targetWidth; i++) {
            for (int j = 0; j < targetHeight; j++) {
                double d2 = Math.pow(j - 2,2) + Math.pow(i - h1,2);
                double d3 = Math.pow(i - x2,2) + Math.pow(j - h1,2);
                if ((j <= ybegin && d2 < po)||(i >= xbegin && d3 > po)) {
                    data[i][j] = 0;
                }  else {
                    data[i][j] = 1;
                }
            }
        }
        return data;
    }

    /**
     *
     * @Description: 有这个轮廓后就可以依据这个二维数组的值来判定抠图并在原图上抠图位置处加阴影,
     * @param oriImage  原图
     * @param targetImage  抠图拼图
     * @param templateImage 颜色
     * @param x
     * @param y
     * @throws
     */
    private static void cutByTemplate(BufferedImage oriImage, BufferedImage targetImage, int[][] templateImage, int x, int y){
        int[][] martrix = new int[3][3];
        int[] values = new int[9];
        //创建shape区域
        for (int i = 0; i < targetWidth; i++) {
            for (int j = 0; j < targetHeight; j++) {
                int rgb = templateImage[i][j];
                // 原图中对应位置变色处理
                int rgb_ori = oriImage.getRGB(x + i, y + j);

                if (rgb == 1) {
                    targetImage.setRGB(i, j, rgb_ori);

                    //抠图区域高斯模糊
                    readPixel(oriImage, x + i, y + j, values);
                    fillMatrix(martrix, values);
                    oriImage.setRGB(x + i, y + j, avgMatrix(martrix));
                }else{
                    //这里把背景设为透明
                    targetImage.setRGB(i, j, rgb_ori & 0x00ffffff);
                }
            }
        }
    }


    private static void readPixel(BufferedImage img, int x, int y, int[] pixels) {
        int xStart = x - 1;
        int yStart = y - 1;
        int current = 0;
        for (int i = xStart; i < 3 + xStart; i++)
            for (int j = yStart; j < 3 + yStart; j++) {
                int tx = i;
                if (tx < 0) {
                    tx = -tx;

                } else if (tx >= img.getWidth()) {
                    tx = x;
                }
                int ty = j;
                if (ty < 0) {
                    ty = -ty;
                } else if (ty >= img.getHeight()) {
                    ty = y;
                }
                pixels[current++] = img.getRGB(tx, ty);

            }
    }

    private static void fillMatrix(int[][] matrix, int[] values) {
        int filled = 0;
        for (int i = 0; i < matrix.length; i++) {
            int[] x = matrix[i];
            for (int j = 0; j < x.length; j++) {
                x[j] = values[filled++];
            }
        }
    }

    private static int avgMatrix(int[][] matrix) {
        int r = 0;
        int g = 0;
        int b = 0;
        for (int i = 0; i < matrix.length; i++) {
            int[] x = matrix[i];
            for (int j = 0; j < x.length; j++) {
                if (j == 1) {
                    continue;
                }
                Color c = new Color(x[j]);
                r += c.getRed();
                g += c.getGreen();
                b += c.getBlue();
            }
        }
        return new Color(r / 8, g / 8, b / 8).getRGB();
    }

    /**
     * 读取本地图片,生成拼图验证码
     * 返回生成的抠图和带抠图阴影的大图 base64码及抠图坐标
     */
    public static ImageVerifyDto createImage(File file){

        ImageVerifyDto returnDto = new ImageVerifyDto();
        ImageDto bigImage = new ImageDto();
        ImageDto smallImage = new ImageDto();
        try {
            BufferedImage bigBufferedImage = ImageIO.read(file);
            Random random = new Random();
            //X轴距离右端targetWidth  Y轴距离底部targetHeight以上
            int widthRandom = random.nextInt(bigBufferedImage.getWidth()-  2*targetWidth) + targetWidth;
            int heightRandom = random.nextInt(bigBufferedImage.getHeight()- targetHeight);
            log.info("原图大小{} x {},随机生成的坐标 X,Y 为({},{})",bigBufferedImage.getWidth(),bigBufferedImage.getHeight(),widthRandom,heightRandom);

            BufferedImage smallBufferedImage= new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_4BYTE_ABGR);
            cutByTemplate(bigBufferedImage,smallBufferedImage,getBlockData(),widthRandom,heightRandom);

            bigImage.setWidth(bigBufferedImage.getWidth());//大图的宽
            bigImage.setHeight(bigBufferedImage.getHeight());//大图的高
            bigImage.setImageBase64(BASE64_JPG+ getImageToBase64(bigBufferedImage,IMG_FORMAT_JPG));//大图
            returnDto.setBigImage(bigImage);

            smallImage.setWidth(smallBufferedImage.getWidth());//小图的宽
            smallImage.setHeight(smallBufferedImage.getHeight());//小图的高
            smallImage.setImageBase64(BASE64_PNG+ getImageToBase64(smallBufferedImage,IMG_FORMAT_PNG));//小图,小图要转PNG背景色才不会变黑
            returnDto.setSmallImage(smallImage);

            returnDto.setXWidth(widthRandom);//小图x坐标
            returnDto.setYHeight(heightRandom);//小图y坐标

        } catch (Exception e) {
            log.error("创建图形验证码异常",e);
        } finally{
            return returnDto;
        }
    }


    /**
     * 读取网络图片,生成拼图验证码
     * 返回生成的抠图和带抠图阴影的大图 base64码及抠图坐标
     */
    public static ImageVerifyDto createImage(String imgUrl){

        ImageVerifyDto returnDto = new ImageVerifyDto();
        ImageDto bigImage = new ImageDto();
        ImageDto smallImage = new ImageDto();

        try {
            //通过URL 读取图片
            URL url = new URL(imgUrl);
            BufferedImage bigBufferedImage = ImageIO.read(url.openStream());
            Random rand = new Random();
            int widthRandom = rand.nextInt(bigBufferedImage.getWidth()-  targetWidth - 100 + 1 ) + 100;
            int heightRandom = rand.nextInt(bigBufferedImage.getHeight()- targetHeight + 1 );
            log.info("原图大小{} x {},随机生成的坐标 X,Y 为({},{})",bigBufferedImage.getWidth(),bigBufferedImage.getHeight(),widthRandom,heightRandom);

            BufferedImage smallBufferedImage= new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_4BYTE_ABGR);
            cutByTemplate(bigBufferedImage,smallBufferedImage,getBlockData(),widthRandom,heightRandom);

            bigImage.setWidth(bigBufferedImage.getWidth());//大图的宽
            bigImage.setHeight(bigBufferedImage.getHeight());//大图的高
            bigImage.setImageBase64(BASE64_JPG+ getImageToBase64(bigBufferedImage,IMG_FORMAT_JPG));//大图
            returnDto.setBigImage(bigImage);

            smallImage.setWidth(smallBufferedImage.getWidth());//小图的宽
            smallImage.setHeight(smallBufferedImage.getHeight());//小图的高
            smallImage.setImageBase64(BASE64_PNG+ getImageToBase64(smallBufferedImage,IMG_FORMAT_PNG));//小图,小图要转PNG背景色才不会变黑
            returnDto.setSmallImage(smallImage);

            returnDto.setXWidth(widthRandom);//小图x坐标
            returnDto.setYHeight(heightRandom);//小图y坐标

        } catch (Exception e) {
            log.error("创建图形验证码异常",e);
        } finally{
            return returnDto;
        }
    }


    /**
     * @Description: 图片转BASE64
     * @param image
     * @return
     * @throws IOException String
     */
    public static String getImageBASE64(BufferedImage image) throws IOException {
        byte[] imagedata = null;
        ByteArrayOutputStream bao=new ByteArrayOutputStream();
        ImageIO.write(image,IMG_FORMAT_PNG,bao);
        imagedata=bao.toByteArray();
        BASE64Encoder encoder = new BASE64Encoder();
        String BASE64IMAGE=encoder.encodeBuffer(imagedata).trim();
        BASE64IMAGE = BASE64IMAGE.replaceAll("\r|\n", "");  //删除 \r\n
        return BASE64IMAGE;
    }

    /**
     * @Description: 图片转BASE64
     * @param image
     * @return
     * @throws IOException String
     */
    public static String getImageToBase64(BufferedImage image, String imageType) throws IOException {
        ByteArrayOutputStream byteBigStream = new ByteArrayOutputStream();
        //写入流中
        ImageIO.write(image, imageType, byteBigStream);
        byte[] bytes_big = byteBigStream.toByteArray();
        return Base64Utils.encodeToString(bytes_big);
    }

    public static void main(String[] args) {

        ImageVerifyDto dto = SlipImageUtil.createImage("http://xxxx/1.jpg");//图片地址,原图大小800*500
        System.out.println("bigImage:\n"+dto.getBigImage().getImageBase64());
        System.out.println("smallImage:\n"+dto.getSmallImage().getImageBase64());
    }

}
@ApiOperation(value = "获取图形验证码")
    @RequestMapping(value = "/getImageVerify/{checkKey}", method = RequestMethod.GET)
    @ResponseBody
    public Result<ImageVerifyDto> getImageVerifyCode(@PathVariable String checkKey) {

        String imageUrl = "http://xxxx/1.jpg";
        //读取网络图片
        ImageVerifyDto imageVerifyDto = SlipImageUtil.createImage(imageUrl);
        //验证码2分钟内有效
        redisUtil.set("imageCode:" + checkKey, imageVerifyDto.getXWidth(), 120);
        imageVerifyDto.setXWidth(null);

        return Result.ok(imageVerifyDto);
    }
//moveLength:拖动的距离
    @ApiOperation(value = "验证图形验证码")
    @RequestMapping(value = "/checkImageVerifyCode", method = RequestMethod.GET)
    @ResponseBody
    public Result<?> checkImageVerifyCode(@RequestParam(value = "moveLength", required = true) Double moveLength,
                                          @RequestParam(value = "checkKey", required = true) String checkKey) {

        if (null == moveLength) {
            return Result.error("验证失败");
        }
        Object obj = redisUtil.get("imageCode:" + checkKey);
        if(null == obj){
            return Result.error("验证过期,请重试");
        }
        Integer xWidth = (Integer) obj;
        if (Math.abs(xWidth - moveLength) > 10) {
            return Result.error("验证不通过");
        } else {
            redisUtil.del("imageCode:" + checkKey);
            return Result.ok("验证通过");
        }
    }