首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实现简单的分片上传和图片处理,解决了大图片上传和显示问题

实现简单的分片上传和图片处理,解决了大图片上传和显示问题

作者头像
品茗IT
发布2021-01-29 10:08:38
2.4K0
发布2021-01-29 10:08:38
举报
文章被收录于专栏:品茗IT品茗IT

实现简单的分片上传和图片处理,解决了大图片上传和显示问题

一、概述

当图片较小时,上传时间很快,而且可以直接显示原像素。

如果我们的图片达到几兆时,我就不说几个G了,我是为了模拟分片上传,并顺便解决我的垃圾服务器的上传速度慢问题。

而且,图片较大时,如果直接显示在前端,会因为文件过大加载很长时间,这就需要对图片进行处理。

如果大家正在寻找一个java的学习环境,或者在开发中遇到困难,可以加入我们的java学习圈,点击即可加入,共同学习,节约学习时间,减少很多在学习中遇到的难题。

二、分片上传

本次分块上传的主要思路是:

  1. 前端发起分片上传请求到后端,后端处理生成唯一标识,返回前端
  2. 前端切割文件,并发起上传动作,后端根据表中bitMap判断是否上传,并处理上传。
  3. 每次执行完前端进度和后端返回进度只要大于99.9%就算完成,调用后端的结束接口,完成上传并校验。
2.1 表与实体

我用一张表来存储上传的图片记录,并配合实现分片上传,也可以用配置文件这种形式。这里只展示表列和字段的对应,不单独列出表:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "f_resources")
public class ResourceInfo {
	@Column(name = "file_path")
	private String filePath;

	@Column(name = "resource_url")
	private String resourceUrl;

	@Id
	@Column(name = "resource_id")
	private Long resourceId;

	@Column(name = "create_time")
	private Date createTime;

	@Column(name = "is_slice")
	private Integer isSlice;

	@Column(name = "slice_num")
	private Integer sliceNum;

	@Column(name = "bit_map")
	private Integer bitMap;
}
  • file_path是文件地址,resource_url是对外地址,resource_id唯一标识文件。
  • is_slice标明是否分块上传,slice_num是分块总数,bit_map是数字(需要转换成二进制)来表示上传进度。
2.2 服务端处理分块上传
2.2.1 开始上传接口

该接口的动作是前端发起分片上传请求到后端,后端处理生成唯一标识,返回前端。

/**
 * 分片上传
 * 
 * @param file
 * @param principal
 * @return
 */
@RequestMapping(value = "/slicePictureStart", method = { RequestMethod.POST })
public ResultModel uploadSlicePicture(SilceFileReq silceFileReq, Principal principal) {
	try {
		if (silceFileReq.getChunkTotalNum() < 1) {
			return new ResultModel(ResultCode.CODE_00003);
		}
		SilceFileRes silceFileRes = resourceService.startSlicePicture(silceFileReq.getFileName(),
				silceFileReq.getChunkTotalNum());
		silceFileRes.setSliceProcess(0f);
		return ResultModel.ok(silceFileRes);
	} catch (Exception e) {
		e.printStackTrace();
		return new ResultModel(ResultCode.CODE_00004);
	}
}

这里,必须要把分片总数(chunkTotalNum)、文件名(fileName)提交,调用resourceService.startSlicePicture进行预处理,并返回对象silceFileRes.

resourceService.startSlicePicture:

/**
 * 开始分片上传
 * 
 * @param file
 * @return
 * @throws Exception
 */
