阅读视图

发现新文章,点击刷新页面。

拒绝卡顿!小程序图片本地“极速”旋转与格式转换,离屏 Canvas 性能调优实战

1. 背景与痛点:高清大图的“崩溃”瞬间

在开发小程序图片工具时,我们经常面临“两难”境地:

  1. 用户上传原图:现代手机拍摄的照片动辄 4000x3000 分辨率,在 iOS 设备上 DPR(设备像素比)通常为 3。
  2. 内存爆炸:如果直接按原图渲染,画布像素高达 (4000*3) * (3000*3) ≈ 1亿像素!这远超小程序的 Canvas 内存限制,导致微信客户端直接闪退
  3. 传统方案弊端:上传服务器处理费流量且慢;普通 Canvas 渲染又卡顿界面。

为了解决这个问题,我们打磨出了一套基于 OffscreenCanvas 的高性能本地处理方案,核心在于“智能计算,动态降级”。

2. 核心思路:离屏渲染 + 智能防爆

我们的方案包含两个关键技术点:

  1. OffscreenCanvas(2D 离屏画布): 相比传统 Canvas,它在内存中渲染,不占用 DOM,没有任何 UI 开销,绘图指令执行极快。

  2. 智能 DPR 限制(核心黑科技): 这是防止闪退的关键。我们在绘制前计算“目标画布尺寸”。

    • 判断:如果 逻辑尺寸 * 系统DPR 超过了安全阈值(如 4096px)。
    • 降级:强制降低使用的 DPR 值,确保最终纹理尺寸在安全范围内。
    • 结果:牺牲肉眼难以察觉的极微小清晰度,换取 100% 不闪退 的稳定性。

3. 硬核代码实现

以下是帮小忙工具箱小程序封装好的 imageUtils.js 核心源代码,包含格式转换带防爆逻辑的旋转功能。

// utils/imageUtils.js

// 1. 获取系统基础信息
const wxt = {
  dpr: wx.getSystemInfoSync().pixelRatio || 2
};

// 2. 图片对象缓存池(避免重复加载同一张图)
const cacheCanvasImageMap = new Map();

/**
 * 内部方法:获取/创建 Canvas Image 对象
 */
async function getCanvasImage(canvas, imageUrl) {
  if (cacheCanvasImageMap.has(imageUrl)) {
    return cacheCanvasImageMap.get(imageUrl);
  }
  
  // 兼容 Promise.withResolvers 或使用 new Promise
  const { promise, resolve, reject } = Promise.withResolvers();
  const image = canvas.createImage();
  image.onload = () => {
    cacheCanvasImageMap.set(imageUrl, image);
    resolve(image);
  };
  image.onerror = (e) => reject(new Error(`图片加载失败: ${e.errMsg}`));
  image.src = imageUrl;
  await promise;
  return image;
}

/**
 * 功能一:离屏 Canvas 转换图片格式 (PNG/HEIC -> JPG)
 * @param {string} imageUrl 图片路径
 * @param {string} destFileType 目标类型 'jpg' | 'png'
 * @param {number} quality 质量 0-1
 */
export async function convertImageType(imageUrl, destFileType = 'jpg', quality = 1) {
  const offscreenCanvas = wx.createOffscreenCanvas({ type: '2d' });
  const image = await getCanvasImage(offscreenCanvas, imageUrl);
  const { width, height } = image;

  // 基础转换:直接使用系统 DPR 保证高清
  offscreenCanvas.width = width * wxt.dpr;
  offscreenCanvas.height = height * wxt.dpr;

  const ctx = offscreenCanvas.getContext('2d');
  ctx.scale(wxt.dpr, wxt.dpr);
  ctx.drawImage(image, 0, 0, width, height);

  const res = await wx.canvasToTempFilePath({
    canvas: offscreenCanvas,
    fileType: destFileType,
    quality: quality,
  });
  return res.tempFilePath;
}

