|
@@ -12,33 +12,230 @@ const aws = new AwsClient({
|
|
|
// retries, // number of retries before giving up, defaults to 10, set to 0 for no retrying
|
|
|
// initRetryMs, // defaults to 50 – timeout doubles each retry
|
|
|
});
|
|
|
-const aws_base_url = 'plat-sg-cloudflare-testing.s3.ap-southeast-1.amazonaws.com';
|
|
|
-const self_host = 'cf-test.hoyoverse.com';
|
|
|
-const text_to_image_url = 'https://text-to-image.hoyoverse.workers.dev/';
|
|
|
-const watermark_fg = 'fff';
|
|
|
-const watermark_bg = '000';
|
|
|
+const globalConfiguration = {
|
|
|
+ aws_base_url: 'plat-sg-cloudflare-testing.s3.ap-southeast-1.amazonaws.com',
|
|
|
+ self_host: 'cf-test.hoyoverse.com',
|
|
|
+ text_to_image_url: 'https://text-to-image.hoyoverse.workers.dev/',
|
|
|
+ watermark_fg: 'fff',
|
|
|
+ watermark_bg: '000',
|
|
|
+ watermark_shadow_offset: 2
|
|
|
+};
|
|
|
+
|
|
|
|
|
|
async function build_request(request) {
|
|
|
let dstUrl = new URL(request.url);
|
|
|
|
|
|
- dstUrl.host = request.headers.has('x-host') ? request.headers.get('x-host') : aws_base_url;
|
|
|
+ dstUrl.host = request.headers.has('x-host') ? request.headers.get('x-host') : globalConfiguration.aws_base_url;
|
|
|
dstUrl.search = '';
|
|
|
// dstUrl.pathname = url.pathname.substring('/imgprocessingtest'.length)
|
|
|
|
|
|
|
|
|
let req = await aws.sign(dstUrl.toString());
|
|
|
- dstUrl.host = self_host;
|
|
|
+ dstUrl.host = globalConfiguration.self_host;
|
|
|
return new Request(dstUrl.toString(), { headers: req.headers });
|
|
|
}
|
|
|
|
|
|
async function getDimensions(req) {
|
|
|
- let s_data = await fetch(req);
|
|
|
- let s_buffer = await s_data.arrayBuffer();
|
|
|
- return sizeOf(Buffer.from(s_buffer));
|
|
|
+ // return {
|
|
|
+ // width: 4096,
|
|
|
+ // height: 4096
|
|
|
+ // };
|
|
|
+ let response = await fetch(req);
|
|
|
+ let buffer = await response.arrayBuffer();
|
|
|
+ return sizeOf(Buffer.from(buffer));
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+function parseArg(arg, default_conf) {
|
|
|
+ let str_arg = arg.reduce((obj, str) => {
|
|
|
+ let pair = str.split('_');
|
|
|
+ if (pair.length === 2) {
|
|
|
+ obj[pair[0]] = pair[1];
|
|
|
+ }
|
|
|
+ return obj;
|
|
|
+ }, {});
|
|
|
+ if (!default_conf) return str_arg;
|
|
|
+
|
|
|
+ let conf_raw = { ...default_conf, ...str_arg };
|
|
|
+
|
|
|
+ let numberFields = Object.keys(default_conf).filter(k => typeof default_conf[k] === 'number');
|
|
|
+
|
|
|
+ return numberFields.reduce((obj, field) => {
|
|
|
+ obj[field] = Number(obj[field]);
|
|
|
+ return obj;
|
|
|
+ }, conf_raw);
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+async function parseWaterMark(arg, ctx) {
|
|
|
+ let { req } = ctx;
|
|
|
+ console.log('parseWaterMark');
|
|
|
+ const default_conf = {
|
|
|
+ t: 100,//透明度。[0,100]
|
|
|
+ g: 'se',//位置。nw:左上north:中上ne:右上west:左中center:中部east:右中sw:左下south:中下se
|
|
|
+ x: 10,//水平边距。[0,4096]
|
|
|
+ y: 10,//垂直边距。[0,4096]
|
|
|
+ // voffset: 0,//中线垂直偏移。[-1000,1000]
|
|
|
+ // fill: 0,//1:将图片水印或文字水印铺满原图。0(默认值):不将图片水印或文字水印铺满全图。
|
|
|
+ // padx: 0,//水印平铺时单个水印间的水平间隔。仅在水印平铺开启时有效。[0,4096]
|
|
|
+ // pady: 0,//水印平铺时单个水印间的垂直间隔。仅在水印平铺开启时有效。[0,4096]
|
|
|
+
|
|
|
+ //text:'required'
|
|
|
+ // type: 'ZHJvaWRzYW5zZmFsbGJhY2s',//字体,默认DroidSansFallback
|
|
|
+ color: '000000',//水印的文字颜色
|
|
|
+ size: 40,//文字大小
|
|
|
+ shadow: 0,//阴影透明度
|
|
|
+ rotate: 0//字顺时针旋转角度
|
|
|
+ };
|
|
|
+
|
|
|
+
|
|
|
+ let conf = parseArg(arg, default_conf);
|
|
|
+
|
|
|
+ if (!conf.text || conf.t === 0) return {};
|
|
|
+
|
|
|
+ let base = {};
|
|
|
+
|
|
|
+ if (['nw', 'west', 'sw'].includes(conf.g)) {
|
|
|
+ base.left = conf.x;
|
|
|
+ } else if (['ne', 'east', 'se'].includes(conf.g)) {
|
|
|
+ base.right = conf.x;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (['nw', 'north', 'ne'].includes(conf.g)) {
|
|
|
+ base.top = conf.y;
|
|
|
+ } else if (['sw', 'south', 'se'].includes(conf.g)) {
|
|
|
+ base.bottom = conf.y;
|
|
|
+ }
|
|
|
+ if (conf.rotate / 90 % 2) base.width = conf.size * 0.9;
|
|
|
+ else base.height = conf.size * 0.9;
|
|
|
+ return {
|
|
|
+ draw: [{
|
|
|
+ url: `${globalConfiguration.text_to_image_url}${conf.text}?size=${conf.size * 2}&color=${conf.color}&shadow=${conf.shadow}&offset=${globalConfiguration.watermark_shadow_offset * 2}`,
|
|
|
+ ...base,
|
|
|
+ opacity: conf.t / 100,
|
|
|
+ rotate: conf.rotate
|
|
|
+ }]
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+async function parseResize(arg, { req }) {
|
|
|
+ console.log('parseResize');
|
|
|
+ const default_conf = {
|
|
|
+ m: 'lfit',//缩放的模式。lfit、mfit、fill、pad、fixed
|
|
|
+ w: 0,//宽度。[1,16384]
|
|
|
+ h: 0,//高度。
|
|
|
+ l: 0,//指定目标缩放图的最长边。
|
|
|
+ s: 0,//指定目标缩放图的最短边。
|
|
|
+ limit: 1,//当目标图片分辨率大于原图分辨率时,是否进行缩放。
|
|
|
+ color: 'FFFFFF',//当缩放模式选择为pad(缩放填充)时,可以设置填充的颜色。
|
|
|
+ p: 0//按百分比缩放图片。
|
|
|
+ };
|
|
|
+ let conf = parseArg(arg, default_conf);
|
|
|
+ if (conf.w || conf.h || conf.l || conf.s) {
|
|
|
+ let base = {
|
|
|
+ width: conf.w === 0 ? undefined : conf.w,
|
|
|
+ height: conf.h === 0 ? undefined : conf.h,
|
|
|
+ fit: 'scale-down'
|
|
|
+ };
|
|
|
+
|
|
|
+ switch (conf.m) {
|
|
|
+ case 'lfit':
|
|
|
+ base.fit = conf.limit === 1 ? 'scale-down' : 'contain';
|
|
|
+ break;
|
|
|
+ case 'mfit':
|
|
|
+ case 'fill':
|
|
|
+ base.fit = conf.limit === 1 ? 'crop' : 'contain';
|
|
|
+ break;
|
|
|
+ case 'pad':
|
|
|
+ base.fit = 'pad';
|
|
|
+ base.background = conf.color;
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (conf.l || conf.s) {
|
|
|
+ let dimensions = await getDimensions(req);
|
|
|
+ if (conf.s) {
|
|
|
+ if (dimensions.width < dimensions.height) {
|
|
|
+ base.width = conf.s;
|
|
|
+ base.height = undefined;
|
|
|
+ } else {
|
|
|
+ base.height = conf.s;
|
|
|
+ base.width = undefined;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (dimensions.width < dimensions.height) {
|
|
|
+ base.height = conf.l;
|
|
|
+ base.width = undefined;
|
|
|
+ } else {
|
|
|
+ base.width = conf.l;
|
|
|
+ base.height = undefined;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return base;
|
|
|
+ }
|
|
|
+ if (conf.p) {
|
|
|
+ let dimensions = await getDimensions(req);
|
|
|
+ return {
|
|
|
+ fit: 'scale-down',
|
|
|
+ height: Math.round(dimensions.height * Number(conf.p) / 100)
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return {};
|
|
|
+}
|
|
|
+
|
|
|
+async function parseQuality(arg, ctx) {
|
|
|
+ console.log('parseQuality');
|
|
|
+ let conf = parseArg(arg);
|
|
|
+ return {
|
|
|
+ quality: conf.q
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+async function parseFormat(args, ctx) {
|
|
|
+ console.log('parseFormat');
|
|
|
+ switch (args[0]) {
|
|
|
+ case 'jpg':
|
|
|
+ return { format: 'jpeg' };
|
|
|
+ case 'gif':
|
|
|
+ return {};
|
|
|
+ default:
|
|
|
+ return { format: args[0] };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function parseCrop(arg, ctx) {
|
|
|
+ console.log('parseCrop');
|
|
|
+ const default_conf = {
|
|
|
+ w: 0,//宽度。
|
|
|
+ h: 0,//高度。
|
|
|
+ x: 0,//指定裁剪起点横坐标(默认左上角为原点)。
|
|
|
+ y: 0,//指定裁剪起点纵坐标(默认左上角为原点)。
|
|
|
+ g: 'nw'//裁剪原点位置。nw:左上,north:中上,ne:右上,west:左中,center:中部,east:右中,sw:左下,south:中下,se:右下
|
|
|
+ };
|
|
|
+ let conf = parseArg(arg, default_conf);
|
|
|
+ return {
|
|
|
+ trim: {
|
|
|
+ width: conf.w,
|
|
|
+ height: conf.h,
|
|
|
+ left: conf.x,
|
|
|
+ top: conf.y
|
|
|
+ }
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
+const processMap = {
|
|
|
+ 'watermark': parseWaterMark,
|
|
|
+ 'resize': parseResize,
|
|
|
+ 'quality': parseQuality,
|
|
|
+ 'format': parseFormat,
|
|
|
+ 'crop': parseCrop
|
|
|
+};
|
|
|
+
|
|
|
export default {
|
|
|
- async fetch(request, env, ctx) {
|
|
|
+ async fetch(request, _env, _ctx) {
|
|
|
|
|
|
let req = await build_request(request);
|
|
|
|
|
@@ -48,124 +245,51 @@ export default {
|
|
|
}
|
|
|
|
|
|
|
|
|
- let map = {};
|
|
|
- let image = {};
|
|
|
- let draw_config = {
|
|
|
- url: `${text_to_image_url}ICBIb1lvTEFCQNGO0L3RjNC60LA8Mw==`
|
|
|
- };
|
|
|
-
|
|
|
+ let process_arg = [];
|
|
|
let url = new URL(request.url);
|
|
|
- let enable_process = false;
|
|
|
- let enable_watermark = false;
|
|
|
for (const [key, value] of url.searchParams) {
|
|
|
if (key === 'x-oss-process') {
|
|
|
- let processes = value.split('/');
|
|
|
- processes.forEach(process => {
|
|
|
- let args = process.split(',');
|
|
|
- switch (args[0]) {
|
|
|
- case 'image':
|
|
|
- enable_process = true;
|
|
|
- break;
|
|
|
- case 'format':
|
|
|
- if (args[1] === 'jpg') {
|
|
|
- image.format = 'jpeg';
|
|
|
- } else if (args[1] !== 'gif') {
|
|
|
- image.format = args[1];
|
|
|
- }
|
|
|
- break;
|
|
|
- // case 'auto-orient': //忽略此参数
|
|
|
- // config.rotate = Number(args[1]);
|
|
|
- // break;
|
|
|
- case 'watermark':
|
|
|
- enable_watermark = true;
|
|
|
- case 'quality':
|
|
|
- case 'resize':
|
|
|
- args.forEach(str => {
|
|
|
- let pair = str.split('_');
|
|
|
- if (pair.length === 2) {
|
|
|
- map[pair[0]] = pair[1];
|
|
|
- }
|
|
|
- });
|
|
|
- break;
|
|
|
-
|
|
|
- }
|
|
|
- });
|
|
|
+ let processes_raw = value.split('/');
|
|
|
+ process_arg = processes_raw.map(process => process.split(','));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- let watermark_text = '';
|
|
|
- for (let k in map) {
|
|
|
- switch (k) {
|
|
|
- //watermark
|
|
|
- case 'text':
|
|
|
- watermark_text = map[k];
|
|
|
- break;
|
|
|
- case 'x':
|
|
|
- draw_config.right = Number(map['x']);
|
|
|
- break;
|
|
|
- case 'y':
|
|
|
- draw_config.bottom = Number(map['y']);
|
|
|
- break;
|
|
|
- case 'size':
|
|
|
- draw_config.height = Number(map['size']);
|
|
|
- break;
|
|
|
- case 't':
|
|
|
- draw_config.opacity = Number(map['t']) / 100;
|
|
|
- break;
|
|
|
- //resize
|
|
|
- case 'w':
|
|
|
- image.width = Number(map['w']);
|
|
|
- break;
|
|
|
- case 'h':
|
|
|
- image.height = Number(map['h']);
|
|
|
- break;
|
|
|
- case 'm':
|
|
|
- image.fit = 'scale-down';
|
|
|
- break;
|
|
|
- case 's':
|
|
|
- image.fit = 'scale-down';
|
|
|
- // image.fit = "scale-down";
|
|
|
- let short = Number(map['s']);
|
|
|
- let s_dimensions = await getDimensions(req);
|
|
|
- if (s_dimensions.width < s_dimensions.height) {
|
|
|
- image.width = short;
|
|
|
- } else {
|
|
|
- image.height = short;
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'p':
|
|
|
- image.fit = 'scale-down';
|
|
|
- let p_dimensions = await getDimensions(req);
|
|
|
- image.height = Math.round(p_dimensions.height * Number(map['p']) / 100);
|
|
|
- break;
|
|
|
- //quality
|
|
|
- case 'q':
|
|
|
- image.quality = Number(map['q']);
|
|
|
- break;
|
|
|
+ //无image参数,返回原图
|
|
|
+ if (!process_arg.some(processes => processes[0] === 'image')) {
|
|
|
+ return await fetch(req);
|
|
|
+ }
|
|
|
|
|
|
+ //过滤不可用选项
|
|
|
+ process_arg = process_arg.filter(process => !!processMap[process[0]]);
|
|
|
+ let ctx = { req, process_arg };
|
|
|
+ //解析参数并构造图像变换选项
|
|
|
+ let image = (await Promise.all(
|
|
|
+ process_arg.map(process => {
|
|
|
+ let process_func = processMap[process[0]];
|
|
|
+ return process_func(process.slice(1), ctx);
|
|
|
+ }))
|
|
|
+ ).reduce((a, b) => ({ ...a, ...b }), {});
|
|
|
+ //Crop与Resize综合处理
|
|
|
+ let resize_index = ctx.process_arg.findIndex(item => item[0] === 'resize');
|
|
|
+ let crop_index = ctx.process_arg.findIndex(item => item[0] === 'crop');
|
|
|
+ if (resize_index !== -1 && crop_index !== -1) {
|
|
|
+ let resize_conf = parseArg(ctx.process_arg[resize_index], { p: 0 });
|
|
|
+ if (resize_conf.p) {
|
|
|
+ if (resize_index < crop_index) {
|
|
|
+ image.height = image.trim.height;
|
|
|
+ Object.keys(image.trim).forEach(key => image.trim[key] /= resize_conf.p / 100);
|
|
|
+ } else {
|
|
|
+ image.height = image.trim.height * resize_conf.p / 100;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- //水印处理
|
|
|
- if (enable_watermark) {
|
|
|
- draw_config.url = `${text_to_image_url}${watermark_text}?size=${draw_config.height}&color=${watermark_fg}`;
|
|
|
- let draw_config_bg = { ...draw_config };
|
|
|
- draw_config_bg.right -= 3;
|
|
|
- draw_config_bg.bottom -= 3;
|
|
|
- draw_config_bg.url = `${text_to_image_url}${watermark_text}?size=${draw_config.height}&color=${watermark_bg}`;
|
|
|
- image.draw = [draw_config_bg, draw_config];
|
|
|
- }
|
|
|
|
|
|
+ console.log('image:', JSON.stringify(image));
|
|
|
|
|
|
- //无image参数,返回原图
|
|
|
- if (!enable_process) {
|
|
|
- return await fetch(req);
|
|
|
- }
|
|
|
|
|
|
//获取返回经过处理的图片
|
|
|
let originalResponse = await fetch(req, {
|
|
|
- cf: {
|
|
|
- image
|
|
|
- }
|
|
|
+ cf: { image }
|
|
|
});
|
|
|
|
|
|
//GIF图片过大,处理失败,返回原图
|