public SilceFileRes startSlicePicture(String fileName, Integer chunckNum) throws Exception {
	SilceFileRes silceFileRes = new SilceFileRes();
	String datePath = DateUtil.format(new Date(), DateUtil.SimpleDatePattern);
	String uploadPath = resourcesDir + resourcesRelative + SLICE_PATH + datePath + "/";
	Long resourceId = resourceIdGenerator.nextId();
	silceFileRes.setFileId(resourceId);
	String newfileName = resourceId + fileName.substring(fileName.lastIndexOf('.'));
	String filePath = uploadPath + newfileName;
	String webPath = resourcesSlice + datePath + "/" + newfileName;
	silceFileRes.setWebUrl(webPath);
	File dir = new File(uploadPath);
	if (!dir.exists()) {
		dir.mkdirs();
	}
	dir.setReadable(true, false);
	dir.setExecutable(true, false);
	dir.setWritable(true, false);
	new File(filePath).createNewFile();

	ResourceInfo resourceInfo = new ResourceInfo();
	resourceInfo.setResourceId(resourceId);
	resourceInfo.setFilePath(filePath);
	resourceInfo.setResourceUrl(webPath);
	resourceInfo.setCreateTime(new Date());
	resourceInfo.setIsSlice(DbConstant.DB_SLICE_FILE);
	resourceInfo.setSliceNum(chunckNum);
	resourceInfo.setBitMap(0);
	resourceInfoDao.save(resourceInfo);
	return silceFileRes;
}

在startSlicePicture这一步,生成了唯一标识resourceId(也就是fileId),入库并生成文件。

2.2.2 分片上传接口
/**
 * 分片上传
 * 
 * @param file
 * @param principal
 * @return
 */
@RequestMapping(value = "/uploadSlicePicture", method = { RequestMethod.POST })
public ResultModel uploadSlicePicture(SilceFileReq silceFileReq, Principal principal) {
	try {
		if (silceFileReq.getFileId() == null || silceFileReq.getFile() == null) {
			return new ResultModel(ResultCode.CODE_00003);
		}
		ResourceInfo resourceInfo = resourceService.findById(silceFileReq.getFileId());
		if (resourceInfo == null) {
			return new ResultModel(ResultCode.CODE_00011);
		}
		if (resourceInfo.getSliceNum() != silceFileReq.getChunkTotalNum()
				|| silceFileReq.getChunkTotalNum() < silceFileReq.getChunkNum()) {
			return new ResultModel(ResultCode.CODE_00017);
		}
		// 将数据库的bitMap转换成2进制计算进度
		Integer bitMap = resourceInfo.getBitMap();
		String totalBinaryNum = Integer.toBinaryString(bitMap);
		char[] bitArray = new char[resourceInfo.getSliceNum()];
		for (int i = 0; i < totalBinaryNum.length() && i < resourceInfo.getSliceNum(); i++) {
			bitArray[i] = totalBinaryNum.charAt(totalBinaryNum.length() - i - 1);
		}
		if (totalBinaryNum.length() < resourceInfo.getSliceNum()) {
			for (int i = totalBinaryNum.length(); i < resourceInfo.getSliceNum(); i++) {
				bitArray[i] = '0';
			}
		}
		SilceFileRes silceFileRes = new SilceFileRes();
		int count = 0;
		for (int i = 0; i < resourceInfo.getSliceNum(); i++) {
			char charZeroOrOne = bitArray[i];
			if (charZeroOrOne == '1') {
				count++;
			}
		}
		// 如果bitmap标识全部上传完了,直接返回100%
		if (count >= resourceInfo.getSliceNum()) {
			silceFileRes.setFileId(silceFileReq.getFileId());
			silceFileRes.setSliceProcess(100f);
			return ResultModel.ok(silceFileRes);
		}
		Integer chunkNum = silceFileReq.getChunkNum();
		// 如果该分片还没上传
		if (bitArray[chunkNum - 1] == '0') {
			// 使用RandomAccessFile定位到文件起始位置并写入
			RandomAccessFile accessTmpFile = new RandomAccessFile(resourceInfo.getFilePath(), "rw");
			accessTmpFile.seek((silceFileReq.getChunkNum() - 1) * (500 * 1024));
			accessTmpFile.write(silceFileReq.getFile().getBytes());
			accessTmpFile.close();
			String bNum = "1";
			for (int i = 1; i < chunkNum; i++) {
				bNum += "0";
			}
			// 将当前块的bitmap拼接成10000这种形式,并转换成10进制,比如第4个分块,是1000,10进制就是8
			Integer cnum = Integer.parseInt(bNum, 2);
			// 写入完成,更新bitmap
			resourceService.updateBitMap(resourceInfo.getResourceId(), cnum);
		}
		count++;
		silceFileRes.setFileId(silceFileReq.getFileId());
		silceFileRes.setSliceProcess((float) count * 100 / (float) resourceInfo.getSliceNum());
		return ResultModel.ok(silceFileRes);
	} catch (Exception e) {
		e.printStackTrace();
		return new ResultModel(ResultCode.CODE_00004);
	}
}

