Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ドライブ関連でにりみすが独自に持っている機能を追加するパッチ #2

Merged
merged 15 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9196,6 +9196,32 @@ export interface Locale extends ILocale {
"mention": string;
};
};
"_imageCompressionMode": {
/**
* 画像の圧縮形式
*/
"title": string;
/**
* オリジナル画像を保持しない場合に、Web公開用画像の圧縮形式を選択できます。縮小する場合は2560x2560より小さくなるように縮小されます。非可逆圧縮を指定しない場合は、元画像に応じて非可逆圧縮か可逆圧縮かが自動的に選択されます。
*/
"description": string;
/**
* 縮小して再圧縮する
*/
"resizeCompress": string;
/**
* 縮小せず再圧縮する
*/
"noResizeCompress": string;
/**
* 縮小して非可逆圧縮する
*/
"resizeCompressLossy": string;
/**
* 縮小せず非可逆圧縮する
*/
"noResizeCompressLossy": string;
};
"_moderationLogTypes": {
/**
* ロールを作成
Expand Down
8 changes: 8 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2440,6 +2440,14 @@ _webhookSettings:
reaction: "リアクションがあったとき"
mention: "メンションされたとき"

_imageCompressionMode:
title: "画像の圧縮形式"
description: "オリジナル画像を保持しない場合に、Web公開用画像の圧縮形式を選択できます。縮小する場合は2560x2560より小さくなるように縮小されます。非可逆圧縮を指定しない場合は、元画像に応じて非可逆圧縮か可逆圧縮かが自動的に選択されます。"
resizeCompress: "縮小して再圧縮する"
noResizeCompress: "縮小せず再圧縮する"
resizeCompressLossy: "縮小して非可逆圧縮する"
noResizeCompressLossy: "縮小せず非可逆圧縮する"

_moderationLogTypes:
createRole: "ロールを作成"
deleteRole: "ロールを削除"
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ type Source = {
perUserNotificationsMaxCount?: number;
deactivateAntennaThreshold?: number;
pidFile: string;

buiso?: {
maxWebImageSize?: number;
}
};

export type Config = {
Sayamame-beans marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -170,6 +174,10 @@ export type Config = {
perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number;
pidFile: string;

buiso: {
maxWebImageSize?: number;
}
};

const _filename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -211,6 +219,7 @@ export function loadConfig(): Config {
const redis = convertRedisOptions(config.redis, host);

return {
buiso: config.buiso ?? {},
version,
publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
url: url.origin,
Expand Down
15 changes: 11 additions & 4 deletions packages/backend/src/core/DriveService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,19 +301,26 @@ export class DriveService {
let img: sharp.Sharp | null = null;
let satisfyWebpublic: boolean;
let isAnimated: boolean;
let compressedWidth: number;
let compressedHeight: number;

try {
img = await sharpBmp(path, type);
const metadata = await img.metadata();
isAnimated = !!(metadata.pages && metadata.pages > 1);

const maxSize = this.config.buiso?.maxWebImageSize ?? 8192;
// buiso Extension (ported from nirila): We want to keep original size as possible
// noinspection PointlessBooleanExpressionJS
satisfyWebpublic = !!(
type !== 'image/svg+xml' && // security reason
type !== 'image/avif' && // not supported by Mastodon and MS Edge
!(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) &&
metadata.width && metadata.width <= 2048 &&
metadata.height && metadata.height <= 2048
metadata.width && metadata.width <= maxSize &&
metadata.height && metadata.height <= maxSize
);
compressedWidth = metadata.width && metadata.width <= maxSize ? metadata.width : maxSize;
compressedHeight = metadata.height && metadata.height <= maxSize ? metadata.height : maxSize;
} catch (err) {
this.registerLogger.warn(`sharp failed: ${err}`);
return {
Expand All @@ -330,9 +337,9 @@ export class DriveService {

try {
if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048);
webpublic = await this.imageProcessingService.convertSharpToWebp(img, compressedWidth, compressedHeight);
} else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
webpublic = await this.imageProcessingService.convertSharpToPng(img, compressedWidth, compressedHeight);
} else {
this.registerLogger.debug('web image not created (not an required image)');
}
Expand Down
10 changes: 10 additions & 0 deletions packages/frontend/src/pages/settings/drive.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.keepOriginalUploading }}</template>
<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
</MkSwitch>
<MkSelect v-model="imageCompressionMode">
<template #label>{{ i18n.ts._imageCompressionMode.title }}</template>
<option value="resizeCompress">{{ i18n.ts._imageCompressionMode.resizeCompress }}</option>
<option value="noResizeCompress">{{ i18n.ts._imageCompressionMode.noResizeCompress }}</option>
<option value="resizeCompressLossy">{{ i18n.ts._imageCompressionMode.resizeCompressLossy }}</option>
<option value="noResizeCompressLossy">{{ i18n.ts._imageCompressionMode.noResizeCompressLossy }}</option>
<template #caption>{{ i18n.ts._imageCompressionMode.description }}</template>
</MkSelect>
<MkSwitch v-model="alwaysMarkNsfw" @update:modelValue="saveProfile()">
<template #label>{{ i18n.ts.alwaysMarkSensitive }}</template>
</MkSwitch>
Expand Down Expand Up @@ -73,6 +81,7 @@ import MkChart from '@/components/MkChart.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { signinRequired } from '@/account.js';
import MkSelect from '@/components/MkSelect.vue';

const $i = signinRequired();

Expand All @@ -96,6 +105,7 @@ const meterStyle = computed(() => {
});

const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading'));
const imageCompressionMode = computed(defaultStore.makeGetterSetter('imageCompressionMode'));

misskeyApi('drive').then(info => {
capacity.value = info.capacity;
Expand Down
104 changes: 89 additions & 15 deletions packages/frontend/src/scripts/upload/compress-config.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,110 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-FileCopyrightText: syuilo and misskey-project, anatawa12
* SPDX-License-Identifier: AGPL-3.0-only
*/

import isAnimated from 'is-file-animated';
import { isWebpSupported } from './isWebpSupported.js';
import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer';
import { defaultStore } from '@/store';

const compressTypeMap = {
'image/jpeg': { quality: 0.90, mimeType: 'image/webp' },
'image/png': { quality: 1, mimeType: 'image/webp' },
'image/webp': { quality: 0.90, mimeType: 'image/webp' },
'image/svg+xml': { quality: 1, mimeType: 'image/webp' },
'lossy': { quality: 0.90, mimeType: 'image/webp' },
'lossless': { quality: 1, mimeType: 'image/webp' },
} as const;

const compressTypeMapFallback = {
'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' },
'image/png': { quality: 1, mimeType: 'image/png' },
'image/webp': { quality: 0.85, mimeType: 'image/jpeg' },
'image/svg+xml': { quality: 1, mimeType: 'image/png' },
'lossy': { quality: 0.85, mimeType: 'image/jpeg' },
'lossless': { quality: 1, mimeType: 'image/png' },
} as const;