/**
 * 功能二:极速旋转图片 (含内存保护)
 * @param {string} imageUrl 图片路径
 * @param {number} degree 旋转角度 (90, 180, 270...)
 */
export async function rotateImage(imageUrl, degree = 90, destFileType = 'jpg', quality = 1) {
  const offscreenCanvas = wx.createOffscreenCanvas({ type: '2d' });
  const image = await getCanvasImage(offscreenCanvas, imageUrl);
  const { width, height } = image;

  const radian = (degree * Math.PI) / 180;
  
  // 1. 计算旋转后的逻辑包围盒宽高
  const newWidth = Math.abs(width * Math.cos(radian)) + Math.abs(height * Math.sin(radian));
  const newHeight = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian));

  // --- ⚡️ 性能优化核心 Start ---
  
  // 2. 智能计算 DPR:避免画布过大炸内存
  // 设定安全纹理阈值,4096px 是大多数移动端 GPU 的安全线
  const LIMIT_SIZE = 4096; 
  let useDpr = wxt.dpr;

  // 核心判断:如果 (逻辑边长 * dpr) 超过限制,自动计算最大允许的 dpr
  if (Math.max(newWidth, newHeight) * useDpr > LIMIT_SIZE) {
    useDpr = LIMIT_SIZE / Math.max(newWidth, newHeight);
    console.warn(`[ImageRotate] 图片过大,触发自动降级,DPR调整为: ${useDpr.toFixed(2)}`);
  }

  // 3. 设置物理画布尺寸 (使用计算后的安全 DPR)
  offscreenCanvas.width = newWidth * useDpr;
  offscreenCanvas.height = newHeight * useDpr;

  const ctx = offscreenCanvas.getContext('2d');
  ctx.scale(useDpr, useDpr); 
  
  // --- 性能优化核心 End ---

  // 4. 绘图逻辑:平移 -> 旋转 -> 绘制
  ctx.translate(newWidth / 2, newHeight / 2);
  ctx.rotate(radian);
  ctx.drawImage(image, -width / 2, -height / 2, width, height);

  // 5. 导出文件 
  const res = await wx.canvasToTempFilePath({
    canvas: offscreenCanvas,
    fileType: destFileType,
    quality: quality,
  });

  return res.tempFilePath;
}

4. 避坑与实战经验

  1. 图片转pdf场景经验 图片转成pdf,在使用pdf-lib插入图片时,只支持jpg、png在插入前先判断一下是否符合,用户可能上传webp等图片(有些人觉得限制上传类型,但图片后缀有可能被篡改过),就需要先转换;另外如果要保证pdf是纵向的,使用canvas提前确保图片为纵向的,就简单很多,无需在pdf-lib做坐标变换
  2. DPR 的取舍艺术: 很多开发者喜欢写死 offscreenCanvas.width = width,这样导出的图是模糊的。也有人写死 width * systemDpr,这会导致大图闪退。 最佳实践就是代码中的 Math.min 逻辑:在安全范围内,尽可能高清
  3. 兼容性提示: 代码中使用了 Promise.withResolvers(),这是 ES2024 新特性。我全局内置兼容代码。
/**
 * 创建withResolvers函数
 */
Promise.withResolvers =
Promise.withResolvers ||
function () {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return {
promise,
resolve,
reject,
};
};

写在最后

通过这一套组合拳,我们成功在小程序中实现了稳定、高效的本地图片处理。无论用户使用几年前的安卓机还是最新的 iPhone,都能流畅地完成图片旋转与转换,再也不用担心内存溢出带来的闪退噩梦了!

希望这篇实战分享能帮你解决 Canvas 开发中的性能难题!

Taro 小程序页面返回传参完整示例

前言

  • 我们在开发的时候,有时候会遇到,A页面跳转到B页面,B页面改一些数据(例如:收藏状态),回到A页面的时候不想刷新A页面,并看到最新的数据状态;
  • 对于以上场景,有以下几种解决方案;

