ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

小程序图片转Base64:彻底解决HTML转PDF图片丢失问题

小程序图片转Base64:彻底解决HTML转PDF图片丢失问题 做小程序开发这几年但凡涉及“生成 PDF 报告”“导出电子合同”“分享带图卡片”十有八九都会撞上同一个难题图片在页面上显示得好好的一进 PDF 就消失或者只有一行排版的“幽灵”占位符。踩过几次坑之后我才彻底明白这个小程序里的图片想lnthtml 转 PDF基本绕不开 base64 这条路。今天就把这个问题的来龙去脉、各种转换方案、以及我在 SelectPdf 上踩过的坑一次性讲清楚。1. 问题本质为什么 PDF 引擎读不到小程序里的图片先别急着写代码搞清楚底层原因比啥都重要。你用 SelectPdf 这类服务端渲染库去转换 PDF 时它本质上是一个独立于小程序环境的 HTML 渲染引擎。这个引擎执行 JS、解析 CSS、加载资源全都发生在你的服务器上。它和你的小程序客户端隔着整个网络。1.1 小程序图片的三种来源后端一个都拿不到小程序里的图片资源无非这三种来源网络 URLhttps://your-cdn.com/images/logo.png。理论上后端能访问但现实中往往被防盗链、跨域策略、临时签名失效挡住而且如果这张图来自小程序云存储URL 里多半带动态签名转 PDF 那一刻可能已经过期。本地临时文件wxfile://tmp_xxx/photo.jpg。这是小程序最常用的方式场景里拍照、选图后得到的都是这种本地路径。可这个路径是客户端文件系统里的路径服务器端 SelectPdf 连你这台电脑的文件都读不到更何况是千里之外的手机沙箱。云文件 IDcloud://env-id.xxxx/xxx.png。这个更特殊只有小程序端通过云能力才能解析后端拿到的就是一个字符串 ID无法直接当图片 URL 用。所以你会看到一种诡异现象在小程序里用 web-view 预览那个 HTML 时图片正常显示因为 web-view 在客户端运行能访问本地文件但一旦把同样的 HTML 字符串 POST 给后端 SelectPdf图片全挂。1.2 PDF 渲染引擎的加载机制要理解怎么办先得理解 SelectPdf 这类引擎的工作方式。它会解析 HTML 字符串构建 DOM碰到img标签时根据src属性去发起资源请求。如果src是wxfile://开头引擎根本不认识这个协议如果src是相对路径/images/a.png引擎会尝试基于一个 BaseUrl 去拼接但你显然没给它配如果是公网 URL 但图片服务器有防盗链Referer 校验失败就直接返回 403。总而言之但凡图片路径存在一点不确定性最终 PDF 里就是一片空白。这也就是标题里那句“图片必须编码”的由来——不是玄学是传输链路决定的。1.3 base64 为什么能成为终极解决方案base64 本质上是用 64 个可打印字符来表示二进制数据。一张图片的二进制内容经过编码后变成一串字符然后以内联方式塞进 HTMLimg srcdata:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD... /data:URI 是 RFC 2397 定义的方案浏览器的渲染引擎见到这种src不需要发任何网络请求直接解码字符串里的二进制数据并渲染。不管你是本地临时文件、网络图片还是云文件只要在小程序端把它转成了 base64 字符串塞进 HTML 里后端 SelectPdf 就一定能渲染出图片来。这个方案跨平台、跨语言、没有防盗链问题笨但是绝对可靠。2. 小程序图片转 base64 的三种实操方案在实际开发中不同场景的图片得用不同姿势去转 base64。我总结下来就是三板斧读临时文件、下载网络图再读、canvas 绘制导出。2.1 方案一FileSystemManager 读取本地临时文件如果你手里的图片已经在小程序的本地文件系统里比如wx.chooseImage、wx.chooseMedia返回的tempFilePath直接用FileSystemManager.readFile指定编码为base64就能拿到 base64 字符串。const fs wx.getFileSystemManager(); function fileToBase64(filePath) { return new Promise((resolve, reject) { fs.readFile({ filePath: filePath, encoding: base64, success(res) { resolve(res.data); // 这里是纯 base64 字符串没有 data:image 前缀 }, fail(err) { reject(err); } }); }); } // 使用示例选择图片后马上转 base64 wx.chooseMedia({ count: 1, mediaType: [image], success: async (res) { const tempFilePath res.tempFiles[0].tempFilePath; const base64 await fileToBase64(tempFilePath); console.log(base64); } });注意这里有个细节readFile返回的base64字符串是不含data:image/jpeg;base64,前缀的。你得自己拼上图片的 MIME 类型才能变成 HTML 能识别的 data URIfunction buildDataUri(base64, mimeType image/jpeg) { return data:${mimeType};base64,${base64}; }MIME 类型怎么拿可以根据文件后缀判断.jpg-image/jpeg.png-image/png.gif-image/gif.webp-image/webp。如果你用wx.chooseMedia返回的tempFiles[0].fileType也可以作为参考。2.2 方案二网络图片先下载再转 base64如果图片本身是网络 URL直接转 base64 需要在服务端解决防盗链小程序端最稳妥的办法是先用wx.downloadFile把图片下载成临时文件然后再走方案一的readFile。function downloadFileToBase64(url) { return new Promise((resolve, reject) { wx.downloadFile({ url: url, success: async (res) { if (res.statusCode 200) { try { const base64 await fileToBase64(res.tempFilePath); resolve({ tempFilePath: res.tempFilePath, base64 }); } catch (e) { reject(e); } } else { reject(new Error(下载失败HTTP ${res.statusCode})); } }, fail: reject }); }); }为什么先下载再读取因为wx.downloadFile帮你绕过了很多浏览器环境和后端环境的限制。小程序内部有自己的一套网络栈能处理一些特殊域名证书、跳过跨域限制前提是在后台配置了合法域名。下载成功后图片就变成了本地临时文件再用readFile转 base64 就顺理成章。注意wx.downloadFile有 10MB 的单文件大小限制iOS/Android 略有差异超过会走 fail 回调。真机测试时尤其要留意大图场景。2.3 方案三canvas 重绘后导出 dataURL有些场景下图片不能直接读取文件内容比如你从后端拿到的是一张需要加水印合成的图片或者你想在导出 PDF 前把图片压缩一下。这时候可以用wx.createOffscreenCanvas或者传统的canvas组件把图片绘制到画布上再通过wx.canvasToDataURL导出。function drawImageToDataUrl(imagePath, { width 750, height 750 } {}) { return new Promise((resolve, reject) { const offscreenCanvas wx.createOffscreenCanvas({ type: 2d, width, height }); const ctx offscreenCanvas.getContext(2d); const img offscreenCanvas.createImage(); img.onload () { ctx.clearRect(0, 0, width, height); // 等比缩放绘制 const scale Math.min(width / img.width, height / img.height); const dw img.width * scale; const dh img.height * scale; const dx (width - dw) / 2; const dy (height - dh) / 2; ctx.drawImage(img, dx, dy, dw, dh); const dataUrl offscreenCanvas.toDataURL(image/jpeg, 0.8); resolve(dataUrl); }; img.onerror reject; img.src imagePath; }); }这里产出的dataUrl是完整的data:image/jpeg;base64,...格式可以直接拼接进 HTML。这种方式最大的好处是可以顺便压缩图片把 2MB 的图压到 200KB后面请求后端接口时压力小很多。2.4 三种方案怎么选一张表讲清楚场景首选方案原因拍照/相册选图后的临时文件方案一直接读文件开销最小网络图片、CDN 图片方案二先下载解决防盗链再转码需要压缩、加水印、裁剪方案三顺便处理图片一举两得云文件 ID先换 https 链接再走方案二wx.cloud.getTempFileURL换临时链接我个人的习惯是只要图片不是特别大一律先走 canvas 压缩到 80% 质量再编码。省下来的流量和时间在弱网环境下体感差异非常明显。3. SelectPdf 集成与图片渲染完整实操当你手里已经有了一堆 base64 字符串接下来要做的就是把它们拼进 HTML交给 SelectPdf 转 PDF。这一节我会给出一套能直接跑通的完整流程。3.1 SelectPdf 的基本定位和工作原理SelectPdf 是一个 .NET 平台的 HTML 转 PDF 组件它基于自家的渲染内核能解析 HTML CSS JavaScript生成 PDF 文件。它解决的核心痛点是PDF 排版极难手工控制而 HTML/CSS 排版有天然优势写完页面模板直接转 PDF省去报表引擎那一大堆代码。使用它非常直观核心就是一个HtmlToPdf类using SelectPdf; var converter new HtmlToPdf(); var pdfDoc converter.ConvertHtmlString(htmlContent); pdfDoc.Save(output.pdf); pdfDoc.Close();你可能会问为什么不在前端用 html2canvas jsPDF老实说小程序环境的 DOM 模型和浏览器差别很大html2canvas 在小程序里水土不服而 jsPDF 是一行行手动追加内容做复杂排版能写到怀疑人生。服务端 SelectPdf 用 HTML CSS 控制样式模板复用度高后端还能顺手加页眉页脚、页码水印所以在正经业务里我更推荐这个链路。3.2 转换前的图片压缩与编码处理回到小程序端。假设用户在小程序里填完一份体检报告里面有一个指标异常提示图、一个趋势图canvas 绘制。你需要把这些图都转成 base64然后塞进待传给后端的 JSON 里。async function buildPdfPayload(formData, images) { const imageDataUris []; for (let i 0; i images.length; i) { const imgInfo images[i]; let dataUri ; if (imgInfo.type temp) { const b64 await fileToBase64(imgInfo.path); dataUri data:${imgInfo.mime};base64,${b64}; } else if (imgInfo.type network) { const res await downloadFileToBase64(imgInfo.url); dataUri data:${imgInfo.mime};base64,${res.base64}; } else if (imgInfo.type canvas) { dataUri await drawImageToDataUrl(imgInfo.path, { width: 600, height: 400 }); } imageDataUris.push(dataUri); } return { ...formData, htmlContent: renderReportHtml(formData, imageDataUris) }; }renderReportHtml就用模板字符串把图片的 data URI 嵌进去function renderReportHtml(formData, imageDataUris) { const imageTags imageDataUris.map((uri, idx) { return img src${uri} stylemax-width:100%;margin:10px 0; /; }).join(); return !DOCTYPE html html head meta charsetutf-8 / style body { font-family: PingFang SC, Microsoft YaHei, sans-serif; padding: 20px; color: #333; } h1 { text-align: center; border-bottom: 2px solid #1890ff; padding-bottom: 10px; } .info-row { display: flex; justify-content: space-between; margin: 8px 0; } .highlight { color: #e6a23c; font-weight: bold; } /style /head body h1${formData.title}/h1 div classinfo-rowspan姓名/spanspan${formData.name}/span/div div classinfo-rowspan报告日期/spanspan${formData.date}/span/div div classinfo-rowspan异常指标/spanspan classhighlight${formData.alertCount} 项/span/div ${imageTags} /body /html; }然后把htmlContent通过wx.request发给后端wx.request({ url: https://your-server.com/api/convert, method: POST, data: { html: htmlContent }, success(res) { if (res.statusCode 200) { // 拿到 PDF 文件临时路径 const pdfPath res.data.pdfPath; wx.openDocument({ filePath: pdfPath, fileType: pdf }); } } });3.3 服务端 C# 接收 HTML 并生成 PDF服务端这边我用 ASP.NET Core 写了个接口接收 JSON 里的 HTML调用 SelectPdf 转换。要注意几个关键配置[HttpPost(api/convert)] public IActionResult ConvertPdf([FromBody] PdfRequest request) { var converter new HtmlToPdf(); // 关键配置 converter.Options.PdfPageSize PdfPageSize.A4; converter.Options.PdfPageOrientation PdfPageOrientation.Portrait; converter.Options.MarginLeft 20; converter.Options.MarginRight 20; converter.Options.MarginTop 20; converter.Options.MarginBottom 20; converter.Options.WebPageWidth 750; // 按小程序设计稿宽度渲染 converter.Options.WebPageHeight 0; // 0 表示高度自适应 // 渲染 HTML var doc converter.ConvertHtmlString(request.Html); // 输出到内存流 using var ms new MemoryStream(); doc.Save(ms); doc.Close(); var pdfBytes ms.ToArray(); return File(pdfBytes, application/pdf, report.pdf); }WebPageWidth我建议设成 750 或者和你的小程序页面宽度一致。SelectPdf 渲染页面时会把 HTML 当成一个 750px 宽的网页来排版这样图片的max-width: 100%、流式布局才会有正确的视觉比例。如果默认 1024有些挤压效果或者换行位置会有偏差。3.4 图片格式与 base64 编码的细节控制SelectPdf 对图片格式的兼容性不错JPG、PNG、WebP 基本都能正确处理。但有几个细节图片格式统一成 JPG 或 PNG 就够了。小程序里经常会遇到image/gif转 PDF 时 gif 动图只会取第一帧而且 base64 体积很大。如果只是静态展示建议在 canvas 方案里强制转成 JPEG。base64 字符串的完整性。后端接到的 base64 可能有换行符、空格SelectPdf 解析 data URI 时比较挑剔。你可以在后端做个清理request.Html Regex.Replace(request.Html, data:image/[^;];base64,([^])(?|), m { var clean m.Groups[1].Value.Replace(\r, ).Replace(\n, ).Replace( , ); return $data:image/jpeg;base64,{clean}; });我遇到过非常诡异的问题前端传给后端时请求体里 base64 被encodeURIComponent了一遍后端忘了decodeURIComponent导致data:image/jpeg;base64,%2F9j%2F...SelectPdf 当然认不出来。这种问题排查起来很烦最好在前端发送前就约定好HTML 原文传不转义后端收到直接进转换器。base64 体积暴涨 33%接口要提前做好预案。原始图片 1MB转 base64 后约 1.37MB。如果一个 PDF 里有 5 张这样的图POST 体积就接近 7MB。很多网关默认有 1MB/10MB 的请求体限制线上环境要确认 nginx 的client_max_body_size和后端框架的MaxRequestBodySize否则会出现“小图正常大图 413”的诡异问题。4. 常见问题与排查技巧实录这段是我最想写的因为光是“图片转 base64 后 PDF 里还是不显示”这一个问题我就在生产环境折腾过整整一天。下面把典型的坑和排查思路都列出来。4.1 图片不显示但 HTML 里明明有 data URI现象后端日志里能看到 HTML 里有data:image/jpeg;base64,...但 PDF 输出中图片位置是空白。排查步骤把后端收到的 HTML 字符串原样保存成.html文件用 Chrome 打开。这是最重要的一步——先确认 HTML 本身没问题。如果 Chrome 里也空白说明 base64 数据本身就是坏的问题出在前端编码环节。检查 base64 前缀里的 MIME 是否和真实文件类型匹配。常见错误是PNG 图片却标注了data:image/jpeg某些渲染内核会比较严格按 jpeg 解码 png 数据直接失败。检查 base64 里是否混入了\n、\r、空格。理论上 data URI 里不应该有这些字符。部分库能容忍SelectPdf 我实测下来对它很敏感清理干净最稳。我踩过的具体例子小程序端用了wx.getFileSystemManager().readFile的encoding: base64但图片路径是云文件 IDcloud://...readFile 直接报错我当时没接fail回调结果 base64 是一个空字符串页面和 HTML 都“正常”就是没图。4.2 图片在部分手机上正常部分手机空白现象同样一套代码iOS 上生成 PDF 有图Android 上没有。这种问题大多数出在图片路径的时效性上。wx.downloadFile的临时文件在onUnload后基本就没了如果用户从选择图片页面跳转到预览页面两个页面之间通过全局变量存了tempFilePath但底层的临时文件已经被回收那你readFile时拿到的是已经失效的路径。解决办法在拿到临时文件后立即转 base64不要存路径只存 base64 字符串。这样图片数据就变成了内存字符串和文件生命周期无关了。// 错误做法只存路径下次页面再用 globalData.tempImagePath tempFilePath; // 正确做法立刻转 base64 存起来 globalData.tempImageBase64 await fileToBase64(tempFilePath);4.3 base64 太大导致请求超时或内存暴涨现象图片一多小程序端wx.request直接fail超时或者后端进程内存突然飙高。这不仅是网络问题还是性能问题。base64 文本在 JSON 序列化/反序列化时会被复制多份内存图片数据动辄几 MB在小程序这种 JSCore 环境下很容易触发内存告警。我的处理思路是分级优化第一级canvas 压缩。把长边压到 800px、质量 80%肉眼基本看不出差异体积却能缩小 70% 以上。 第二级PDF 不需要透明通道的场景全部转 JPEG不要用 PNG。PNG 的 base64 膨胀率更高。 第三级大图拆分请求不要一次性把 10 张图塞一个请求里按 3~4 张一批后端分页合成 PDF。4.4 防盗链与 Referer 校验的坑小程序端wx.downloadFile的网络栈是白名单制的所以能下载的图基本都是合法域名。但有时候你从某个图片 CDN 下载没问题后端 SelectPdf 直接访问原图 URL 却被 403这就是防盗链。遇到这种情况不必和后端去纠结配 Referer 白名单小程序端直接把图下载转成 base64 就完事了。base64 内联进 HTML 后SelectPdf 不会再去发图片请求防盗链规则形同虚设。这是我强烈推荐“先下载再转码”的核心理由。4.5 临时文件堆满存储空间小程序端每downloadFile一次都会在用户设备上残留临时文件。如果用户高频操作临时文件积累多了会占满存储空间。虽然wx.downloadFile返回的tempFilePath会在小程序退出时清理但同一会话内频繁生成也会有问题。实操建议转完 base64 以后主动清理临时文件const fs wx.getFileSystemManager(); // 不需要的临时文件直接删掉 try { fs.unlinkSync(tempFilePath); } catch (e) { // 忽略删除失败临时文件后续还会被系统回收 }4.6 常见问题速查表症状可能原因快速解决PDF 图片空白但 HTML 正常base64 含换行空格、MIME 类型不对后端正则清理严格校验 MIME部分机型失败临时文件被回收拿到路径立刻转 base64别存路径请求 413 或超时base64 体积过大、网关限制压缩图片、分批提交、调大请求体限制图片拉伸变形canvas 绘制时未等比缩放用Math.min计算缩放比例居中绘制图片模糊原图分辨率低、canvas 导出尺寸小提高 canvas 尺寸到 2 倍设置 quality 0.9WebP 格式异常SelectPdf 对部分 WebP 解码兼容性差统一转 JPEG5. 这套流程还能怎么扩展从单图到批量 PDF 的工程化改造前面讲的都是单份 PDF 的生成流程。真实业务里往往是一个订单下有多个报告或者一个批次要生成几百份合同。在这个基础上我对这个方案又做了几层工程化改造也算是给读者一条进阶路径。5.1 模板与数据分离小程序的 HTML 模板不要硬编码在后端 C# 里也不要在前端字符串拼建议统一放到后端模板管理表里。小程序端只传业务数据name、date、images 数组后端用 Razor 模板引擎或者简单的字符串模板去渲染最终 HTML。好处是排版的调整不需要发版小程序只更新服务端模板就行。5.2 批量生成 异步任务队列当图片数量巨大时同步ConvertHtmlString会长时间占用后端线程。建议引入消息队列比如 RabbitMQ 或者简单的 Redis 队列。请求进来先返回“任务ID”后台 Worker 逐份渲染 PDF完成后推送到小程序。小程序的体验就是用户点了“生成报告”页面出现一个“处理中”的进度条1~2 秒后服务端主动推送 PDF 地址再wx.openDocument打开。比同步卡死优雅太多。5.3 PDF 的页眉页脚、页码、水印SelectPdf 这块做得比较完善。合同场景下页脚可以放页码、公司电话页眉放公司 logo这个 logo 又是一张图同样转 base64。水印可以在converter.Options里配置也可以在 HTML 里用 CSS 画一个半透明背景层。我实测下来CSS 水印在 PDF 渲染里很稳定多页内容会重复出现在每页上效果比组件自带的更可控。6. 写在最后的一点经验如果要在这一堆实操里挑一句最想对后来人说的别在小程序端做任何依赖“文件路径”的持久化图片数据必须在你还在当前页面环境时立刻转化为 base64。这个原则能帮你规避掉上面 90% 的坑。另外大家可能也发现了整个链路里所有图片在服务端看来都不是“文件”而是“字符串”——这其实是 PDF 生成领域的一种稳定哲学把资源内联化消除外部依赖。小到一篇文章大到一份合同只要图片全部以 base64 内嵌PDF 生成器就成了一个无状态引擎不会因为路径、权限、防盗链、过期时间这些变量而翻车。最后再补充一个个人习惯小程序端每次转完 base64 后顺手把生成 HTML 的片段打个日志打印出来挑几个字符看一眼前缀是不是data:image。这个动作只要 5 秒钟却能让你在后续“图片又没了”的排查里少走一个小时弯路。祝你一次跑通再也不被图片空白问题折磨。
返回列表