小程序制作证件照过程
利用canvas制作生活中常用的证件照,压缩图片,修改图片dpi。希望给大家带来方便。
证件照小程序制作要点
- 上传合适的图片,方便制作证件照
- 调用AI接口,将图像进行人像分割。这里我用的是百度AI
- 调用人体关键点为分析图片中头部和肩部的位置信息。为后满裁剪图片提供依据
- 利用canvas 将头部和肩部位置制作为新的证件照尺寸照片
- 改变图片的背景颜色,生成不同要求的背景证件照
- 导出图品前将图片修改为符合打印要求的dpi。
- 下载最终生成好的证件照
上传合适的图片,方便制作证件照
selectImg(selectid){
let _this = this
let typelist = selectid === 1 ? ['camera'] : ['album']
uni.chooseImage({
count: 1,
sourceType: typelist,
success: (res)=> {
}
});
}
调用AI接口,把图像进行人像分割,分析图像中头部肩部位置信息
该接口中要求上传的图片格式为base64 格式,大小不超过4M. 并且需要access_token
获取access_token 参照百度AI 的文档 https://ai.baidu.com/ai-doc/REFERENCE/Ck3dwjhhu
定义好请求地址。和请求的请求方法
const baseUrl = 'https://picapp.gxwj123.top/prod-api/'
const baidubce = 'https://aip.baidubce.com/rest/2.0/image-classify/v1/'
export const tokenUrl = `${baseUrl}txy/zjz/token`
export const body_seg_url = `${baidubce}body_seg?access_token=`
export const body_analysis_url = `${baidubce}body_analysis?access_token=`
import {tokenUrl, body_seg_url, body_analysis_url} from './url.js'
export const request = async (url) => {
let header = {
'Content-Type': 'application/json',
};
let result = await new Promise((resolve, reject) => {
uni.request({
url: url,
method: 'post',
header: header,
success(res) {
if (res.statusCode == 200 && res.data.code == 200) {
resolve(res.data.data);
}
},
fail(err) {
reject(err);
}
});
});
return result
};
export const baiduRequest = async (url, data) => {
let header = {
'Content-Type': 'application/x-www-form-urlencoded',
};
let result = await new Promise((resolve, reject) => {
uni.request({
url: url,
method: 'post',
header: header,
data: {
image: data.image
},
success(res) {
resolve(res);
},
fail(err) {
reject(err);
}
});
});
return result
};
export const getAccessToken = (data) => {
return request(tokenUrl, data,)
}
export const body_seg = (data) => {
let url = `${body_seg_url}${data.access_token}`;
return baiduRequest(url, data)
}
export const body_analysis = (data) => {
let url = `${body_analysis_url}${data.access_token}`;
return baiduRequest(url, data)
}
export const getImageInfos = (data) => {
return new Promise((resolve, reject) => {
Promise.all([body_seg(data), body_analysis(data)]).then(([seg, analysis]) => {
console.log(seg, analysis)
if (seg.statusCode == 200 && analysis.statusCode == 200) {
let data = {
bodySeg: seg.data,
bodyAns: analysis.data
}
resolve(data)
}else {
reject('请求任务出错')
}
})
})
}
- 上传的图片格式调整为base64
toBase64(file) {
let _this = this
uni.getFileSystemManager().readFile({
filePath: file, //选择图片返回的相对路径
encoding: 'base64', //编码格式
success: res => {
// 成功的回调
// 'data:image/jpeg;base64,'
let base64 = res.data;
_this.getImgInfos(base64)
}
});
},
将人像分割接口返回的图片和人体位置信息分析的坐标结合。生成用于制作证件照的素材。下面的将使用1寸证件照的尺寸和dpi 来进行分析。
- 从位置信息分析接口中取出要使用的位置,比如头部,肩部。人像分析中取foreground,为去掉原图中人物信息以外的图片
initImgData(bodyAns,bodySeg) {
if (bodyAns.person_num > 1) {
uni.showToast({
title: '图片检测到多个人像,请重新上传',
icon:'none',
duration: 2000
});
return
}
if (bodyAns.person_num == 0) {
uni.showToast({
title: '图片未检测到人像,请重新上传',
icon:'none',
duration: 2000
});
return
}
let widthInfo = bodyAns.person_info[0]
let location = this.imgwidthsum(widthInfo)
this.location = location
let foreground = bodySeg.foreground
this.foreground = foreground
this.previewImg('data:image/png;base64,' + foreground, location).then(filePath => {
this.canvasImages = filePath
this.buildOver = true
})
},
imgwidthsum(data) {
let body_parts = data.body_parts
return {
top_head: body_parts.top_head,
left_shoulder: body_parts.left_shoulder,
right_shoulder: body_parts.right_shoulder
}
},
- 使用uni.getImageInfo 读取图片,需要先将上一步中base64d 图片转为本地图片
const fsm = wx.getFileSystemManager();
const FILE_BASE_NAME = 'tmp_base64src';
const base64src = function(base64data, pathName) {
return new Promise((resolve, reject) => {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || [];
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'));
}
const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME+pathName}.${format}`;
const buffer = wx.base64ToArrayBuffer(bodyData);
fsm.writeFile({
filePath,
data: buffer,
encoding: 'binary',
success() {
resolve(filePath);
},
fail() {
reject(new Error('ERROR_BASE64SRC_WRITE'));
},
});
});
};
export default base64src;
- 将图片按照要一定的比列绘制在canvas 中
let IMG_RATIO
let ratio = 295/413
let initWidth = 295
let initHeight = 413
let scrollTop = 250
let IMG_REAL_W,IMG_REAL_H
IMG_REAL_H = initHeight
IMG_REAL_W = IMG_REAL_H*IMG_RATIO
let canH = imgW * IMG_REAL_H / IMG_REAL_W
const ctx = uni.createCanvasContext("myselfCanvas", _this);
if (color) {
ctx.setFillStyle(color)
ctx.fillRect(0,0,IMG_REAL_W,IMG_REAL_H)
}
// 绘制的时候将选中的背景颜色填充到画布中
ctx.drawImage(res.path, 0, 0, IMG_REAL_W, IMG_REAL_H);
- 根据原图中头像位置坐标。换算出需要在原图上裁剪出来的区域
let x = location.right_shoulder.x //右肩位置的坐标 x
let y = location.top_head.y - scrollTop // 头部坐标位置 减去一定比列的坐标 y
let x1 = location.left_shoulder.x // 左肩位置坐标 x
var canvasW = ((x1 - x) / imgW) * IMG_REAL_W;
// 左肩坐标 减去右肩坐标 和原图的宽度比列 计算出 在上一步绘制的图中裁剪的宽度
var canvasH = canvasW/ratio // 根据证件照的比列 计算出 裁剪的高度
var canvasL = (x / imgW) * IMG_REAL_W;
var canvasT = (y / imgH) * IMG_REAL_H;
// 计算裁剪的起始坐标位置
- 在canvas 绘制图后导出证件照需要的尺寸
ctx.draw(false,(ret)=>{
uni.showToast({
icon:'success',
mask:true,
title: '绘制完成',
});
uni.canvasToTempFilePath({ // 保存canvas为图片
x: canvasL,
y: canvasT,
width: canvasW, //canvasH,
height: canvasH, //canvasH,
destWidth: initWidth,
destHeight: initHeight,
canvasId: 'myselfCanvas',
quality: 1,
fileType: color? 'jpg': 'png',
complete: function(res) {
resolve(res.tempFilePath)
} ,
})
});
导出证件照之前,还需要修改图片的dpi
- 修改图片dpi 是将图片转为base64 格式后修改。本项目使用changedpi 插件
- npm install changedpi
import {changeDpiDataUrl} from 'changedpi'
export const changeDpi = (url, dpi) => {
return new Promise((resolve) => {
if (dpi) {
uni.getFileSystemManager().readFile({
filePath: url, //选择图片返回的相对路径
encoding: 'base64', //编码格式
success: res => {
// 成功的回调
// 'data:image/jpeg;base64,'
let base64 = res.data;
let str = changeDpiDataUrl('data:image/jpeg;base64,' + base64, dpi)
base64src(str).then(filePath => {
resolve(filePath)
})
}
});
}else {
resolve(url)
}
})
}
- 在小程序中使用需要注意 插件中直接使用了btoa atob 两个函数。 但是在小程序是不支持直接调用的。需要重写这两个方法
- 重写的方法
(function(f) {
'use strict';
/* istanbul ignore else */
if (typeof exports === 'object' && exports != null &&
typeof exports.nodeType !== 'number') {
module.exports = f ();
} else if (typeof define === 'function' && define.amd != null) {
define ([], f);
} else {
var base64 = f ();
var global = typeof self !== 'undefined' ? self : $.global;
if (typeof global.btoa !== 'function') global.btoa = base64.btoa;
if (typeof global.atob !== 'function') global.atob = base64.atob;
}
} (function() {
'use strict';
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
function InvalidCharacterError(message) {
this.message = message;
}
InvalidCharacterError.prototype = new Error ();
InvalidCharacterError.prototype.name = 'InvalidCharacterError';
// encoder
// [https://gist.github.com/999166] by [https://github.com/nignag]
function btoa(input) {
var str = String (input);
for (
// initialize result and counter
var block, charCode, idx = 0, map = chars, output = '';
// if the next str index does not exist:
// change the mapping table to "="
// check if d has no fractional digits
str.charAt (idx | 0) || (map = '=', idx % 1);
// "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8
output += map.charAt (63 & block >> 8 - idx % 1 * 8)
) {
charCode = str.charCodeAt (idx += 3 / 4);
if (charCode > 0xFF) {
throw new InvalidCharacterError ("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
}
block = block << 8 | charCode;
}
return output;
}
// decoder
// [https://gist.github.com/1020396] by [https://github.com/atk]
function atob(input) {
var str = (String (input)).replace (/[=]+$/, ''); // #31: ExtendScript bad parse of /=
// if (str.length % 4 === 1) {
// throw new InvalidCharacterError ("'atob' failed: The string to be decoded is not correctly encoded.");
// }
for (
// initialize result and counters
var bc = 0, bs, buffer, idx = 0, output = '';
// get next character
buffer = str.charAt (idx++); // eslint-disable-line no-cond-assign
// character found in table? initialize bit storage and add its ascii value;
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ? output += String.fromCharCode (255 & bs >> (-2 * bc & 6)) : 0
) {
// try to find character in table (0-63, not found => -1)
buffer = chars.indexOf (buffer);
}
return output;
}
return {btoa: btoa, atob: atob};
}));
- 在源码中修改调用
const polyfill = require('../../../util/btoa.js');
const {btoa, atob} = polyfill;
下载证件照到手机相册
export const savePoster = (url) => {
const that = this
wx.saveImageToPhotosAlbum({
filePath: url,
success: function() {
wx.showToast({
title: '保存成功',
icon: 'none',
duration: 1500
});
},
fail(err) {
if (err.errMsg === "saveImageToPhotosAlbum:fail:auth denied" || err.errMsg === "saveImageToPhotosAlbum:fail auth deny" || err.errMsg === "saveImageToPhotosAlbum:fail authorize no response") {
wx.showModal({
title: '提示',
content: '需要您授权保存相册',
showCancel: false,
success: modalSuccess => {
wx.openSetting({
success(settingdata) {
if (settingdata.authSetting['scope.writePhotosAlbum']) {
wx.saveImageToPhotosAlbum({
filePath: url,
success: function () {
wx.showToast({
title: '保存成功',
icon: 'success',
duration: 2000
})
},
})
} else {
wx.showToast({
title: '授权失败,请稍后重新获取',
icon: 'none',
duration: 1500
});
}
}
})
}
})
}
}
})
}
下面是利用canvas 做的小应用,欢迎大家扫描体验,并提出建议。让我们共同进步
[项目代码] https://gitee.com/eyes-star/txy-mp.git [项目代码] https://gitee.com/eyes-star/zjz-mp.git
标签:
留言评论