Skip to content

Commit bd8b2e1

Browse files
committed
fix: add bypass for gif image if nothing to do
1 parent a4984f2 commit bd8b2e1

10 files changed

Lines changed: 194 additions & 25 deletions

File tree

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,72 @@
1-
import { IAction, IActionOpts, IProcessContext, ReadOnly } from '..';
1+
import { IImageContext } from '.';
2+
import { IAction, IActionOpts, IProcessContext, ReadOnly, IActionMask } from '..';
23

34

45
export abstract class BaseImageAction implements IAction {
56
public name: string = 'unknown';
67
abstract validate(params: string[]): ReadOnly<IActionOpts>;
78
abstract process(ctx: IProcessContext, params: string[]): Promise<void>;
8-
public beforeNewContext(_: IProcessContext, params: string[]): void {
9+
public beforeNewContext(_1: IProcessContext, params: string[], _3: number): void {
910
this.validate(params);
1011
}
12+
public beforeProcess(_1: IImageContext, _2: string[], _3: number): void { }
1113
}
14+
15+
export class ActionMask implements IActionMask {
16+
private readonly _masks: boolean[];
17+
18+
public constructor(private readonly _actions: string[]) {
19+
this._masks = _actions.map(() => true);
20+
}
21+
22+
public get length(): number {
23+
return this._actions.length;
24+
}
25+
26+
private _check(index: number): void {
27+
if (!(0 <= index && index < this.length)) {
28+
throw new Error('Index out of range');
29+
}
30+
}
31+
32+
public getAction(index: number): string {
33+
this._check(index);
34+
return this._actions[index];
35+
}
36+
37+
public isEnabled(index: number): boolean {
38+
this._check(index);
39+
return this._masks[index];
40+
}
41+
42+
public isDisabled(index: number): boolean {
43+
this._check(index);
44+
return !this._masks[index];
45+
}
46+
47+
public enable(index: number) {
48+
this._check(index);
49+
this._masks[index] = true;
50+
}
51+
52+
public disable(index: number) {
53+
this._check(index);
54+
this._masks[index] = false;
55+
}
56+
57+
public disableAll() {
58+
for (let i = 0; i < this._masks.length; i++) {
59+
this._masks[i] = false;
60+
}
61+
}
62+
63+
public filterEnabledActions(): string[] {
64+
return this._actions.filter((_, index) => this._masks[index]);
65+
}
66+
67+
public forEachAction(cb: (action: string, enabled: boolean, index: number) => void): void {
68+
this._actions.forEach((action, index) => {
69+
cb(action, this.isEnabled(index), index);
70+
});
71+
}
72+
}

source/new-image-handler/src/processor/image/format.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ export class FormatAction extends BaseImageAction {
1818
}
1919
}
2020