这里,bitmap的计算是这样的,

  • 假如总共5个分片,那上传完成应该是11111,十进制就是31,表里的bitmap字段就是31.
  • 假如总共5个分片,1、2、5已经上传完成,那bitmap就是10011。这时,第4片上传成功,第4片的二进制是1000,10进制是8,这时,更新数据库就是19 | 8 = 10011 | 1000 = 27,等第3片上传完成再加上 4 即可。

updateBitMap:

@Update({
	"<script>",
	" update f_resources set",
	"bit_map = bit_map | #{bitMap}",
	" where resource_id=#{resourceId}",
	"</script>"
})
int updateBitMap(@Param("resourceId") Long resourceId, @Param("bitMap") int bitMap);

因为多线程的存在,后端这个进度可能是不准确的,可以增加一次查询,使进度更准确点,也可以让前端去控制进度。

2.2.3 分片完成接口

完成接口,就是给一个进度的反馈(如果分片的文件并不是最终名称,可以在这个方法中将文件重命名)。

/**
 * 分片上传
 * 
 * @param file
 * @param principal
 * @return
 */
@RequestMapping(value = "/slicePictureEnd", method = { RequestMethod.POST })
public ResultModel uploadSlicePicture(SilceFileReq silceFileReq, Principal principal) {
	try {
		ResourceInfo resourceInfo = resourceService.findById(silceFileReq.getFileId());
		if (resourceInfo == null) {
			return new ResultModel(ResultCode.CODE_00011);
		}
		int complateProgress = (int) (Math.pow(2, resourceInfo.getSliceNum()) - 1);
		if (resourceInfo.getBitMap() == complateProgress) {
			String md5 = DigestUtils.md5DigestAsHex(new FileInputStream(resourceInfo.getFilePath()));
			SilceFileRes silceFileRes = new SilceFileRes();
			silceFileRes.setWebUrl(resourceInfo.getResourceUrl());
			silceFileRes.setFileId(silceFileReq.getFileId());
			silceFileRes.setSliceProcess(100f);
			silceFileRes.setMd5(md5);
			return ResultModel.ok(silceFileRes);
		}else {
			String totalBinaryNum = Integer.toBinaryString(resourceInfo.getBitMap());
			int count = 0;
			for (int i = 0; i < totalBinaryNum.length() && i < resourceInfo.getSliceNum(); i++) {
				if(totalBinaryNum.charAt(totalBinaryNum.length() - i - 1) == '1') {
					count ++;
				}
			}
			return new ResultModel(ResultCode.CODE_00011, (float) count * 100 / (float) resourceInfo.getSliceNum());
		}	
	} catch (Exception e) {
		e.printStackTrace();
		return new ResultModel(ResultCode.CODE_00004);
	}
}

这里,同时计算了个md5给前端。

2.2.4 汇总接口(可以不汇总)

我这里将三个接口汇总成一个接口,用type区分:

/**
 * 分片上传
 * 
 * @param file
 * @param principal
 * @return
 */
