微信小程序里面如何生成海报

Database and Ruby, Python, History


需求是,小程序里面需要给用户上传的图片配上文字,最下面再打上我们自己的 logo。

步骤基本是

  1. 先创建一个 Canvas

<Canvas
  type="2d"
  id="posterCanvas"
  style={{
    width: `${posterWidth}px`,
    height: `${posterHeight}px`,
    position: "fixed",
    left: "-9999px",
    top: "-9999px",
  }}
/>

  1. 然后是绘制 Canvas

依次把上传的图片缩放到 Canvas 指定的大小区域。注意 Canvas 是以左上为起点(0, 0),然后开始绘制。

至于图片的清晰度,需要const dpr = Taro.getSystemInfoSync().pixelRatio; 获取到设备的像素比,然后 scale 出来,否则绘制上去的图片清晰度有问题。

依次绘制图片,描述和 logo。

const padding = 32;
const designWidth = 750;
const posterWidth = designWidth - padding * 2;
const posterHeight = 1010;
const imageHeight = 760;
const logoUrl = "https://xxx.com/public/logo.png";

const drawPoster = async () => {
  return new Promise<void>((resolve, reject) => {
    Taro.createSelectorQuery()
      .select("#posterCanvas")
      .fields({ node: true, size: true })
      .exec(async (res) => {
        if (!res[0] || !res[0].node) {
          reject(new Error("Canvas not found"));
          return;
        }

        const canvas = res[0].node;
        const ctx = canvas.getContext("2d");
        const width = posterWidth;
        const height = posterHeight;
        const dpr = Taro.getSystemInfoSync().pixelRatio;

        // Set canvas dimensions
        canvas.width = width * dpr;
        canvas.height = height * dpr;
        ctx.scale(dpr, dpr);

        // Draw background
        ctx.fillStyle = "#ffffff";
        ctx.fillRect(0, 0, width, height);

        try {
          // Draw Photo
          if (photos.length > 0 && photos[0].path) {
            const photoSrc = photos[0].path;
            const photoInfo = await Taro.getImageInfo({ src: photoSrc });
            const photoImg = canvas.createImage();
            photoImg.src = photoInfo.path;

            await new Promise((resolveImg) => {
              photoImg.onload = resolveImg;
              photoImg.onerror = reject;
            });

            const targetWidth = posterWidth - padding * 2;
            const targetHeight = imageHeight;
            const startX = (width - targetWidth) / 2;
            const startY = padding;

            // Draw Image (Aspect Fit)
            const imgRatio = photoInfo.width / photoInfo.height;
            const targetRatio = targetWidth / targetHeight;

            let drawWidth, drawHeight, drawX, drawY;

            if (imgRatio > targetRatio) {
              // Image is wider than target
              drawWidth = targetWidth;
              drawHeight = targetWidth / imgRatio;
              drawX = startX;
              drawY = startY + (targetHeight - drawHeight) / 2;
            } else {
              // Image is taller than target
              drawHeight = targetHeight;
              drawWidth = targetHeight * imgRatio;
              drawX = startX + (targetWidth - drawWidth) / 2;
              drawY = startY;
            }

            ctx.drawImage(photoImg, drawX, drawY, drawWidth, drawHeight);
          }

          // Draw Description
          if (photos.length > 0 && photos[0].description) {
            ctx.font = "24px sans-serif"; // Adjust size as needed
            ctx.fillStyle = "rgba(0, 0, 0, 0.44)";
            ctx.textBaseline = "top";

            const text = photos[0].description;
            const textX = padding;
            const textY = imageHeight + padding * 2;
            const maxWidth = posterWidth - padding * 2;
            const lineHeight = 40;

            // Simple text wrapping
            const words = text.split("");
            let line = "";
            let y = textY;

            for (let n = 0; n < words.length; n++) {
              const testLine = line + words[n];
              const metrics = ctx.measureText(testLine);
              const testWidth = metrics.width;
              if (testWidth > maxWidth && n > 0) {
                ctx.fillText(line, textX, y);
                line = words[n];
                y += lineHeight;
              } else {
                line = testLine;
              }
            }

            ctx.fillText(line, textX, y);
          }

          // Draw Logo

          const logoInfo = await Taro.getImageInfo({ src: logoUrl });
          const logoImg = canvas.createImage();
          logoImg.src = logoInfo.path;

          await new Promise((resolveLogo) => {
            logoImg.onload = resolveLogo;
            logoImg.onerror = reject;
          });

          const logoWidth = 225;
          const logoHeight = 43;
          const logoX = (width - logoWidth) / 2;
          const logoY = height - 32 - logoHeight;

          ctx.drawImage(logoImg, logoX, logoY, logoWidth, logoHeight);

          resolve();
        } catch (error) {
          reject(error);
        }
      });
  });
};
  1. 处理下载

下载需要把 Canvas 先导成临时文件,然后再调用saveImageToPhotosAlbum即可。

const saveCanvasToAlbum = () => {
  return new Promise<void>((resolve, reject) => {
    // Get canvas node again to convert to temp file path
    Taro.createSelectorQuery()
      .select("#posterCanvas")
      .fields({ node: true, size: true })
      .exec(async (res) => {
        if (res[0] && res[0].node) {
          const canvas = res[0].node;
          const dpr = Taro.getSystemInfoSync().pixelRatio;
          Taro.canvasToTempFilePath({
            canvas,
            destWidth: posterWidth * dpr,
            destHeight: posterHeight * dpr,
            success: (res) => {
              Taro.saveImageToPhotosAlbum({
                filePath: res.tempFilePath,
                success: () => {
                  Taro.hideLoading();
                  Taro.showToast({
                    title: "已下载",
                    icon: "success",
                  });
                  resolve();
                },
                fail: (err) => {
                  Taro.hideLoading();
                  console.error("Save to album failed:", err);
                  // Handle permission denied specifically if needed
                  if (err.errMsg.includes("auth deny")) {
                    Taro.showToast({
                      title: "授权失败",
                      icon: "none",
                    });
                  } else {
                    Taro.showToast({
                      title: "保存失败",
                      icon: "none",
                    });
                  }
                  reject(err);
                },
              });
            },
            fail: (err) => {
              Taro.hideLoading();
              console.error("Canvas to temp file failed:", err);
              Taro.showToast({
                title: "生成失败",
                icon: "none",
              });
              reject(err);
            },
          });
        } else {
          reject(new Error("Canvas not found"));
        }
      });
  });
};
  1. 分享

同理,如果需要分享图片,调用showShareImageMenu即可。

Reference

  1. https://juejin.cn/post/7234181082883604537