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 | 2 + yarn.lock | 36 +++- 20 files changed, 456 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 0000000000..6a1dfe1ed3 --- /dev/null +++ b/.imgbotconfig @@ -0,0 +1,5 @@ +{ + "ignoredFiles": [ + "test/resources/*" + ] +} diff --git a/package.json b/package.json index 0f0dda57ad..d5b85fc8ca 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 0000000000..665edcf2e7 --- /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 8ddeefede9..0000000000 --- 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 f47f127353..0000000000 --- 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 eef64cfc56..8d71cd0137 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 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 { + 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 5345876da8..3a17760e53 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 f4a01a3976..0651b8d283 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 2283435794..0b14378589 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 232b7a09cd..6b90e99921 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 350e4dfe19..7cc710c8b6 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 0000000000..920df07382 --- /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 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 RTKPnHxWi9$Gchn&0{|_KP=5db 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 zcmeAS@N?(olHy`uVBq!ia0y~yU}OMc4rT@hhU_&FAq)%*9Er&xJ`4;DObiSR44-=^ z7cej|a0d8?UUC+)l3NYk;Ue3VL zuCSw`ec(PwsUnK^eP6Q(k);BlyCcs2RHq{512 zjX!G{w$(9wY-6~^7BKtII)=jSuN=a?&&zXcU}Ijvm+)IXLD9VNaq%w36dyCDtCzS1 z7z-AMOq>5-mvPgw*Y!@P&%cjrj9!q+@M_|JwF58v7uYi9WmPdO@xS#sBrK5OIMesoA?f7sRkve~7K9W(qx z{|GnCkQ6;?&t5R!!}C{h!&kH8k_Tq-d`M#lC1sl8Jr6#zYcsLU`!l1w1S#Kw%7MKM z3=DG^7#Nfo7#I{F`Sxs0rZNKq(~YzgM^M?oRB`L=-Pd`|20W|@zxVuiUVmxHT3vxh z-ZIfM=1)%AC(Go?5!9(If92P;?f)vPLase7opN>YmU+?le_h+YZ~hF{4|-L3fB9@r zz25oN`b)-Ny}x;TJon4~kNI1-C-i*i*A;8dH-x_qe&Ab`SNnSYQuowap+CZ3BR^Ex zZr}HHZGLT!+|#cyq5Yq#)~#|+tySCOcRuv1#`@>0s-|2oeqH_|Fz)=?`wQJuYpwni zfBE?#$aZ^OyWG^Tk{|gmEEzFGAlJ z^BumFId|zp@2<up-eo)Dl=hykG_u2ODaGs>~%e3m}U6<(>t>-BgM;+U* zO=fP}99ORE*;)S1>QvSjQ;W0wSLapbihQ{lV{pFU%IfvXwE-%VRm}G$dN=o7EM9SM zi@bCDi|O%s_J7vLZQuOhp!vI&iDp&P_a)ywy10Jpj!BXE`>MVypD^7>?fHh_1WL@A?}p$@3SJy`MzDXUe~|qW8QwR?TznN zJg@lm`UhiQS>xPxr{*T*q!uX{>6ssV Se7OUZGd*4XT-G@yGywq9v>u!Q literal 0 HcmV?d00001 diff --git a/test/resources/emptyfile b/test/resources/emptyfile new file mode 100644 index 0000000000..e69de29bb2 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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 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 0000000000..90971215a6 --- /dev/null +++ b/test/resources/with-xml-def.svg @@ -0,0 +1,2 @@ + + diff --git a/yarn.lock b/yarn.lock index d6208cc76b..764d48cbec 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"