小程式制作證件照過程
利用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接口,把圖像進行人像分割,分析圖像中頭部肩部位置資訊
- [參考連結位址] https://cloud.baidu.com/doc/BODY/s/Fk3cpyxua
- 該接口中要求上傳的圖檔格式為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
});
}
}
})
}
})
}
}
})
}