From 9703ba53405b2f355c6e0317f714d82ff3d4dee3 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Sun, 12 Jan 2020 16:40:58 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=A8?= =?UTF-8?q?=E7=94=BB=E5=83=8F=E8=AA=8D=E8=AD=98=E5=87=A6=E7=90=86=E3=81=AE?= =?UTF-8?q?=E6=94=B9=E5=96=84=20(#5690)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * dimensions制限とリファクタ * comment * 不要な変更削除 * use fromFile など * Add probe-image-size.d.ts * えーCRLFで作るなよ… * Update src/@types/probe-image-size.d.ts Co-Authored-By: Acid Chicken (硫酸鶏) * fix d.ts * Update src/@types/probe-image-size.d.ts Co-Authored-By: Acid Chicken (硫酸鶏) * Update src/@types/probe-image-size.d.ts Co-Authored-By: Acid Chicken (硫酸鶏) * fix Co-authored-by: Acid Chicken (硫酸鶏) --- .imgbotconfig | 5 + package.json | 1 + src/@types/probe-image-size.d.ts | 27 +++ src/misc/check-svg.ts | 12 -- src/misc/detect-mine.ts | 31 --- ...{detect-url-mine.ts => detect-url-mime.ts} | 8 +- src/misc/get-file-info.ts | 201 ++++++++++++++++++ src/server/api/endpoints/admin/emoji/add.ts | 4 +- .../api/endpoints/admin/emoji/update.ts | 4 +- src/server/file/send-drive-file.ts | 12 +- src/server/proxy/proxy-media.ts | 12 +- src/services/drive/add-file.ts | 103 ++------- test/get-file-info.ts | 152 +++++++++++++ test/resources/25000x25000.png | Bin 0 -> 75933 bytes test/resources/anime.gif | Bin 0 -> 2248 bytes test/resources/anime.png | Bin 0 -> 1868 bytes test/resources/emptyfile | 0 test/resources/with-alpha.png | Bin 0 -> 3772 bytes test/resources/with-xml-def.svg | Bin 0 -> 544 bytes yarn.lock | 36 +++- 20 files changed, 454 insertions(+), 154 deletions(-) create mode 100644 .imgbotconfig create mode 100644 src/@types/probe-image-size.d.ts delete mode 100644 src/misc/check-svg.ts delete mode 100644 src/misc/detect-mine.ts rename src/misc/{detect-url-mine.ts => detect-url-mime.ts} (57%) create mode 100644 src/misc/get-file-info.ts create mode 100644 test/get-file-info.ts create mode 100644 test/resources/25000x25000.png create mode 100644 test/resources/anime.gif create mode 100644 test/resources/anime.png create mode 100644 test/resources/emptyfile create mode 100644 test/resources/with-alpha.png create mode 100644 test/resources/with-xml-def.svg diff --git a/.imgbotconfig b/.imgbotconfig new file mode 100644 index 000000000..6a1dfe1ed --- /dev/null +++ b/.imgbotconfig @@ -0,0 +1,5 @@ +{ + "ignoredFiles": [ + "test/resources/*" + ] +} diff --git a/package.json b/package.json index 0f0dda57a..d5b85fc8c 100644 --- a/package.json +++ b/package.json @@ -180,6 +180,7 @@ "portscanner": "2.2.0", "postcss-loader": "3.0.0", "prismjs": "1.18.0", + "probe-image-size": "5.0.0", "progress-bar-webpack-plugin": "1.12.1", "promise-limit": "2.7.0", "promise-sequential": "1.1.1", diff --git a/src/@types/probe-image-size.d.ts b/src/@types/probe-image-size.d.ts new file mode 100644 index 000000000..665edcf2e --- /dev/null +++ b/src/@types/probe-image-size.d.ts @@ -0,0 +1,27 @@ +declare module 'probe-image-size' { + import { ReadStream } from 'fs'; + + type ProbeOptions = { + retries: 1; + timeout: 30000; + }; + + type ProbeResult = { + width: number; + height: number; + length?: number; + type: string; + mime: string; + wUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex'; + hUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex'; + url?: string; + }; + + function probeImageSize(src: string | ReadStream, options?: ProbeOptions): Promise; + function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void; + function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void; + + namespace probeImageSize {} // Hack + + export = probeImageSize; +} diff --git a/src/misc/check-svg.ts b/src/misc/check-svg.ts deleted file mode 100644 index 8ddeefede..000000000 --- a/src/misc/check-svg.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as fs from 'fs'; -import isSvg from 'is-svg'; - -export default function(path: string) { - try { - const size = fs.statSync(path).size; - if (size > 1 * 1024 * 1024) return false; - return isSvg(fs.readFileSync(path)); - } catch { - return false; - } -} diff --git a/src/misc/detect-mine.ts b/src/misc/detect-mine.ts deleted file mode 100644 index f47f12735..000000000 --- a/src/misc/detect-mine.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as fs from 'fs'; -import checkSvg from '../misc/check-svg'; -const FileType = require('file-type'); - -export async function detectMine(path: string) { - return new Promise<[string, string | null]>((res, rej) => { - const readable = fs.createReadStream(path); - readable - .on('error', rej) - .once('data', async (buffer: Buffer) => { - readable.destroy(); - const type = await FileType.fromBuffer(buffer); - if (type) { - if (type.mime == 'application/xml' && checkSvg(path)) { - res(['image/svg+xml', 'svg']); - } else { - res([type.mime, type.ext]); - } - } else if (checkSvg(path)) { - res(['image/svg+xml', 'svg']); - } else { - // 種類が同定できなかったら application/octet-stream にする - res(['application/octet-stream', null]); - } - }) - .on('end', () => { - // maybe 0 bytes - res(['application/octet-stream', null]); - }); - }); -} diff --git a/src/misc/detect-url-mine.ts b/src/misc/detect-url-mime.ts similarity index 57% rename from src/misc/detect-url-mine.ts rename to src/misc/detect-url-mime.ts index eef64cfc5..8d71cd013 100644 --- a/src/misc/detect-url-mine.ts +++ b/src/misc/detect-url-mime.ts @@ -1,14 +1,14 @@ import { createTemp } from './create-temp'; import { downloadUrl } from './donwload-url'; -import { detectMine } from './detect-mine'; +import { detectType } from './get-file-info'; -export async function detectUrlMine(url: string) { +export async function detectUrlMime(url: string) { const [path, cleanup] = await createTemp(); try { await downloadUrl(url, path); - const [type] = await detectMine(path); - return type; + const { mime } = await detectType(path); + return mime; } finally { cleanup(); } diff --git a/src/misc/get-file-info.ts b/src/misc/get-file-info.ts new file mode 100644 index 000000000..5ccb28026 --- /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 { + 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 { + return new Promise((res, rej) => { + fs.stat(path, (err, stats) => { + if (err) return rej(err); + res(stats.size); + }); + }); +} + +/** + * Calculate MD5 hash + */ +async function calcHash(path: string): Promise { + return new Promise((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 { + 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]; + } +} diff --git a/src/server/api/endpoints/admin/emoji/add.ts b/src/server/api/endpoints/admin/emoji/add.ts index 5345876da..3a17760e5 100644 --- a/src/server/api/endpoints/admin/emoji/add.ts +++ b/src/server/api/endpoints/admin/emoji/add.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import define from '../../../define'; -import { detectUrlMine } from '../../../../../misc/detect-url-mine'; +import { detectUrlMime } from '../../../../../misc/detect-url-mime'; import { Emojis } from '../../../../../models'; import { genId } from '../../../../../misc/gen-id'; import { getConnection } from 'typeorm'; @@ -46,7 +46,7 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const type = await detectUrlMine(ps.url); + const type = await detectUrlMime(ps.url); const exists = await Emojis.findOne({ name: ps.name, diff --git a/src/server/api/endpoints/admin/emoji/update.ts b/src/server/api/endpoints/admin/emoji/update.ts index f4a01a397..0651b8d28 100644 --- a/src/server/api/endpoints/admin/emoji/update.ts +++ b/src/server/api/endpoints/admin/emoji/update.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import define from '../../../define'; -import { detectUrlMine } from '../../../../../misc/detect-url-mine'; +import { detectUrlMime } from '../../../../../misc/detect-url-mime'; import { ID } from '../../../../../misc/cafy-id'; import { Emojis } from '../../../../../models'; import { getConnection } from 'typeorm'; @@ -52,7 +52,7 @@ export default define(meta, async (ps) => { if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - const type = await detectUrlMine(ps.url); + const type = await detectUrlMime(ps.url); await Emojis.update(emoji.id, { updatedAt: new Date(), diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts index 228343579..0b1437858 100644 --- a/src/server/file/send-drive-file.ts +++ b/src/server/file/send-drive-file.ts @@ -8,7 +8,7 @@ import { contentDisposition } from '../../misc/content-disposition'; import { DriveFiles } from '../../models'; import { InternalStorage } from '../../services/drive/internal-storage'; import { downloadUrl } from '../../misc/donwload-url'; -import { detectMine } from '../../misc/detect-mine'; +import { detectType } from '../../misc/get-file-info'; import { convertToJpeg, convertToPng } from '../../services/drive/image-processor'; import { GenerateVideoThumbnail } from '../../services/drive/generate-video-thumbnail'; @@ -52,15 +52,15 @@ export default async function(ctx: Koa.Context) { try { await downloadUrl(file.uri, path); - const [type, ext] = await detectMine(path); + const { mime, ext } = await detectType(path); const convertFile = async () => { if (isThumbnail) { - if (['image/jpeg', 'image/webp'].includes(type)) { + if (['image/jpeg', 'image/webp'].includes(mime)) { return await convertToJpeg(path, 498, 280); - } else if (['image/png'].includes(type)) { + } else if (['image/png'].includes(mime)) { return await convertToPng(path, 498, 280); - } else if (type.startsWith('video/')) { + } else if (mime.startsWith('video/')) { return await GenerateVideoThumbnail(path); } } @@ -68,7 +68,7 @@ export default async function(ctx: Koa.Context) { return { data: fs.readFileSync(path), ext, - type, + type: mime, }; }; diff --git a/src/server/proxy/proxy-media.ts b/src/server/proxy/proxy-media.ts index 232b7a09c..6b90e9992 100644 --- a/src/server/proxy/proxy-media.ts +++ b/src/server/proxy/proxy-media.ts @@ -4,7 +4,7 @@ import { serverLogger } from '..'; import { IImage, convertToPng, convertToJpeg } from '../../services/drive/image-processor'; import { createTemp } from '../../misc/create-temp'; import { downloadUrl } from '../../misc/donwload-url'; -import { detectMine } from '../../misc/detect-mine'; +import { detectType } from '../../misc/get-file-info'; export async function proxyMedia(ctx: Koa.Context) { const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url; @@ -15,21 +15,21 @@ export async function proxyMedia(ctx: Koa.Context) { try { await downloadUrl(url, path); - const [type, ext] = await detectMine(path); + const { mime, ext } = await detectType(path); - if (!type.startsWith('image/')) throw 403; + if (!mime.startsWith('image/')) throw 403; let image: IImage; - if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(type)) { + if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(mime)) { image = await convertToPng(path, 498, 280); - } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(type)) { + } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(mime)) { image = await convertToJpeg(path, 200, 200); } else { image = { data: fs.readFileSync(path), ext, - type, + type: mime, }; } diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index 350e4dfe1..7cc710c8b 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -1,9 +1,6 @@ -import { Buffer } from 'buffer'; import * as fs from 'fs'; -import * as crypto from 'crypto'; import { v4 as uuid } from 'uuid'; -import * as sharp from 'sharp'; import { publishMainStream, publishDriveStream } from '../stream'; import { deleteFile } from './delete-file'; @@ -12,7 +9,7 @@ import { GenerateVideoThumbnail } from './generate-video-thumbnail'; import { driveLogger } from './logger'; import { IImage, convertToJpeg, convertToWebp, convertToPng } from './image-processor'; import { contentDisposition } from '../../misc/content-disposition'; -import { detectMine } from '../../misc/detect-mine'; +import { getFileInfo } from '../../misc/get-file-info'; import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models'; import { InternalStorage } from './internal-storage'; import { DriveFile } from '../../models/entities/drive-file'; @@ -271,41 +268,16 @@ export default async function( uri: string | null = null, sensitive: boolean | null = null ): Promise { - // Calc md5 hash - const calcHash = new Promise((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')); - }); - }); - - // Get file size - const getFileSize = new Promise((res, rej) => { - fs.stat(path, (err, stats) => { - if (err) return rej(err); - res(stats.size); - }); - }); - - const [hash, [mime, ext], size] = await Promise.all([calcHash, detectMine(path), getFileSize]); - - logger.info(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`); + const info = await getFileInfo(path); + logger.info(`${JSON.stringify(info)}`); // detect name - const detectedName = name || (ext ? `untitled.${ext}` : 'untitled'); + const detectedName = name || (info.type.ext ? `untitled.${info.type.ext}` : 'untitled'); if (!force) { // Check if there is a file with the same hash const much = await DriveFiles.findOne({ - md5: hash, + md5: info.md5, userId: user.id, }); @@ -325,7 +297,7 @@ export default async function( logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); // If usage limit exceeded - if (usage + size > driveCapacity) { + if (usage + info.size > driveCapacity) { if (Users.isLocalUser(user)) { throw new Error('no-free-space'); } else { @@ -351,57 +323,24 @@ export default async function( return driveFolder; }; - const properties: {[key: string]: any} = {}; + const properties: { + width?: number; + height?: number; + avgColor?: string; + } = {}; - let propPromises: Promise[] = []; + if (info.width) { + properties['width'] = info.width; + properties['height'] = info.height; + } - const isImage = ['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'].includes(mime); - - if (isImage) { - const img = sharp(path); - - // Calc width and height - const calcWh = async () => { - logger.debug('calculating image width and height...'); - - // Calculate width and height - const meta = await img.metadata(); - - logger.debug(`image width and height is calculated: ${meta.width}, ${meta.height}`); - - properties['width'] = meta.width; - properties['height'] = meta.height; - }; - - // Calc average color - const calcAvg = async () => { - logger.debug('calculating average color...'); - - try { - const info = await img.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); - - logger.debug(`average color is calculated: ${r}, ${g}, ${b}`); - - properties['avgColor'] = `rgb(${r},${g},${b})`; - } else { - logger.debug(`this image is not opaque so average color is 255, 255, 255`); - - properties['avgColor'] = `rgb(255,255,255)`; - } - } catch (e) { } - }; - - propPromises = [calcWh(), calcAvg()]; + if (info.avgColor) { + properties['avgColor'] = `rgb(${info.avgColor.join(',')}`; } const profile = await UserProfiles.findOne(user.id); - const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]); + const folder = await fetchFolder(); let file = new DriveFile(); file.id = genId(); @@ -436,9 +375,9 @@ export default async function( if (isLink) { try { file.size = 0; - file.md5 = hash; + file.md5 = info.md5; file.name = detectedName; - file.type = mime; + file.type = info.type.mime; file.storedInternal = false; file = await DriveFiles.save(file); @@ -457,7 +396,7 @@ export default async function( } } } else { - file = await (save(file, path, detectedName, mime, hash, size)); + file = await (save(file, path, detectedName, info.type.mime, info.md5, info.size)); } logger.succ(`drive file has been created ${file.id}`); diff --git a/test/get-file-info.ts b/test/get-file-info.ts new file mode 100644 index 000000000..920df0738 --- /dev/null +++ b/test/get-file-info.ts @@ -0,0 +1,152 @@ +/* + * Tests for detection of file information + * + * How to run the tests: + * > TS_NODE_FILES=true npx mocha test/get-file-info.ts --require ts-node/register + * + * To specify test: + * > TS_NODE_FILES=true npx mocha test/get-file-info.ts --require ts-node/register -g 'test name' + */ + +import * as assert from 'assert'; +import { async } from './utils'; +import { getFileInfo } from '../src/misc/get-file-info'; + +describe('Get file info', () => { + it('Empty file', async (async () => { + const path = `${__dirname}/resources/emptyfile`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 0, + md5: 'd41d8cd98f00b204e9800998ecf8427e', + type: { + mime: 'application/octet-stream', + ext: null + }, + width: undefined, + height: undefined, + avgColor: undefined + }); + })); + + it('Generic JPEG', async (async () => { + const path = `${__dirname}/resources/Lenna.jpg`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 25360, + md5: '091b3f259662aa31e2ffef4519951168', + type: { + mime: 'image/jpeg', + ext: 'jpg' + }, + width: 512, + height: 512, + avgColor: [ 181, 99, 106 ] + }); + })); + + it('Generic APNG', async (async () => { + const path = `${__dirname}/resources/anime.png`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 1868, + md5: '08189c607bea3b952704676bb3c979e0', + type: { + mime: 'image/apng', + ext: 'apng' + }, + width: 256, + height: 256, + avgColor: [ 249, 253, 250 ] + }); + })); + + it('Generic AGIF', async (async () => { + const path = `${__dirname}/resources/anime.gif`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 2248, + md5: '32c47a11555675d9267aee1a86571e7e', + type: { + mime: 'image/gif', + ext: 'gif' + }, + width: 256, + height: 256, + avgColor: [ 249, 253, 250 ] + }); + })); + + it('PNG with alpha', async (async () => { + const path = `${__dirname}/resources/with-alpha.png`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 3772, + md5: 'f73535c3e1e27508885b69b10cf6e991', + type: { + mime: 'image/png', + ext: 'png' + }, + width: 256, + height: 256, + avgColor: [ 255, 255, 255 ] + }); + })); + + it('Generic SVG', async (async () => { + const path = `${__dirname}/resources/image.svg`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 505, + md5: 'b6f52b4b021e7b92cdd04509c7267965', + type: { + mime: 'image/svg+xml', + ext: 'svg' + }, + width: 256, + height: 256, + avgColor: [ 255, 255, 255 ] + }); + })); + + it('SVG with XML definition', async (async () => { + // https://github.com/syuilo/misskey/issues/4413 + const path = `${__dirname}/resources/with-xml-def.svg`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 544, + md5: '4b7a346cde9ccbeb267e812567e33397', + type: { + mime: 'image/svg+xml', + ext: 'svg' + }, + width: 256, + height: 256, + avgColor: [ 255, 255, 255 ] + }); + })); + + it('Dimension limit', async (async () => { + const path = `${__dirname}/resources/25000x25000.png`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 75933, + md5: '268c5dde99e17cf8fe09f1ab3f97df56', + type: { + mime: 'application/octet-stream', // do not treat as image + ext: null + }, + width: 25000, + height: 25000, + avgColor: undefined + }); + })); +}); diff --git a/test/resources/25000x25000.png b/test/resources/25000x25000.png new file mode 100644 index 0000000000000000000000000000000000000000..0ed4666925f715eca6fdd9831206ec478b304047 GIT binary patch literal 75933 zcmeI&y-HhQ0LS6?cp^w3Q3pW{loTAi19gy^56yo);7$+;8+ za|eD*p>uJ_kV$%)H+S*EbKrb%&T{_G{QK5EJFb*B%OQkH>!kT5g#K+jvwZeu@?r1m z?3TArJBLM4{CKF1Lbly)H5;AvjlaLMZ1!<*^Y7=|(!%V?L4W`O0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PCN8FnAf}F$M_r;_0Z}br2vxfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009E23-p((rPu-t-g~u1>SO#r0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAke+u-(86-`!F#XPNPdk01PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfWY^X~Kc)qni7KGgsK literal 0 HcmV?d00001 diff --git a/test/resources/anime.gif b/test/resources/anime.gif new file mode 100644 index 0000000000000000000000000000000000000000..256ba495ce11b5f1ec7649c739feb750844eb4df GIT binary patch literal 2248 zcmV;(2sigfNk%w1VE_RD0g(Uz0000Yu}uH}{{R30A^!_bMO0HmK~P09E-(WD0000X z`2+{owRzhow?MP^}6TAXXa9I&E#S>*`kr#5idk8=|b) zvuHiYS{oIuueAc=j%8&QfnBV%^6F*VHiM~Sd!@eW3OI0u!^#RX9o#rEFt2nNCwANf zGdyVMH2-5%Ig>lFR6J)EUC#6o(pN?$R9z6Z64&=+Bb2Nr)j-=0d9SwQ(Kkimu_uuv zei8R%Mhsbh28{~1MC3P|tCOz00ripFBQmdOy*Tvf#zS_G5x#W!><`YrbgvQncns-# zvqvMJ@&@=S>I?sbYt?<53D^sO0TSqhe^S6T9|!ye^`H+7ju0V*6@p}-g$;&-pfq%q zaA8&*CX=Ct8!i}Pa(+R0;yfdgxFU=CsrRB76CRZ!eL0%+;Z8Ni_v14$;@Bb+JMuWB zQbA(4$St%7mwwbP^hP@pdn~gTq<14B!U9=nw1x0$UzFA+f^D`nGN>#d$a)8Ex#or|uCoW8%Py|D zjVoTe<*EYjLh^1)in?C`t60DO{tIxx0uM}Z!3H0UaKZ{N%y7dFKMZli5>HHV#TH+T zamE^N%yGvae++WSB9BaR$tItSa>^>N%yP>vzYKHCGS5tN%{JeRbIv;N%yZ8^{|t1{ zLJv)J(MBJQbka&M&2-aF4^;pFA^8LaG5`PoEFu6P0KEX500092gpaAq?GGamRN9NP z-n@HSdE!Wx=4pTe%C_!1s_;zLwruD6-r@WYBn3Xcpz&x4D2JOO@(E2Wl}%^#`kYon z*lu>IQLVgWqc&qLt8M3tcn#OJE$O(OHIKaS{MM8yvDY_P@`q7Jn3$0VA*lGM!T5j( zS=nGnM`^iZxmpk-df}$eVwZmZ11oC8f|21zIUzlc3^?(Z z$dffVu4FmGcQ(*upH`bC5ts{yrF1!TszygrdoeZ1H}2cZm?cks@Ypcsj)p%=*i0?5;=rZ>5JsnZb?MfZ zUb7vSnQQ0+`f^L%ZMmZFq+)Gl9Dd4k2h?1)A`dm3Id4tRIYqBbdJJ5~^L)eG!?=1S z?iXyY;4c0VdCE(*${?R!k#}U^mAhvRZM|dj+0oelC#N3-y9F3uAM>?$UkD5ZcngD| zVfTiE1;*jvg9y?EONA4zQK19h?V{m^WOP_tWf9IKqEZyiHiB*aiI5^25F$t4dmY6P zV_Fx=#$t;#R?uS)|Gh&Bi5{Bh2p}*H$%iyX9=YRm=wNpaerkNkWF$oESCbOXyTSAUn)@rM! zbM?uno!9;<>9`vVb7;89AlntN%a*d7to9UI?o_<`#R8ELM!gUF#KUHyZP!n(83gZ*{ur@5A5)*3G-GK!wp}tF{tgT)v2ul4@|PK5_>!f zU6EcqXcUsfcCxEVz9jFgU!>HTTY0qia?fi8HBo0BTdeb|Gk@Ic%TEKMG!L}wEcMD& zEgiBsLH9H?))^NIwHqm7GMYm}J3Sc9`}RA;*U6?tbY4btT(A;y&)r_qnQ1+8ooSOh zk>64)a5vs^F)=vdi+WA1r*20*c5PD09^UrjM&W!y;#+eK>*b&$RyY`(cP=vJl|PH& z=q9xBN8_lcZg%9D&%Trvr~l>o=Su(H)ap6qKE>{M(5^c0Rm&`qIk6kh`{W!b4?Oeq zGGPex(Ia0w_1cP{1=1LcElSfDg~T)#yvlzRl!IJHO*Un{RLZ3gPc@`(UHL z5&rE5fV2`|=E_F8^*PRe8dD&9445|v`bdJ$(jPc1SU&!}4}o!@;OjOB!Vrp(gOeGd W2~9|w)0}XH4ipRvSICP30028Jj!=I9 literal 0 HcmV?d00001 diff --git a/test/resources/anime.png b/test/resources/anime.png new file mode 100644 index 0000000000000000000000000000000000000000..f13600f7a49951d0bd67cd0c8c34c06379b6a4cf GIT binary patch literal 1868 zcmbtVYgAKL8a?-t1Plby0#i{W6e~DGL5d2DJQ4&3k*5$!DHwx1Mz9dZ0Kq6BC}6P` zgz#!@2@)_2?-4ZU0srXfKnEi@<$kY3ItAd_7Qzg2C={{w`!ha-psAnN9eVE}aU{k%QNnX|8- z_-8zEzmNX8Tu3~1IymdV21^_Q*j+^cwm%Uq ze$6+p^L6yt6lFVf$h`WL24){dR_z$(Vl9E{g$RWQxD6Xvk6ozT0EpnMhWxAG&?pyW z3GTlXL3oh7!3TQm9_2xRA}y+SAQtGF%xw!*L^Se?N2zR$5^7seE<#gOB%E2G9z(43 z3g9#Qd-bDn&`po4+>Z- z=zQQdLc~$iL8oI)ylFKcQP0$ApV%`EG05mmoK@nU@Eo|Fj=Uwn;1#R4woLOO0`p{WM7S{9+$X97Rqn`gBse z=WIIkWo2-<+L&EwW;#IBP30c*Q_|lVVzU$I@wT{;|+u{G^ zZyr_2<(%%>1N-TP7T6LdR8s=Yc_w*kF}-w>*HINEMgl$DO0>+LnX+Z*J|w-TaFw6g zvfoPS@)PdSMsKmY!bRbV`X&)OLK-$7$V|zSa_fBKv))%5Dbr-O%Sd=Nu){MsZ{a?wE z${|&4W=>V(^8b_+b*{%J8Vc8p$d^nBC=I^n-6JM>w>|#MY7!L^mLD(D6i-!C{)n^S z%LIr0S;8EyMU?E^R$?P=+N(7<)%IHOAIEeS)HjSvs?B~-IxHQK9W&l4)71XF$~~$O zL>7OndbIn4<6H6aVx!T5l;fP&vKHS;%0ZEtH(_T_D6xMqkM_vue&{vU@`78Pw#lT1 zyE`&LeyYq$&Us)p$9T7TJcgO^D7J!f$I)#-H+Vf}+(^GUJML&R@hgwaVn-sU?xw*^ zTmSn34-zM6Ez78-@Kw6ugYm@g>lXa4K0YZQTV+;H6L>|euzgV%#_O=I@*naJp7bF$ zwKpy5>p%9Xb^fqvqCGOZqR-0RKqZKSB$xG&!9Dd2bP@uH^04F zdzIxWJ~7~VWs0Md-%erJ`4uo$jtnKq54(&SO;$0JP2@jEJBU&z8e8{fg?z;M$hkYl z|8n6Nz5ZDT!RyLW#ibe2TYdYE$l^$JI8Nn4hR zKV9nhy*PHY;#o%Vd*@%BbZSXvrcX0w(`}v@_AGGU?6muFQ<?e^tmoclp68d>s~$q!dw|;$oSo;3E%L|q-#r2&031^xFBbxUXrPj zZbF3_=lNFu=9D65dv}`8XK7)ygfvFXxp?4-yp)!nbb5U9=`Z5XSvuM~_bopD5|IHv MpCIp+qtr{k0MoP{oB#j- literal 0 HcmV?d00001 diff --git a/test/resources/emptyfile b/test/resources/emptyfile new file mode 100644 index 000000000..e69de29bb diff --git a/test/resources/with-alpha.png b/test/resources/with-alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..adc8d01805dbcf1bf38f506096b99bc157f25de5 GIT binary patch literal 3772 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 Gh5idy2NOjA literal 0 HcmV?d00001 diff --git a/test/resources/with-xml-def.svg b/test/resources/with-xml-def.svg new file mode 100644 index 0000000000000000000000000000000000000000..90971215a64a6ad9e0e48aed2d72c98a881dcf70 GIT binary patch literal 544 zcmcJM!ES>v42JK03d=hOoYYI*iBPT8xa}b(N+}Y9sxau|&!Ic@vR!tFtvLSe&rY_x zeYP-F*P-dVfHon7sw?|r)71gL#*-1;<*RI%YS7GX2zVaH%S)Qh^PJ4S@_x&v(0fBq=@nD`^KE^Ygnzjx2R1{3FjBpJ2Zk>QoX{-k}P8Ew~nXE4f#0Xfg z@nj7GtaeF>Uav9gcyMGK>w{61)+tFv5(`F|k%RRN!eE(U1{+L# AmH+?% literal 0 HcmV?d00001 diff --git a/yarn.lock b/yarn.lock index d6208cc76..764d48cbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3038,6 +3038,13 @@ debug-fabulous@1.X: memoizee "0.4.X" object-assign "4.X" +debug@2, debug@^2.2.0, debug@^2.3.3, debug@^2.5.2: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@3.1.0, debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -3059,13 +3066,6 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" -debug@^2.2.0, debug@^2.3.3, debug@^2.5.2: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debuglog@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -3110,7 +3110,7 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -deepmerge@^4.2.2: +deepmerge@^4.0.0, deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== @@ -8040,6 +8040,17 @@ prismjs@1.18.0: optionalDependencies: clipboard "^2.0.0" +probe-image-size@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-5.0.0.tgz#1b87d20340ab8fcdb4324ec77fbc8a5f53419878" + integrity sha512-V6uBYw5eBc5UVIE7MUZD6Nxg0RYuGDWLDenEn0B1WC6PcTvn1xdQ6HLDDuznefsiExC6rNrCz7mFRBo0f3Xekg== + dependencies: + deepmerge "^4.0.0" + inherits "^2.0.3" + next-tick "^1.0.0" + request "^2.83.0" + stream-parser "~0.3.1" + process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -8652,7 +8663,7 @@ request-stats@3.0.0: http-headers "^3.0.1" once "^1.4.0" -request@2.88.0, request@^2.73.0, request@^2.88.0: +request@2.88.0, request@^2.73.0, request@^2.83.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -9356,6 +9367,13 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" +stream-parser@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773" + integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M= + dependencies: + debug "2" + stream-shift@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"