Merge pull request #50 from anatawa12/compression-methods

feat(frontend): selectable compression kind

Cherry-picks 300d272fc1e40ac6e9b86b7322eb906f61265e22
This commit is contained in:
anatawa12 2023-08-16 22:18:12 +09:00
parent e82c2e7cf9
commit ffe8a4cd61
No known key found for this signature in database
GPG key ID: 9CA909848B8E4EA6
6 changed files with 121 additions and 14 deletions

View file

@ -33,6 +33,9 @@
- Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように
- `$[rainbow ]`記法が、動きのあるMFMが無効になっていても使用できるようになりました
- Playの操作を行うAPI TokenをAPIコンソールから発行できるように
- 画像の圧縮方法を選択可能にしました
- サイズ変更を行うかを選択可能にしました
- 強制的に非可逆圧縮できるようになりました
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
- Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正

8
locales/index.d.ts vendored
View file

@ -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;

View file

@ -2115,3 +2115,11 @@ _webhookSettings:
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"
_imageCompressionMode:
title: "画像の圧縮形式"
description: "オリジナル画像を保持しない場合に、Web公開用画像の圧縮形式を選択できます。縮小する場合は2048x2048より小さくなるように縮小されます。非可逆圧縮を指定しない場合は、元画像に応じて非可逆圧縮か可逆圧縮かが自動的に選択されます。"
resizeCompress: "縮小して再圧縮する"
noResizeCompress: "縮小せず再圧縮する"
resizeCompressLossy: "縮小して非可逆圧縮する"
noResizeCompressLossy: "縮小せず非可逆圧縮する"

View file

@ -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>
@ -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);
@ -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;

View file

@ -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,
};
}

View file

@ -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,