diff --git a/eslint.config.js b/eslint.config.js index a30aa30..970d019 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,6 +6,7 @@ export default antfu({ }, { ignores: [ '.github/workflows/client.yml', + 'packages/client/public/UPNG.js', ], }, { rules: { diff --git a/packages/client/index.html b/packages/client/index.html index b27bb3a..79240bd 100644 --- a/packages/client/index.html +++ b/packages/client/index.html @@ -8,8 +8,6 @@ 七牛云OSS图床 | 粥里有勺糖 - diff --git a/packages/client/package.json b/packages/client/package.json index f294718..0afd210 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -24,6 +24,7 @@ "qiniu": "^7.11.0", "qiniu-js": "^3.4.1", "spark-md5": "^3.0.2", + "upng-js": "^2.1.0", "vue": "^3.4.15", "vue-router": "^4.2.5" }, diff --git a/packages/client/src/components/ImageList.vue b/packages/client/src/components/ImageList.vue index cd6fe62..60ff96c 100644 --- a/packages/client/src/components/ImageList.vue +++ b/packages/client/src/components/ImageList.vue @@ -27,6 +27,7 @@ const checkInfo = (image: IImage) => {
  • 链接:${image.url}
  • 上传时间:${image.date && formatDate(image.date)}
  • 大小:${image.size ? formatSize(image.size) : '未知'}
  • + ${image.originSize ? `
  • 压缩前大小:${formatSize(image.originSize)}
  • ` : ''} ` }) @@ -56,7 +57,8 @@ const showImage = computed(() => { - {{ formatSize(image.size) }} + {{ formatSize(image.size) + }} 🔍 url markdown @@ -85,7 +87,7 @@ ul.el-upload-list { display: flex; justify-content: space-between; - .left{ + .left { flex: 1; } @@ -127,4 +129,5 @@ ul.el-upload-list { li { word-break: break-all; } -} \ No newline at end of file +} + \ No newline at end of file diff --git a/packages/client/src/components/ImageUpload.vue b/packages/client/src/components/ImageUpload.vue index 779ec60..1e20a42 100644 --- a/packages/client/src/components/ImageUpload.vue +++ b/packages/client/src/components/ImageUpload.vue @@ -2,11 +2,12 @@ import { UploadFilled } from '@element-plus/icons-vue' import { computed, ref, watch } from 'vue'; import { ElMessage, type UploadInstance, type UploadProps, type UploadUserFile } from 'element-plus' -import { compressImage, uploadFile } from '../utils/qiniu' +import { uploadFile } from '../utils/qiniu' import { useFocus } from '@vueuse/core'; import { useConfigStore, useImageStore } from '@/store' import { storeToRefs } from 'pinia'; -import { formatSize } from '@/utils/stringUtil'; +import { useUploadConfig } from '@/composables'; +import { compressImage } from '@/utils/file'; const imageStore = useImageStore() const configStore = useConfigStore() @@ -23,6 +24,7 @@ const handleChange: UploadProps['onChange'] = (_, uploadFiles) => { return ok }) } +const cacheConfig = useUploadConfig() watch(files, async () => { for (const file of files.value) { @@ -33,12 +35,14 @@ watch(files, async () => { if (!file.raw) { continue } - compressImage(file.raw,{ - noCompressIfLarger: true - }).then(v=>{ - console.log(v.dist, formatSize(v.dist.size)); - }) - uploadFile(file.raw, qiniu.value, { + let fileRaw = file.raw + if (cacheConfig.value.compressImage) { + // TODO: 未来开放自定义调整 + // 采取自动压缩策略 + fileRaw = await compressImage(file.raw) as any + } + + uploadFile(fileRaw, qiniu.value, { process(percent) { file.percentage = percent if (percent === 100) { @@ -54,8 +58,9 @@ watch(files, async () => { imageStore.addImage({ url: v, name: file.name || 'image', - file: file.raw, - size: file.raw?.size || 0, + file: fileRaw, + size: fileRaw?.size || 0, + originSize: fileRaw === file.raw? 0 : file.raw?.size, }) }).catch(err => { ElMessage.error(err) @@ -123,6 +128,7 @@ watch($pasteArea, () => { const { focused } = useFocus($pasteArea) const pasteText = computed(() => focused.value ? '现在你可以粘贴了' : '你也可以点击此处,然后粘贴你要上传的图片') + + \ No newline at end of file diff --git a/packages/client/src/composables/index.ts b/packages/client/src/composables/index.ts index 7b7a166..083b824 100644 --- a/packages/client/src/composables/index.ts +++ b/packages/client/src/composables/index.ts @@ -23,7 +23,8 @@ export function useIsMobile() { }) } +const defaultUploadConfig = { autoCopy: true, copyType: 'markdown', pageSize: 20, compressImage: true, compressPreview: true } export function useUploadConfig() { - const cacheConfig = useLocalStorage('uploadConfig', { autoCopy: true, copyType: 'markdown', pageSize: 20 }) + const cacheConfig = useLocalStorage('uploadConfig', defaultUploadConfig) return cacheConfig } diff --git a/packages/client/src/shims-vue.d.ts b/packages/client/src/shims-vue.d.ts new file mode 100644 index 0000000..b021f5e --- /dev/null +++ b/packages/client/src/shims-vue.d.ts @@ -0,0 +1,4 @@ +// 第三方库的类型定义 +// interface Window { +// UPNG: any +// } diff --git a/packages/client/src/store/modules/imageStore.ts b/packages/client/src/store/modules/imageStore.ts index 030a077..7195f09 100644 --- a/packages/client/src/store/modules/imageStore.ts +++ b/packages/client/src/store/modules/imageStore.ts @@ -1,6 +1,13 @@ import { defineStore } from 'pinia' -export interface IImage { url: string, name: string, file?: File, date?: number, size: number } +export interface IImage { + url: string + name: string + file?: File + date?: number + size: number + originSize?: number +} const imgStore = defineStore('imgStore', { state: () => ({ diff --git a/packages/client/src/utils/file.ts b/packages/client/src/utils/file.ts new file mode 100644 index 0000000..f1ba6f5 --- /dev/null +++ b/packages/client/src/utils/file.ts @@ -0,0 +1,94 @@ +// @ts-expect-error +import UPNG from 'upng-js' +interface CompressOptions { + /** + * 压缩质量(0-100) + * @default 80 + */ + quality?: number + /** + * 压缩后更大是否使用原图 + * @default true + */ + noCompressIfLarger?: boolean + /** + * 压缩后的新宽度 + * @default 原尺寸 + */ + width?: number + /** + * 压缩后新高度 + * @default 原尺寸 + */ + height?: number +} +async function compressImage(file: File, ops: CompressOptions = {}) { + const { width, height, quality = 80, noCompressIfLarger } = ops + const isPng = await isPNG(file) + let newFile: File | null = null + if (isPng) { + const arrayBuffer = await getBlobArrayBuffer(file) + const decoded = UPNG.decode(arrayBuffer) + const rgba8 = UPNG.toRGBA8(decoded) + const compressed = UPNG.encode(rgba8, width || decoded.width, height || decoded.height, convertQualityToBit(quality)) + newFile = new File([compressed], file.name, { type: 'image/png' }) + } + + if (!newFile) { + return file + } + + if (!noCompressIfLarger) { + return newFile + } + + return file.size > newFile.size ? newFile : file +} + +function getBlobArrayBuffer(file: Blob): Promise { + return file.arrayBuffer() +} + +async function isPNG(file: File) { + const arraybuffer = await getBlobArrayBuffer(file.slice(0, 8)) + return signatureEqual(arraybuffer, [137, 80, 78, 71, 13, 10, 26, 10]) +} + +function signatureEqual(source: ArrayBuffer, signature: number[]) { + const array = new Uint8Array(source) + for (let i = 0; i < signature.length; i++) { + if (array[i] !== signature[i]) { + return false + } + } + return true +} + +function getImageDimensions(file: File): Promise<{ width: number, height: number }> { + return new Promise ((resolve) => { + const img = new Image() + img.onload = function () { + resolve({ width: img.width, height: img.height }) + } + img.onerror = function () { + resolve({ width: 0, height: 0 }) + } + img.src = URL.createObjectURL(file) + }) +} + +function convertQualityToBit(quality: number): number { + let bit = 0 + if (quality > 100 || quality < 0) { + bit = 0 + } + else { + bit = !quality ? 0 : quality * 256 * 0.01 + } + return bit +} + +export { + compressImage, + getImageDimensions, +} diff --git a/packages/client/src/utils/qiniu.ts b/packages/client/src/utils/qiniu.ts index 8928ecc..4f320ce 100644 --- a/packages/client/src/utils/qiniu.ts +++ b/packages/client/src/utils/qiniu.ts @@ -1,5 +1,4 @@ import * as qiniu from 'qiniu-js' -import type { CompressOptions } from 'qiniu-js/esm/utils/compress' import { getFileMd5Hash } from './stringUtil' import type { QiNiuConfig } from '@/store/modules/configStore' @@ -50,11 +49,7 @@ async function generateNewFileKey(file: File, prefix = 'mdImg', scope = 'sugar') return `${prefix}/${scope}/${md5}` } -async function compressImage(file: File, ops: CompressOptions) { - return qiniu.compressImage(file, ops) -} export { uploadFile, generateNewFileKey, - compressImage, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6270787..0827788 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,9 @@ importers: spark-md5: specifier: ^3.0.2 version: 3.0.2 + upng-js: + specifier: ^2.1.0 + version: 2.1.0 vue: specifier: ^3.4.15 version: 3.4.15(typescript@5.3.3) @@ -4431,6 +4434,10 @@ packages: netmask: 2.0.2 dev: false + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: false + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -5541,6 +5548,12 @@ packages: picocolors: 1.0.0 dev: true + /upng-js@2.1.0: + resolution: {integrity: sha512-d3xzZzpMP64YkjP5pr8gNyvBt7dLk/uGI67EctzDuVp4lCZyVMo0aJO6l/VDlgbInJYDY6cnClLoBp29eKWI6g==} + dependencies: + pako: 1.0.11 + dev: false + /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: