bc518174
王天杨
提交两个项目文件
|
1
2
3
|
import { saveAs } from "file-saver";
import JSZip from "jszip";
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
function sniffImageMime(u8: Uint8Array): string | null {
if (u8.length >= 3 && u8[0] === 0xff && u8[1] === 0xd8 && u8[2] === 0xff) {
return "image/jpeg";
}
if (u8.length >= 8 && u8[0] === 0x89 && u8[1] === 0x50 && u8[2] === 0x4e && u8[3] === 0x47) {
return "image/png";
}
if (u8.length >= 12) {
const a = String.fromCharCode(u8[0], u8[1], u8[2], u8[3]);
const b = String.fromCharCode(u8[8], u8[9], u8[10], u8[11]);
if (a === "RIFF" && b === "WEBP") {
return "image/webp";
}
}
if (u8.length >= 6 && u8[0] === 0x47 && u8[1] === 0x49 && u8[2] === 0x46) {
return "image/gif";
}
return null;
}
/**
* 经 fetch 拉取图片二进制;用 arrayBuffer + 魔数校验,避免把 JSON/HTML 当图片写入 zip。
* @param init 跨域带 Bearer 时不要 credentials: "include",与 Access-Control-Allow-Origin: * 不兼容。
*/
export async function fetchImageBlob(url: string, init?: RequestInit): Promise<Blob> {
const r = await fetch(url, {
credentials: "omit",
...init,
});
const buf = await r.arrayBuffer();
const u8 = new Uint8Array(buf);
|
bc518174
王天杨
提交两个项目文件
|
36
|
if (!r.ok) {
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
const text = new TextDecoder().decode(u8.slice(0, 400));
throw new Error(`加载图片失败(${r.status})${text.trim().slice(0, 120)}`);
}
const sniffed = sniffImageMime(u8);
if (sniffed) {
return new Blob([buf], { type: sniffed });
}
const head = new TextDecoder("utf-8", { fatal: false }).decode(u8.slice(0, Math.min(120, u8.length))).trimStart();
if (head.startsWith("{") || head.startsWith("<")) {
throw new Error(
"接口返回的不是图片(可能是登录失效或错误页)。请重新登录后再试,并确认后端已部署 admin/v1/uploads/raw。"
);
}
const ct = (r.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
if (ct.startsWith("image/")) {
return new Blob([buf], { type: ct });
}
throw new Error("无法识别图片数据(魔数异常),请检查网络或联系管理员。");
}
async function decodeImageToBitmap(blob: Blob): Promise<ImageBitmap> {
const tries: Array<ImageBitmapOptions | undefined> = [
{ colorSpaceConversion: "none", premultiplyAlpha: "premultiply" },
{ colorSpaceConversion: "none" },
undefined,
];
for (const opts of tries) {
try {
if (opts === undefined) {
return await createImageBitmap(blob);
}
return await createImageBitmap(blob, opts);
} catch {
/* try next */
}
}
const url = URL.createObjectURL(blob);
const img = new Image();
img.decoding = "async";
try {
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error("图片无法解码(格式不受支持或文件已损坏)"));
img.src = url;
});
return await createImageBitmap(img);
} finally {
URL.revokeObjectURL(url);
|
bc518174
王天杨
提交两个项目文件
|
90
|
}
|
bc518174
王天杨
提交两个项目文件
|
91
92
|
}
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
93
|
/** 在图片右下角叠加白字描边(作者、日期);导出为 JPEG */
|
bc518174
王天杨
提交两个项目文件
|
94
95
96
97
98
|
export async function blobWithWatermark(
blob: Blob,
line1: string,
line2: string
): Promise<Blob> {
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
const bmp = await decodeImageToBitmap(blob);
try {
const canvas = document.createElement("canvas");
const width = bmp.width;
const height = bmp.height;
if (!width || !height) {
throw new Error("图片尺寸无效");
}
const maxSide = 8192;
if (width > maxSide || height > maxSide) {
throw new Error(`图片过大(${width}×${height}),超过浏览器可处理上限,请换用原图下载或压缩后再传。`);
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("canvas");
}
ctx.drawImage(bmp, 0, 0);
|
bc518174
王天杨
提交两个项目文件
|
118
|
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
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;
}
|
bc518174
王天杨
提交两个项目文件
|
148
|
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
149
150
151
152
153
154
155
156
157
158
|
return new Promise((resolve, reject) => {
canvas.toBlob(
(b) => (b ? resolve(b) : reject(new Error("导出失败"))),
"image/jpeg",
0.92
);
});
} finally {
bmp.close();
}
|
bc518174
王天杨
提交两个项目文件
|
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
|
}
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" });
}
|