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:
Kagami Sascha Rosylight 2022-12-18 15:40:38 +09:00 committed by GitHub
parent b6995f6e4b
commit a47d172d60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 55 additions and 24 deletions

View file

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

View file

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

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

View file

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