[Bootstrap

网友投稿 191 2023-06-24


[Bootstrap

很久没有更新博客了,再不写点东西都烂了。

这次更新一个小内容,是两个插件的组合使用,实现头像上传功能。

业务需求:

头像上传功能,要对上传的文件进行剪切,且保证头像到服务器时必须是正方形的。

优化的显示样式,基础的样式实在太难看了。

上传的头像需要进行质量压缩跟大小裁剪,以减缓浏览器的压力。

成果预览:

使用到的技术插件

Jcrop:用于前端“裁剪”图片

bootstrap-fileinput:用于前端优化上传控件样式

ARTtemplate:js版的JSTL?反正就是一个腾讯的模板化插件,很好用,真心。

bootstrap-sco.modal.js:这个是bootstrap的一个模态插件

SpringMVC:使用框架自带的MultipartFile来获取文件(效率能够大大的提高)

Image:这个是java的内置类,用于处理图片很方便。

原理说明

首先是Jcrop这个前端JS插件,这个插件很好用,其实在各大网站中也很常见,先上图:

说说原理,实际上,Jcrop并没有在客户端帮我们把图片进行裁剪,只是收集了用户的“裁剪信息”,然后传到后端,最后的裁剪和压缩,还是要依靠服务器上的代码来进行。

我们可以看到这个插件在图片上显示出了8个控制点,让用户选择裁剪区域,当用户选择成功后,会自动的返回“裁剪信息”,所谓的裁剪信息,其实就是选框的左上角原点坐标,及裁剪框的宽度和高度,通过这四个值,在后端就可以进行裁剪工作了。

但是我们要注意,用户在上传图片的时候,长度宽度都是不规则的,当然我们可以用bootstap-fileinput这个插件去限制用户只能上传指定宽高的图片,但这就失去了我们“裁剪”的意义,而且用户的体验就非常差劲。然而jcrop所返回的坐标值及宽高,并不是基于所上传图片自身的像素,而是如图中所示,是外层DIV的宽高。举一个例子,上图我实际放入的个人照片宽度是852px,但是Jcrop的截取宽度是312px,这个312px并不是真正图片上的实际宽度,是经过缩放后的宽度,所以我们后端一定需要重新对这个312px进行一次还原,还原到照片实际比例的宽度。

好啦,原理就是这样子。接下来,就是上代码了。

HTML

这个就是一个ArtTemplate的模板代码,就写在

标签上方就行了,因为text/html这个类型,不会被识别,所以实际上用Chrome调试就可以看得到,前端用户是看不到这段代码的。

简单解释一下这个模板,这个模板是我最后放入模态窗口时用的模板,就是把这段代码,直接丢进模态弹出来的内容部分。因为是文件上传,自然需要套一个

"embed-responsive embed-responsive-16by9"这个类就是用来限制待编辑图片加载后的宽度大小,值得注意的是,我在其内种,加了一个

pre-scrollable这个类,会让加载的图片不会因为太大而“变形”,因为我外层通过embed-responsive-16by9限制死了图片的宽高,图片本身又加了img-responsive这个添加响应式属性的类,为了防止图片缩放,导致截图障碍,所以就给内层加上pre-scrollable,这个会给图片这一层div加上滚动条,如果图片高度太高,超过了外层框,则会出现滚动条,而这不会影响图片截取,同时又保证了模态窗口不会“太长”,导致体验糟糕(尤其在移动端)。

底下四个隐藏域相信大家看他们的name值也就知道个大概,这个就是用于存放Jcrop截取时所产生的原点坐标和截取宽高的值。

JS

$(document).ready(function () {

new PageInit().init();

});

function PageInit() {

var api = null;

var _this = this;

this.init = function () {

$("[name='upload']").on('click', this.portraitUpload)

};

this.portraitUpload = function () {

var model = $.scojs_modal({

title: '头像上传',

content: template('portraitUpload'),

onClose: refresh

}

);

model.show();

var fileUp = new FileUpload();

var portrait = $('#fileUpload');

var alert = $('#alert');

fileUp.portrait(portrait, '/file/portrait', _this.getExtraData);

portrait.on('change', _this.readURL);

portrait.on('fileuploaderror', function (event, data, msg) {

alert.removeClass('hidden').html(msg);

fileUp.fileinput('disable');

});

portrait.on('fileclear', function (event) {

alert.addClass('hidden').html();

});

portrait.on('fileloaded', function (event, file, previewId, index, reader) {

alert.addClass('hidden').html();

});

portrait.on('fileuploaded', function (event, data) {

if (!data.response.status) {

alert.html(data.response.message).removeClass('hidden');

}

})

};

this.readURL = function () {

var img = $('#cut-img');

var input = $('#fileUpload');

if (input[0].files && input[0].files[0]) {

var reader = new FileReader();

reader.readAsDataURL(input[0].files[0]);

reader.onload = function (e) {

img.removeAttr('src');

img.attr('src', e.target.result);

img.Jcrop({

setSelect: [20, 20, 200, 200],

handleSize: 10,

aspectRatio: 1,

onSelect: updateCords

}, function () {

api = this;

});

};

if (api != undefined) {

api.destroy();

}

}

function updateCords(obj) {

$("#x").val(obj.x);

$("#y").val(obj.y);

$("#w").val(obj.w);

$("#h").val(obj.h);

}

};

this.getExtraData = function () {

return {

sw: $('.jcrop-holder').css('width'),

sh: $('.jcrop-holder').css('height'),

x: $('#x').val(),

y: $('#y').val(),

w: $('#w').val(),

h: $('#h').val()

}

}

}

这个JS是上传页面的相关逻辑。会JS的人都看得懂它的意义,我就简单说一下几个事件的意义:

portrait.on('fileuploaderror', function (event, data, msg) {

alert.removeClass('hidden').html(msg);

fileUp.fileinput('disable');

});

这个事件,是用于bootstrap-fileinput插件在校验文件格式、文件大小等的时候,如果不符合我们的要求,则会对前面HTML代码中有一个

进行一些错误信息的显示操作。

portrait.on('fileclear', function (event) {

alert.addClass('hidden').html();

});

这部分代码,是当文件移除时,隐藏错误信息提示区,以及清空内容,当然这是符合我们的业务逻辑的。

portrait.on('fileloaded', function (event, file, previewId, index, reader) {

alert.addClass('hidden').html();

});

这部分代码是当选择文件时(此时还没进行文件校验),隐藏错误信息,清空错误内容,这么做是为了应对如果上一次文件校验时有错误,而重新选择文件时,肯定要清空上一次的错误信息,再显示本次的错误信息。

portrait.on('fileuploaded', function (event, data) {

if (!data.response.status) {

alert.html(data.response.message).removeClass('hidden');

}

})

这部分是当文件上传后,后端如果返回了错误信息,则需要进行相关的提示信息处理。

this.getExtraData = function () {

return {

sw: $('.jcrop-holder').css('width'),

sh: $('.jcrop-holder').css('height'),

x: $('#x').val(),

y: $('#y').val(),

w: $('#w').val(),

h: $('#h').val()

}

}

这部分代码是获取上传文件时,附带需要发往后端的参数,这里面可以看到,x、y自然是Jcrop截取时,选框的左上角原点坐标,w、h自然就是截取的宽高,但是刚才我说了,这个是经过缩放后的宽高,不是依据图片实际像素的宽高。而sw、sh代表的是scaleWidth、scaleHeight,就是缩放宽高的意思。这个.jcrop-holder的对象是当Jcrop插件启用后,加载的图片外层容器的对象,只需要获取这个对象的宽高,就是图片被压缩的宽高,但是因为我限制了图片的宽度和高度,宽度的比例是定死的(不是宽高定死,只是比例定死,bootstrap本身就是响应式框架,所以不能单纯的说宽高定死,宽高会随着使用终端的变化而变化),高度是根据宽度保持16:4,可是我又加了pre-scrollable这个类让图片过高时以滚动条的方式不破坏外层容器的高度,所以我们实际能拿来计算缩放比例的,是宽度,而不是高度,但是这里我一起传,万一以后有其他的使用场景,要以高度为准也说不定。

好了,然后我需要贴上bootstrap-fileinput插件的配置代码:

this.portrait = function (target, uploadUrl, data) {

target.fileinput({

language: 'zh', //设置语言

maxFileSize: 2048,//文件最大容量

uploadExtraData: data,//上传时除了文件以外的其他额外数据

showPreview: false,//隐藏预览

uploadAsync: true,//ajax同步

dropZoneEnabled: false,//是否显示拖拽区域

uploadUrl: uploadUrl, //上传的地址

allowedFileExtensions: ['jpg'],//接收的文件后缀

showUpload: true, //是否显示上传按钮

showCaption: true,//是否显示标题

browseClass: "btn btn-primary", //按钮样式

previewFileIcon: "",

ajaxSettings: {//这个是因为我使用了SpringSecurity框架,有csrf跨域提交防御,所需需要设置这个值

beforeSend: function (xhr) {

xhr.setRequestHeader(header, token);

}

}

});

}

这个代码有写了注释,我就不多解释了。关于Ajax同步,是因为我个人认为,上传文件这个还是做成同步比较好,等文件上传完成后,js代码才能继续执行下去。因为文件上传毕竟是一个耗时的工作,有的逻辑又确实需要当文件上传成功以后才执行,比如刷新页面,所以为了避免出现问题,还是做成同步的比较好。还有就是去掉预览,用过bootstrap-fileinput插件的都知道,这个插件的图片预览功能很强大,甚至可以单独依靠这个插件来制作相册管理。但是因为我们这次要结合Jcrop,所以要割掉这部分功能。

SpringMVC-Controller获取文件

@ResponseBody

@RequestMapping(value = "/portrait", method = {RequestMethod.POST})

public JsonResult upload(HttpServletRequest request) throws Exception {

Integer x = Integer.parseInt(MyStringTools.checkParameter(request.getParameter("x"), "图片截取异常:X!"));

Integer y = Integer.parseInt(MyStringTools.checkParameter(request.getParhttp://ameter("y"), "图片截取异常:Y!"));

Integer w = Integer.parseInt(MyStringTools.checkParameter(request.getParameter("w"), "图片截取异常:W!"));

Integer h = Integer.parseInt(MyStringTools.checkParameter(request.getParameter("h"), "图片截取异常:H!"));

String scaleWidthString = MyStringTools.checkParameter(request.getParameter("sw"), "图片截取异常:SW!");

int swIndex = scaleWidthString.indexOf("px");

Integer sw = Integer.parseInt(scaleWidthString.substring(0, swIndex));

String scaleHeightString = MyStringTools.checkParameter(request.getParameter("sh"), "图片截取异常:SH!");

int shIndex = scaleHeightString.indexOf("px");

Integer sh = Integer.parseInt(scaleHeightString.substring(0, shIndex));

//获取用户ID用于指向对应文件夹

SysUsers sysUsers = HttpTools.getSessionUser(request);

int userID = sysUsers.getUserId();

//获取文件路径

String filePath = FileTools.getPortraitPath(userID);

CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(

request.getSession().getServletContext());

String path;

//检查form中是否有enctype="multipart/form-data"

if (multipartResolver.isMultipart(request)) {

//将request变成多部分request

MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request;

//获取multiRequest 中所有的文件名

Iterator iterator = multiRequest.getFileNames();

while (iterator.hasNext()) {

//一次遍历所有文件

MultipartFile multipartFile = multiRequest.getFile(iterator.next().toString());

if (multipartFile != null) {

String[] allowSuffix = {".jpg",".JPG"};

if (!FileTools.checkSuffix(multipartFile.getOriginalFilename(), allowSuffix)) {

throw new BusinessException("文件后缀名不符合要求!");

}

path = filePath + FileTools.getPortraitFileName(multipartFile.getOriginalFilename());

//存入硬盘

multipartFile.transferTo(new File(path));

//图片截取

if (FileTools.imgCut(path, x, y, w, h, sw, sh)) {

CompressTools compressTools = new CompressTools();

if (compressTools.simpleCompress(new File(path))) {

return JsonResult.success(FileTools.filePathToSRC(path, FileTools.IMG));

} else {

return JsonResult.error("图片压缩失败!请重新上传!");

}

} else {

return JsonResult.error("图片截取失败!请重新上传!");

}

}

}

}

return JsonResult.error("图片获取失败!请重新上传!");

}

Image图片切割

/**

* 截图工具,根据截取的比例进行缩放裁剪

*

* @param path 图片路径

* @param zoomX 缩放后的X坐标

* @param zoomY 缩放后的Y坐标

* @param zoomW 缩放后的截取宽度

* @param zoomH 缩放后的截取高度

* @param scaleWidth 缩放后图片的宽度

* @param scaleHeight 缩放后的图片高度

* @return 是否成功

* @throws Exception 任何异常均抛出

*/

public static boolean imgCut(String path, int zoomX, int zoomY, int zoomW,

int zoomH, int scaleWidth, int scaleHeight) throws Exception {

Image img;

ImageFilter cropFilter;

BufferedImage bi = ImageIO.read(new File(path));

int fileWidth = bi.getWidth();

int fileHeight = bi.getHeight();

double scale = (double) fileWidth / (double) scaleWidth;

double realX = zoomX * scale;

double realY = zoomY * scale;

double realW = zoomW * scale;

double realH = zoomH * scale;

if (fileWidth >= realW && fileHeight >= realH) {

Image image = bi.getScaledInstance(fileWidth, fileHeight, Image.SCALE_DEFAULT);

cropFilter = new CropImageFilter((int) realX, (int) realY, (int) realW, (int) realH);

img = Toolkit.getDefaultToolkit().createImage(

new FilteredImageSource(image.getSource(), cropFilter));

BufferedImage bufferedImage = new BufferedImage((int) realW, (int) realH, BufferedImage.TYPE_INT_RGB);

Graphics g = bufferedImage.getGraphics();

g.drawImage(img, 0, 0, null);

g.dispose();

//输出文件

return ImageIO.write(bufferedImage, "JPEG", new File(path));

} else {

return true;

}

}

缩放比例scale一定要用double,并且宽高也要转换成double后再相除,否则会变成求模运算,这样会降低精度,别小看这里的精度下降,最终的截图效果根据图片的缩放程度,误差可是有可能被放大的很离谱的。

图片压缩

package com.magic.rent.tools;

/**

* 创建者: wu 创建时间: 2016/12/15

* 类说明: 缩略图类(通用) 本java类能将jpg、bmp、png、gif图片文件,进行等比或非等比的大小转换。 具体使用方法

* 更新记录:

*/

import com.magic.rent.exception.custom.BusinessException;

import com.sun.image.codec.jpeg.JPEGCodec;

import com.sun.image.codec.jpeg.JPEGImageEncoder;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;

import java.awt.*;

import java.awt.image.BufferedImage;

import java.io.File;

import java.io.FileOutputStream;

public class CompressTools {

private File file; // 文件对象

private String inputDir; // 输入图路径

private String outputDir; // 输出图路径

private String inputFileName; // 输入图文件名

private String outputFileName; // 输出图文件名

private int outputWidth = 100; // 默认输出图片宽

private int outputHeight = 100; // 默认输出图片高

private boolean proportion = true; // 是否等比缩放标记(默认为等比缩放)

private static Logger logger = LoggerFactory.getLogger(CompressTools.class);

public CompressTools() {

}

public CompressTools(boolean proportion) {

this.proportion = proportion;

}

/**

* 设置输入参数

*

* @param inputDir

* @param inputFileName

* @return

*/

public CompressTools setInputInfo(String inputDir, String inputFileName) {

this.inputDir = inputDir;

this.inputFileName = inputFileName;

return this;

}

/**

* 设置输出参数

*

* @param outputDir

* @param outputFileName

* @param outputHeight

* @param outputWidth

* @param proportion

* @return

*/

public CompressTools setOutputInfo(String outputDir, String outputFileName, int outputHeight, int outputWidth, boolean proportion) {

this.outputDir = outputDir;

this.outputFileName = outputFileName;

this.outputWidth = outputWidth;

this.outputHeight = outputHeight;

this.proportion = proportion;

return this;

}

// 图片处理

public boolean compress() throws Exception {

//获得源文件

file = new File(inputDir);

if (!file.exists()) {

throw new BusinessException("文件不存在!");

}

Image img = ImageIO.read(file);

// 判断图片格式是否正确

if (img.getWidth(null) == -1) {

System.out.println(" can't read,retry!" + "
");

return false;

} else {

int newWidth;

int newHeight;

// 判断是否是等比缩放

if (this.proportion) {

// 为等比缩放计算输出的图片宽度及高度

double rate1 = ((double) img.getWidth(null)) / (double) outputWidth + 0.1;

double rate2 = ((double) img.getHeight(null)) / (double) outputHeight + 0.1;

// 根据缩放比率大的进行缩放控制

double rate = rate1 > rate2 ? rate1 : rate2;

newWidth = (int) (((double) img.getWidth(null)) / rate);

newHeight = (int) (((double) img.getHeight(null)) / rate);

} else {

newWidth = outputWidth; // 输出的图片宽度

newHeight = outputHeight; // 输出的图片高度

}

long start = System.currentTimeMillis();

BufferedImage tag = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);

/*

* Image.SCALE_SMOOTH 的缩略算法 生成缩略图片的平滑度的

* 优先级比速度高 生成的图片质量比较好 但速度慢

*/

tag.getGraphics().drawImage(img.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH), 0, 0, null);

FileOutputStream out = new FileOutputStream(outputDir);

// JPEGImageEncoder可适用于其他图片类型的转换

JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);

encoder.encode(tag);

out.close();

long time = System.currentTimeMillis() - start;

logger.info("[输出路径]:" + outputDir + "\t[图片名称]:" + outputFileName + "\t[压缩前大小]:" + getPicSize() + "\t[耗时]:" + time + "毫秒");

return true;

}

}

/**

* 简单压缩方法,压缩后图片将直接覆盖源文件

*

* @param images

* @return

* @throws Exception

*/

public boolean simpleCompress(File images) throws Exception {

setInputInfo(images.getPath(), images.getName());

setOutputInfo(images.getPath(), images.getName(), 300, 300, true);

return compress();

}

/**

* 获取图片大小,单位KB

*

* @return

*/

private String getPicSize() {

return file.length() / 1024 + "KB";

}

public static void main(String[] args) throws Exception {

CompressTools compressTools = new CompressTools();

compressTools.setInputInfo("/Users/wu/Downloads/background.jpg", "background.jpg");

compressTools.setOutputInfo("/Users/wu/Downloads/background2.jpg", "background2.jpg", 633, 1920, false);

compressTools.compress();

}

}

我专门把图片压缩写成了一个类。

其中可以看到一些关于文件路径的方法,其实没有什么特别的,就是截取后缀获取路径之类的,我这边也贴出来吧,免得有些朋友看的云里雾里的。

package com.magic.rent.tools;

import com.magic.rent.exception.custom.BusinessException;

import javax.imageio.ImageIO;

import java.awt.*;

import java.awt.image.BufferedImage;

import java.awt.image.CropImageFilter;

import java.awt.image.FilteredImageSource;

import java.awt.image.ImageFilter;

import java.io.File;

import java.util.ArrayList;

/**

* 创建者: wu 创建时间: 2016/11/25

* 类说明:

* 更新记录:

*/

public class FileTools {

public static final int IMG = 1;

/**

* 获取项目根目录

*

* @return 根目录

*/

public static String getWebRootPath() {

return System.getProperty("web.root");

}

/**

* 获取头像目录,若不存在则直接创建一个

*

* @param userID 用户ID

* @return

*/

public static String getPortraitPath(int userID) {

String realPath = getWebRootPath() + "img/portrait/" + userID + "/";

File file = new File(realPath);

//判断文件夹是否存在,不存在则创建一个

if (!file.exists() || !file.isDirectory()) {

if (!file.mkdirs()) {

throw new BusinessException("创建头像文件夹失败!");

}

}

return realPath;

}

/**

* 重命名头像文件

*

* @param fileName 文件名

* @return

*/

public static String getPortraitFileName(String fileName) {

// 获取文件后缀

String suffix = getSuffix(fileName);

return "portrait" + suffix;

}

/**

* 判断文件后缀是否符合要求

*

* @param fileName 文件名

* @param allowSuffix 允许的后缀集合

* @return

* @throws Exception

*/

public static boolean checkSuffix(String fileName, String[] allowSuffix) throws Exception {

String fileExtension = getSuffix(fileName);

boolean flag = false;

for (String extension : allowSuffix) {

if (fileExtension.equals(extension)) {

flag = true;

}

}

return flag;

}

public static String getSuffix(String fileName) {

return fileName.substring(fileName.lastIndexOf(".")).toLowerCase();

}

/**

* 将文件地址转成链接地址

*

* @param filePath 文件路径

* @param fileType 文件类型

* @return

*/

public static String filePathToSRC(String filePath, int fileType) {

String href = "";

if (null != filePath && !filePath.equals("")) {

switch (fileType) {

case IMG:

if (filePath.contains("/img/")) {

int index = filePath.indexOf("/img/");

href = filePath.substring(index);

} else {

href = "";

}

return href;

}

}

return href;

}

/**

* 获取指定文件或文件路径下的所有文件清单

*

* @param fileOrPath 文件或文件路径

* @return

*/

public static ArrayList getListFiles(Object fileOrPath) {

File directory;

if (fileOrPath instanceof File) {

directory = (File) fileOrPath;

} else {

directory = new File(fileOrPath.toString());

}

ArrayList files = new ArrayList();

if (directory.isFile()) {

files.add(directory);

return files;

} else if (directory.isDirectory()) {

File[] fileArr = directory.listFiles();

if (null != fileArr && fileArr.length != 0) {

for (File fileOne : fileArr) {

files.addAll(getListFiles(fileOne));

}

}

}

return files;

}

/**

* 截图工具,根据截取的比例进行缩放裁剪

*

* @param path 图片路径

* @param zoomX 缩放后的X坐标

* @param zoomY 缩放后的Y坐标

* @param zoomW 缩放后的截取宽度

* @param zoomH 缩放后的截取高度

* @param scaleWidth 缩放后图片的宽度

* @param scaleHeight 缩放后的图片高度

* @return 是否成功

* @throws Exception 任何异常均抛出

*/

public static boolean imgCut(String path, int zoomX, int zoomY, int zoomW,

int zoomH, int scaleWidth, int scaleHeight) throws Exception {

Image img;

ImageFilter cropFilter;

BufferedImage bi = ImageIO.read(new File(path));

int fileWidth = bi.getWidth();

int fileHeight = bi.getHeight();

double scale = (double) fileWidth / (double) scaleWidth;

double realX = zoomX * scale;

double realY = zoomY * scale;

double realW = zoomW * scale;

double realH = zoomH * scale;

if (fileWidth >= realW && fileHeight >= realH) {

Image image = bi.getScaledInstance(fileWidth, fileHeight, Image.SCALE_DEFAULT);

cropFilter = new CropImageFilter((int) realX, (int) realY, (int) realW, (int) realH);

img = Toolkit.getDefaultToolkit().createImage(

new FilteredImageSource(image.getSource(), cropFilter));

BufferedImage bufferedImage = new BufferedImage((int) realW, (int) realH, BufferedImage.TYPE_INT_RGB);

Graphics g = bufferedImage.getGraphics();

g.drawImage(img, 0, 0, null);

g.dispose();

//输出文件

return ImageIO.write(bufferedImage, "JPEG", new File(path));

} else {

return true;

}

}

}

顺便一提:getWebRootPath这个方法,要生效,必须在Web.xml中做一个配置:

webAppRootKey

web.root

否则是无法动态获取项目的本地路径的。这个配置只要跟在Spring配置后面就行了,应该就不会有什么大碍,其实就是获取本地路径然后设置到系统参数当中。

好了,这就是整个插件的功能了。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:SpringMVC 文件上传配置,多文件上传,使用的MultipartFile的实例
下一篇:vue项目中做编辑功能传递数据时遇到问题的解决方法
相关文章

 发表评论

暂时没有评论,来抢沙发吧~