Skip to content
This repository was archived by the owner on Feb 5, 2024. It is now read-only.

Commit 9703ba5

Browse files
mei23acid-chicken
andauthored
ファイルと画像認識処理の改善 (misskey-dev#5690)
* dimensions制限とリファクタ * comment * 不要な変更削除 * use fromFile など * Add probe-image-size.d.ts * えーCRLFで作るなよ… * Update src/@types/probe-image-size.d.ts Co-Authored-By: Acid Chicken (硫酸鶏) <[email protected]> * fix d.ts * Update src/@types/probe-image-size.d.ts Co-Authored-By: Acid Chicken (硫酸鶏) <[email protected]> * Update src/@types/probe-image-size.d.ts Co-Authored-By: Acid Chicken (硫酸鶏) <[email protected]> * fix Co-authored-by: Acid Chicken (硫酸鶏) <[email protected]>
1 parent d09d06e commit 9703ba5

20 files changed

+456
-154
lines changed

.imgbotconfig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"ignoredFiles": [
3+
"test/resources/*"
4+
]
5+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
"portscanner": "2.2.0",
181181
"postcss-loader": "3.0.0",
182182
"prismjs": "1.18.0",
183+
"probe-image-size": "5.0.0",
183184
"progress-bar-webpack-plugin": "1.12.1",
184185
"promise-limit": "2.7.0",
185186
"promise-sequential": "1.1.1",

src/@types/probe-image-size.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
declare module 'probe-image-size' {
2+
import { ReadStream } from 'fs';
3+
4+
type ProbeOptions = {
5+
retries: 1;
6+
timeout: 30000;
7+
};
8+
9+
type ProbeResult = {
10+
width: number;
11+
height: number;
12+
length?: number;
13+
type: string;
14+
mime: string;
15+
wUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex';
16+
hUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex';
17+
url?: string;
18+
};
19+
20+
function probeImageSize(src: string | ReadStream, options?: ProbeOptions): Promise<ProbeResult>;
21+
function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void;
22+
function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void;
23+
24+
namespace probeImageSize {} // Hack
25+
26+
export = probeImageSize;
27+
}

src/misc/check-svg.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/misc/detect-mine.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/misc/detect-url-mine.ts renamed to src/misc/detect-url-mime.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { createTemp } from './create-temp';
22
import { downloadUrl } from './donwload-url';
3-
import { detectMine } from './detect-mine';
3+
import { detectType } from './get-file-info';
44

5-
export async function detectUrlMine(url: string) {
5+
export async function detectUrlMime(url: string) {
66
const [path, cleanup] = await createTemp();
77

88
try {
99
await downloadUrl(url, path);
10-
const [type] = await detectMine(path);
11-
return type;
10+
const { mime } = await detectType(path);
11+
return mime;
1212
} finally {
1313
cleanup();
1414
}

src/misc/get-file-info.ts

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import * as fs from 'fs';
2+
import * as crypto from 'crypto';
3+
import * as fileType from 'file-type';
4+
import isSvg from 'is-svg';
5+
import * as probeImageSize from 'probe-image-size';
6+
import * as sharp from 'sharp';
7+
8+
export type FileInfo = {
9+
size: number;
10+
md5: string;
11+
type: {
12+
mime: string;
13+
ext: string | null;
14+
};
15+
width?: number;
16+
height?: number;
17+
avgColor?: number[];
18+
warnings: string[];
19+
};
20+
21+
const TYPE_OCTET_STREAM = {
22+
mime: 'application/octet-stream',
23+
ext: null
24+
};
25+
26+
const TYPE_SVG = {
27+
mime: 'image/svg+xml',
28+
ext: 'svg'
29+
};
30+
31+
/**
32+
* Get file information
33+
*/
34+
export async function getFileInfo(path: string): Promise<FileInfo> {
35+
const warnings = [] as string[];
36+
37+
const size = await getFileSize(path);
38+
const md5 = await calcHash(path);
39+
40+
let type = await detectType(path);
41+
42+
// image dimensions
43+
let width: number | undefined;
44+
let height: number | undefined;
45+
46+
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) {
47+
const imageSize = await detectImageSize(path).catch(e => {
48+
warnings.push(`detectImageSize failed: ${e}`);
49+
return undefined;
50+
});
51+
52+
// うまく判定できない画像は octet-stream にする
53+
if (!imageSize) {
54+
warnings.push(`cannot detect image dimensions`);
55+
type = TYPE_OCTET_STREAM;
56+
} else if (imageSize.wUnits === 'px') {
57+
width = imageSize.width;
58+
height = imageSize.height;
59+
60+
// 制限を超えている画像は octet-stream にする
61+
if (imageSize.width > 16383 || imageSize.height > 16383) {
62+
warnings.push(`image dimensions exceeds limits`);
63+
type = TYPE_OCTET_STREAM;
64+
}
65+
} else {
66+
warnings.push(`unsupported unit type: ${imageSize.wUnits}`);
67+
}
68+
}
69+
70+
// average color
71+
let avgColor: number[] | undefined;
72+
73+
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) {
74+
avgColor = await calcAvgColor(path).catch(e => {
75+
warnings.push(`calcAvgColor failed: ${e}`);
76+
return undefined;
77+
});
78+
}
79+
80+
return {
81+
size,
82+
md5,
83+
type,
84+
width,
85+
height,
86+
avgColor,
87+
warnings,
88+
};
89+
}
90+
91+
/**
92+
* Detect MIME Type and extension
93+
*/
94+
export async function detectType(path: string) {
95+
// Check 0 byte
96+
const fileSize = await getFileSize(path);
97+
if (fileSize === 0) {
98+
return TYPE_OCTET_STREAM;
99+
}
100+
101+
const type = await fileType.fromFile(path);
102+
103+
if (type) {
104+
// XMLはSVGかもしれない
105+
if (type.mime === 'application/xml' && await checkSvg(path)) {
106+
return TYPE_SVG;
107+
}
108+
109+
return {
110+
mime: type.mime,
111+
ext: type.ext
112+
};
113+
}
114+
115+
// 種類が不明でもSVGかもしれない
116+
if (await checkSvg(path)) {
117+
return TYPE_SVG;
118+
}
119+
120+
// それでも種類が不明なら application/octet-stream にする
121+
return TYPE_OCTET_STREAM;
122+
}
123+
124+
/**
125+
* Check the file is SVG or not
126+
*/
127+
export async function checkSvg(path: string) {
128+
try {
129+
const size = await getFileSize(path);
130+
if (size > 1 * 1024 * 1024) return false;
131+
return isSvg(fs.readFileSync(path));
132+
} catch {
133+
return false;
134+
}
135+
}
136+
137+
/**
138+
* Get file size
139+
*/
140+
export async function getFileSize(path: string): Promise<number> {
141+
return new Promise<number>((res, rej) => {
142+
fs.stat(path, (err, stats) => {
143+
if (err) return rej(err);
144+
res(stats.size);
145+
});
146+
});
147+
}
148+
149+
/**
150+
* Calculate MD5 hash
151+
*/
152+
async function calcHash(path: string): Promise<string> {
153+
return new Promise<string>((res, rej) => {
154+
const readable = fs.createReadStream(path);
155+
const hash = crypto.createHash('md5');
156+
const chunks: Buffer[] = [];
157+
readable
158+
.on('error', rej)
159+
.pipe(hash)
160+
.on('error', rej)
161+
.on('data', chunk => chunks.push(chunk))
162+
.on('end', () => {
163+
const buffer = Buffer.concat(chunks);
164+
res(buffer.toString('hex'));
165+
});
166+
});
167+
}
168+
169+
/**
170+
* Detect dimensions of image
171+
*/
172+
async function detectImageSize(path: string): Promise<{
173+
width: number;
174+
height: number;
175+
wUnits: string;
176+
hUnits: string;
177+
}> {
178+
const readable = fs.createReadStream(path);
179+
const imageSize = await probeImageSize(readable);
180+
readable.destroy();
181+
return imageSize;
182+
}
183+
184+
/**
185+
* Calculate average color of image
186+
*/
187+
async function calcAvgColor(path: string): Promise<number[]> {
188+
const img = sharp(path);
189+
190+
const info = await (img as any).stats();
191+
192+
if (info.isOpaque) {
193+
const r = Math.round(info.channels[0].mean);
194+
const g = Math.round(info.channels[1].mean);
195+
const b = Math.round(info.channels[2].mean);
196+
197+
return [r, g, b];
198+
} else {
199+
return [255, 255, 255];
200+
}
201+
}

src/server/api/endpoints/admin/emoji/add.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import $ from 'cafy';
22
import define from '../../../define';
3-
import { detectUrlMine } from '../../../../../misc/detect-url-mine';
3+
import { detectUrlMime } from '../../../../../misc/detect-url-mime';
44
import { Emojis } from '../../../../../models';
55
import { genId } from '../../../../../misc/gen-id';
66
import { getConnection } from 'typeorm';
@@ -46,7 +46,7 @@ export const meta = {
4646
};
4747

4848
export default define(meta, async (ps, me) => {
49-
const type = await detectUrlMine(ps.url);
49+
const type = await detectUrlMime(ps.url);
5050

5151
const exists = await Emojis.findOne({
5252
name: ps.name,

src/server/api/endpoints/admin/emoji/update.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import $ from 'cafy';
22
import define from '../../../define';
3-
import { detectUrlMine } from '../../../../../misc/detect-url-mine';
3+
import { detectUrlMime } from '../../../../../misc/detect-url-mime';
44
import { ID } from '../../../../../misc/cafy-id';
55
import { Emojis } from '../../../../../models';
66
import { getConnection } from 'typeorm';
@@ -52,7 +52,7 @@ export default define(meta, async (ps) => {
5252

5353
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
5454

55-
const type = await detectUrlMine(ps.url);
55+
const type = await detectUrlMime(ps.url);
5656

5757
await Emojis.update(emoji.id, {
5858
updatedAt: new Date(),

src/server/file/send-drive-file.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { contentDisposition } from '../../misc/content-disposition';
88
import { DriveFiles } from '../../models';
99
import { InternalStorage } from '../../services/drive/internal-storage';
1010
import { downloadUrl } from '../../misc/donwload-url';
11-
import { detectMine } from '../../misc/detect-mine';
11+
import { detectType } from '../../misc/get-file-info';
1212
import { convertToJpeg, convertToPng } from '../../services/drive/image-processor';
1313
import { GenerateVideoThumbnail } from '../../services/drive/generate-video-thumbnail';
1414

@@ -52,23 +52,23 @@ export default async function(ctx: Koa.Context) {
5252
try {
5353
await downloadUrl(file.uri, path);
5454

55-
const [type, ext] = await detectMine(path);
55+
const { mime, ext } = await detectType(path);
5656

5757
const convertFile = async () => {
5858
if (isThumbnail) {
59-
if (['image/jpeg', 'image/webp'].includes(type)) {
59+
if (['image/jpeg', 'image/webp'].includes(mime)) {
6060
return await convertToJpeg(path, 498, 280);
61-
} else if (['image/png'].includes(type)) {
61+
} else if (['image/png'].includes(mime)) {
6262
return await convertToPng(path, 498, 280);
63-
} else if (type.startsWith('video/')) {
63+
} else if (mime.startsWith('video/')) {
6464
return await GenerateVideoThumbnail(path);
6565
}
6666
}
6767

6868
return {
6969
data: fs.readFileSync(path),
7070
ext,
71-
type,
71+
type: mime,
7272
};
7373
};
7474

0 commit comments

Comments
 (0)