方法一:EventChannel(推荐)

PageA.jsx - 跳转页面

import React, { useState } from 'react'
import { View, Button, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'

const PageA = () => {
  const [receivedData, setReceivedData] = useState(null)

  const goToPageB = () => {
    Taro.navigateTo({
      url: '/pages/pageB/index',
      events: {
        // 监听返回数据
        // ⚠️ 这里监听的事件名 必须和 子页面绑定的事件名称相同
        onReturnData: (data) => {
          console.log('接收到返回数据:', data)
          setReceivedData(data)
        },
        // 可以监听多个事件
        onSelectItem: (item) => {
          console.log('选中的项目:', item)
        }
      }
    })
  }

  return (
    <View className="page-a">
      <Button onClick={goToPageB}>跳转到页面B</Button>
      
      {receivedData && (
        <View className="received-data">
          <Text>接收到的数据:</Text>
          <Text>{JSON.stringify(receivedData)}</Text>
        </View>
      )}
    </View>
  )
}

export default PageA

PageB.jsx - 返回页面

  • 这是
import React, { useState, useEffect } from 'react'
import { View, Button, Input } from '@tarojs/components'
import Taro from '@tarojs/taro'

const PageB = () => {
  // 可以使用 useState 或 useRef 存储 EventChannel
  const [eventChannel, setEventChannel] = useState(null)
  const [inputValue, setInputValue] = useState('')
  const [count, setCount] = useState(0)

  useEffect(() => {
    // 获取 EventChannel
    const channel = Taro.getCurrentInstance().page?.getOpenerEventChannel?.()
    if (channel) {
      setEventChannel(channel)
    }
  }, [])

  const handleReturn = () => {
    if (eventChannel) {
      // 发送数据给上个页面
      eventChannel.emit('onReturnData', {
        inputValue,
        timestamp: Date.now(),
        source: 'pageB'
      })
    }
    
    // 返回上个页面
    Taro.navigateBack()
  }

  const handleSelectItem = (item) => {
    if (eventChannel) {
      eventChannel.emit('onSelectItem', item)
    }
  }
  
  // ---- 页面销毁传递参数 Start ----
  // 若是使用小程序的导航栏的返回按钮,可以在页面销毁的时候,向父页面传递参数
  // 需要注意的是,useUnload 的参数若是依赖于一些数据,需要使用 useCallback 对函数进行缓存
  const handleBack = useCallback(() => {
    if (eventChannel) {
      eventChannel?.emit('onReturnPageA', { name: 'PageA', count })
      console.log('数据发送成功')
    }
  }, [eventChannel, count])

  useUnload(handleBack)
  // ---- 页面销毁传递参数 End ----

  return (
    <View className="page-b">
      <Input
        value={inputValue}
        onInput={(e) => setInputValue(e.detail.value)}
        placeholder="输入要传递的数据"
      />
      
      <Button onClick={handleReturn}>返回并传递数据</Button>
      
      <Button onClick={() => handleSelectItem({ id: 1, name: '选项1' })}>
        选择选项1
      </Button>
      
      <Button onClick={() => setCount(v => v++)}>
        改变count
      </Button>
      
      <Button onClick={() => handleSelectItem({ id: 2, name: '选项2' })}>
        选择选项2
      </Button>
    </View>
  )
}

export default PageB

方法二:使用 Zustand 状态管理

store/index.js

import { create } from 'zustand'

const useAppStore = create((set, get) => ({
  // 页面返回数据
  pageReturnData: null,
  
  // 设置返回数据
  setPageReturnData: (data) => set({ pageReturnData: data }),
  
  // 清除返回数据
  clearPageReturnData: () => set({ pageReturnData: null }),
  
  // 获取并清除返回数据
  getAndClearReturnData: () => {
    const data = get().pageReturnData
    set({ pageReturnData: null })
    return data
  }
}))

export default useAppStore

PageA.jsx - 使用状态管理

import React, { useEffect } from 'react'
import { View, Button, Text } from '@tarojs/components'
import Taro, { useDidShow } from '@tarojs/taro'
import useAppStore from '../store'

const PageA = () => {
  const { pageReturnData, clearPageReturnData } = useAppStore()

  // 页面显示时检查返回数据
  useDidShow(() => {
    if (pageReturnData) {
      console.log('接收到返回数据:', pageReturnData)
      // 处理数据后清除
      handleReturnData(pageReturnData)
      clearPageReturnData()
    }
  })

  const handleReturnData = (data) => {
    // 处理返回的数据
    Taro.showToast({
      title: `接收到: ${data.message}`,
      icon: 'success'
    })
  }

  const goToPageB = () => {
    Taro.navigateTo({
      url: '/pages/pageB/index'
    })
  }

  return (
    <View className="page-a">
      <Button onClick={goToPageB}>跳转到页面B</Button>
    </View>
  )
}

export default PageA

PageB.jsx - 设置状态并返回

import React, { useState } from 'react'
import { View, Button, Input } from '@tarojs/components'
import Taro from '@tarojs/taro'
import useAppStore from '../store'

const PageB = () => {
  const [message, setMessage] = useState('')
  const setPageReturnData = useAppStore(state => state.setPageReturnData)

  const handleReturn = () => {
    // 设置要传递的数据
    setPageReturnData({
      message,
      timestamp: Date.now(),
      type: 'user_input'
    })
    
    // 返回上个页面
    Taro.navigateBack()
  }

  return (
    <View className="page-b">
      <Input
        value={message}
        onInput={(e) => setMessage(e.detail.value)}
        placeholder="输入消息"
      />
      
      <Button onClick={handleReturn}>返回并传递消息</Button>
    </View>
  )
}

export default PageB

方法三:自定义 Hook 封装

hooks/usePageReturn.js

import { useState, useEffect } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'

// 全局存储返回数据
let globalReturnData = new Map()

export const usePageReturn = (pageKey) => {
  const [returnData, setReturnData] = useState(null)

  useDidShow(() => {
    const data = globalReturnData.get(pageKey)
    if (data) {
      setReturnData(data)
      globalReturnData.delete(pageKey)
    }
  })

  const setReturnDataForPage = (targetPageKey, data) => {
    globalReturnData.set(targetPageKey, data)
  }

  const clearReturnData = () => {
    setReturnData(null)
  }

  return {
    returnData,
    setReturnDataForPage,
    clearReturnData
  }
}

// 导航并设置返回监听
export const navigateToWithReturn = (url, pageKey, onReturn) => {
  return Taro.navigateTo({
    url,
    events: {
      returnData: (data) => {
        if (onReturn) {
          onReturn(data)
        }
      }
    }
  })
}

使用自定义 Hook

// PageA.jsx
import React from 'react'
import { View, Button } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { usePageReturn } from '../hooks/usePageReturn'

const PageA = () => {
  const { returnData, clearReturnData } = usePageReturn('pageA')

  useEffect(() => {
    if (returnData) {
      console.log('接收到返回数据:', returnData)
      // 处理数据
      clearReturnData()
    }
  }, [returnData])

  const goToPageB = () => {
    Taro.navigateTo({
      url: '/pages/pageB/index?fromPage=pageA'
    })
  }

  return (
    <View>
      <Button onClick={goToPageB}>跳转到页面B</Button>
    </View>
  )
}

export default PageA

最佳实践建议

  1. 简单场景:使用 EventChannel(方法一)
  2. 复杂应用:使用状态管理(方法二)
  3. 多页面复用:封装自定义 Hook(方法三)
  4. 数据量大:避免使用 URL 参数,选择状态管理
  5. 临时数据:使用 EventChannel,自动清理
  6. 持久数据:结合本地存储使用

注意事项

  • EventChannel 只在 navigateTo 时可用,redirectTo 不支持
  • 状态管理要注意及时清理数据,避免内存泄漏
  • 复杂对象传递时注意序列化问题
  • 考虑页面栈的层级关系,避免数据传递错乱

告别服务器!小程序纯前端“图片转 PDF”工具,隐私安全又高效

1. 背景与痛点:纯前端实践的动力

在开发小程序时,实现如“图片转 PDF”这样的功能时,常常面临以下挑战:

  • 隐私担忧:将图片上传到服务器进行转换,用户担心图片内容泄露。对于个人证件、私密照片等敏感内容,这一顾虑尤为突出。
  • 网络依赖与效率:转换过程需要频繁与服务器交互,在弱网环境下速度慢、不稳定,甚至可能因上传大文件而失败。
  • 服务器成本:每一次转换都意味着服务器资源的消耗(存储、计算、带宽),对于开发者而言,成本不容忽视。

为了解决这些痛点,我们探索了一个更优的实现路径:纯前端、在小程序本地完成图片到 PDF 的转换

2. 核心思路:本地文件系统与 pdf-lib 的巧妙结合

在小程序中实现纯前端图片转 PDF,我们的核心思路是:

  1. 图片本地化处理:充分利用小程序强大的本地文件系统能力,将用户选择的图片读取到本地临时路径。
  2. PDF 文档构建:引入功能丰富的 JavaScript 库 pdf-lib,在小程序运行时直接在前端环境创建和操作 PDF 文件。
  3. 最终文件保存:将 pdf-lib 生成的 PDF 数据流保存为本地文件,供用户直接预览或分享。

这种方式让整个转换过程都在用户的小程序沙箱环境内完成,图片数据不会离开用户手机,极大保障了数据隐私和安全性,同时显著提升了转换效率并降低了服务器成本。

3. 技术核心:pdf-lib 的引入与应用

pdf-lib 是一个强大的纯 JavaScript PDF 库,支持在多种 JavaScript 环境下创建和修改 PDF 文件,完美契合小程序这种前端应用场景。

3.1 库的引入

你需要将 pdf-lib 的小程序兼容版本(通常是 pdf-lib.min.js)放置在你的项目目录中,并通过 require 引入:

const { PDFDocument, degrees, PageSizes } = require('./pdf-lib.min.js');
const fs = wx.getFileSystemManager(); // 小程序文件管理器实例

3.2 转换逻辑概览

整个图片转 PDF 的流程可分解为以下几个关键步骤:

  1. 图片预处理:获取每张图片的尺寸、类型 (wx.getImageInfo),并将其读取为 Base64 格式 (fs.readFile),这是 pdf-lib 嵌入图片所需的标准数据格式。
  2. 创建 PDF 文档:初始化一个空的 PDFDocument 对象。
  3. 逐页添加图片:遍历所有图片,为每张图片创建一个新的 PDF 页面。根据图片的原始尺寸和类型,将其嵌入到 PDF 中,并进行智能缩放、居中。对于横向图片,还会自动旋转页面 90 度以更好地适应 A4 纸张。
  4. 生成与保存:将构建好的 PDF 文档保存为 Base64 编码的字符串,再通过小程序文件系统的 fs.writeFile 接口,写入到本地的临时文件路径。
  5. 返回结果:将生成的 PDF 文件本地路径返回给业务层,用于后续的预览或分享。

4. 核心代码:img2pdf.js

以下是我们帮小忙工具箱实现图片转 PDF 功能的核心源代码。

const { PDFDocument, degrees, PageSizes } = require('./pdf-lib.min.js');
const fs = wx.getFileSystemManager()
/**
 * 把图片转成pdf
 * @param {Array} urls 图片url数组
 * @returns {String} pdfUrl pdf文件url
 */
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);
console.log(imageInfoRes);

