enhance(client): Compress non-animated PNG files (#9334)
* style: fix TS lint errors about `ev.target` * enhance: compress non-animated PNG * PNG to PNG? * defer jest things (add it later) * Delete jest.config.cjs * check the compressed file size * log compression stats * use ?? * handle if ($i == null) Co-authored-by: tamaina <tamaina@hotmail.co.jp>
This commit is contained in:
parent
b6995f6e4b
commit
a47d172d60
4 changed files with 55 additions and 24 deletions
|
@ -31,6 +31,7 @@
|
||||||
"eventemitter3": "5.0.0",
|
"eventemitter3": "5.0.0",
|
||||||
"idb-keyval": "6.2.0",
|
"idb-keyval": "6.2.0",
|
||||||
"insert-text-at-cursor": "0.3.0",
|
"insert-text-at-cursor": "0.3.0",
|
||||||
|
"is-file-animated": "1.0.1",
|
||||||
"json5": "2.2.1",
|
"json5": "2.2.1",
|
||||||
"katex": "0.15.6",
|
"katex": "0.15.6",
|
||||||
"matter-js": "0.18.0",
|
"matter-js": "0.18.0",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { readAndCompressImage } from 'browser-image-resizer';
|
import { readAndCompressImage } from 'browser-image-resizer';
|
||||||
|
import { getCompressionConfig } from './upload/compress-config';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { apiUrl } from '@/config';
|
import { apiUrl } from '@/config';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
|
@ -16,12 +17,6 @@ type Uploading = {
|
||||||
};
|
};
|
||||||
export const uploads = ref<Uploading[]>([]);
|
export const uploads = ref<Uploading[]>([]);
|
||||||
|
|
||||||
const compressTypeMap = {
|
|
||||||
'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' },
|
|
||||||
'image/webp': { quality: 0.85, mimeType: 'image/jpeg' },
|
|
||||||
'image/svg+xml': { quality: 1, mimeType: 'image/png' },
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const mimeTypeMap = {
|
const mimeTypeMap = {
|
||||||
'image/webp': 'webp',
|
'image/webp': 'webp',
|
||||||
'image/jpeg': 'jpg',
|
'image/jpeg': 'jpg',
|
||||||
|
@ -34,16 +29,18 @@ export function uploadFile(
|
||||||
name?: string,
|
name?: string,
|
||||||
keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
|
keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
|
||||||
): Promise<Misskey.entities.DriveFile> {
|
): Promise<Misskey.entities.DriveFile> {
|
||||||
|
if ($i == null) throw new Error('Not logged in');
|
||||||
|
|
||||||
if (folder && typeof folder === 'object') folder = folder.id;
|
if (folder && typeof folder === 'object') folder = folder.id;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = Math.random().toString();
|
const id = Math.random().toString();
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async (ev) => {
|
reader.onload = async (): Promise<void> => {
|
||||||
const ctx = reactive<Uploading>({
|
const ctx = reactive<Uploading>({
|
||||||
id: id,
|
id: id,
|
||||||
name: name || file.name || 'untitled',
|
name: name ?? file.name ?? 'untitled',
|
||||||
progressMax: undefined,
|
progressMax: undefined,
|
||||||
progressValue: undefined,
|
progressValue: undefined,
|
||||||
img: window.URL.createObjectURL(file),
|
img: window.URL.createObjectURL(file),
|
||||||
|
@ -51,20 +48,22 @@ export function uploadFile(
|
||||||
|
|
||||||
uploads.value.push(ctx);
|
uploads.value.push(ctx);
|
||||||
|
|
||||||
let resizedImage: any;
|
const config = !keepOriginal ? await getCompressionConfig(file) : undefined;
|
||||||
if (!keepOriginal && file.type in compressTypeMap) {
|
let resizedImage: Blob | undefined;
|
||||||
const imgConfig = compressTypeMap[file.type];
|
if (config) {
|
||||||
|
|
||||||
const config = {
|
|
||||||
maxWidth: 2048,
|
|
||||||
maxHeight: 2048,
|
|
||||||
debug: true,
|
|
||||||
...imgConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resizedImage = await readAndCompressImage(file, config);
|
const resized = await readAndCompressImage(file, config);
|
||||||
ctx.name = file.type !== imgConfig.mimeType ? `${ctx.name}.${mimeTypeMap[compressTypeMap[file.type].mimeType]}` : ctx.name;
|
if (resized.size < file.size || file.type === 'image/webp') {
|
||||||
|
// The compression may not always reduce the file size
|
||||||
|
// (and WebP is not browser safe yet)
|
||||||
|
resizedImage = resized;
|
||||||
|
}
|
||||||
|
if (_DEV_) {
|
||||||
|
const saved = ((1 - resized.size / file.size) * 100).toFixed(2);
|
||||||
|
console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to resize image', err);
|
console.error('Failed to resize image', err);
|
||||||
}
|
}
|
||||||
|
@ -73,13 +72,13 @@ export function uploadFile(
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('i', $i.token);
|
formData.append('i', $i.token);
|
||||||
formData.append('force', 'true');
|
formData.append('force', 'true');
|
||||||
formData.append('file', resizedImage || file);
|
formData.append('file', resizedImage ?? file);
|
||||||
formData.append('name', ctx.name);
|
formData.append('name', ctx.name);
|
||||||
if (folder) formData.append('folderId', folder);
|
if (folder) formData.append('folderId', folder);
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||||
xhr.onload = (ev) => {
|
xhr.onload = ((ev: ProgressEvent<XMLHttpRequest>) => {
|
||||||
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
|
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
|
||||||
// TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
|
// TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
|
||||||
uploads.value = uploads.value.filter(x => x.id !== id);
|
uploads.value = uploads.value.filter(x => x.id !== id);
|
||||||
|
@ -122,7 +121,7 @@ export function uploadFile(
|
||||||
resolve(driveFile);
|
resolve(driveFile);
|
||||||
|
|
||||||
uploads.value = uploads.value.filter(x => x.id !== id);
|
uploads.value = uploads.value.filter(x => x.id !== id);
|
||||||
};
|
}) as (ev: ProgressEvent<EventTarget>) => any;
|
||||||
|
|
||||||
xhr.upload.onprogress = ev => {
|
xhr.upload.onprogress = ev => {
|
||||||
if (ev.lengthComputable) {
|
if (ev.lengthComputable) {
|
||||||
|
|
23
packages/client/src/scripts/upload/compress-config.ts
Normal file
23
packages/client/src/scripts/upload/compress-config.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import isAnimated from 'is-file-animated';
|
||||||
|
import type { BrowserImageResizerConfig } from 'browser-image-resizer';
|
||||||
|
|
||||||
|
const compressTypeMap = {
|
||||||
|
'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' },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfig | undefined> {
|
||||||
|
const imgConfig = compressTypeMap[file.type];
|
||||||
|
if (!imgConfig || await isAnimated(file)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxWidth: 2048,
|
||||||
|
maxHeight: 2048,
|
||||||
|
debug: true,
|
||||||
|
...imgConfig,
|
||||||
|
};
|
||||||
|
}
|
|
@ -4975,6 +4975,7 @@ __metadata:
|
||||||
eventemitter3: 5.0.0
|
eventemitter3: 5.0.0
|
||||||
idb-keyval: 6.2.0
|
idb-keyval: 6.2.0
|
||||||
insert-text-at-cursor: 0.3.0
|
insert-text-at-cursor: 0.3.0
|
||||||
|
is-file-animated: 1.0.1
|
||||||
json5: 2.2.1
|
json5: 2.2.1
|
||||||
katex: 0.15.6
|
katex: 0.15.6
|
||||||
matter-js: 0.18.0
|
matter-js: 0.18.0
|
||||||
|
@ -9574,6 +9575,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"is-file-animated@npm:1.0.1":
|
||||||
|
version: 1.0.1
|
||||||
|
resolution: "is-file-animated@npm:1.0.1"
|
||||||
|
checksum: bcc281e0694e1ba74adfdef75f83f1637ab6470eceecef867d21b4a98e112c32188514b3172348dd137b82cbe8771b6d683de1439d8e1e86011fed77da896c4e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"is-fullwidth-code-point@npm:^1.0.0":
|
"is-fullwidth-code-point@npm:^1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "is-fullwidth-code-point@npm:1.0.0"
|
resolution: "is-fullwidth-code-point@npm:1.0.0"
|
||||||
|
|
Loading…
Reference in a new issue