module.exports = (function () { const __MODS__ = {} const __DEFINE__ = function (modId, func, req) { const m = { exports: {}, _tempexports: {} } __MODS__[modId] = { status: 0, func, req, m } } const __REQUIRE__ = function (modId, source) { if (!__MODS__[modId]) return require(source) if (!__MODS__[modId].status) { const m = __MODS__[modId].m m._exports = m._tempexports const desp = Object.getOwnPropertyDescriptor(m, 'exports') if (desp && desp.configurable) { Object.defineProperty(m, 'exports', { set(val) { if (typeof val === 'object' && val !== m._exports) { m._exports.__proto__ = val.__proto__ Object.keys(val).forEach((k) => { m._exports[k] = val[k] }) } m._tempexports = val }, get() { return m._tempexports }, }) } __MODS__[modId].status = 1 __MODS__[modId].func(__MODS__[modId].req, m, m.exports) } return __MODS__[modId].m.exports } const __REQUIRE_WILDCARD__ = function (obj) { if (obj && obj.__esModule) { return obj } else { const newObj = {} if (obj != null) { for (const k in obj) { if (Object.prototype.hasOwnProperty.call(obj, k)) newObj[k] = obj[k] } } newObj.default = obj return newObj } } const __REQUIRE_DEFAULT__ = function (obj) { return obj && obj.__esModule ? obj.default : obj } __DEFINE__( 1685064731946, (require, module, exports) => { const __TEMP__ = require('./src/index') const Wxml2Canvas = __REQUIRE_DEFAULT__(__TEMP__) if (!exports.__esModule) Object.defineProperty(exports, '__esModule', { value: true }) exports.default = Wxml2Canvas }, (modId) => { const map = { './src/index': 1685064731947 } return __REQUIRE__(map[modId], modId) }, ) __DEFINE__( 1685064731947, (require, module, exports) => { const __TEMP__ = require('./util') const Util = __REQUIRE_DEFAULT__(__TEMP__) const imageMode = [ 'scaleToFill', 'aspectFit', 'aspectFill', 'widthFix', 'top', 'bottom', 'center', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right', ] class Wxml2Canvas { constructor(options = {}) { this.device = (wx.getSystemInfoSync && wx.getSystemInfoSync()) || {} if (!options.zoom) { this.zoom = this.device.windowWidth / 375 } else { this.zoom = options.zoom || 1 } this.element = options.element this.object = options.obj this.width = options.width * this.zoom || 0 this.height = options.height * this.zoom || 0 this.destZoom = options.destZoom || 3 this.destWidth = this.width * this.destZoom this.destHeight = this.height * this.destZoom this.translateX = options.translateX * this.zoom || 0 this.translateY = options.translateY * this.zoom || 0 this.gradientBackground = options.gradientBackground || null this.background = options.background || '#ffffff' this.finishDraw = options.finish || function finish(params) {} this.errorHandler = options.error || function error(params) {} this.progress = options.progress || function progress(params) {} this.textAlign = options.textAlign || 'left' this.fullText = options.fullText || false this.font = options.font || '14px PingFang SC' this._init() } draw(data = {}, that) { const self = this this.data = data this.fef = that this.progress(10) this._preloadImage(data.list) .then((result) => { this.progress(30) self._draw() }) .catch((res) => { self.errorHandler(res) }) } measureWidth(text, font) { if (font) { this.ctx.font = font } const res = this.ctx.measureText(text) || {} return res.width || 0 } _init() { this.progressPercent = 0 // 绘制进度百分比 this.data = null this.ref = null this.allPic = [] this.screenList = [] this.asyncList = [] this.imgUrl = '' this.progressPercent = 0 this.distance = 0 this.progress(0) this.ctx = wx.createCanvasContext(this.element, this.object) this.ctx.font = this.font this.ctx.setTextBaseline('top') this.ctx.setStrokeStyle('white') this.debug = this.device.platform === 'devtools' this._drawBakcground() } _drawBakcground() { if (this.gradientBackground) { const line = this.gradientBackground.line || [0, 0, 0, this.height] const color = this.gradientBackground.color || ['#fff', '#fff'] const style = { fill: { line, color } } this._drawRectToCanvas(0, 0, this.width, this.height, style) } else { const style = { fill: this.background } this._drawRectToCanvas(0, 0, this.width, this.height, style) } } _draw() { const self = this let list = this.data.list || [] let index = 0 const all = [] let count = 0 list.forEach((item) => { if (item.type === 'wxml') { count += 3 } else { count += 1 } }) this.distance = 60 / (count || 1) // 进度条的间距 this.progressPercent = 30 this.asyncList = list.filter(item => item.delay == true) list = list.filter(item => item.delay != true) drawList(list) Promise.all(all) .then((results) => { index = 0 drawList(self.asyncList, true) Promise.all(all).then((results) => { self.progress(90) self._saveCanvasToImage() }) }) .catch((e) => { console.log(e) self.errorHandler(e) }) function drawList(list = [], noDelay) { list.forEach((item, i) => { all[index++] = new Promise((resolve, reject) => { const attr = item.style item.progress = self.distance if (noDelay) { item.delay = 0 } if (item.type === 'radius-image') { self._drawCircle(item, attr, resolve, reject, 'image') } else if (item.type === 'text') { self._drawText(item, attr, resolve, reject) } else if (item.type === 'line') { self._drawLine(item, attr, resolve, reject) } else if (item.type === 'circle') { self._drawCircle(item, attr, resolve, reject) } else if (item.type === 'rect') { self._drawRect(item, attr, resolve, reject) } else if (item.type === 'image') { self._drawRect(item, attr, resolve, reject, 'image') } else if (item.type === 'wxml') { self._drawWxml(item, attr, resolve, reject) } else { resolve() } }) }) } } _saveCanvasToImage() { const self = this // 延时保存有两个原因,一个是等待绘制delay的元素,另一个是安卓上样式会错乱 setTimeout( () => { self.progress(95) const obj = { x: 0, y: 0, width: self.width, height: self.height, canvasId: self.element, success(res) { self.progress(100) self.imgUrl = res.tempFilePath self.finishDraw(self.imgUrl) }, fail(res) { self.errorHandler({ errcode: 1000, errmsg: 'save canvas error', e: res }) }, } if (self.destZoom !== 3) { obj.destWidth = self.destWidth obj.destHeight = self.destHeight } wx.canvasToTempFilePath(obj, self.object) }, !self.device.system.includes('iOS') ? 300 : 100, ) } _preloadImage(list = []) { const self = this const all = [] let count = 0 list.forEach((item, i) => { if (item.url && self._findPicIndex(item.url) === -1) { // 避免重复下载同一图片 self.allPic.push({ url: item.url, local: '', }) all[count++] = new Promise((resolve, reject) => { // 非http(s)域名的就不下载了 if ( !item.url.startsWith('http') || /^http:\/\/(tmp)|(usr)\//.test(item.url) || /^http:\/\/127\.0\.0\.1/.test(item.url) ) { if (item.isBase64) { const fileManager = wx.getFileSystemManager() fileManager.writeFile({ filePath: item.url, data: item.isBase64.replace(/data:image\/(.*);base64,/, ''), encoding: 'base64', success(res) { imageInfo(item.url) }, fail(res) { reject(res) }, }) } else { imageInfo(item.url) } function imageInfo(url) { wx.getImageInfo({ src: url, success(res) { const index = self._findPicIndex(url) if (index > -1) { self.allPic[index].local = url self.allPic[index].width = res.width self.allPic[index].height = res.height } resolve({ tempFilePath: url }) }, fail(res) { reject(res) }, }) } } else { wx.downloadFile({ url: item.url.replace(/^https?/, 'https'), success(res) { wx.getImageInfo({ src: res.tempFilePath, success(img) { const index = self._findPicIndex(item.url) if (index > -1) { self.allPic[index].local = res.tempFilePath self.allPic[index].width = img.width self.allPic[index].height = img.height } resolve(res) }, fail(res) { reject(res) }, }) }, fail: (res) => { reject({ errcode: 1001, errmsg: 'download pic error' }) }, }) } }) } }) return Promise.all(all) .then((results) => { return new Promise((resolve) => { resolve() }) }) .catch((results) => { return new Promise((resolve, reject) => { reject(results) }) }) } _findPicIndex(url) { const index = this.allPic.findIndex(pic => pic.url === url) return index } _drawRect(item, style, resolve, reject, isImage, isWxml) { const zoom = this.zoom let leftOffset = 0 const topOffset = 0 let width = style.width let height = style.height let imgWidth = style.width let imgHeight = style.height let mode = null try { item.x = this._resetPositionX(item, style) item.y = this._resetPositionY(item, style) let url if (isImage) { const index = this._findPicIndex(item.url) if (index > -1) { url = this.allPic[index].local imgWidth = this.allPic[index].width imgHeight = this.allPic[index].height } else { url = item.url } } style.padding = style.padding || [] if (isWxml === 'inline-wxml') { item.x = item.x + ((style.padding[3] && style.padding[3]) || 0) item.y = item.y + ((style.padding[0] && style.padding[0]) || 0) } leftOffset = item.x + style.width + ((style.padding[1] && style.padding[1]) || 0) if (!isWxml) { width = width * zoom height = height * zoom } if (style.dataset && style.dataset.mode && imageMode.includes(style.dataset.mode)) { mode = { type: style.dataset.mode, width: imgWidth, height: imgHeight, } } this._drawRectToCanvas(item.x, item.y, width, height, style, url, mode) this._updateProgress(item.progress) if (resolve) { resolve() } else { return { leftOffset, topOffset, } } } catch (e) { reject && reject({ errcode: isImage ? 1003 : 1002, errmsg: isImage ? 'drawImage error' : 'drawRect error', e }) } } _drawRectToCanvas(x, y, width, height, style, url, mode) { const { fill, border, boxShadow } = style this.ctx.save() this._drawBoxShadow(boxShadow, (res) => { // 真机上填充渐变色时,没有阴影,先画个相等大小的纯色矩形来实现阴影 if (fill && typeof fill !== 'string' && !this.debug) { this.ctx.setFillStyle(res.color || '#ffffff') this.ctx.fillRect(x, y, width, height) } }) if (url) { // 开发者工具有bug,先不裁剪 if (mode) { this._resetImageByMode(url, x, y, width, height, mode) } else { this.ctx.drawImage(url, x, y, width, height) } } else { this._setFill(fill, () => { this.ctx.fillRect(x, y, width, height) }) } this._drawBorder(border, style, (border) => { const fixBorder = border.width this.ctx.strokeRect(x - fixBorder / 2, y - fixBorder / 2, width + fixBorder, height + fixBorder) }) this.ctx.draw(true) this.ctx.restore() this.ctx.strokeStyle = '#fff' } _resetImageByMode(url, x, y, width, height, mode) { const self = this let offsetX = 0 let offsetY = 0 let imgWidth = mode.width let imgHeight = mode.height switch (mode.type) { case 'scaleToFill': imgWidth = width imgHeight = height self.ctx.drawImage(url, x, y, width, height) break case 'widthFix': height = width / ((imgWidth || 1) / (imgHeight || 1)) self.ctx.drawImage(url, x, y, width, height) break case 'aspectFit': if (imgWidth > imgHeight) { const realHeight = width / ((imgWidth || 1) / (imgHeight || 1)) offsetY = -(height - realHeight) / 2 imgWidth = width imgHeight = realHeight } else { const realWidth = height / ((imgHeight || 1) / (imgWidth || 1)) offsetX = -(width - realWidth) / 2 imgWidth = realWidth imgHeight = height } _clip() break case 'aspectFill': if (imgWidth > imgHeight) { const realWidth = imgWidth / ((imgHeight || 1) / (height || 1)) offsetX = (realWidth - width) / 2 imgWidth = realWidth imgHeight = height } else { const realHeight = imgHeight / ((imgWidth || 1) / (width || 1)) offsetY = (realHeight - height) / 2 imgWidth = width imgHeight = realHeight } _clip() break case 'top left': _clip() break case 'top': offsetX = (mode.width - width) / 2 _clip() break case 'top right': offsetX = mode.width - width _clip() break case 'left': offsetY = (mode.height - height) / 2 _clip() break case 'center': offsetX = (mode.width - width) / 2 offsetY = (mode.height - height) / 2 _clip() break case 'right': offsetX = mode.width - width offsetY = (mode.height - height) / 2 _clip() break case 'bottom left': offsetY = mode.height - height _clip() break case 'bottom': offsetX = (mode.width - width) / 2 offsetY = mode.height - height _clip() break case 'bottom right': offsetX = mode.width - width offsetY = mode.height - height _clip() break default: imgWidth = width imgHeight = height break } function _clip() { self.ctx.save() self.ctx.beginPath() self.ctx.rect(x, y, width, height) self.ctx.clip() self.ctx.drawImage(url, x - offsetX, y - offsetY, imgWidth, imgHeight) self.ctx.closePath() self.ctx.restore() } } _drawText(item, style, resolve, reject, type, isWxml) { const zoom = this.zoom let leftOffset = 0 let topOffset = 0 try { style.fontSize = this._parseNumber(style.fontSize) const fontSize = Math.ceil((style.fontSize || 14) * zoom) this.ctx.setTextBaseline('top') this.ctx.font = `${style.fontWeight ? style.fontWeight : 'normal'} ${fontSize}px ${ style.fontFamily || 'PingFang SC' }` this.ctx.setFillStyle(style.color || '#454545') let text = item.text || '' let textWidth = Math.floor(this.measureWidth(text, style.font || this.ctx.font)) const lineHeight = this._getLineHeight(style) const textHeight = Math.ceil(textWidth / (style.width || textWidth)) * lineHeight let width = Math.ceil((style.width || textWidth) * (!isWxml ? zoom : 1)) const whiteSpace = style.whiteSpace || 'wrap' let x = 0 let y = 0 if (typeof style.padding === 'string') { style.padding = Util.transferPadding(style.padding) } item.x = this._resetPositionX(item, style) item.y = this._resetPositionY(item, style, textHeight) this._drawBoxShadow(style.boxShadow) if (style.background || style.border) { this._drawTextBackgroud(item, style, textWidth, textHeight, isWxml) } // 行内文本 if (type === 'inline-text') { width = item.maxWidth if (item.leftOffset + textWidth > width) { // 如果上一个行内元素换行了,这个元素要继续在后面补足一行 let lineNum = Math.max(Math.floor(textWidth / width), 1) const length = text.length const singleLength = Math.floor(length / lineNum) const widthOffset = item.leftOffset ? item.leftOffset - item.originX : 0 let { endIndex: currentIndex, single, singleWidth, } = this._getTextSingleLine(text, width, singleLength, 0, widthOffset) x = this._resetTextPositionX(item, style, singleWidth) y = this._resetTextPositionY(item, style) this.ctx.fillText(single, x, y) leftOffset = x + singleWidth topOffset = y // 去除第一行补的内容,然后重置 text = text.substring(currentIndex, text.length) currentIndex = 0 lineNum = Math.max(Math.floor(textWidth / width), 1) textWidth = Math.floor(this.measureWidth(text, style.font || this.ctx.font)) item.x = item.originX // 还原换行后的x for (let i = 0; i < lineNum; i++) { const { endIndex, single, singleWidth } = this._getTextSingleLine( text, width, singleLength, currentIndex, ) currentIndex = endIndex if (single) { x = this._resetTextPositionX(item, style, singleWidth, width) y = this._resetTextPositionY(item, style, i + 1) this.ctx.fillText(single, x, y) if (i === lineNum - 1) { leftOffset = x + singleWidth topOffset = lineHeight * lineNum } } } const last = text.substring(currentIndex, length) const lastWidth = this.measureWidth(last) if (last) { x = this._resetTextPositionX(item, style, lastWidth, width) y = this._resetTextPositionY(item, style, lineNum + 1) this.ctx.fillText(last, x, y) leftOffset = x + lastWidth topOffset = lineHeight * (lineNum + 1) } } else { x = this._resetTextPositionX(item, style, textWidth, width) y = this._resetTextPositionY(item, style) this.ctx.fillText(item.text, x, y) leftOffset = x + textWidth topOffset = lineHeight } } else { // block文本,如果文本长度超过宽度换行 if (width && textWidth > width && whiteSpace !== 'nowrap') { let lineNum = Math.max(Math.floor(textWidth / width), 1) const length = text.length const singleLength = Math.floor(length / lineNum) let currentIndex = 0 // lineClamp参数限制最多行数 if (style.lineClamp && lineNum + 1 > style.lineClamp) { lineNum = style.lineClamp - 1 } for (let i = 0; i < lineNum; i++) { const { endIndex, single, singleWidth } = this._getTextSingleLine( text, width, singleLength, currentIndex, ) currentIndex = endIndex x = this._resetTextPositionX(item, style, singleWidth, width) y = this._resetTextPositionY(item, style, i) this.ctx.fillText(single, x, y) } // 换行后剩余的文字,超过一行则截断增加省略号 let last = text.substring(currentIndex, length) let lastWidth = this.measureWidth(last) if (lastWidth > width) { const { single, singleWidth } = this._getTextSingleLine(last, width, singleLength) lastWidth = singleWidth last = `${single.substring(0, single.length - 1)}...` } x = this._resetTextPositionX(item, style, lastWidth, width) y = this._resetTextPositionY(item, style, lineNum) this.ctx.fillText(last, x, y) } else { x = this._resetTextPositionX(item, style, textWidth, width) y = this._resetTextPositionY(item, style) this.ctx.fillText(item.text, x, y) } } this.ctx.draw(true) this._updateProgress(item.progress) if (resolve) { resolve() } else { return { leftOffset, topOffset, } } } catch (e) { reject && reject({ errcode: 1004, errmsg: 'drawText error', e }) } } _drawTextBackgroud(item, style, textWidth, textHeight, isWxml) { if (!style.width) return const zoom = isWxml ? 1 : this.zoom let width = style.width || textWidth let height = style.height || textHeight const rectStyle = { fill: style.background, border: style.border, } style.padding = style.padding || [0, 0, 0, 0] width += (style.padding[1] || 0) + (style.padding[3] || 0) height += (style.padding[0] || 0) + (style.padding[2] || 0) width = width * zoom height = height * zoom this._drawRectToCanvas(item.x, item.y, width, height, rectStyle) } _drawCircle(item, style, resolve, reject, isImage, isWxml) { const zoom = this.zoom let r = style.r try { item.x = this._resetPositionX(item, style) item.y = this._resetPositionY(item, style) let url if (isImage) { const index = this._findPicIndex(item.url) if (index > -1) { url = this.allPic[index].local } else { url = item.url } } if (!isWxml) { r = r * zoom } this._drawCircleToCanvas(item.x, item.y, r, style, url) this._updateProgress(item.progress) resolve && resolve() } catch (e) { reject && reject({ errcode: isImage ? 1006 : 1005, errmsg: isImage ? 'drawCircleImage error' : 'drawCircle error', e, }) } } _drawCircleToCanvas(x, y, r, style, url) { const { fill, border, boxShadow } = style this.ctx.save() this._drawBoxShadow(boxShadow, (res) => { // 真机上填充渐变色时,没有阴影,先画个相等大小的纯色矩形来实现阴影 if ((fill && typeof fill !== 'string') || (url && res.color)) { this.ctx.setFillStyle(res.color || '#ffffff') this.ctx.beginPath() this.ctx.arc(x + r, y + r, r, 0, 2 * Math.PI) this.ctx.closePath() this.ctx.fill() } }) if (url) { this.ctx.save() this.ctx.beginPath() this.ctx.arc(x + r, y + r, r, 0, 2 * Math.PI) this.ctx.clip() this.ctx.drawImage(url, x, y, r * 2, r * 2) this.ctx.closePath() this.ctx.restore() } else { this._setFill(fill, () => { this.ctx.beginPath() this.ctx.arc(x + r, y + r, r, 0, 2 * Math.PI) this.ctx.closePath() this.ctx.fill() }) } this._drawBorder(border, style, (border) => { this.ctx.beginPath() this.ctx.arc(x + r, y + r, r + border.width / 2, 0, 2 * Math.PI) this.ctx.stroke() this.ctx.closePath() }) this.ctx.draw(true) this.ctx.restore() } _drawLine(item, style, resolve, reject, isWxml) { const zoom = this.zoom try { const x1 = item.x * zoom + this.translateX const y1 = item.y * zoom + this.translateY const x2 = item.x2 * zoom + this.translateX const y2 = item.y2 * zoom + this.translateY this._drawLineToCanvas(x1, y1, x2, y2, style) this._updateProgress(item.progress) resolve && resolve() } catch (e) { reject && reject({ errcode: 1007, errmsg: 'drawLine error', e }) } } _drawLineToCanvas(x1, y1, x2, y2, style) { const { stroke, dash, boxShadow } = style this.ctx.save() if (stroke) { this._setStroke(stroke) } this._drawBoxShadow(boxShadow) if (dash) { const dash = [style.dash[0] || 5, style.dash[1] || 5] const offset = style.dash[2] || 0 this.ctx.setLineDash(dash, offset || 0) } this.ctx.moveTo(x1, y1) this.ctx.setLineWidth((style.width || 1) * this.zoom) this.ctx.lineTo(x2, y2) this.ctx.stroke() this.ctx.draw(true) this.ctx.restore() } // 废弃,合并到_drawRect _drawImage(item, style, resolve, reject, isWxml) { const zoom = this.zoom try { item.x = this._resetPositionX(item, style) item.y = this._resetPositionY(item, style) item.x = item.x + (style.padding[3] || 0) item.y = item.y + (style.padding[0] || 0) const index = this._findPicIndex(item.url) const url = index > -1 ? this.allPic[index].local : item.url this._drawImageToCanvas(url, item.x, item.y, style.width * zoom, style.height * zoom, style) this._updateProgress(item.progress) resolve && resolve() } catch (e) { reject && reject({ errcode: 1012, errmsg: 'drawRect error', e }) } } // 废弃,合并到_drawRect _drawImageToCanvas(url, x, y, width, height, style) { const { fill, border, boxShadow } = style this.ctx.save() this._drawBoxShadow(boxShadow) this.ctx.drawImage(url, x, y, width, height) this._drawBorder(border, style, (border) => { const fixBorder = border.width this.ctx.strokeRect(x - fixBorder / 2, y - fixBorder / 2, width + fixBorder, height + fixBorder) }) this.ctx.draw(true) this.ctx.restore() } _drawWxml(item, style, resolve, reject) { const self = this let all = [] try { this._getWxml(item, style).then((results) => { // 上 -> 下 const sorted = self._sortListByTop(results[0]) let count = 0 let progress = 0 Object.keys(sorted).forEach((item) => { count += sorted[item].length }) progress = (this.distance * 3) / (count || 1) all = this._drawWxmlBlock(item, sorted, all, progress, results[1]) all = this._drawWxmlInline(item, sorted, all, progress, results[1]) Promise.all(all) .then((results) => { resolve && resolve() }) .catch((e) => { reject && reject(e) }) }) } catch (e) { reject && reject({ errcode: 1008, errmsg: 'drawWxml error' }) } } _drawWxmlBlock(item, sorted, all, progress, results) { const self = this // 用来限定位置范围,取相对位置 const limitLeft = results ? results.left : 0 const limitTop = results ? results.top : 0 Object.keys(sorted).forEach((top, topIndex) => { // 左 -> 右 let list = sorted[top].sort((a, b) => { return a.left - b.left }) list = list.filter(sub => sub.dataset.type && !sub.dataset.type.includes('inline')) list.forEach((sub, index) => { all[index] = new Promise((resolve2, reject2) => { sub = self._transferWxmlStyle(sub, item, limitLeft, limitTop) sub.progress = progress const type = sub.dataset.type if (sub.dataset.delay) { setTimeout(() => { drawWxmlItem() }, sub.dataset.delay) } else { drawWxmlItem() } function drawWxmlItem() { if (type === 'text') { self._drawWxmlText(sub, resolve2, reject2) } else if (type === 'image') { self._drawWxmlImage(sub, resolve2, reject2) } else if (type === 'radius-image') { self._drawWxmlCircleImage(sub, resolve2, reject2) } else if (type === 'background-image') { self._drawWxmlBackgroundImage(sub, resolve2, reject2) } } }) }) }) return all } _drawWxmlInline(item, sorted, all, progress, results) { const self = this let topOffset = 0 let leftOffset = 0 let lastTop = 0 const limitLeft = results ? results.left : 0 const limitTop = results ? results.top : 0 const p = new Promise((resolve2, reject2) => { let maxWidth = 0 let minLeft = Infinity let maxRight = 0 // 找出同一top下的最小left和最大right,得到最大的宽度,用于换行 Object.keys(sorted).forEach((top) => { const inlineList = sorted[top].filter(sub => sub.dataset.type && sub.dataset.type.includes('inline')) inlineList.forEach((sub) => { if (sub.left < minLeft) { minLeft = sub.left } if (sub.right > maxRight) { maxRight = sub.right } }) }) maxWidth = Math.ceil(maxRight - minLeft || self.width) Object.keys(sorted).forEach((top, topIndex) => { // 左 -> 右 const list = sorted[top].sort((a, b) => { return a.left - b.left }) // 换行的行内元素left放到后面,version2.0.6后无法获取高度,改用bottom值来判断是否换行了 let position = -1 for (let i = 0, len = list.length; i < len; i++) { if (list[i] && list[i + 1]) { if (list[i].bottom > list[i + 1].bottom) { position = i break } } } if (position > -1) { list.push(list.splice(position, 1)[0]) } const inlineList = list.filter(sub => sub.dataset.type && sub.dataset.type.includes('inline')) const originLeft = inlineList[0] ? inlineList[0].left : 0 // 换行后和top不相等时,认为是换行了,要清除左边距;当左偏移量大于最大宽度时,也要清除左边距; 当左偏移小于左边距时,也要清除 if ( Math.abs(topOffset + lastTop - top) > 2 || leftOffset - originLeft - limitLeft >= maxWidth || leftOffset <= originLeft - limitLeft - 2 ) { leftOffset = 0 } lastTop = +top topOffset = 0 inlineList.forEach((sub, index) => { sub = self._transferWxmlStyle(sub, item, limitLeft, limitTop) sub.progress = progress const type = sub.dataset.type if (type === 'inline-text') { const drawRes = self._drawWxmlInlineText(sub, leftOffset, maxWidth) leftOffset = drawRes.leftOffset topOffset = drawRes.topOffset } else if (type === 'inline-image') { const drawRes = self._drawWxmlImage(sub) || {} leftOffset = drawRes.leftOffset || 0 topOffset = drawRes.topOffset || 0 } }) }) resolve2() }) all.push(p) return all } _drawWxmlInlineText(sub, leftOffset = 0, maxWidth) { let text = sub.dataset.text || '' if (sub.dataset.maxlength && text.length > sub.dataset.maxlength) { text = `${text.substring(0, sub.dataset.maxlength)}...` } const textData = { text, originX: sub.left, x: leftOffset || sub.left, y: sub.top, progress: sub.progress, leftOffset, maxWidth, // 行内元素的最大宽度,取决于limit的宽度 } if (sub.backgroundColor !== 'rgba(0, 0, 0, 0)') { sub.background = sub.backgroundColor } else { sub.background = 'rgba(0, 0, 0, 0)' } if (sub.dataset.background) { sub.background = sub.dataset.background } const res = this._drawText(textData, sub, null, null, 'inline-text', 'wxml') return res } _drawWxmlText(sub, resolve, reject) { let text = sub.dataset.text || '' if (sub.dataset.maxlength && text.length > sub.dataset.maxlength) { text = `${text.substring(0, sub.dataset.maxlength)}...` } const textData = { text, x: sub.left, y: sub.top, progress: sub.progress, } if (sub.backgroundColor !== 'rgba(0, 0, 0, 0)') { sub.background = sub.backgroundColor } else { sub.background = 'rgba(0, 0, 0, 0)' } if (sub.dataset.background) { sub.background = sub.dataset.background } this._drawText(textData, sub, resolve, reject, 'text', 'wxml') } _drawWxmlImage(sub, resolve, reject) { const imageData = { url: sub.dataset.url, x: sub.left, y: sub.top, progress: sub.progress, } const res = this._drawRect(imageData, sub, resolve, reject, 'image', 'inline-wxml') return res } _drawWxmlCircleImage(sub, resolve, reject) { const imageData = { url: sub.dataset.url, x: sub.left, y: sub.top, progress: sub.progress, } sub.r = sub.width / 2 this._drawCircle(imageData, sub, resolve, reject, true, 'wxml') } _drawWxmlBackgroundImage(sub, resolve, reject) { let url = sub.dataset.url const index = this._findPicIndex(url) url = index > -1 ? this.allPic[index].local : url const size = sub.backgroundSize.replace(/px/g, '').split(' ') const imageData = { url, x: sub.left, y: sub.top, progress: sub.progress, } this._drawRect(imageData, sub, resolve, reject, 'image', 'wxml') } _getWxml(item, style) { const self = this let query // if (this.obj) { // query = wx.createSelectorQuery().in(this.obj); // } else { // query = wx.createSelectorQuery(); // } if (this.object) { query = wx.createSelectorQuery().in(this.object) } else { query = wx.createSelectorQuery() } const p1 = new Promise((resolve, reject) => { // 会触发两次,要限制 let count = 0 query .selectAll(`${item.class}`) .fields( { dataset: true, size: true, rect: true, computedStyle: [ 'width', 'height', 'font', 'fontSize', 'fontFamily', 'fontWeight', 'fontStyle', 'textAlign', 'color', 'lineHeight', 'border', 'borderColor', 'borderStyle', 'borderWidth', 'verticalAlign', 'boxShadow', 'background', 'backgroundColor', 'backgroundImage', 'backgroundPosition', 'backgroundSize', 'paddingLeft', 'paddingTop', 'paddingRight', 'paddingBottom', ], }, (res) => { if (count++ === 0) { const formated = self._formatImage(res) const list = formated.list res = formated.res self ._preloadImage(list) .then((result) => { resolve(res) }) .catch((res) => { reject && reject({ errcode: 1009, errmsg: 'drawWxml preLoadImage error' }) }) } }, ) .exec() }) const p2 = new Promise((resolve, reject) => { if (!item.limit) { resolve({ top: 0, width: self.width / self.zoom }) } query .select(`${item.limit}`) .fields( { dataset: true, size: true, rect: true, }, (res) => { resolve(res) }, ) .exec() }) return Promise.all([p1, p2]) } _getLineHeight(style) { let zoom = this.zoom if (style.dataset && style.dataset.type) { zoom = 1 } let lineHeight if (!isNaN(style.lineHeight) && style.lineHeight > style.fontSize) { lineHeight = style.lineHeight } else { style.lineHeight = `${style.lineHeight || ''}` lineHeight = +style.lineHeight.replace('px', '') lineHeight = lineHeight || (style.fontSize || 14) * 1.2 } return lineHeight * zoom } _formatImage(res = []) { const list = [] res.forEach((item, index) => { const dataset = item.dataset const uid = Util.getUid() const filename = `${wx.env.USER_DATA_PATH}/${uid}.png` if ((dataset.type === 'image' || dataset.type === 'radius-image') && dataset.url) { const sub = { url: dataset.base64 ? filename : dataset.url, isBase64: dataset.base64 ? dataset.url : false, } res[index].dataset = Object.assign(res[index].dataset, sub) list.push(sub) } else if (dataset.type === 'background-image' && item.backgroundImage.includes('url')) { const url = item.backgroundImage.replace(/url\(("|')?/, '').replace(/("|')?\)$/, '') const sub = { url: dataset.base64 ? filename : url, isBase64: dataset.base64 ? url : false, } res[index].dataset = Object.assign(res[index].dataset, sub) list.push(sub) } }) return { list, res } } _updateProgress(distance) { this.progressPercent += distance this.progress(this.progressPercent) } _sortListByTop(list = []) { const sorted = {} // 粗略地认为2px相差的元素在同一行 list.forEach((item, index) => { let top = item.top if (!sorted[top]) { if (sorted[top - 2]) { top = top - 2 } else if (sorted[top - 1]) { top = top - 1 } else if (sorted[top + 1]) { top = top + 1 } else if (sorted[top + 2]) { top = top + 2 } else { sorted[top] = [] } } sorted[top].push(item) }) return sorted } _parseNumber(number) { return isNaN(number) ? +(number || '').replace('px', '') : number } _transferWxmlStyle(sub, item, limitLeft, limitTop) { const leftFix = +sub.dataset.left || 0 const topFix = +sub.dataset.top || 0 sub.width = this._parseNumber(sub.width) sub.height = this._parseNumber(sub.height) sub.left = this._parseNumber(sub.left) - limitLeft + (leftFix + (item.x || 0)) * this.zoom sub.top = this._parseNumber(sub.top) - limitTop + (topFix + (item.y || 0)) * this.zoom let padding = sub.dataset.padding || '0 0 0 0' if (typeof padding === 'string') { padding = Util.transferPadding(padding) } const paddingTop = Number(sub.paddingTop.replace('px', '')) + Number(padding[0]) const paddingRight = Number(sub.paddingRight.replace('px', '')) + Number(padding[1]) const paddingBottom = Number(sub.paddingBottom.replace('px', '')) + Number(padding[2]) const paddingLeft = Number(sub.paddingLeft.replace('px', '')) + Number(padding[3]) sub.padding = [paddingTop, paddingRight, paddingBottom, paddingLeft] return sub } /** * 支持负值绘制,从右边计算 * @param {*} item * @param {*} style */ _resetPositionX(item, style) { let zoom = this.zoom let x = 0 if (style.dataset && style.dataset.type) { zoom = 1 } // 通过wxml获取的不需要重置坐标 if (item.x < 0 && item.type) { x = this.width + item.x * zoom - style.width * zoom } else { x = item.x * zoom } if (Number.parseInt(style.borderWidth)) { x += Number.parseInt(style.borderWidth) } return x + this.translateX } /** * 支持负值绘制,从底部计算 * @param {*} item * @param {*} style */ _resetPositionY(item, style, textHeight) { let zoom = this.zoom let y = 0 if (style.dataset && style.dataset.type) { zoom = 1 } if (item.y < 0) { y = this.height + item.y * zoom - (textHeight || style.height * zoom) } else { y = item.y * zoom } if (Number.parseInt(style.borderWidth)) { y += Number.parseInt(style.borderWidth) } return y + this.translateY } /** * 文字的padding、text-align * @param {*} item * @param {*} style * @param {*} textWidth */ _resetTextPositionX(item, style, textWidth, width) { const textAlign = style.textAlign || 'left' let x = item.x if (textAlign === 'center') { x = (width - textWidth) / 2 + item.x } else if (textAlign === 'right') { x = width - textWidth + item.x } const left = style.padding ? style.padding[3] || 0 : 0 return x + left + this.translateX } /** * 文字的padding、text-align * @param {*} item * @param {*} style * @param {*} textWidth */ _resetTextPositionY(item, style, lineNum = 0) { let zoom = this.zoom if (style.dataset && style.dataset.type) { zoom = 1 } const lineHeight = this._getLineHeight(style) const fontSize = Math.ceil((style.fontSize || 14) * zoom) const blockLineHeightFix = ((style.dataset && style.dataset.type) || '').includes('inline') ? 0 : (lineHeight - fontSize) / 2 const top = style.padding ? style.padding[0] || 0 : 0 // y + lineheight偏移 + 行数 + paddingTop + 整体画布位移 return item.y + blockLineHeightFix + lineNum * lineHeight + top + this.translateY } /** * 当文本超过宽度时,计算每一行应该绘制的文本 * @param {*} text * @param {*} width * @param {*} singleLength * @param {*} currentIndex * @param {*} widthOffset */ _getTextSingleLine(text, width, singleLength, currentIndex = 0, widthOffset = 0) { let offset = 0 let endIndex = currentIndex + singleLength + offset let single = text.substring(currentIndex, endIndex) let singleWidth = this.measureWidth(single) while (Math.round(widthOffset + singleWidth) > width) { offset-- endIndex = currentIndex + singleLength + offset single = text.substring(currentIndex, endIndex) singleWidth = this.measureWidth(single) } return { endIndex, single, singleWidth, } } _drawBorder(border, style, callback) { let zoom = this.zoom if (style.dataset && style.dataset.type) { zoom = 1 } border = Util.transferBorder(border) if (border && border.width) { // 空白阴影,清空掉边框的阴影 this._drawBoxShadow() if (border) { this.ctx.setLineWidth(border.width * zoom) if (border.style === 'dashed') { const dash = style.dash || [5, 5, 0] const offset = dash[2] || 0 const array = [dash[0] || 5, dash[1] || 5] this.ctx.setLineDash(array, offset) } this.ctx.setStrokeStyle(border.color) } callback && callback(border) } } _drawBoxShadow(boxShadow, callback) { boxShadow = Util.transferBoxShadow(boxShadow) if (boxShadow) { this.ctx.setShadow(boxShadow.offsetX, boxShadow.offsetY, boxShadow.blur, boxShadow.color) } else { this.ctx.setShadow(0, 0, 0, '#ffffff') } callback && callback(boxShadow || {}) } _setFill(fill, callback) { if (fill) { if (typeof fill === 'string') { this.ctx.setFillStyle(fill) } else { const line = fill.line const color = fill.color const grd = this.ctx.createLinearGradient(line[0], line[1], line[2], line[3]) grd.addColorStop(0, color[0]) grd.addColorStop(1, color[1]) this.ctx.setFillStyle(grd) } callback && callback() } } _setStroke(stroke, callback) { if (stroke) { if (typeof stroke === 'string') { this.ctx.setStrokeStyle(stroke) } else { const line = stroke.line const color = stroke.color const grd = this.ctx.createLinearGradient(line[0], line[1], line[2], line[3]) grd.addColorStop(0, color[0]) grd.addColorStop(1, color[1]) this.ctx.setStrokeStyle(grd) } callback && callback() } } } if (!exports.__esModule) Object.defineProperty(exports, '__esModule', { value: true }) exports.default = Wxml2Canvas }, (modId) => { const map = { './util': 1685064731948 } return __REQUIRE__(map[modId], modId) }, ) __DEFINE__( 1685064731948, (require, module, exports) => { /** * 获取字符的长度,full为true时,一个汉字算两个长度 * @param {string} str * @param {boolean} full */ function getTextLength(str, full) { let len = 0 for (let i = 0; i < str.length; i++) { const c = str.charCodeAt(i) // 单字节加1 if ((c >= 0x0001 && c <= 0x007E) || (c >= 0xFF60 && c <= 0xFF9F)) { len++ } else { len += full ? 2 : 1 } } return len } /** * rgba(255, 255, 255, 1) => #ffffff * @param {string} color */ function transferColor(color = '') { let res = '#' color = color.replace(/^rgba?\(/, '').replace(/\)$/, '') color = color.split(', ') color.length > 3 ? (color.length = 3) : '' for (let item of color) { item = Number.parseInt(item || 0) if (item < 10) { res += `0${item}` } else { res += item.toString(16) } } return res } function transferBorder(border = '') { const res = border.match(/(\w+)px\s(\w+)\s(.*)/) let obj = {} if (res) { obj = { width: +res[1], style: res[2], color: res[3], } } return res ? obj : null } /** * 内边距,依次为上右下左 * @param {*} padding */ function transferPadding(padding = '0 0 0 0') { padding = padding.split(' ') for (let i = 0, len = padding.length; i < len; i++) { padding[i] = +padding[i].replace('px', '') } return padding } /** * type1: 0, 25, 17, rgba(0, 0, 0, 0.3) * type2: rgba(0, 0, 0, 0.3) 0px 25px 17px 0px => (0, 25, 17, rgba(0, 0, 0, 0.3)) * @param {*} shadow */ function transferBoxShadow(shadow = '', type) { if (!shadow || shadow === 'none') return let color let split split = shadow.match(/(\w+)\s(\w+)\s(\w+)\s(rgb.*)/) if (split) { split.shift() shadow = split color = split[3] || '#ffffff' } else { split = shadow.split(') ') color = `${split[0]})` shadow = split[1].split('px ') } return { offsetX: +shadow[0] || 0, offsetY: +shadow[1] || 0, blur: +shadow[2] || 0, color, } } function getUid(prefix) { prefix = prefix || '' return ( prefix + 'xxyxxyxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0 const v = c === 'x' ? r : (r & 0x3) | 0x8 return v.toString(16) }) ) } if (!exports.__esModule) Object.defineProperty(exports, '__esModule', { value: true }) exports.default = { getTextLength, transferBorder, transferColor, transferPadding, transferBoxShadow, getUid, } }, (modId) => { const map = {} return __REQUIRE__(map[modId], modId) }, ) return __REQUIRE__(1685064731946) })() // miniprogram-npm-outsideDeps=[] // # sourceMappingURL=index.js.map