import { saveAs } from "file-saver"; import JSZip from "jszip"; export async function fetchImageBlob(url: string): Promise { 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 { 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 { const zip = new JSZip(); for (const { path, blob } of entries) { zip.file(path, blob); } return zip.generateAsync({ type: "blob" }); }