summaryrefslogtreecommitdiff
path: root/src/misc/get-file-info.ts
diff options
context:
space:
mode:
authorMeiMei <30769358+mei23@users.noreply.github.com>2020-01-12 16:40:58 +0900
committerGitHub <noreply@github.com>2020-01-12 16:40:58 +0900
commit9703ba53405b2f355c6e0317f714d82ff3d4dee3 (patch)
tree4cd80df78b5c78bb60d47179836d393ee8d805d4 /src/misc/get-file-info.ts
parentRefactor (diff)
downloadmisskey-9703ba53405b2f355c6e0317f714d82ff3d4dee3.tar.gz
misskey-9703ba53405b2f355c6e0317f714d82ff3d4dee3.tar.bz2
misskey-9703ba53405b2f355c6e0317f714d82ff3d4dee3.zip
ファイルと画像認識処理の改善 (#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 (硫酸鶏) <root@acid-chicken.com> * fix d.ts * Update src/@types/probe-image-size.d.ts Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * Update src/@types/probe-image-size.d.ts Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * fix Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Diffstat (limited to '')
-rw-r--r--src/misc/get-file-info.ts201
1 files changed, 201 insertions, 0 deletions
diff --git a/src/misc/get-file-info.ts b/src/misc/get-file-info.ts
new file mode 100644
index 0000000000..5ccb280260
--- /dev/null
+++ b/src/misc/get-file-info.ts
@@ -0,0 +1,201 @@
+import * as fs from 'fs';
+import * as crypto from 'crypto';
+import * as fileType from 'file-type';
+import isSvg from 'is-svg';
+import * as probeImageSize from 'probe-image-size';
+import * as sharp from 'sharp';
+
+export type FileInfo = {
+ size: number;
+ md5: string;
+ type: {
+ mime: string;
+ ext: string | null;
+ };
+ width?: number;
+ height?: number;
+ avgColor?: number[];
+ warnings: string[];
+};
+
+const TYPE_OCTET_STREAM = {
+ mime: 'application/octet-stream',
+ ext: null
+};
+
+const TYPE_SVG = {
+ mime: 'image/svg+xml',
+ ext: 'svg'
+};
+
+/**
+ * Get file information
+ */
+export async function getFileInfo(path: string): Promise<FileInfo> {
+ const warnings = [] as string[];
+
+ const size = await getFileSize(path);
+ const md5 = await calcHash(path);
+
+ let type = await detectType(path);
+
+ // image dimensions
+ let width: number | undefined;
+ let height: number | undefined;
+
+ 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)) {
+ const imageSize = await detectImageSize(path).catch(e => {
+ warnings.push(`detectImageSize failed: ${e}`);
+ return undefined;
+ });
+
+ // うまく判定できない画像は octet-stream にする
+ if (!imageSize) {
+ warnings.push(`cannot detect image dimensions`);
+ type = TYPE_OCTET_STREAM;
+ } else if (imageSize.wUnits === 'px') {
+ width = imageSize.width;
+ height = imageSize.height;
+
+ // 制限を超えている画像は octet-stream にする
+ if (imageSize.width > 16383 || imageSize.height > 16383) {
+ warnings.push(`image dimensions exceeds limits`);
+ type = TYPE_OCTET_STREAM;
+ }
+ } else {
+ warnings.push(`unsupported unit type: ${imageSize.wUnits}`);
+ }
+ }
+
+ // average color
+ let avgColor: number[] | undefined;
+
+ if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) {
+ avgColor = await calcAvgColor(path).catch(e => {
+ warnings.push(`calcAvgColor failed: ${e}`);
+ return undefined;
+ });
+ }
+
+ return {
+ size,
+ md5,
+ type,
+ width,
+ height,
+ avgColor,
+ warnings,
+ };
+}
+
+/**
+ * Detect MIME Type and extension
+ */
+export async function detectType(path: string) {
+ // Check 0 byte
+ const fileSize = await getFileSize(path);
+ if (fileSize === 0) {
+ return TYPE_OCTET_STREAM;
+ }
+
+ const type = await fileType.fromFile(path);
+
+ if (type) {
+ // XMLはSVGかもしれない
+ if (type.mime === 'application/xml' && await checkSvg(path)) {
+ return TYPE_SVG;
+ }
+
+ return {
+ mime: type.mime,
+ ext: type.ext
+ };
+ }
+
+ // 種類が不明でもSVGかもしれない
+ if (await checkSvg(path)) {
+ return TYPE_SVG;
+ }
+
+ // それでも種類が不明なら application/octet-stream にする
+ return TYPE_OCTET_STREAM;
+}
+
+/**
+ * Check the file is SVG or not
+ */
+export async function checkSvg(path: string) {
+ try {
+ const size = await getFileSize(path);
+ if (size > 1 * 1024 * 1024) return false;
+ return isSvg(fs.readFileSync(path));
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Get file size
+ */
+export async function getFileSize(path: string): Promise<number> {
+ return new Promise<number>((res, rej) => {
+ fs.stat(path, (err, stats) => {
+ if (err) return rej(err);
+ res(stats.size);
+ });
+ });
+}
+
+/**
+ * Calculate MD5 hash
+ */
+async function calcHash(path: string): Promise<string> {
+ return new Promise<string>((res, rej) => {
+ const readable = fs.createReadStream(path);
+ const hash = crypto.createHash('md5');
+ const chunks: Buffer[] = [];
+ readable
+ .on('error', rej)
+ .pipe(hash)
+ .on('error', rej)
+ .on('data', chunk => chunks.push(chunk))
+ .on('end', () => {
+ const buffer = Buffer.concat(chunks);
+ res(buffer.toString('hex'));
+ });
+ });
+}
+
+/**
+ * Detect dimensions of image
+ */
+async function detectImageSize(path: string): Promise<{
+ width: number;
+ height: number;
+ wUnits: string;
+ hUnits: string;
+}> {
+ const readable = fs.createReadStream(path);
+ const imageSize = await probeImageSize(readable);
+ readable.destroy();
+ return imageSize;
+}
+
+/**
+ * Calculate average color of image
+ */
+async function calcAvgColor(path: string): Promise<number[]> {
+ const img = sharp(path);
+
+ const info = await (img as any).stats();
+
+ if (info.isOpaque) {
+ const r = Math.round(info.channels[0].mean);
+ const g = Math.round(info.channels[1].mean);
+ const b = Math.round(info.channels[2].mean);
+
+ return [r, g, b];
+ } else {
+ return [255, 255, 255];
+ }
+}