// 图片base64
const imageBase64 = urls.map((url) => {
return readFile(url, "base64");
});
const imageBase64Res = await Promise.all(imageBase64);
console.log(imageBase64Res);

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: scaled.height + (pageWidth - scaled.height) / 2,   // 注意这里用的是 scaled.height
x: (pageWidth - scaled.height) / 2,
y: (pageHeight - scaled.width) / 2 + scaled.width,
width: scaled.width,
height: scaled.height,
rotate: degrees(270),
};
console.log('drawOptions', drawOptions);
} else {
// 图片是纵向或方形的
const scaled = pdfImage.scaleToFit(pageWidth, pageHeight);
drawOptions = {
x: (pageWidth - scaled.width) / 2, // 居中 X
y: (pageHeight - scaled.height) / 2, // 居中 Y
width: scaled.width,
height: scaled.height,
};
}
page.drawImage(pdfImage, drawOptions);
}

// 3. 获取 PDF 的 Uint8Array
const docBase64 = await pdfDoc.saveAsBase64();
const timestamp = Date.now();
const pdfPath = await base64ToFile(docBase64, `/${timestamp}.pdf`);


return pdfPath;
}

/**
 * base64转本地文件
 * @param {string} base64 base64字符串
 * @param {string} fileName  文件名
 * @returns {Promise} Promise 文件路径
 */
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;
}

