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", | ||||
| 		"idb-keyval": "6.2.0", | ||||
| 		"insert-text-at-cursor": "0.3.0", | ||||
| 		"is-file-animated": "1.0.1", | ||||
| 		"json5": "2.2.1", | ||||
| 		"katex": "0.15.6", | ||||
| 		"matter-js": "0.18.0", | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { reactive, ref } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { readAndCompressImage } from 'browser-image-resizer'; | ||||
| import { getCompressionConfig } from './upload/compress-config'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { apiUrl } from '@/config'; | ||||
| import { $i } from '@/account'; | ||||
|  | @ -16,12 +17,6 @@ type 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 = { | ||||
| 	'image/webp': 'webp', | ||||
| 	'image/jpeg': 'jpg', | ||||
|  | @ -34,16 +29,18 @@ export function uploadFile( | |||
| 	name?: string, | ||||
| 	keepOriginal: boolean = defaultStore.state.keepOriginalUploading, | ||||
| ): Promise<Misskey.entities.DriveFile> { | ||||
| 	if ($i == null) throw new Error('Not logged in'); | ||||
| 
 | ||||
| 	if (folder && typeof folder === 'object') folder = folder.id; | ||||
| 
 | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		const id = Math.random().toString(); | ||||
| 
 | ||||
| 		const reader = new FileReader(); | ||||
| 		reader.onload = async (ev) => { | ||||
| 		reader.onload = async (): Promise<void> => { | ||||
| 			const ctx = reactive<Uploading>({ | ||||
| 				id: id, | ||||
| 				name: name || file.name || 'untitled', | ||||
| 				name: name ?? file.name ?? 'untitled', | ||||
| 				progressMax: undefined, | ||||
| 				progressValue: undefined, | ||||
| 				img: window.URL.createObjectURL(file), | ||||
|  | @ -51,20 +48,22 @@ export function uploadFile( | |||
| 
 | ||||
| 			uploads.value.push(ctx); | ||||
| 
 | ||||
| 			let resizedImage: any; | ||||
| 			if (!keepOriginal && file.type in compressTypeMap) { | ||||
| 				const imgConfig = compressTypeMap[file.type]; | ||||
| 
 | ||||
| 				const config = { | ||||
| 					maxWidth: 2048, | ||||
| 					maxHeight: 2048, | ||||
| 					debug: true, | ||||
| 					...imgConfig, | ||||
| 				}; | ||||
| 
 | ||||
| 			const config = !keepOriginal ? await getCompressionConfig(file) : undefined; | ||||
| 			let resizedImage: Blob | undefined; | ||||
| 			if (config) { | ||||
| 				try { | ||||
| 					resizedImage = await readAndCompressImage(file, config); | ||||
| 					ctx.name = file.type !== imgConfig.mimeType ? `${ctx.name}.${mimeTypeMap[compressTypeMap[file.type].mimeType]}` : ctx.name; | ||||
| 					const resized = await readAndCompressImage(file, config); | ||||
| 					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) { | ||||
| 					console.error('Failed to resize image', err); | ||||
| 				} | ||||
|  | @ -73,13 +72,13 @@ export function uploadFile( | |||
| 			const formData = new FormData(); | ||||
| 			formData.append('i', $i.token); | ||||
| 			formData.append('force', 'true'); | ||||
| 			formData.append('file', resizedImage || file); | ||||
| 			formData.append('file', resizedImage ?? file); | ||||
| 			formData.append('name', ctx.name); | ||||
| 			if (folder) formData.append('folderId', folder); | ||||
| 
 | ||||
| 			const xhr = new XMLHttpRequest(); | ||||
| 			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) { | ||||
| 					// TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
 | ||||
| 					uploads.value = uploads.value.filter(x => x.id !== id); | ||||
|  | @ -122,7 +121,7 @@ export function uploadFile( | |||
| 				resolve(driveFile); | ||||
| 
 | ||||
| 				uploads.value = uploads.value.filter(x => x.id !== id); | ||||
| 			}; | ||||
| 			}) as (ev: ProgressEvent<EventTarget>) => any; | ||||
| 
 | ||||
| 			xhr.upload.onprogress = ev => { | ||||
| 				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 | ||||
|     idb-keyval: 6.2.0 | ||||
|     insert-text-at-cursor: 0.3.0 | ||||
|     is-file-animated: 1.0.1 | ||||
|     json5: 2.2.1 | ||||
|     katex: 0.15.6 | ||||
|     matter-js: 0.18.0 | ||||
|  | @ -9574,6 +9575,13 @@ __metadata: | |||
|   languageName: node | ||||
|   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": | ||||
|   version: 1.0.0 | ||||
|   resolution: "is-fullwidth-code-point@npm:1.0.0" | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue