// 本文件由FirstUI授权予四川政采招投标咨询有限公司(会员ID:1 63,营业执照号: 9 1510 13 1 33 2 00 6193K)专用,请尊重知识产权,勿私下传播,违者追究法律责任。 /*! * 生成海报 * poster - v1.8.0 (2023/02/02, 16:52:14 PM) * updated V2.2.0+ 2023/11/07 * * * 官网地址:https://firstui.cn/ * 文档地址:https://doc.firstui.cn/ */ import fuiQr from './fui-qr/index.js' 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); }, async _getImageInfo(src) { return new Promise((resolve, reject) => { uni.getImageInfo({ src: src, success: res => { resolve({ width: res.width, height: res.height }) }, fail: err => { reject(false) } }); }) }, async _drawImage(context, params) { let { imgResource, x, y, width, height, sx, sy, sw, sh, borderRadius = 0, borderWidth = 0, borderColor, imgWidth = 0, imgHeight = 0, crop = false } = 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(); } const bg_w = poster._toPx(width) const bg_h = poster._toPx(height) x = poster._toPx(x) y = poster._toPx(y) // 等比例裁剪图片,保证图片绘制不变形 if (imgWidth && imgHeight && crop) { let dWidth = bg_w / imgWidth; let dHeight = bg_h / imgHeight; if (imgWidth > bg_w && imgHeight > bg_h || imgWidth < bg_w && imgHeight < bg_h) { if (dWidth > dHeight) { context.drawImage(imgResource, 0, (imgHeight - bg_h / dWidth) / 2, imgWidth, bg_h / dWidth, x, y, bg_w, bg_h) } else { context.drawImage(imgResource, (imgWidth - bg_w / dHeight) / 2, 0, bg_w / dHeight, imgHeight, x, y, bg_w, bg_h) } } else { if (imgWidth < bg_w) { context.drawImage(imgResource, 0, (imgHeight - bg_h / dWidth) / 2, imgWidth, bg_h / dWidth, x, y, bg_w, bg_h) } else { context.drawImage(imgResource, (imgWidth - bg_w / dHeight) / 2, 0, bg_w / dHeight, imgHeight, x, y, bg_w, bg_h) } } } else { context.drawImage(imgResource, x, y, bg_w, bg_h) } 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(); }, _drawQrcode(context, params) { let { x, y, width, height, value, foreground = "#181818", background = "#ffffff" } = params; x = poster._toPx(x) y = poster._toPx(y) width = poster._toPx(width) height = poster._toPx(height) const qrcode = fuiQr(poster.utf16to8(value), { typeNumber: -1, errorCorrectLevel: 2, }) const cells = qrcode.modules const tileW = width / cells.length const tileH = height / cells.length cells.forEach((row, rdx) => { row.forEach((cell, cdx) => { context.setFillStyle(cell ? foreground : background) const w = (Math.ceil((cdx + 1) * tileW) - Math.floor(cdx * tileW)) const h = (Math.ceil((rdx + 1) * tileH) - Math.floor(rdx * tileH)) context.fillRect(Math.round(cdx * tileW) + x, Math.round(rdx * tileH) + y, w, h) }) }) 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 }) }, async generatePoster(cw, ch, queue, callback) { for (let i = 0; i < queue.length; i++) { const item = queue[i] if (item.type === 'image') { const res = await poster._getImageInfo(item.imgResource) if (res) { item.imgWidth = res.width item.imgHeight = res.height } } } 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) } else if (params.type === 'qrcode') { poster._drawQrcode(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 }, utf16to8(str) { const len = str.length let out = '' for (let i = 0; i < len; i++) { const c = str.charCodeAt(i) if ((c >= 0x0001) && (c <= 0x007F)) { out += str.charAt(i) } else if (c > 0x07FF) { out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F)) out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F)) out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)) } else { out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F)) out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)) } } return out } } export default { create: poster.create, generatePoster: poster.generatePoster, getImage: poster.getImage, getImagebyBase64: poster.getImagebyBase64, saveImage: poster.saveImage };