watermarkDownload.ts 2.44 KB
import { saveAs } from "file-saver";
import JSZip from "jszip";

export async function fetchImageBlob(url: string): Promise<Blob> {
  const r = await fetch(url, { credentials: "include" });
  if (!r.ok) {
    throw new Error(`加载图片失败(${r.status})`);
  }
  return r.blob();
}

/** 在图片右下角叠加白字描边(作者、日期) */
export async function blobWithWatermark(
  blob: Blob,
  line1: string,
  line2: string
): Promise<Blob> {
  const bmp = await createImageBitmap(blob);
  const canvas = document.createElement("canvas");
  const width = bmp.width;
  const height = bmp.height;
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext("2d");
  if (!ctx) {
    throw new Error("canvas");
  }
  ctx.drawImage(bmp, 0, 0);
  bmp.close();

  const fs = Math.max(12, Math.round(width / 52));
  const pad = Math.round(fs * 0.85);
  ctx.font = `${fs}px "PingFang SC","Microsoft YaHei","Helvetica Neue",sans-serif`;
  ctx.textBaseline = "bottom";
  ctx.textAlign = "right";
  const lines = [line1, line2].filter((s) => s.trim().length > 0);
  const maxTextWidth = lines.reduce((m, t) => Math.max(m, Math.ceil(ctx.measureText(t).width)), 0);
  const lineGap = Math.round(fs * 0.35);
  const panelPadX = Math.round(fs * 0.7);
  const panelPadY = Math.round(fs * 0.55);
  const panelW = maxTextWidth + panelPadX * 2;
  const panelH = lines.length * fs + (lines.length - 1) * lineGap + panelPadY * 2;
  const panelX = width - pad - panelW;
  const panelY = height - pad - panelH;
  if (lines.length > 0) {
    ctx.fillStyle = "rgba(30, 30, 30, 0.38)";
    ctx.fillRect(panelX, panelY, panelW, panelH);
  }
  let y = height - pad;
  const x = width - pad;
  for (let i = lines.length - 1; i >= 0; i--) {
    const t = lines[i];
    ctx.strokeStyle = "rgba(0,0,0,0.52)";
    ctx.lineWidth = Math.max(1.5, fs / 9);
    ctx.fillStyle = "rgba(255,255,255,0.95)";
    ctx.strokeText(t, x, y);
    ctx.fillText(t, x, y);
    y -= fs + lineGap;
  }

  return new Promise((resolve, reject) => {
    canvas.toBlob(
      (b) => (b ? resolve(b) : reject(new Error("导出失败"))),
      "image/jpeg",
      0.92
    );
  });
}

export function triggerSave(blob: Blob, filename: string) {
  saveAs(blob, filename);
}

export async function zipBlobs(
  entries: Array<{ path: string; blob: Blob }>
): Promise<Blob> {
  const zip = new JSZip();
  for (const { path, blob } of entries) {
    zip.file(path, blob);
  }
  return zip.generateAsync({ type: "blob" });
}