2026-04-15 10:26:13
告别服务器!小程序纯前端“图片转 PDF”工具,隐私安全又高效
背景与痛点
隐私担忧:用户上传图片到服务器进行转换时,担心图片内容泄露,尤其是个人证件、私密照片等敏感内容。
网络依赖与效率:转换过程需要频繁与服务器交互,在弱网环境下速度慢、不稳定,甚至可能因上传大文件而失败。
服务器成本:每一次转换都意味着服务器资源的消耗(存储、计算、带宽),对于开发者而言,成本不容忽视。
核心思路
图片本地化处理:利用小程序强大的本地文件系统能力,将用户选择的图片读取到本地临时路径。
PDF 文档构建:引入功能丰富的 JavaScript 库 pdf-lib,在小程序运行时直接在前端环境创建和操作 PDF 文件。
最终文件保存:将 pdf-lib 生成的 PDF 数据流保存为本地文件,供用户直接预览或分享。
技术核心
pdf-lib 的引入与应用
库的引入:将 pdf-lib 的小程序兼容版本(通常是 pdf-lib.min.js)放置在项目目录中,并通过 require 引入。
转换逻辑概览
图片预处理:获取每张图片的尺寸、类型,并将其读取为 Base64 格式。
创建 PDF 文档:初始化一个空的 PDFDocument 对象。
逐页添加图片:遍历所有图片,为每张图片创建一个新的 PDF 页面,并根据图片的原始尺寸和类型嵌入到 PDF 中,进行智能缩放、居中。对于横向图片,自动旋转页面 90 度以适应 A4 纸张。
生成与保存:将构建好的 PDF 文档保存为 Base64 编码的字符串,再通过小程序文件系统的 fs.writeFile 接口写入到本地的临时文件路径。
返回结果:将生成的 PDF 文件本地路径返回给业务层,用于后续的预览或分享。
核心代码
img2pdf.js
const { PDFDocument, degrees, PageSizes } = require('./pdf-lib.min.js');const fs = wx.getFileSystemManager();export async function img2pdf(urls) { if (typeof urls == 'string') { urls = [urls]; } const imageInfo = urls.map((url) => { return wx.getImageInfo({ src: url }); }); const imageInfoRes = await Promise.all(imageInfo); const imageBase64 = urls.map((url) => { return readFile(url, "base64"); }); const imageBase64Res = await Promise.all(imageBase64); const pdfDoc = await PDFDocument.create(); for (let i = 0; i < imageInfoRes.length; i++) { const { type, width, height } = imageInfoRes[i]; let pdfImage = ""; if (type === 'jpeg') { pdfImage = await pdfDoc.embedJpg(imageBase64Res[i]); } else if (type === 'png') { pdfImage = await pdfDoc.embedPng(imageBase64Res[i]); } const page = pdfDoc.addPage(PageSizes.A4); const { width: pageWidth, height: pageHeight } = page.getSize(); let drawOptions = {}; if (width > height) { const scaled = pdfImage.scaleToFit(pageHeight, pageWidth); drawOptions = { x: (pageWidth - scaled.height) / 2, y: (pageHeight - scaled.width) / 2 + scaled.width, width: scaled.width, height: scaled.height, rotate: degrees(270), }; } else { const scaled = pdfImage.scaleToFit(pageWidth, pageHeight); drawOptions = { x: (pageWidth - scaled.width) / 2, y: (pageHeight - scaled.height) / 2, width: scaled.width, height: scaled.height, }; } page.drawImage(pdfImage, drawOptions); } const docBase64 = await pdfDoc.saveAsBase64(); const timestamp = Date.now(); const pdfPath = await base64ToFile(docBase64, `/${timestamp}.pdf`); return pdfPath;}function base64ToFile(base64, fileName) { const { promise, resolve, reject } = Promise.withResolvers(); const filePath = wx.env.USER_DATA_PATH + fileName; fs.writeFile({ filePath, data: base64, encoding: "base64", success: res => { resolve(filePath); }, fail: err => { reject(err); } }); return promise;}function readFile(filePath, encoding = 'utf8') { const { promise, resolve, reject } = Promise.withResolvers(); fs.readFile({ filePath, encoding, success(fileRes) { resolve(fileRes.data); }, fail(err) { reject(err); } }); return promise;}小程序端应用示例
import { img2pdf } from '../../utils/img2pdf';Page({ data: { selectedImages: [], pdfPath: '', loading: false }, async chooseImage() { const { tempFiles } = await wx.chooseMedia({ count: 9, mediaType: ['image'], sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], }); this.setData({ selectedImages: tempFiles.map(file => file.tempFilePath) }); }, async convertToPdf() { if (this.data.selectedImages.length === 0) { wx.showToast({ title: '请先选择图片', icon: 'none' }); return; } this.setData({ loading: true }); wx.showLoading({ title: '转换中...' }); try { const pdfFilePath = await img2pdf(this.data.selectedImages); this.setData({ pdfPath: pdfFilePath }); wx.hideLoading(); wx.showToast({ title: '转换成功!', icon: 'success' }); wx.openDocument({ filePath: pdfFilePath, fileType: 'pdf', success: res => console.log('打开 PDF 成功', res), fail: err => console.error('打开 PDF 失败', err) }); } catch (error) { wx.hideLoading(); wx.showToast({ title: '转换失败!', icon: 'error' }); console.error('图片转 PDF 发生错误', error); } finally { this.setData({ loading: false }); } }})经验总结与注意事项
文件体积与性能:pdf-lib 库本身有一定体积(通常在几百 KB),会增加小程序包体大小。图片数量越多、分辨率越高,转换耗时越长,内存占用越大。建议在选择图片时提示用户合理数量或适当压缩。
pdf横向图片旋转:需要额外计算和处理,可能会略微增加复杂性。如果觉得复杂,也可以直接判断图片是否是纵向,如果是横向使用 canvas 旋转图片,逻辑上就比较简单了。
Promise.withResolvers() 兼容性:代码使用了 Promise.withResolvers(),目前大多数小程序环境和浏览器中兼容性可能不好,需要自行做兼容处理。
本地文件系统限制:wx.env.USER_DATA_PATH 路径下的文件是小程序沙箱环境特有的,用户无法直接在系统文件管理器中找到。生成的文件是临时文件,小程序关闭或长时间不用可能被系统清理。如果需要长期保存,需引导用户通过 wx.saveFile (保存到相册或本地文件) 或上传云存储。
图片类型支持:pdf-lib 主要支持 JPEG 和 PNG 格式。其他格式(如 WebP、GIF)需要先转换为 JPEG/PNG 再进行嵌入,可以利用 canvas 实现。
写在最后
纯前端实现“图片转 PDF”功能,不仅提升了用户体验,更重要的是有效保护了用户的数字隐私。这在追求用户信任和数据安全的小程序生态中,无疑是一个值得推广的实践。