Image Service
Image Service is an image processing service embedded in the Blocklet Server, and its basic capabilities include image cropping, scaling, format conversion, and caching.
Live Playground#
By visiting the address below, you can see comparisons of different image operations, parameters, and transformations. Of course, you can set the image parameter in the query string yourself.
Through Playground, we can see that the order of image transmission efficiency from low to high is: png < jpeg < webp < avif, with webp also supporting animated images.
处理范围#
目前能够被 Image Service 处理的图片包括:
- 以
/.blocklet/proxy
为前缀的资源,即 blocklet bundle 中包括的静态资源 - 在
blocklet.yml#interfaces.n.cachable
中配置的可缓存资源列表中的图片,比如 image-bin 里面的 /.well-known/service/blocklet/logo
请求 logo 的时候/.well-known/service/blocklet/favicon
请求 favicon 的时候/.well-known/service/blocklet/og.png
请求 open graph 的时候
缓存机制#
因为图片处理是 CPU 密集型的工作,所以 Blocklet Server 在设计上为 Image Service 启用了两级缓存,相同参数只处理一次:
- Routing Engine:在前端网关直接缓存,以完整的请求 url 为缓存 key,可以通过在 query 参数中携带 nocache=1 跳过
- Image Service:在图片处理服务中,以标准化的处理参数为缓存 key,目前无法跳过
请求链路示意图如下:
基本操作#
下面介绍如何使用 Image Service 实现图片基本操作:
格式转换#
只做格式转换,不做任何图片裁剪和缩放,该模式下必须制定 f
参数,除不能和 resize、crop 同时调用之外,其他图片操作都可以
https://example.com/uploads/test.png?imageFilter=convert&f=webp
尺寸缩放#
进行尺寸缩放,同时支持格式转换,必须指定 w 或者 h 参数,此外还支持其他丰富的参数和操作
https://example.com/uploads/test.png?imageFilter=resize&w=100 // 限定宽度,保持源格式
https://example.com/uploads/test.png?imageFilter=resize&w=100&f=webp // 限定宽度,转为 webp
https://example.com/uploads/test.png?imageFilter=resize&w=100&f=webp&q=50 // 限定宽度,转为 webp,且图片质量设置为 50
https://example.com/uploads/test.png?imageFilter=resize&h=100 // 限定高度,保持源格式
https://example.com/uploads/test.png?imageFilter=resize&h=100&f=webp // 限定高度,转为 webp
https://example.com/uploads/test.png?imageFilter=resize&w=100&h=100&f=webp // 同时限定宽和高,且转为 webp
https://example.com/uploads/test.png?imageFilter=resize&w=100&h=100&m=cover // 同时限定宽和高,且指定缩放模式,默认为 inside
https://example.com/.well-known/service/blocklet/logo?imageFilter=resize&h=80 // 左上角展示的 logo,限定高度,宽度不限,用在全局 loading 上
https://example.com/.well-known/service/blocklet/favicon?imageFilter=resize&w=32 // 浏览器 tab 中展示的图标,默认是 16px 正方形
内容裁剪#
进行内容裁剪,同时支持格式转换,必须制定 w 和 h 参数,可选指定 t 和 l 参数,此外还支持其他丰富的参数和操作
https://example.com/uploads/test.png?imageFilter=crop&w=100&h=100 // 限定宽度,左上角开始裁剪 100 X 100 px
https://example.com/uploads/test.png?imageFilter=crop&h=100&h=100&l=100&t=100 // 限定宽度,从 (100,100) 开始裁剪 100 px
异常处理#
如果遇到异常情况,比如参数不合理,处理时出错,原图有问题等,图片服务会返回 Oops 字样的图片,如果想要知道错误原因,可以把图片地址复制到浏览器,然后在 URL 末尾加上 &e=1
让图片服务返回具体的错误消息,根据错误消息排查即可。常见错误消息:
- Image filter failed: either extension or format must be specified:请求的文件路径,和图片处理参数中都没有指定格式,至少指定1个即可
- Image service error: At least one of `w` or `h` must be provided to resize:resize 操作的时候至少指定 w 或者 h 参数
- Image service error: upstream response is not an image 原图未能返回正常的图片格式
API 参考#
目前 Image Service 处理的参数规格如下:
const FORMATS = ['png', 'gif', 'jpeg', 'webp', 'avif', 'heif'];
const OPERATIONS = ['convert', 'resize', 'crop'];
const MODES = ['cover', 'contain', 'fill', 'inside', 'outside'];
const QUALITIES = {
png: 100,
jpeg: 80,
webp: 80,
avif: 50,
heif: 50,
};
const schema = Joi.object({
// 图片操作
imageFilter: Joi.string()
.lowercase()
.valid(...OPERATIONS)
.required(),
// 缩放或者裁剪的宽度,不能超过 2048
w: Joi.number().integer().min(1).max(2048).when('imageFilter', {
is: 'crop',
then: Joi.required(),
otherwise: Joi.optional(),
}),
// 缩放或者裁剪的高度,不能超过 2048
h: Joi.number().integer().min(1).max(2048).when('imageFilter', {
is: 'crop',
then: Joi.required(),
otherwise: Joi.optional(),
}),
// 图片质量,不同的图片格式下,默认的图片质量不同
q: Joi.number()
.integer()
.min(10)
.max(100)
.default((input) => {
if (input.f && QUALITIES[input.f]) {
return QUALITIES[input.f];
}
return 90;
}),
p: Joi.number().valid(0, 1).optional().default(1), // 渐进式编码,默认启用
g: Joi.number().valid(0, 1).optional().default(0), // 灰度处理,默认不启用
r: Joi.number().integer().valid(90, 180, 270).optional(), // 旋转角度,逆时针,
s: Joi.number().integer().min(0).max(2000).optional(), // 锐化参数,默认不锐化
b: Joi.number().integer().min(1).max(2000).optional(), // 高斯模糊的半径,默认不模糊
a: Joi.number().valid(0, 1).optional().default(1), // 是否保留透明度,默认为保留
n: Joi.number().valid(0, 1).optional().default(0), // 反化
// 图片格式
f: Joi.string()
.lowercase()
.valid(...FORMATS)
.when('imageFilter', {
is: 'convert',
then: Joi.required(),
otherwise: Joi.optional(),
}),
// 图片缩放的模式,不同模式缩放的结果不同
m: Joi.string()
.lowercase()
.valid(...MODES)
.optional()
.default('inside'),
// 图片裁剪的起始坐标,相对图片左上角
t: Joi.number().integer().min(0).max(2048).optional().default(0),
l: Joi.number().integer().min(0).max(2048).optional().default(0),
e: Joi.number().valid(0, 1).optional().default(0), // return error
})
.rename('i', 'p')
.options({ stripUnknown: true, allowUnknown: true, noDefaults: false });