const inputCompressKindMap = {
'image/jpeg': 'lossy',
'image/png': 'lossless',
'image/webp': 'lossy',
'image/svg+xml': 'lossless',
} as const;

const resizeSizeConfig = { maxWidth: 2560, maxHeight: 2560 } as const;
const noResizeSizeConfig = { maxWidth: Number.MAX_SAFE_INTEGER, maxHeight: Number.MAX_SAFE_INTEGER } as const;

async function isLosslessWebp(file: Blob): Promise<boolean> {
// file header
// 'RIFF': u32 @ 0x00
// file size: u32 @ 0x04
// 'WEBP': u32 @ 0x08
// for simple lossless
// 'VP8L': u32 @ 0x0C
// so read 16 bytes and check those three magic numbers
const buffer = new Uint8Array(await file.slice(0, 16).arrayBuffer());

const header = 'RIFF\x00\x00\x00\x00WEBPVP8L';
for (let i = 0; i < header.length; i++) {
const code = header.charCodeAt(i);
if (code === 0) continue;
if (buffer[i] !== code) return false;
}
return true;
}

async function inputImageKind(file: File): Promise<'lossy' | 'lossless' | undefined> {
let compressKind: 'lossy' | 'lossless' | undefined = inputCompressKindMap[file.type];
if (!compressKind) return undefined; // unknown image format
if (await isAnimated(file)) return undefined; // animated image format
// WEBPs can be lossless
if (await isLosslessWebp(file)) compressKind = 'lossless';
return compressKind;
}

export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfigWithConvertedOutput | undefined> {
const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type];
if (!imgConfig || await isAnimated(file)) {
return;
const inputCompressKind = await inputImageKind(file);
if (!inputCompressKind) return undefined;

let compressKind: 'lossy' | 'lossless';
let resize: boolean;

switch (defaultStore.state.imageCompressionMode) {
case 'resizeCompress':
case null:
default:
resize = true;
compressKind = inputCompressKind;
break;
case 'noResizeCompress':
resize = false;
compressKind = inputCompressKind;
break;
case 'resizeCompressLossy':
resize = true;
compressKind = 'lossy';
break;
case 'noResizeCompressLossy':
resize = false;
compressKind = 'lossy';
break;
}

const webpSupported = isWebpSupported();

const imgFormatConfig = (webpSupported ? compressTypeMap : compressTypeMapFallback)[compressKind];
const sizeConfig = resize ? resizeSizeConfig : noResizeSizeConfig;

if (!resize) {
// we don't resize images so we may omit recompression
if (imgFormatConfig.mimeType === file.type && inputCompressKind === compressKind) {
// we don't have to recompress already compressed to preferred image format.
return undefined;
}

if (!webpSupported && file.type === 'image/webp' && compressKind === 'lossless') {
// lossless webp -> png recompression likely to increase image size so don't recompress
return undefined;
}
}

return {
maxWidth: 2048,
maxHeight: 2048,
debug: true,
...imgConfig,
...imgFormatConfig,
...sizeConfig,
};
}
4 changes: 4 additions & 0 deletions packages/frontend/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'account',
default: false,
},
imageCompressionMode: {
where: 'account',
default: 'resizeCompress' as 'resizeCompress' | 'noResizeCompress' | 'resizeCompressLossy' | 'noResizeCompressLossy' | null,
},
memo: {
where: 'account',
default: null,
Expand Down
Loading