// 本文件由FirstUI授权予新疆天衡创新研究院有限公司(手机号: 186 14072 5 4 9,身份证尾号:5A07X5)专用,请尊重知识产权,勿私下传播,违者追究法律责任。 /*! * 生成海报 * poster - v1.8.0 (2023/02/02, 16:52:14 PM) * * * 官网地址:https://firstui.cn/ * 文档地址:https://doc.firstui.cn/ */ const poster = { _pixelRatio: 2, _ctx: null, _canvasId: null, _this: null, create(pixelRatio, canvasId, _this) { poster._pixelRatio = pixelRatio; poster._canvasId = canvasId; poster._this = _this; poster._ctx = uni.createCanvasContext(canvasId, _this) }, _toPx(rpx) { return uni.upx2px(rpx * poster._pixelRatio) }, _getTextWidth(context, text, fontSize) { let width = 0; // #ifndef MP-ALIPAY || MP-BAIDU width = context.measureText(text).width // #endif //支付宝小程序ios真机测试measureText获取长度有bug //百度小程序有frontText时误差较大,影响绘制效果 // #ifdef MP-ALIPAY || MP-BAIDU let sum = 0; for (let i = 0, len = text.length; i < len; i++) { if (text.charCodeAt(i) >= 0 && text.charCodeAt(i) <= 255) sum = sum + 1; else sum = sum + 2; } width = sum / 2 * poster._toPx(fontSize) // #endif return width; }, //canvas文字换行,rows=-1则不限制行数 _wrapText(text, fontSize, textWidth, width, context, rows = 2) { let textArr = []; if (textWidth > width) { let fillText = ''; let lines = 1; let arr = text.split('') for (let i = 0, len = arr.length; i < len; i++) { fillText = fillText + arr[i]; if (poster._getTextWidth(context, fillText, fontSize) >= width) { if (lines === rows && rows !== -1) { if (i !== arr.length - 1) { fillText = fillText.substring(0, fillText.length - 1) + '...'; } textArr.push(fillText); break; } textArr.push(fillText); fillText = ''; lines++; } else if (i === arr.length - 1) { textArr.push(fillText); } } } else { textArr.push(text) } return textArr; }, _drawText(context, params) { let { x, y, fontSize, color, baseLine = 'normal', textAlign = 'left', frontText, frontSize, spacing, //单位rpx text, opacity = 1, lineThrough = false, width = 500, //单位rpx rows = 1, lineHeight = 0, fontWeight = 'normal', fontStyle = 'normal', fontFamily = "sans-serif" } = params; context.save(); context.beginPath(); context.font = fontStyle + " " + fontWeight + " " + poster._toPx(fontSize) + "px " + fontFamily context.setGlobalAlpha(opacity); // #ifdef MP-TOUTIAO context.setFontSize(poster._toPx(fontSize)); // #endif context.setFillStyle(color); context.setTextBaseline(baseLine); context.setTextAlign(textAlign); let textWidth = poster._getTextWidth(context, text, fontSize); width = poster._toPx(width); let textArr = poster._wrapText(text, fontSize, textWidth, width, context, rows) //如果文本前面有其他文本内容 if (frontText) { context.setFontSize(poster._toPx(frontSize)); x = poster._getTextWidth(context, frontText, frontSize) + poster._toPx(x + spacing); context.setFontSize(poster._toPx(fontSize)); } else { x = poster._toPx(x) } textArr.forEach((item, index) => { context.fillText(item, x, poster._toPx(y + (lineHeight || fontSize) * index)) }) context.restore(); if (lineThrough) { let lineY = y; // 根据baseLine的不同对贯穿线的Y坐标做相应调整 switch (baseLine) { case 'top': lineY += fontSize / 2 + 4; break; case 'middle': break; case 'bottom': lineY -= fontSize / 2 + 4; break; default: // #ifdef MP-WEIXIN lineY -= fontSize / 2 - 3; // #endif // #ifndef MP-WEIXIN lineY -= fontSize / 2 - 4; // #endif break; } context.save(); context.moveTo(x, poster._toPx(lineY)); context.lineTo(x + textWidth + 2, poster._toPx(lineY)); context.setStrokeStyle(color); context.stroke(); context.restore(); } }, _drawRadiusRect(context, params) { let { x, y, width, height, borderRadius } = params; let r = poster._toPx(borderRadius / 2); x = poster._toPx(x) y = poster._toPx(y) width = poster._toPx(width) height = poster._toPx(height) context.beginPath(); context.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5); context.moveTo(x + r, y); context.lineTo(x + width - r, y); context.lineTo(x + width, y + r); context.arc(x + width - r, y + r, r, Math.PI * 1.5, Math.PI * 2); context.lineTo(x + width, y + height - r); context.lineTo(x + width - r, y + height); context.arc(x + width - r, y + height - r, r, 0, Math.PI * 0.5); context.lineTo(x + r, y + height); context.lineTo(x, y + height - r); context.arc(x + r, y + height - r, r, Math.PI * 0.5, Math.PI); context.lineTo(x, y + r); context.lineTo(x + r, y); }, _drawImage(context, params) { let { imgResource, x, y, width, height, sx, sy, sw, sh, borderRadius = 0, borderWidth = 0, borderColor } = params; context.save(); if (borderRadius > 0) { this._drawRadiusRect(context, params); context.strokeStyle = 'rgba(255,255,255,0)' //处理百度/头条小程序黑边问题 // #ifndef MP-BAIDU || MP-TOUTIAO context.stroke(); // #endif context.clip(); } if (sw && sh) { //根据画布的宽高计算出图片绘制的大小,可保证图片绘制不变形 //暂时未使用此api,后期如果有需要再进行优化 context.drawImage(imgResource, poster._toPx(sx), poster._toPx(sy), poster._toPx(sw), poster._toPx(sh), poster ._toPx(x), poster._toPx(y), poster._toPx(width), poster._toPx(height)); } else { context.drawImage(imgResource, poster._toPx(x), poster._toPx(y), poster._toPx(width), poster._toPx( height)) } if (borderWidth && borderWidth > 0) { context.setStrokeStyle(borderColor); context.setLineWidth(poster._toPx(borderWidth)); context.stroke(); } context.restore(); }, _drawBlock(context, params) { let { width, height, x, y, borderWidth, backgroundColor, gradientColor, gradientType = 1, borderColor, borderRadius = 0, opacity = 1, shadow } = params; if (backgroundColor) { context.save(); context.setGlobalAlpha(opacity); if (gradientColor) { // #ifndef MP-KUAISHOU let grd = null; if (gradientType == 1) { //从上到下 grd = context.createLinearGradient(0, 0, poster._toPx(width), poster._toPx(height)) } else { //从左到右 grd = context.createLinearGradient(0, poster._toPx(width), poster._toPx(height), 0) } grd.addColorStop(0, backgroundColor) grd.addColorStop(1, gradientColor) // Fill with gradient context.setFillStyle(grd); // #endif // #ifdef MP-KUAISHOU context.setFillStyle(backgroundColor); // #endif } else { context.setFillStyle(backgroundColor); } if (shadow) { const { offsetX, offsetY, blur, color } = shadow; context.shadowOffsetX = poster._toPx(offsetX) context.shadowOffsetY = poster._toPx(offsetY) context.shadowBlur = blur context.shadowColor = color // context.setShadow(poster._toPx(offsetX), poster._toPx(offsetY), blur, color); } if (borderRadius > 0) { // 画圆角矩形 poster._drawRadiusRect(context, params); context.fill(); } else { context.fillRect(poster._toPx(x), poster._toPx(y), poster._toPx(width), poster._toPx(height)); } context.restore(); } if (borderWidth) { // 画线 context.save(); context.setGlobalAlpha(opacity); context.setStrokeStyle(borderColor); context.setLineWidth(poster._toPx(borderWidth)); if (borderRadius > 0) { // 画圆角矩形边框 poster._drawRadiusRect(context, params); context.stroke(); } else { context.strokeRect(poster._toPx(x), poster._toPx(y), poster._toPx(width), poster._toPx(height)); } context.restore(); } }, _drawLine(context, params) { let { x, y, endX, endY, color, width = 1 } = params; context.save(); context.beginPath(); context.setStrokeStyle(color); context.setLineWidth(poster._toPx(width)); context.moveTo(poster._toPx(x), poster._toPx(y)); context.lineTo(poster._toPx(endX), poster._toPx(endY)); context.stroke(); context.closePath(); context.restore(); }, //ios用户拒绝相册访问 ,引导用户到设置页面,开启相册访问权限 //-1=未请求 1 = 已允许,0 = 拒绝|受限 _judgeIosPermissionPhotoLibrary() { // #ifdef APP-PLUS var result = 0; var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary"); var authStatus = PHPhotoLibrary.authorizationStatus(); if (authStatus === 0) { result = -1; } else if (authStatus == 3) { result = 1; console.log("相册权限已经开启"); } else { result = 0; console.log("相册权限没有开启"); } plus.ios.deleteObject(PHPhotoLibrary); return result; // #endif }, // Android权限查询 _requestAndroidPermission(permissionID) { // #ifdef APP-PLUS return new Promise((resolve, reject) => { plus.android.requestPermissions( // 理论上支持多个权限同时查询,本函数封装只处理了一个权限的情况。有需要的可自行扩展封装 [permissionID], function(resultObj) { var result = 0; for (var i = 0; i < resultObj.granted.length; i++) { var grantedPermission = resultObj.granted[i]; console.log('已获取的权限:' + grantedPermission); result = 1 } for (var i = 0; i < resultObj.deniedPresent.length; i++) { var deniedPresentPermission = resultObj.deniedPresent[i]; console.log('拒绝本次申请的权限:' + deniedPresentPermission); result = 0 } for (var i = 0; i < resultObj.deniedAlways.length; i++) { var deniedAlwaysPermission = resultObj.deniedAlways[i]; console.log('永久拒绝申请的权限:' + deniedAlwaysPermission); result = -1 } resolve(result); // 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限 }, function(error) { console.log('申请权限错误:' + error.code + " = " + error.message); resolve({ code: error.code, message: error.message }); } ); }); // #endif }, // 跳转到**应用**的权限页面 _gotoAppPermissionSetting(isAndroid) { // #ifdef APP-PLUS if (!isAndroid) { var UIApplication = plus.ios.import("UIApplication"); var application2 = UIApplication.sharedApplication(); var NSURL2 = plus.ios.import("NSURL"); // var setting2 = NSURL2.URLWithString("prefs:root=LOCATION_SERVICES"); var setting2 = NSURL2.URLWithString("app-settings:"); application2.openURL(setting2); plus.ios.deleteObject(setting2); plus.ios.deleteObject(NSURL2); plus.ios.deleteObject(application2); } else { // console.log(plus.device.vendor); var Intent = plus.android.importClass("android.content.Intent"); var Settings = plus.android.importClass("android.provider.Settings"); var Uri = plus.android.importClass("android.net.Uri"); var mainActivity = plus.android.runtimeMainActivity(); var intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); var uri = Uri.fromParts("package", mainActivity.getPackageName(), null); intent.setData(uri); mainActivity.startActivity(intent); } // #endif }, _modal: function(callback, confirmColor, confirmText) { uni.showModal({ title: '提示', content: '您还没有开启相册权限,是否立即设置?', showCancel: true, cancelColor: '#B2B2B2', confirmColor: confirmColor || "#181818", confirmText: confirmText || "确定", success(res) { if (res.confirm) { callback && callback(true) } else { callback && callback(false) } } }) }, //相册权限查询,如果没有权限则提示打开设置页面 _judgePermissionPhotoLibrary: async function(callback) { // #ifdef H5 || MP-ALIPAY|| MP-360 //H5端不支持调用api保存到相册 callback && callback(true) // #endif // #ifdef APP-PLUS const res = uni.getSystemInfoSync(); let result; let isAndroid = res.platform.toLocaleLowerCase() == "android"; if (isAndroid) { result = await poster._requestAndroidPermission('android.permission.WRITE_EXTERNAL_STORAGE') } else { result = poster._judgeIosPermissionPhotoLibrary() } if (result == 1) { callback && callback(true) } else { if (!(!isAndroid && result == -1)) { poster._modal((res) => { if (res) { poster._gotoAppPermissionSetting(isAndroid) } }) } else { callback && callback(true) } } // #endif // #ifdef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO uni.authorize({ scope: 'scope.writePhotosAlbum', success() { callback && callback(true) }, fail() { poster._modal((res) => { if (res) { uni.openSetting({ success(res) { console.log(res.authSetting) } }); } }) } }) // #endif }, //图片转成本地文件 getImage(url) { return new Promise((resolve, reject) => { uni.downloadFile({ url: url, success: res => { resolve(res.tempFilePath); }, fail: res => { reject(false) } }) }) }, //当服务器端返回图片base64时,转成本地文件 //微信小程序不支持直接绘制base64,其他平台可根据支持情况进行处理 getImagebyBase64(base64) { //使用前先查看支持平台 const uniqueId = `fui_${Math.ceil(Math.random() * 10e5).toString(36)}` return new Promise((resolve, reject) => { // #ifdef MP-WEIXIN const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || []; let arrayBuffer = wx.base64ToArrayBuffer(bodyData) //uniqueId:注意这里名称需要动态生成(名称相同部分机型会出现写入失败,显示的是上次生成的图片) const filePath = `${wx.env.USER_DATA_PATH}/${uniqueId}.${format}`; //此处可能会出现存储空间不足的情况,可清理缓存解决 //fail the maximum size of the file storage limit is exceeded wx.getFileSystemManager().writeFile({ filePath, data: arrayBuffer, encoding: 'binary', success() { resolve(filePath); }, fail() { reject(false) } }) // #endif // #ifdef APP-PLUS let bitmap = new plus.nativeObj.Bitmap(uniqueId); bitmap.loadBase64Data(base64, function() { //console.log("加载Base64图片数据成功"); bitmap.save(`_doc/${uniqueId}.png`, {}, function(e) { //console.log('保存图片成功:' + JSON.stringify(i)); // let width = e.width; // 保存后图片的实际宽度,单位为px // let height = e.height; // 保存后图片的实际高度,单位为px let target = e.target; // 保存后的图片url路径,以"file://"开头 resolve(target); }, function(e) { console.log('保存图片失败:' + JSON.stringify(e)); reject(false) }); }, function() { console.log('加载Base64图片数据失败:' + JSON.stringify(e)); reject(false) }); // #endif // #ifdef H5 // let img = new Image(); // img.src = base64; resolve(base64); // #endif //后面查看文档说明再进行转换 // #ifdef MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-360 reject(false) // #endif }) }, generatePoster(cw, ch, queue, callback) { const context = poster._ctx; if (context) { context.clearRect(0, 0, poster._toPx(cw), poster._toPx(ch)) queue.forEach((params) => { if (params.type === 'image') { poster._drawImage(context, params) } else if (params.type === 'text') { poster._drawText(context, params) } else if (params.type === 'block') { poster._drawBlock(context, params) } else if (params.type === 'line') { poster._drawLine(context, params) } }); const platform = uni.getSystemInfoSync().platform; let time = 50; if (platform === 'android') { time = 300; } setTimeout(() => { context.draw(false, () => { setTimeout(() => { // #ifdef MP-ALIPAY context.toTempFilePath({ success: res => { callback && callback(res.apFilePath) }, fail: err => { callback && callback(false) } }); // #endif // #ifndef MP-ALIPAY uni.canvasToTempFilePath({ x: 0, y: 0, canvasId: poster._canvasId, fileType: 'png', quality: 1, success: function(res) { callback && callback(res.tempFilePath) }, fail() { callback && callback(false) } }, poster._this) // #endif }, time) }) }, 50) } else { callback && callback(false) } }, // 将海报图片保存到本地,H5只可预览然后长按保存 saveImage(file) { // #ifndef H5 //检查是否授权相册权限 poster._judgePermissionPhotoLibrary((res) => { //保存图片 if (res) { uni.saveImageToPhotosAlbum({ filePath: file, success(res) { uni.showToast({ title: '图片保存成功', icon: 'none' }) }, fail(res) { uni.showToast({ title: '图片保存失败', icon: 'none' }) } }) } }) // #endif // #ifdef H5 uni.previewImage({ urls: [file] }); // #endif } } export default { create: poster.create, generatePoster: poster.generatePoster, getImage: poster.getImage, getImagebyBase64: poster.getImagebyBase64, saveImage: poster.saveImage };