mirror of
https://github.com/kokonect-link/cherrypick
synced 2024-10-30 06:41:46 +09:00
Merge pull request #50 from anatawa12/compression-methods
feat(frontend): selectable compression kind Cherry-picks 300d272fc1e40ac6e9b86b7322eb906f61265e22
This commit is contained in:
parent
e82c2e7cf9
commit
ffe8a4cd61
@ -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
8
locales/index.d.ts
vendored
@ -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;
|
||||
|
@ -2115,3 +2115,11 @@ _webhookSettings:
|
||||
renote: "Renoteされたとき"
|
||||
reaction: "リアクションがあったとき"
|
||||
mention: "メンションされたとき"
|
||||
|
||||
_imageCompressionMode:
|
||||
title: "画像の圧縮形式"
|
||||
description: "オリジナル画像を保持しない場合に、Web公開用画像の圧縮形式を選択できます。縮小する場合は2048x2048より小さくなるように縮小されます。非可逆圧縮を指定しない場合は、元画像に応じて非可逆圧縮か可逆圧縮かが自動的に選択されます。"
|
||||
resizeCompress: "縮小して再圧縮する"
|
||||
noResizeCompress: "縮小せず再圧縮する"
|
||||
resizeCompressLossy: "縮小して非可逆圧縮する"
|
||||
noResizeCompressLossy: "縮小せず非可逆圧縮する"
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user