21+
public beforeProcess(ctx: IImageContext, params: string[], index: number): void {
22+
const opts = this.validate(params);
23+
if (('gif' === ctx.metadata.format) && ('gif' === opts.format)) {
24+
ctx.mask.disable(index);
25+
}
26+
}
27+
2128
public validate(params: string[]): ReadOnly<FormatOpts> {
2229
let opt: FormatOpts = { format: '' };
2330

source/new-image-handler/src/processor/image/index.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as sharp from 'sharp';
22
import { Features, IAction, InvalidArgument, IProcessContext, IProcessor, IProcessResponse } from '../../processor';
33
import { IBufferStore } from '../../store';
4+
import { ActionMask } from './_base';
45
import { AutoOrientAction } from './auto-orient';
56
import { BlurAction } from './blur';
67
import { BrightAction } from './bright';
@@ -46,14 +47,16 @@ export class ImageProcessor implements IProcessor {
4647
const ctx: IProcessContext = {
4748
uri,
4849
actions,
50+
mask: new ActionMask(actions),
4951
bufferStore,
5052
features: {
5153
[Features.AutoOrient]: true,
5254
[Features.ReadAllAnimatedFrames]: true,
5355
},
5456
headers: {},
5557
};
56-
for (const action of actions) {
58+
for (let i = 0; i < actions.length; i++) {
59+
const action = actions[i];
5760
if ((this.name === action) || (!action)) {
5861
continue;
5962
}
@@ -64,7 +67,7 @@ export class ImageProcessor implements IProcessor {
6467
if (!act) {
6568
throw new InvalidArgument(`Unkown action: "${name}"`);
6669
}
67-
act.beforeNewContext.bind(act)(ctx, params);
70+
act.beforeNewContext.bind(act)(ctx, params, i);
6871
}
6972
const { buffer, headers } = await bufferStore.get(uri);
7073
const image = sharp(buffer, { failOnError: false, animated: ctx.features[Features.ReadAllAnimatedFrames] });
@@ -77,7 +80,7 @@ export class ImageProcessor implements IProcessor {
7780
return {
7881
uri: ctx.uri,
7982
actions: ctx.actions,
80-
effectiveActions: ctx.effectiveActions,
83+
mask: ctx.mask,
8184
bufferStore: ctx.bufferStore,
8285
features: ctx.features,
8386
headers: Object.assign(ctx.headers, headers),
@@ -96,8 +99,28 @@ export class ImageProcessor implements IProcessor {
9699

97100
if (ctx.features[Features.AutoOrient]) { ctx.image.rotate(); }
98101

99-
const actions = (ctx.effectiveActions && ctx.effectiveActions.length) ? ctx.effectiveActions : ctx.actions;
100-
for (const action of actions) {
102+
ctx.mask.forEachAction((action, _, index) => {
103+
if ((this.name === action) || (!action)) {
104+
return;
105+
}
106+
// "<action-name>,<param-1>,<param-2>,..."
107+
const params = action.split(',');
108+
const name = params[0];
109+
const act = this.action(name);
110+
if (!act) {
111+
throw new InvalidArgument(`Unkown action: "${name}"`);
112+
}
113+
act.beforeProcess.bind(act)(ctx, params, index);
114+
});
115+
const enabledActions = ctx.mask.filterEnabledActions();
116+
const nothing2do = (enabledActions.length === 1) && (this.name === enabledActions[0]);
117+
118+
if (nothing2do && (!ctx.features[Features.AutoWebp])) {
119+
const { buffer } = await ctx.bufferStore.get(ctx.uri);
120+
return { data: buffer, type: ctx.metadata.format! };
121+
}
122+
123+
for (const action of enabledActions) {
101124
if ((this.name === action) || (!action)) {
102125
continue;
103126
}

source/new-image-handler/src/processor/image/info.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
import { IImageContext } from '.';
2-
import { IActionOpts, ReadOnly, InvalidArgument, Features, IProcessContext } from '..';
2+
import { IActionOpts, ReadOnly, InvalidArgument, Features } from '..';
33
import { BaseImageAction } from './_base';
44

55

66
export class InfoAction extends BaseImageAction {
77
public readonly name: string = 'info';
88

9-
public beforeNewContext(ctx: IProcessContext, params: string[]): void {
10-
this.validate(params);
11-
12-
const action = params.join(',');
13-
if (ctx.effectiveActions) {
14-
ctx.effectiveActions.push(action);
15-
} else {
16-
ctx.effectiveActions = [action];
17-
}
9+
public beforeProcess(ctx: IImageContext, _2: string[], index: number): void {
10+
ctx.mask.disableAll();
11+
ctx.mask.enable(index);
1812
}
1913

2014
public validate(params: string[]): ReadOnly<IActionOpts> {

source/new-image-handler/src/processor/image/quality.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export interface QualityOpts extends IActionOpts {
1818
export class QualityAction extends BaseImageAction {
1919
public readonly name: string = 'quality';
2020

21+
public beforeProcess(ctx: IImageContext, _2: string[], index: number): void {
22+
if ('gif' === ctx.metadata.format) {
23+
ctx.mask.disable(index);
24+
}
25+
}
26+
2127
public validate(params: string[]): ReadOnly<QualityOpts> {
2228
const opt: QualityOpts = {};
2329
for (const param of params) {

source/new-image-handler/src/processor/index.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ export type ReadOnly<T> = {
88
readonly [K in keyof T]: ReadOnly<T[K]>;
99
}
1010

11+
export interface IActionMask {
12+
readonly length: number;
13+
getAction(index: number): string;
14+
isEnabled(index: number): boolean;
15+
isDisabled(index: number): boolean;
16+
enable(index: number): void;
17+
disable(index: number): void;
18+
disableAll(): void;
19+
filterEnabledActions(): string[];
20+
forEachAction(cb: (action: string, enabled: boolean, index: number) => void): void;
21+
}
22+
1123
/**
1224
* Context object for processor.
1325
*/
@@ -22,12 +34,7 @@ export interface IProcessContext {
2234
*/
2335
readonly actions: string[];
2436

25-
/**
26-
* The effective actions.
27-
* If this value is undefined or empty list. All actions will be effective.
28-
* Otherwise, only the action in this list will be effective.
29-
*/
30-
effectiveActions?: string[];
37+
readonly mask: IActionMask;
3138

3239
/**
3340
* A abstract store to get file data.
@@ -149,7 +156,9 @@ export interface IAction {
149156
* @param ctx the context
150157
* @param params the parameters
151158
*/
152-
beforeNewContext(ctx: IProcessContext, params: string[]): void;
159+
beforeNewContext(ctx: IProcessContext, params: string[], index: number): void;
160+
161+
beforeProcess(ctx: IProcessContext, params: string[], index: number): void;
153162
}
154163

155164
/**

source/new-image-handler/src/processor/style.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IAction, InvalidArgument, IProcessContext, IProcessor, IProcessResponse } from '.';
22
import * as is from '../is';
33
import { IBufferStore, IKVStore, MemKVStore } from '../store';
4+
import { ActionMask } from './image/_base';
45
import { ImageProcessor } from './image/index';
56
import { VideoProcessor } from './video';
67

@@ -32,6 +33,7 @@ export class StyleProcessor implements IProcessor {
3233
return Promise.resolve({
3334
uri,
3435
actions,
36+
mask: new ActionMask(actions),
3537
bufferStore,
3638
headers: {},
3739
features: {},

source/new-image-handler/src/processor/video.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as child_process from 'child_process';
22
import { IAction, InvalidArgument, IProcessContext, IProcessor, IProcessResponse, IActionOpts, ReadOnly } from '.';
33
import * as is from '../is';
44
import { IBufferStore } from '../store';
5+
import { ActionMask } from './image/_base';
56

67
export interface VideoOpts extends IActionOpts {
78
t: number; // 指定截图时间, 单位:s
@@ -27,6 +28,7 @@ export class VideoProcessor implements IProcessor {
2728
return Promise.resolve({
2829
uri,
2930
actions,
31+
mask: new ActionMask(actions),
3032
bufferStore,
3133
features: {},
3234
headers: {},
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { ActionMask } from '../../../src/processor/image/_base';
2+
3+
test(`${ActionMask.name} all enabled by default`, () => {
4+
const actions = '1 1 1 1 1 1'.split(' ');
5+
const s = new ActionMask(actions);
6+
7+
expect(s.length).toBe(actions.length);
8+
expect(s.filterEnabledActions()).toEqual(actions);
9+
});
10+
11+
test(`${ActionMask.name} getAction`, () => {
12+
const actions = '1 2 3 4 5 6'.split(' ');
13+
const s = new ActionMask(actions);
14+
15+
expect(s.length).toBe(actions.length);
16+
17+
s.disable(0);
18+
s.enable(0);
19+
expect(s.isDisabled(0)).toBeFalsy();
20+
21+
s.forEachAction((action, enable, index) => {
22+
expect(s.getAction(index)).toEqual(action);
23+
expect(s.isEnabled(index)).toBe(enable);
24+
expect(action).toBe(`${1 + index}`);
25+
});
26+
});
27+
28+
test(`${ActionMask.name} enable/disable`, () => {
29+
const actions = '1 2 3 4 5 6'.split(' ');
30+
const s = new ActionMask(actions);
31+
32+
expect(s.length).toBe(actions.length);
33+
s.disable(0);
34+
s.disable(1);
35+
s.disable(2);
36+
expect(s.filterEnabledActions().length).toBe(3);
37+
expect(s.filterEnabledActions()).toEqual('4 5 6'.split(' '));
38+
});
39+
40+
test(`${ActionMask.name} forEach enable/disable`, () => {
41+
const actions = '1 2 3 4 5 6'.split(' ');
42+
const s = new ActionMask(actions);
43+
44+
expect(s.length).toBe(actions.length);
45+
s.forEachAction((_, enabled, index) => {
46+
expect(enabled).toBeTruthy();
47+
s.disable(index);
48+
});
49+
expect(s.filterEnabledActions()).toEqual([]);
50+
});
51+
52+
test(`${ActionMask.name} index out of range`, () => {
53+
const actions = '1 2 3 4 5 6'.split(' ');
54+
const s = new ActionMask(actions);
55+
56+
expect(() => {
57+
s.enable(-1);
58+
}).toThrowError(/Index out of range/);
59+
});
60+
61+

source/new-image-handler/test/processor/image/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from 'path';
22
import * as sharp from 'sharp';
33
import { IImageContext } from '../../../src/processor/image';
4+
import { ActionMask } from '../../../src/processor/image/_base';
45
import { IBufferStore, LocalStore } from '../../../src/store';
56

67
export const fixtureStore = new LocalStore(path.join(__dirname, '../../fixtures'));
@@ -12,5 +13,8 @@ export async function mkctx(name: string, actions?: string[], bufferStore?: IBuf
1213
const image = sharp((await bufferStore.get(name)).buffer, {
1314
animated: (name.endsWith('.gif') || name.endsWith('.webp')),
1415
});
15-
return { uri: name, actions: actions ?? [], image, bufferStore, features: {}, headers: {}, metadata: await image.metadata() };
16+
17+
const actions2 = actions ?? [];
18+
const mask = new ActionMask(actions2);
19+
return { uri: name, actions: actions2, mask, image, bufferStore, features: {}, headers: {}, metadata: await image.metadata() };
1620
}

0 commit comments

Comments
 (0)