/**
 * 使用Promise读取文件
 * @param {string} filePath 文件路径
 * @param {string} encoding 文件编码
 * @returns {Promise} 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;
}

5. 小程序端应用示例

在页面中,可以通过简单的交互完成转换。

// pages/image-to-pdf/index.js
import { img2pdf } from '../../utils/img2pdf'; // 引入转换工具

Page({
  data: {
    selectedImages: [], // 用户选择的图片临时路径数组
    pdfPath: '',
    loading: false
  },

  // 触发图片选择
  async chooseImage() {
    const { tempFiles } = await wx.chooseMedia({
      count: 9, // 最多选择 9 张图片
      mediaType: ['image'],
      sizeType: ['original', 'compressed'], // 可以选择原图或压缩图
      sourceType: ['album', 'camera'],
    });
    this.setData({ selectedImages: tempFiles.map(file => file.tempFilePath) });
  },

  // 执行图片转 PDF 转换
  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' });
      
      // 转换成功后,自动打开 PDF 预览
      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 });
    }
  }
})

6. 经验总结与注意事项

  1. 文件体积与性能

    • pdf-lib 库本身有一定体积(通常在几百 KB),会增加小程序包体大小,我们是使用分包,所以不影响主包。
    • 图片数量越多、分辨率越高,转换耗时越长,内存占用越大。建议在选择图片时提示用户合理数量或适当压缩。
    • pdf横向图片旋转需要额外计算和处理,可能会略微增加复杂性,如果觉得复杂,也可以直接判断图片是否是纵向,如果是横向使用canvas旋转图片,逻辑上就毕竟简单了。
  2. Promise.withResolvers() 兼容性

    • 代码使用了 Promise.withResolvers(),目前大多数小程序环境和浏览器中兼容性可能不好,我自己做了兼容。
  3. 本地文件系统限制

    • wx.env.USER_DATA_PATH 路径下的文件是小程序沙箱环境特有的,用户无法直接在系统文件管理器中找到。
    • 生成的文件是临时文件,小程序关闭或长时间不用可能被系统清理。如果需要长期保存,需引导用户通过 wx.saveFile (保存到相册或本地文件) 或上传云存储。
  4. 图片类型支持pdf-lib 主要支持 JPEG 和 PNG 格式。其他格式(如 WebP、GIF)需要先转换为 JPEG/PNG 再进行嵌入,可以利用canvas实现,后面会分享。

写在最后

纯前端实现“图片转 PDF”功能,不仅提升了用户体验,更重要的是有效保护了用户的数字隐私。这在追求用户信任和数据安全的小程序生态中,无疑是一个值得推广的实践。

希望这次分享能为你带来启发,共同探索小程序前端能力的更多可能性!


❌