@RequestMapping(value = "/uploadSlicePicture", method = { RequestMethod.POST })
public ResultModel uploadSlicePicture(SilceFileReq silceFileReq, Principal principal) {
	try {
		if (silceFileReq.getType() == 0) {
			if (silceFileReq.getChunkTotalNum() < 1) {
				return new ResultModel(ResultCode.CODE_00003);
			}
			SilceFileRes silceFileRes = resourceService.startSlicePicture(silceFileReq.getFileName(),
					silceFileReq.getChunkTotalNum());
			silceFileRes.setSliceProcess(0f);
			return ResultModel.ok(silceFileRes);
		} else if (silceFileReq.getType() == 2) {
			if (silceFileReq.getChunkTotalNum() < 1) {
				return new ResultModel(ResultCode.CODE_00003);
			}
			ResourceInfo resourceInfo = resourceService.findById(silceFileReq.getFileId());
			if (resourceInfo == null) {
				return new ResultModel(ResultCode.CODE_00011);
			}
			int complateProgress = (int) (Math.pow(2, resourceInfo.getSliceNum()) - 1);
			if (resourceInfo.getBitMap() == complateProgress) {
				String md5 = DigestUtils.md5DigestAsHex(new FileInputStream(resourceInfo.getFilePath()));
				SilceFileRes silceFileRes = new SilceFileRes();
				silceFileRes.setWebUrl(resourceInfo.getResourceUrl());
				silceFileRes.setFileId(silceFileReq.getFileId());
				silceFileRes.setSliceProcess(100f);
				silceFileRes.setMd5(md5);
				return ResultModel.ok(silceFileRes);
			}else {
				String totalBinaryNum = Integer.toBinaryString(resourceInfo.getBitMap());
				int count = 0;
				for (int i = 0; i < totalBinaryNum.length() && i < resourceInfo.getSliceNum(); i++) {
					if(totalBinaryNum.charAt(totalBinaryNum.length() - i - 1) == '1') {
						count ++;
					}
				}
				return new ResultModel(ResultCode.CODE_00011, (float) count * 100 / (float) resourceInfo.getSliceNum());
			}
		} else {
			if (silceFileReq.getFileId() == null || silceFileReq.getFile() == null) {
				return new ResultModel(ResultCode.CODE_00003);
			}
			ResourceInfo resourceInfo = resourceService.findById(silceFileReq.getFileId());
			if (resourceInfo == null) {
				return new ResultModel(ResultCode.CODE_00011);
			}
			if (resourceInfo.getSliceNum() != silceFileReq.getChunkTotalNum()
					|| silceFileReq.getChunkTotalNum() < silceFileReq.getChunkNum()) {
				return new ResultModel(ResultCode.CODE_00017);
			}
			Integer bitMap = resourceInfo.getBitMap();
			String totalBinaryNum = Integer.toBinaryString(bitMap);
			char[] bitArray = new char[resourceInfo.getSliceNum()];
			for (int i = 0; i < totalBinaryNum.length() && i < resourceInfo.getSliceNum(); i++) {
				bitArray[i] = totalBinaryNum.charAt(totalBinaryNum.length() - i - 1);
			}
			if (totalBinaryNum.length() < resourceInfo.getSliceNum()) {
				for (int i = totalBinaryNum.length(); i < resourceInfo.getSliceNum(); i++) {
					bitArray[i] = '0';
				}
			}
			SilceFileRes silceFileRes = new SilceFileRes();
			int count = 0;
			for (int i = 0; i < resourceInfo.getSliceNum(); i++) {
				char charZeroOrOne = bitArray[i];
				if (charZeroOrOne == '1') {
					count++;
				}
			}
			if (count >= resourceInfo.getSliceNum()) {
				silceFileRes.setFileId(silceFileReq.getFileId());
				silceFileRes.setSliceProcess(100f);
				return ResultModel.ok(silceFileRes);
			}
			Integer chunkNum = silceFileReq.getChunkNum();
			if (bitArray[chunkNum - 1] == '0') {
				RandomAccessFile accessTmpFile = new RandomAccessFile(resourceInfo.getFilePath(), "rw");
				accessTmpFile.seek((silceFileReq.getChunkNum() - 1) * (500 * 1024));
				accessTmpFile.write(silceFileReq.getFile().getBytes());
				accessTmpFile.close();
				String bNum = "1";
				for (int i = 1; i < chunkNum; i++) {
					bNum += "0";
				}
				Integer cnum = Integer.parseInt(bNum, 2);
				resourceService.updateBitMap(resourceInfo.getResourceId(), cnum);
			}
			count++;
			silceFileRes.setFileId(silceFileReq.getFileId());
			silceFileRes.setSliceProcess((float) count * 100 / (float) resourceInfo.getSliceNum());
			return ResultModel.ok(silceFileRes);
		}
	} catch (Exception e) {
		e.printStackTrace();
		return new ResultModel(ResultCode.CODE_00004);
	}
}
2.3 前端处理分块上传

