mirror of
https://github.com/misskey-dev/media-proxy.git
synced 2025-08-08 09:13:51 +09:00
Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
6bf5fbd794 | |||
72431d805e | |||
c3738a339a | |||
99f0160e17 | |||
bd6f63b60e | |||
384c51569f | |||
57aa87f370 | |||
d86823e32d | |||
405e6a5adb | |||
78cf2469d4 | |||
969d27e1b3 | |||
d5f5f4023c |
@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
Misskeyの/proxyが単体で動作します(Misskeyのコードがほぼそのまま移植されています)。
|
Misskeyの/proxyが単体で動作します(Misskeyのコードがほぼそのまま移植されています)。
|
||||||
|
|
||||||
/proxyは画像ではないと403を返しますが、Media Proxyではそのまま内容を送信します。
|
|
||||||
|
|
||||||
**Fastifyプラグインとして動作する気がします。**
|
**Fastifyプラグインとして動作する気がします。**
|
||||||
`pnpm start`は[fastify-cli](https://github.com/fastify/fastify-cli)が動作します。
|
`pnpm start`は[fastify-cli](https://github.com/fastify/fastify-cli)が動作します。
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ Acceptヘッダーは無視される。
|
|||||||
Cache-Controlは、正常なレスポンスの場合`max-age=31536000, immutable`、エラーレスポンスの場合`max-age=300`である。
|
Cache-Controlは、正常なレスポンスの場合`max-age=31536000, immutable`、エラーレスポンスの場合`max-age=300`である。
|
||||||
Content-Typeは、ファイルの内容について適切なものが挿入される。
|
Content-Typeは、ファイルの内容について適切なものが挿入される。
|
||||||
Content-Security-Policyは、`default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`となっている。
|
Content-Security-Policyは、`default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`となっている。
|
||||||
Content-Dispositionは、filenameは元画像のContent-Disposition.filenameもしくはファイル名に基づいて挿入される。inlineが指定される。
|
Content-Dispositionは、filenameは元画像のContent-Disposition.filenameもしくはファイル名に基づいて挿入される。拡張子は適宜変更され、octet-streamの場合は拡張子として.unknownが付加される。inlineが指定される。
|
||||||
|
|
||||||
### クエリの一覧
|
### クエリの一覧
|
||||||
#### url (必須)
|
#### url (必須)
|
||||||
@ -52,6 +52,9 @@ https://www.google.com/images/errors/robot.png をプロキシする場合:
|
|||||||
変換形式が指定されていなかった場合は、画像ファイルもしくは許可されたファイル(FILE_TYPE_BROWSERSAFE)である場合のみプロキシ(ファイルの再配信)が行われる。
|
変換形式が指定されていなかった場合は、画像ファイルもしくは許可されたファイル(FILE_TYPE_BROWSERSAFE)である場合のみプロキシ(ファイルの再配信)が行われる。
|
||||||
ただし、svgは、webpに変換される(最大サイズ2048x2048)。
|
ただし、svgは、webpに変換される(最大サイズ2048x2048)。
|
||||||
|
|
||||||
|
#### 変換クエリ付加時の挙動
|
||||||
|
一方、以下の変換クエリが指定されているが、元ファイルがsharp.jsで変換できない形式の場合、404が返される。
|
||||||
|
|
||||||
#### emoji
|
#### emoji
|
||||||
存在すると、高さ128px以下のwebpが応答される。
|
存在すると、高さ128px以下のwebpが応答される。
|
||||||
ただし、sharp.jsの都合により、元画像がapngの場合は無変換で応答される。
|
ただし、sharp.jsの都合により、元画像がapngの場合は無変換で応答される。
|
||||||
@ -69,7 +72,7 @@ https://www.google.com/images/errors/robot.png をプロキシする場合:
|
|||||||
#### static
|
#### static
|
||||||
存在すると、アニメーション画像では最初のフレームのみの静止画のwebpが応答される。
|
存在すると、アニメーション画像では最初のフレームのみの静止画のwebpが応答される。
|
||||||
|
|
||||||
emojiまたはavatarとstaticが同時に指定された場合は、それぞれに応じた高さが、指定されていない場合は幅498px・高さ280pxに収まるサイズ以下に縮小される。
|
emojiまたはavatarとstaticが同時に指定された場合は、それぞれに応じた高さが、指定されていない場合は幅498px・高さ422pxに収まるサイズ以下に縮小される。
|
||||||
|
|
||||||
#### preview
|
#### preview
|
||||||
存在すると、幅200px・高さ200pxに収まるサイズ以下のwebpが応答される。
|
存在すると、幅200px・高さ200pxに収まるサイズ以下のwebpが応答される。
|
||||||
|
@ -61,9 +61,14 @@ export async function downloadUrl(url, path, settings = defaultDownloadConfig) {
|
|||||||
}
|
}
|
||||||
const contentDisposition = res.headers['content-disposition'];
|
const contentDisposition = res.headers['content-disposition'];
|
||||||
if (contentDisposition != null) {
|
if (contentDisposition != null) {
|
||||||
const parsed = parse(contentDisposition);
|
try {
|
||||||
if (parsed.parameters.filename) {
|
const parsed = parse(contentDisposition);
|
||||||
filename = parsed.parameters.filename;
|
if (parsed.parameters.filename) {
|
||||||
|
filename = parsed.parameters.filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(`Failed to parse content-disposition: ${contentDisposition}\n${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).on('downloadProgress', (progress) => {
|
}).on('downloadProgress', (progress) => {
|
||||||
|
@ -55,7 +55,7 @@ async function checkSvg(path) {
|
|||||||
import { FILE_TYPE_BROWSERSAFE } from './const.js';
|
import { FILE_TYPE_BROWSERSAFE } from './const.js';
|
||||||
const dictionary = {
|
const dictionary = {
|
||||||
'safe-file': FILE_TYPE_BROWSERSAFE,
|
'safe-file': FILE_TYPE_BROWSERSAFE,
|
||||||
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'],
|
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'],
|
||||||
'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'],
|
'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'],
|
||||||
};
|
};
|
||||||
export const isMimeImage = (mime, type) => dictionary[type].includes(mime);
|
export const isMimeImage = (mime, type) => dictionary[type].includes(mime);
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
export const webpDefault = {
|
export const webpDefault = {
|
||||||
quality: 85,
|
quality: 77,
|
||||||
alphaQuality: 95,
|
alphaQuality: 95,
|
||||||
lossless: false,
|
lossless: false,
|
||||||
nearLossless: false,
|
nearLossless: false,
|
||||||
smartSubsample: true,
|
smartSubsample: true,
|
||||||
mixed: true,
|
mixed: true,
|
||||||
|
effort: 2,
|
||||||
};
|
};
|
||||||
export function convertToWebpStream(path, width, height, options = webpDefault) {
|
export function convertToWebpStream(path, width, height, options = webpDefault) {
|
||||||
return convertSharpToWebpStream(sharp(path), width, height, options);
|
return convertSharpToWebpStream(sharp(path), width, height, options);
|
||||||
|
@ -4,9 +4,10 @@ import { dirname } from 'node:path';
|
|||||||
import fastifyStatic from '@fastify/static';
|
import fastifyStatic from '@fastify/static';
|
||||||
import { createTemp } from './create-temp.js';
|
import { createTemp } from './create-temp.js';
|
||||||
import { FILE_TYPE_BROWSERSAFE } from './const.js';
|
import { FILE_TYPE_BROWSERSAFE } from './const.js';
|
||||||
import { convertToWebpStream, webpDefault } from './image-processor.js';
|
import { convertToWebpStream, webpDefault, convertSharpToWebpStream } from './image-processor.js';
|
||||||
import { detectType, isMimeImage } from './file-info.js';
|
import { detectType, isMimeImage } from './file-info.js';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
import { sharpBmp } from 'sharp-read-bmp';
|
||||||
import { StatusError } from './status-error.js';
|
import { StatusError } from './status-error.js';
|
||||||
import { defaultDownloadConfig, downloadUrl } from './download.js';
|
import { defaultDownloadConfig, downloadUrl } from './download.js';
|
||||||
import { getAgents } from './http.js';
|
import { getAgents } from './http.js';
|
||||||
@ -107,7 +108,7 @@ async function proxyHandler(request, reply) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const data = sharp(file.path, { animated: !('static' in request.query) })
|
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
|
||||||
.resize({
|
.resize({
|
||||||
height: 'emoji' in request.query ? 128 : 320,
|
height: 'emoji' in request.query ? 128 : 320,
|
||||||
withoutEnlargement: true,
|
withoutEnlargement: true,
|
||||||
@ -121,15 +122,16 @@ async function proxyHandler(request, reply) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ('static' in request.query) {
|
else if ('static' in request.query) {
|
||||||
image = convertToWebpStream(file.path, 498, 280);
|
image = convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
|
||||||
}
|
}
|
||||||
else if ('preview' in request.query) {
|
else if ('preview' in request.query) {
|
||||||
image = convertToWebpStream(file.path, 200, 200);
|
image = convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
|
||||||
}
|
}
|
||||||
else if ('badge' in request.query) {
|
else if ('badge' in request.query) {
|
||||||
const mask = sharp(file.path)
|
const mask = (await sharpBmp(file.path, file.mime))
|
||||||
.resize(96, 96, {
|
.resize(96, 96, {
|
||||||
fit: 'inside',
|
fit: 'contain',
|
||||||
|
position: 'centre',
|
||||||
withoutEnlargement: false,
|
withoutEnlargement: false,
|
||||||
})
|
})
|
||||||
.greyscale()
|
.greyscale()
|
||||||
@ -183,7 +185,7 @@ async function proxyHandler(request, reply) {
|
|||||||
}
|
}
|
||||||
reply.header('Content-Type', image.type);
|
reply.header('Content-Type', image.type);
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||||
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
|
reply.header('Content-Disposition', contentDisposition('inline', correctFilename(file.filename, image.ext)));
|
||||||
return reply.send(image.data);
|
return reply.send(image.data);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
@ -210,15 +212,16 @@ async function downloadAndDetectTypeFromUrl(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function correctFilename(filename, ext) {
|
function correctFilename(filename, ext) {
|
||||||
if (!ext)
|
const dotExt = ext ? `.${ext}` : '.unknown';
|
||||||
return filename;
|
|
||||||
const dotExt = `.${ext}`;
|
|
||||||
if (filename.endsWith(dotExt)) {
|
if (filename.endsWith(dotExt)) {
|
||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
if (ext === 'jpg' && filename.endsWith('.jpeg')) {
|
if (ext === 'jpg' && filename.endsWith('.jpeg')) {
|
||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
|
if (ext === 'tif' && filename.endsWith('.tiff')) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
return `${filename}${dotExt}`;
|
return `${filename}${dotExt}`;
|
||||||
}
|
}
|
||||||
function contentDisposition(type, filename) {
|
function contentDisposition(type, filename) {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey-media-proxy",
|
"name": "misskey-media-proxy",
|
||||||
"version": "0.0.14",
|
"version": "0.0.21",
|
||||||
"description": "The Media Proxy for Misskey",
|
"description": "The Media Proxy for Misskey",
|
||||||
"main": "built/index.js",
|
"main": "built/index.js",
|
||||||
"packageManager": "pnpm@7.26.0",
|
"packageManager": "pnpm@7.28.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"built",
|
"built",
|
||||||
@ -47,6 +47,7 @@
|
|||||||
"is-svg": "^4.3.2",
|
"is-svg": "^4.3.2",
|
||||||
"private-ip": "^3.0.0",
|
"private-ip": "^3.0.0",
|
||||||
"sharp": "^0.31.3",
|
"sharp": "^0.31.3",
|
||||||
|
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||||
"tmp": "^0.2.1"
|
"tmp": "^0.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
@ -19,6 +19,7 @@ specifiers:
|
|||||||
is-svg: ^4.3.2
|
is-svg: ^4.3.2
|
||||||
private-ip: ^3.0.0
|
private-ip: ^3.0.0
|
||||||
sharp: ^0.31.3
|
sharp: ^0.31.3
|
||||||
|
sharp-read-bmp: github:misskey-dev/sharp-read-bmp
|
||||||
tmp: ^0.2.1
|
tmp: ^0.2.1
|
||||||
typescript: ^4.9.5
|
typescript: ^4.9.5
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ dependencies:
|
|||||||
is-svg: 4.3.2
|
is-svg: 4.3.2
|
||||||
private-ip: 3.0.0
|
private-ip: 3.0.0
|
||||||
sharp: 0.31.3
|
sharp: 0.31.3
|
||||||
|
sharp-read-bmp: github.com/misskey-dev/sharp-read-bmp/c5fb82e26fa50fca8b3b2d7e22467626d7f61baa
|
||||||
tmp: 0.2.1
|
tmp: 0.2.1
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@ -48,6 +50,10 @@ devDependencies:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
/@canvas/image-data/1.0.0:
|
||||||
|
resolution: {integrity: sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@chainsafe/is-ip/2.0.1:
|
/@chainsafe/is-ip/2.0.1:
|
||||||
resolution: {integrity: sha512-nqSJ8u2a1Rv9FYbyI8qpDhTYujaKEyLknNrTejLYoSWmdeg+2WB7R6BZqPZYfrJzDxVi3rl6ZQuoaEvpKRZWgQ==}
|
resolution: {integrity: sha512-nqSJ8u2a1Rv9FYbyI8qpDhTYujaKEyLknNrTejLYoSWmdeg+2WB7R6BZqPZYfrJzDxVi3rl6ZQuoaEvpKRZWgQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -643,6 +649,23 @@ packages:
|
|||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/decode-bmp/0.2.1:
|
||||||
|
resolution: {integrity: sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==}
|
||||||
|
engines: {node: '>=8.6.0'}
|
||||||
|
dependencies:
|
||||||
|
'@canvas/image-data': 1.0.0
|
||||||
|
to-data-view: 1.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/decode-ico/0.4.1:
|
||||||
|
resolution: {integrity: sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==}
|
||||||
|
engines: {node: '>=8.6'}
|
||||||
|
dependencies:
|
||||||
|
'@canvas/image-data': 1.0.0
|
||||||
|
decode-bmp: 0.2.1
|
||||||
|
to-data-view: 1.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/decompress-response/6.0.0:
|
/decompress-response/6.0.0:
|
||||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -1978,6 +2001,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
|
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/to-data-view/1.1.0:
|
||||||
|
resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/to-regex-range/5.0.1:
|
/to-regex-range/5.0.1:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
@ -2064,3 +2091,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
github.com/misskey-dev/sharp-read-bmp/c5fb82e26fa50fca8b3b2d7e22467626d7f61baa:
|
||||||
|
resolution: {tarball: https://codeload.github.com/misskey-dev/sharp-read-bmp/tar.gz/c5fb82e26fa50fca8b3b2d7e22467626d7f61baa}
|
||||||
|
name: sharp-read-bmp
|
||||||
|
version: 1.0.0
|
||||||
|
dependencies:
|
||||||
|
decode-bmp: 0.2.1
|
||||||
|
decode-ico: 0.4.1
|
||||||
|
sharp: 0.31.3
|
||||||
|
dev: false
|
||||||
|
@ -82,9 +82,13 @@ export async function downloadUrl(url: string, path: string, settings:DownloadCo
|
|||||||
|
|
||||||
const contentDisposition = res.headers['content-disposition'];
|
const contentDisposition = res.headers['content-disposition'];
|
||||||
if (contentDisposition != null) {
|
if (contentDisposition != null) {
|
||||||
const parsed = parse(contentDisposition);
|
try {
|
||||||
if (parsed.parameters.filename) {
|
const parsed = parse(contentDisposition);
|
||||||
filename = parsed.parameters.filename;
|
if (parsed.parameters.filename) {
|
||||||
|
filename = parsed.parameters.filename;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Failed to parse content-disposition: ${contentDisposition}\n${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).on('downloadProgress', (progress: Got.Progress) => {
|
}).on('downloadProgress', (progress: Got.Progress) => {
|
||||||
|
@ -66,8 +66,8 @@ import { FILE_TYPE_BROWSERSAFE } from './const.js';
|
|||||||
|
|
||||||
const dictionary = {
|
const dictionary = {
|
||||||
'safe-file': FILE_TYPE_BROWSERSAFE,
|
'safe-file': FILE_TYPE_BROWSERSAFE,
|
||||||
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'],
|
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'],
|
||||||
'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'],
|
'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);
|
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);
|
||||||
|
@ -16,12 +16,13 @@ export type IImageStream = {
|
|||||||
export type IImageStreamable = IImage | IImageStream;
|
export type IImageStreamable = IImage | IImageStream;
|
||||||
|
|
||||||
export const webpDefault: sharp.WebpOptions = {
|
export const webpDefault: sharp.WebpOptions = {
|
||||||
quality: 85,
|
quality: 77,
|
||||||
alphaQuality: 95,
|
alphaQuality: 95,
|
||||||
lossless: false,
|
lossless: false,
|
||||||
nearLossless: false,
|
nearLossless: false,
|
||||||
smartSubsample: true,
|
smartSubsample: true,
|
||||||
mixed: true,
|
mixed: true,
|
||||||
|
effort: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function convertToWebpStream(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream {
|
export function convertToWebpStream(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream {
|
||||||
|
38
src/index.ts
38
src/index.ts
@ -6,10 +6,11 @@ import { dirname } from 'node:path';
|
|||||||
import fastifyStatic from '@fastify/static';
|
import fastifyStatic from '@fastify/static';
|
||||||
import { createTemp } from './create-temp.js';
|
import { createTemp } from './create-temp.js';
|
||||||
import { FILE_TYPE_BROWSERSAFE } from './const.js';
|
import { FILE_TYPE_BROWSERSAFE } from './const.js';
|
||||||
import { IImageStreamable, convertToWebpStream, webpDefault } from './image-processor.js';
|
import { IImageStreamable, convertToWebpStream, webpDefault, convertSharpToWebpStream } from './image-processor.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||||
import { detectType, isMimeImage } from './file-info.js';
|
import { detectType, isMimeImage } from './file-info.js';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
import { sharpBmp } from 'sharp-read-bmp';
|
||||||
import { StatusError } from './status-error.js';
|
import { StatusError } from './status-error.js';
|
||||||
import { DownloadConfig, defaultDownloadConfig, downloadUrl } from './download.js';
|
import { DownloadConfig, defaultDownloadConfig, downloadUrl } from './download.js';
|
||||||
import { getAgents } from './http.js';
|
import { getAgents } from './http.js';
|
||||||
@ -153,12 +154,12 @@ async function proxyHandler(request: FastifyRequest<{ Params: { url: string; };
|
|||||||
type: file.mime,
|
type: file.mime,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const data = sharp(file.path, { animated: !('static' in request.query) })
|
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
|
||||||
.resize({
|
.resize({
|
||||||
height: 'emoji' in request.query ? 128 : 320,
|
height: 'emoji' in request.query ? 128 : 320,
|
||||||
withoutEnlargement: true,
|
withoutEnlargement: true,
|
||||||
})
|
})
|
||||||
.webp(webpDefault);
|
.webp(webpDefault);
|
||||||
|
|
||||||
image = {
|
image = {
|
||||||
data,
|
data,
|
||||||
@ -167,13 +168,14 @@ async function proxyHandler(request: FastifyRequest<{ Params: { url: string; };
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if ('static' in request.query) {
|
} else if ('static' in request.query) {
|
||||||
image = convertToWebpStream(file.path, 498, 280);
|
image = convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
|
||||||
} else if ('preview' in request.query) {
|
} else if ('preview' in request.query) {
|
||||||
image = convertToWebpStream(file.path, 200, 200);
|
image = convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
|
||||||
} else if ('badge' in request.query) {
|
} else if ('badge' in request.query) {
|
||||||
const mask = sharp(file.path)
|
const mask = (await sharpBmp(file.path, file.mime))
|
||||||
.resize(96, 96, {
|
.resize(96, 96, {
|
||||||
fit: 'inside',
|
fit: 'contain',
|
||||||
|
position: 'centre',
|
||||||
withoutEnlargement: false,
|
withoutEnlargement: false,
|
||||||
})
|
})
|
||||||
.greyscale()
|
.greyscale()
|
||||||
@ -231,7 +233,12 @@ async function proxyHandler(request: FastifyRequest<{ Params: { url: string; };
|
|||||||
|
|
||||||
reply.header('Content-Type', image.type);
|
reply.header('Content-Type', image.type);
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||||
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
|
reply.header('Content-Disposition',
|
||||||
|
contentDisposition(
|
||||||
|
'inline',
|
||||||
|
correctFilename(file.filename, image.ext)
|
||||||
|
)
|
||||||
|
);
|
||||||
return reply.send(image.data);
|
return reply.send(image.data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if ('cleanup' in file) file.cleanup();
|
if ('cleanup' in file) file.cleanup();
|
||||||
@ -261,15 +268,16 @@ async function downloadAndDetectTypeFromUrl(url: string): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
function correctFilename(filename: string, ext: string | null) {
|
function correctFilename(filename: string, ext: string | null) {
|
||||||
if (!ext) return filename;
|
const dotExt = ext ? `.${ext}` : '.unknown';
|
||||||
|
|
||||||
const dotExt = `.${ext}`;
|
|
||||||
if (filename.endsWith(dotExt)) {
|
if (filename.endsWith(dotExt)) {
|
||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
if (ext === 'jpg' && filename.endsWith('.jpeg')) {
|
if (ext === 'jpg' && filename.endsWith('.jpeg')) {
|
||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
|
if (ext === 'tif' && filename.endsWith('.tiff')) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
return `${filename}${dotExt}`;
|
return `${filename}${dotExt}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user