first release
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/* 地点图片:在浏览器里转成 WebP 再上传。
|
||||
*
|
||||
* 为什么放在前端转:1 核的服务器解一张 4000px 的原图要好几秒,几张就够卡住整站;
|
||||
* 而用户自己的手机/电脑转一张是几十毫秒的事。服务端只负责把收到的 WebP 字节原样写出去。
|
||||
*
|
||||
* 尺寸与质量策略(在真机照片上量过才定的):
|
||||
* 长边 1440px,质量 0.72 → 超 220KB 再试 0.58 → 还超就把整张缩到 1080px 重编。
|
||||
* 关键在于「体积大头是像素数,不是质量」:实测一张 13MB 的城市照片,
|
||||
* 1600px 从 q0.74 一路降到 q0.5 只从 483KB 掉到 391KB(掉 19%,画面却明显糊了),
|
||||
* 而同样 q0.72 把长边从 1600 缩到 1024 直接掉到 217KB(掉 55%)。
|
||||
* 所以阶梯走到底还超标时,宁可缩尺寸也不再压质量。
|
||||
* 这几张图只用于详情页画廊与灯箱(最大显示到一千出头的宽度),1440px 留了余量。
|
||||
*/
|
||||
(function () {
|
||||
var MAX_EDGE = 1440;
|
||||
var QUALITY_LADDER = [0.72, 0.58];
|
||||
var TARGET_BYTES = 220 * 1024;
|
||||
// 兜底档:纹理特别密的照片在 1440px 下怎么降质都超标时才用
|
||||
var FALLBACK_EDGE = 1080;
|
||||
var FALLBACK_QUALITY = 0.7;
|
||||
|
||||
function loadImage(file) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var url = URL.createObjectURL(file);
|
||||
var img = new Image();
|
||||
img.onload = function () { URL.revokeObjectURL(url); resolve(img); };
|
||||
img.onerror = function () { URL.revokeObjectURL(url); reject(new Error('这张图片读不出来')); };
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
function encode(cv, quality) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
cv.toBlob(function (blob) {
|
||||
// 不支持 WebP 编码的浏览器会静默给回 PNG,这里必须显式挡住——
|
||||
// 否则上传的是 PNG,服务端会以「只接受 WebP」拒掉,用户却看不出原因
|
||||
if (!blob || blob.type !== 'image/webp') {
|
||||
reject(new Error('这个浏览器不支持 WebP 转码,请换新版 Chrome / Edge / Safari'));
|
||||
return;
|
||||
}
|
||||
resolve(blob);
|
||||
}, 'image/webp', quality);
|
||||
});
|
||||
}
|
||||
|
||||
// render 把原图按长边上限放到一张画布上,返回画布与落地的宽高
|
||||
function render(img, limit) {
|
||||
var w = img.naturalWidth || img.width || 1;
|
||||
var h = img.naturalHeight || img.height || 1;
|
||||
var scale = Math.min(1, limit / Math.max(w, h));
|
||||
var tw = Math.max(1, Math.round(w * scale));
|
||||
var th = Math.max(1, Math.round(h * scale));
|
||||
var cv = document.createElement('canvas');
|
||||
cv.width = tw;
|
||||
cv.height = th;
|
||||
var g = cv.getContext('2d');
|
||||
if (g.imageSmoothingQuality !== undefined) g.imageSmoothingQuality = 'high';
|
||||
// 白底:源图带透明通道时(截图、PNG)不会在 WebP 里变成黑块
|
||||
g.fillStyle = '#fff';
|
||||
g.fillRect(0, 0, tw, th);
|
||||
g.drawImage(img, 0, 0, tw, th);
|
||||
return { cv: cv, width: tw, height: th };
|
||||
}
|
||||
|
||||
// toWebP(file) -> Promise<{blob, width, height, quality, bytes}>
|
||||
function toWebP(file, opts) {
|
||||
opts = opts || {};
|
||||
var maxEdge = opts.maxEdge || MAX_EDGE;
|
||||
var target = opts.targetBytes || TARGET_BYTES;
|
||||
var ladder = opts.ladder || QUALITY_LADDER;
|
||||
var fallbackEdge = opts.fallbackEdge === undefined ? FALLBACK_EDGE : opts.fallbackEdge;
|
||||
var fallbackQuality = opts.fallbackQuality || FALLBACK_QUALITY;
|
||||
return loadImage(file).then(function (img) {
|
||||
var box = render(img, maxEdge);
|
||||
function attempt(i) {
|
||||
var q = ladder[i];
|
||||
return encode(box.cv, q).then(function (blob) {
|
||||
var out = { blob: blob, width: box.width, height: box.height, quality: q, bytes: blob.size };
|
||||
if (out.bytes <= target || i === ladder.length - 1) return out;
|
||||
return attempt(i + 1);
|
||||
});
|
||||
}
|
||||
return attempt(0).then(function (out) {
|
||||
// 阶梯走到底还超标 ⇒ 这张图纹理特别密,再降质量只会又糊又大,改成缩尺寸
|
||||
var longest = Math.max(box.width, box.height);
|
||||
if (out.bytes <= target || !fallbackEdge || longest <= fallbackEdge) return out;
|
||||
var small = render(img, fallbackEdge);
|
||||
return encode(small.cv, fallbackQuality).then(function (blob) {
|
||||
// 兜底档同样是有损重编,纹理极端的图上未必比阶梯档小,所以比一次体积、取小的
|
||||
if (blob.size >= out.bytes) return out;
|
||||
return { blob: blob, width: small.width, height: small.height, quality: fallbackQuality, bytes: blob.size };
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function post(url, form) {
|
||||
return fetch(url, { method: 'POST', body: form, credentials: 'same-origin' })
|
||||
.then(function (r) { return r.json(); });
|
||||
}
|
||||
|
||||
// upload(srcPK, blob, w, h) -> Promise<{ok, id, url} | {error}>
|
||||
function upload(srcPK, blob, w, h) {
|
||||
var fd = new FormData();
|
||||
fd.append('src_pk', srcPK);
|
||||
fd.append('w', w);
|
||||
fd.append('h', h);
|
||||
fd.append('file', blob, 'image.webp');
|
||||
return post('/admin/place/images', fd);
|
||||
}
|
||||
|
||||
function remove(id) {
|
||||
var fd = new FormData();
|
||||
fd.append('id', id);
|
||||
return post('/admin/place/images/delete', fd);
|
||||
}
|
||||
|
||||
window.rondImage = { toWebP: toWebP, upload: upload, remove: remove };
|
||||
})();
|
||||
Reference in New Issue
Block a user