前端使用后端提供的汇总接口user/uploadSlicePicture

function uploadLargeFile(file, fn){
	var chunkSize = 500 * 1024;
	var totalNum = Math.ceil(file.size / chunkSize);
	var fileName = file.name;
	var formData = new FormData();
	formData.append('chunkNum', 0);
	formData.append('chunkTotalNum', totalNum);
	formData.append('type', 0);
	formData.append('fileName', fileName);
	$.ajax({
		type : "post",
		url : "user/uploadSlicePicture",
		data: formData,
		contentType: false,
		processData: false,
		dataType : "json",
		beforeSend: function(){
			if($("#modalUploadProcess")){
				$("#modalUploadProcess").modal('open');
			}
		},
		success : function(data) {
			if (data.errorCode == "00000") {
				var process = 0;
				var percentProcess = (1 / totalNum) * 100;
				$("#processBarProcess").width("1%");
				var fileId = data.data.fileId;
				for(var i =0;i<totalNum;i++){
					var chunckFormData = new FormData();
					chunckFormData.append('fileId', fileId); 
					chunckFormData.append('chunkNum', i + 1); 
					chunckFormData.append('chunkTotalNum', totalNum);
					chunckFormData.append('type', 1);
					chunckFormData.append('fileName', fileName);
					chunckFormData.append('file', file.slice(i*chunkSize,(i+1) * chunkSize));
					$.ajax({
						type : "post",
						url : "user/uploadSlicePicture",
						data: chunckFormData,
						contentType: false,
						processData: false,
						dataType : "json",
						success : function(cdata) {
							if (cdata.errorCode == "00000") {
								process += percentProcess;
								// 进度条更新进度
								$("#processBarProcess").width(process+"%");
								// 上传结束同步结果
								if(cdata.data.sliceProcess > 99.9 || process > 99.9){
									sliceUploadEnd(fileId, totalNum, fileName, fn);
								}
							}
						},
						error : function(XMLHttpRequest, textStatus, errorThrown) {
							$("#progressMessage").html("图片上传失败" + data.message);
							return;
						}
					});
				}
				
			}else{
				if($("#modalUploadProcess")){
					$("#modalUploadProcess").modal('close');
				}
				$("#modalAlertMessage").modal("open");
				$("#alertMessage").html("图片上传失败" + data.message);
			}
		},
		error : function(XMLHttpRequest, textStatus, errorThrown) {
			if($("#modalUploadProcess")){
				$("#modalUploadProcess").modal('close');
			}
			$("#modalAlertMessage").modal("open");
			$("#alertMessage").html("图片上传失败" + data.message);
		}
	});
}

这里,默认分块是500K,先请求接口分片开始,然后循环调用分片上传,当前端进度或者后端进度返回大于99.9,调用sliceUploadEnd()方法请求后端分片结束接口。

function sliceUploadEnd(fileId, totalNum, fileName, fn){
	var endFormData = new FormData();
	endFormData.append('fileId', fileId); 
	endFormData.append('chunkNum', totalNum); 
	endFormData.append('chunkTotalNum', totalNum);
	endFormData.append('type', 2);
	endFormData.append('fileName', fileName);
	$.ajax({
        type : "post",
        url : apiUrl + "user/uploadSlicePicture",
        data: endFormData,
        contentType: false,
        processData: false,
        dataType : "json",
        success : function(edata) {
        	var spark = new SparkMD5(); 
        	var reader = new FileReader();
        	reader.onload = function(e) { 
        		spark.appendBinary(e.target.result);
        		var fileMd5 = spark.end();
	        	console.log(edata.data.md5);
	        	console.log(fileMd5);
	        	if (cdata.errorCode == "00000") {
	        		if(edata.data.md5 == fileMd5){
	    	        	if(typeof fn == "function"){
		                    //调用它,既然我们已经确定了它是可调用的
	    	        		$("#progressMessage").html("上传完成!");
		                      fn(edata.data.webUrl);
		                      return;
		                }
	        		}else{
	        			$("#progressMessage").html("上传进度" + edata.data.sliceProcess + ",上传未完成");
	        			return;
	        		}
	        	}else{
        			$("#progressMessage").html("图片上传失败!");
        			return;
	        	}
        	}
        	reader.readAsBinaryString(file);
        },
        error : function(XMLHttpRequest, textStatus, errorThrown) {
			$("#progressMessage").html("图片上传失败" + data.message);
			return;
        }
	});
}

