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

feat(frontend): selectable compression kind #11760

Closed
wants to merge 1 commit into from
Closed
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
- Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように
- `$[rainbow ]`記法が、動きのあるMFMが無効になっていても使用できるようになりました
- Playの操作を行うAPI TokenをAPIコンソールから発行できるように
- 画像の圧縮方法を選択可能にしました
- サイズ変更を行うかを選択可能にしました
- 強制的に非可逆圧縮できるようになりました
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
- Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正
Expand Down
8 changes: 8 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2202,6 +2202,14 @@ export interface Locale {
"mention": string;
};
};
"_imageCompressionMode": {
"title": string;
"description": string;
"resizeCompress": string;
"noResizeCompress": string;
"resizeCompressLossy": string;
"noResizeCompressLossy": string;
};
}
declare const locales: {
[lang: string]: Locale;
Expand Down
8 changes: 8 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2115,3 +2115,11 @@ _webhookSettings:
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"

_imageCompressionMode:
title: "画像の圧縮形式"
description: "オリジナル画像を保持しない場合に、Web公開用画像の圧縮形式を選択できます。縮小する場合は2048x2048より小さくなるように縮小されます。非可逆圧縮を指定しない場合は、元画像に応じて非可逆圧縮か可逆圧縮かが自動的に選択されます。"
resizeCompress: "縮小して再圧縮する"
noResizeCompress: "縮小せず再圧縮する"
resizeCompressLossy: "縮小して非可逆圧縮する"
noResizeCompressLossy: "縮小せず非可逆圧縮する"
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 @@ -71,6 +79,7 @@ import MkChart from '@/components/MkChart.vue';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import { $i } from '@/account';
import MkSelect from '@/components/MkSelect.vue';

const fetching = ref(true);
const usage = ref<any>(null);
Expand All @@ -91,6 +100,7 @@ const meterStyle = computed(() => {
});

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

os.api('drive').then(info => {
capacity.value = info.capacity;
Expand Down
102 changes: 88 additions & 14 deletions packages/frontend/src/scripts/upload/compress-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,105 @@
import isAnimated from 'is-file-animated';
import { isWebpSupported } from './isWebpSupported';
import type { BrowserImageResizerConfig } from '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: 2048, maxHeight: 2048 } 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<BrowserImageResizerConfig | 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 @@ -87,6 +87,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