这里,使用SparkMD5进行前端的文件md5的计算。并和后端文件进行对比。

2.4 Html分块进度
<div id="modalUploadPicture" class="modal">
  <div class="modal-content">
    <h4>上传图片</h4>
	    <div class="file-field input-field">
	      <div class="btn" style="color: white;">
	        <span>图片</span>
	        <input type="file" id="uploadPictureFile">
	      </div>
	      <div class="file-path-wrapper">
	        <input class="file-path validate" type="text">
	      </div>
	    </div>
  </div>
  <div class="modal-footer">
    <a href="javascript:uploadPictureNote()" class=" modal-action modal-close waves-effect waves-green btn-flat">确定</a>
    <a href="javascript:void(0)" class=" modal-action modal-close waves-effect waves-green btn-flat">关闭</a>
  </div>
</div>
<div id="modalUploadProcess" class="modal">
  <div class="modal-content">
    <h4>文件上传进度</h4>
    <div class="progress">
	      <div class="determinate" id="processBarProcess" style="width: 0%"></div>
	</div>
	<div id="progressMessage" class="red-text">
	     
	</div>
  </div>
  <div class="modal-footer">
    <a href="javascript:void(0)" class=" modal-action modal-close waves-effect waves-green btn-flat">关闭</a>
  </div>
</div>

modalUploadPicture窗口弹出之后,是这样的:

在这里插入图片描述

点击上传,弹出modalUploadProcess窗口,是这样的:

在这里插入图片描述

分开上传完成有返回图片的地址,可以将图片显示在任意位置。

function uploadPictureNote(){
	var file = $('#uploadPictureFile')[0].files[0];
	uploadLargeFile(function(data){
		var url = staticUrl + data;
		var htmlContent = $("#contentDetail").html();
		htmlContent += `
			<img style="max-width:80%;max-height:90%;" src="${url}"/>
		`;
		$("#contentDetail").html(htmlContent);
	});
}

三、大图片压缩处理

因为图片比较大,直接显示流量不允许,可以将图片进行压缩。

nginx有一个模块叫ngx_http_image_filter_module,网上有很多配置方法,但是在我的机器上,这个模块是nginx默认加载的。

因为加载了配置文件/usr/share/nginx/modules/mod-http-image-filter.conf;文件里就一句话load_module "/usr/lib64/nginx/modules/ngx_http_image_filter_module.so";

3.1 压缩图片

在nginx的配置文件中,增加这几行即可。

location /largepic/ {
	alias /upload/slice/;
	index index.html;
	image_filter resize 300 400;
	image_filter_buffer 10M;
}

这是将图片按照300 * 400的像素显示出来,缓冲区大小为10M,如果设置太小,就会出现无法加载图片的情况,默认是1M.

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-01-25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 实现简单的分片上传和图片处理,解决了大图片上传和显示问题
    • 一、概述
    • 二、分片上传
      • 2.1 表与实体
        • 2.2 服务端处理分块上传
          • 2.2.1 开始上传接口
          • 2.2.2 分片上传接口
          • 2.2.3 分片完成接口
          • 2.2.4 汇总接口(可以不汇总)
        • 2.3 前端处理分块上传
          • 2.4 Html分块进度
          • 三、大图片压缩处理
            • 3.1 压缩图片
            相关产品与服务
            图片处理
            图片处理(Image Processing,IP)是由腾讯云数据万象提供的丰富的图片处理服务,广泛应用于腾讯内部各产品。支持对腾讯云对象存储 COS 或第三方源的图片进行处理,提供基础处理能力(图片裁剪、转格式、缩放、打水印等)、图片瘦身能力(Guetzli 压缩、AVIF 转码压缩)、盲水印版权保护能力,同时支持先进的图像 AI 功能(图像增强、图像标签、图像评分、图像修复、商品抠图等),满足多种业务场景下的图片处理需求。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档