vue3-image-compressor 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -0
- package/dist/assets/imageWorker-DyeUTFOy.js +67 -0
- package/dist/components/ImageCompressor.vue.d.ts +38 -0
- package/dist/composables/useCompression.d.ts +170 -0
- package/dist/composables/useEncoderRegistry.d.ts +17 -0
- package/dist/composables/useWorker.d.ts +11 -0
- package/dist/constants/encoders.d.ts +5 -0
- package/dist/constants/resizeMethods.d.ts +39 -0
- package/dist/index.d.ts +15 -0
- package/dist/style.css +1 -0
- package/dist/types/compression.d.ts +35 -0
- package/dist/types/encoder.d.ts +100 -0
- package/dist/types/processor.d.ts +30 -0
- package/dist/types/worker.d.ts +21 -0
- package/dist/utils/file.d.ts +23 -0
- package/dist/utils/image.d.ts +31 -0
- package/dist/vue-image-compressor.js +660 -0
- package/dist/vue-image-compressor.umd.cjs +1 -0
- package/dist/workers/imageWorker.d.ts +4 -0
- package/dist/workers/utils/emscripten.d.ts +6 -0
- package/package.json +49 -0
- package/src/components/ImageCompressor.vue +304 -0
- package/src/composables/useCompression.ts +314 -0
- package/src/composables/useEncoderRegistry.ts +70 -0
- package/src/composables/useWorker.ts +132 -0
- package/src/constants/encoders.ts +137 -0
- package/src/constants/resizeMethods.ts +23 -0
- package/src/index.ts +63 -0
- package/src/types/compression.ts +38 -0
- package/src/types/encoder.ts +144 -0
- package/src/types/processor.ts +36 -0
- package/src/types/worker.ts +29 -0
- package/src/utils/file.ts +48 -0
- package/src/utils/image.ts +90 -0
- package/src/workers/imageWorker.ts +107 -0
- package/src/workers/utils/emscripten.ts +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Vue Image Compressor
|
|
2
|
+
|
|
3
|
+
基于 [Squoosh](https://github.com/GoogleChromeLabs/squoosh) 核心原理封装的 Vue3 + TypeScript 通用图片压缩组件库。
|
|
4
|
+
|
|
5
|
+
## 架构分析
|
|
6
|
+
|
|
7
|
+
### 图片压缩原理
|
|
8
|
+
|
|
9
|
+
本项目完整还原了 Squoosh 的压缩流程:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
输入文件 → 解码 → 预处理(旋转) → 处理(Resize/Quantize) → 编码 → 输出文件
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- **解码**:浏览器原生支持 JPEG/PNG/GIF,特殊格式(AVIF/WebP/JXL 等)通过 WASM Worker 解码
|
|
16
|
+
- **预处理**:旋转操作(0°/90°/180°/270°)
|
|
17
|
+
- **处理**:
|
|
18
|
+
- **Resize**:使用多种重采样算法(Lanczos3、Catrom、Mitchell、Triangle)
|
|
19
|
+
- **Quantize**:颜色量化(调色板缩减),用于减少 PNG 体积
|
|
20
|
+
- **编码**:
|
|
21
|
+
- **浏览器内置**:Canvas.toBlob() 生成 JPEG/PNG
|
|
22
|
+
- **WASM 编码器**:MozJPEG、WebP、AVIF、JXL、OxiPNG、QOI、WebP2
|
|
23
|
+
|
|
24
|
+
### 关键文件映射
|
|
25
|
+
|
|
26
|
+
| 原始文件 | 封装文件 | 说明 |
|
|
27
|
+
|---------|---------|------|
|
|
28
|
+
| `src/features/decoders/*/dec.ts` | `src/workers/imageWorker.ts` | 解码器聚合,Worker 中加载 WASM |
|
|
29
|
+
| `src/features/encoders/*/enc.ts` | `src/workers/imageWorker.ts` | 编码器聚合,Worker 中加载 WASM |
|
|
30
|
+
| `src/features/processors/resize/*.ts` | `src/workers/imageWorker.ts` | 缩放处理器(resize/quantize) |
|
|
31
|
+
| `src/features/preprocessors/rotate/*.ts` | `src/workers/imageWorker.ts` | 旋转预处理 |
|
|
32
|
+
| `src/client/lazy-app/Compress/index.tsx` | `src/composables/useCompression.ts` | 核心编排逻辑 |
|
|
33
|
+
| `src/client/lazy-app/worker-bridge/index.ts` | `src/composables/useWorker.ts` | Worker 封装与生命周期管理 |
|
|
34
|
+
| `src/client/lazy-app/feature-meta/index.ts` | `src/constants/encoders.ts` | 编码器注册表与默认参数 |
|
|
35
|
+
| `src/features/encoders/*/shared/meta.ts` | `src/types/encoder.ts` | 编码器类型定义 |
|
|
36
|
+
| `src/features/encoders/mozjpeg/encoder-meta.ts` | `src/constants/encoders.ts` | MozJPEG 参数映射 |
|
|
37
|
+
|
|
38
|
+
## 项目结构
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
vue-compressor/
|
|
42
|
+
├── src/
|
|
43
|
+
│ ├── components/
|
|
44
|
+
│ │ └── ImageCompressor.vue # 主组件(上传+预览+设置+操作)
|
|
45
|
+
│ ├── composables/
|
|
46
|
+
│ │ ├── useCompression.ts # 压缩流程编排(解码→处理→编码)
|
|
47
|
+
│ │ ├── useEncoderRegistry.ts # 编码器注册表管理
|
|
48
|
+
│ │ └── useWorker.ts # Worker 封装(懒加载+超时回收+取消)
|
|
49
|
+
│ ├── workers/
|
|
50
|
+
│ │ ├── imageWorker.ts # Web Worker 入口(聚合编解码器)
|
|
51
|
+
│ │ └── utils/
|
|
52
|
+
│ │ └── emscripten.ts # WASM 初始化工具
|
|
53
|
+
│ ├── types/
|
|
54
|
+
│ │ ├── encoder.ts # 编码器类型(10种编码器)
|
|
55
|
+
│ │ ├── processor.ts # 处理器类型(resize/quantize)
|
|
56
|
+
│ │ ├── compression.ts # 压缩结果类型
|
|
57
|
+
│ │ └── worker.ts # Worker API 接口
|
|
58
|
+
│ ├── constants/
|
|
59
|
+
│ │ ├── encoders.ts # 编码器注册表与默认参数
|
|
60
|
+
│ │ └── resizeMethods.ts # 重采样算法常量
|
|
61
|
+
│ ├── utils/
|
|
62
|
+
│ │ ├── image.ts # 图像工具(blob→ImageData、编码检测)
|
|
63
|
+
│ │ └── file.ts # 文件工具(格式化、节省计算、BlobURL)
|
|
64
|
+
│ └── index.ts # 库入口(导出所有组件/Composables/类型/工具)
|
|
65
|
+
├── package.json
|
|
66
|
+
├── vite.config.ts
|
|
67
|
+
├── tsconfig.json
|
|
68
|
+
└── tsconfig.node.json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 安装
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install vue-image-compressor
|
|
75
|
+
# 或
|
|
76
|
+
pnpm add vue-image-compressor
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 使用方式
|
|
80
|
+
|
|
81
|
+
### 1. 组件方式(最简)
|
|
82
|
+
|
|
83
|
+
```vue
|
|
84
|
+
<template>
|
|
85
|
+
<ImageCompressor
|
|
86
|
+
default-encoder="mozJPEG"
|
|
87
|
+
:default-options="{ quality: 80 }"
|
|
88
|
+
@success="onSuccess"
|
|
89
|
+
@error="onError"
|
|
90
|
+
/>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<script setup>
|
|
94
|
+
import { ImageCompressor } from 'vue-image-compressor'
|
|
95
|
+
|
|
96
|
+
function onSuccess(result) {
|
|
97
|
+
console.log('压缩后:', result.compressed.size)
|
|
98
|
+
console.log('节省:', result.savingsPercent + '%')
|
|
99
|
+
}
|
|
100
|
+
function onError(err) {
|
|
101
|
+
console.error('失败:', err.message)
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 2. Composable 方式(灵活定制)
|
|
107
|
+
|
|
108
|
+
```vue
|
|
109
|
+
<script setup>
|
|
110
|
+
import { useCompression, useEncoderRegistry } from 'vue-image-compressor'
|
|
111
|
+
|
|
112
|
+
const { compress, progress, isCompressing, result, cancel } = useCompression()
|
|
113
|
+
const { selectedEncoder, encoderOptions, updateOption } = useEncoderRegistry()
|
|
114
|
+
|
|
115
|
+
// 自定义压缩流程
|
|
116
|
+
async function handleCompress(file) {
|
|
117
|
+
selectedEncoder.value = 'webP'
|
|
118
|
+
updateOption('quality', 85)
|
|
119
|
+
|
|
120
|
+
const result = await compress(file, {
|
|
121
|
+
encoder: 'webP',
|
|
122
|
+
encoderOptions: { ...encoderOptions.value },
|
|
123
|
+
resize: { enabled: true, width: 800, height: 600, method: 'lanczos3' },
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// 上传结果
|
|
127
|
+
uploadToServer(result.compressed.file)
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 3. 纯工具方式(无 Worker 依赖)
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { blobToImageData, imageDataToBlob, formatBytes } from 'vue-image-compressor'
|
|
136
|
+
|
|
137
|
+
// 使用浏览器原生能力进行简单压缩
|
|
138
|
+
async function simpleCompress(file: File, quality: number): Promise<Blob> {
|
|
139
|
+
const imageData = await blobToImageData(file)
|
|
140
|
+
return imageDataToBlob(imageData, 'image/jpeg', quality)
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 编码器支持
|
|
145
|
+
|
|
146
|
+
| 编码器 | 类型 | 质量参数 | 说明 |
|
|
147
|
+
|--------|------|---------|------|
|
|
148
|
+
| mozJPEG | WASM | quality | 优化的 JPEG 编码器,体积更小 |
|
|
149
|
+
| webP | WASM | quality | Google 的 WebP 格式 |
|
|
150
|
+
| avif | WASM | cqLevel | 下一代图像格式,体积最小 |
|
|
151
|
+
| jxl | WASM | quality | JPEG XL 未来格式 |
|
|
152
|
+
| oxiPNG | WASM | level | 优化的 PNG 编码器 |
|
|
153
|
+
| browserJPEG | 内置 | quality | 浏览器 Canvas.toBlob() |
|
|
154
|
+
| browserPNG | 内置 | - | 浏览器原生 PNG |
|
|
155
|
+
| browserGIF | 内置 | - | 浏览器原生 GIF |
|
|
156
|
+
| qoi | WASM | - | 快速有损格式 |
|
|
157
|
+
| wp2 | WASM | quality | WebP2 实验格式 |
|
|
158
|
+
|
|
159
|
+
## 核心特性
|
|
160
|
+
|
|
161
|
+
1. **Web Worker 异步**:压缩任务在 Worker 线程执行,不阻塞 UI
|
|
162
|
+
2. **WASM 高性能**:引用 Squoosh 的 C/Rust 编码器,性能远超 JS
|
|
163
|
+
3. **AbortController 取消**:支持取消正在进行的压缩任务
|
|
164
|
+
4. **Worker 懒加载+超时回收**:首次使用时加载,空闲 10 秒自动回收
|
|
165
|
+
5. **10 种编码器**:覆盖所有主流图片格式
|
|
166
|
+
6. **处理链式编排**:解码 → 旋转 → 缩放 → 量化 → 编码
|
|
167
|
+
7. **类型安全**:完整的 TypeScript 类型定义
|
|
168
|
+
|
|
169
|
+
## 依赖说明
|
|
170
|
+
|
|
171
|
+
- **peer**: `vue ^3.3.0`
|
|
172
|
+
- **dev**: `vite`, `@vitejs/plugin-vue`, `vite-plugin-dts`, `typescript`
|
|
173
|
+
|
|
174
|
+
**注意**:WASM 编解码器文件需要额外从 Squoosh 项目中复制到 `public/codecs/` 目录下,并在 Worker 中加载。
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const r = self, d = {
|
|
2
|
+
// Decoders
|
|
3
|
+
async avifDecode(e) {
|
|
4
|
+
throw new Error("AVIF decode not implemented in worker");
|
|
5
|
+
},
|
|
6
|
+
async jxlDecode(e) {
|
|
7
|
+
throw new Error("JXL decode not implemented in worker");
|
|
8
|
+
},
|
|
9
|
+
async qoiDecode(e) {
|
|
10
|
+
throw new Error("QOI decode not implemented in worker");
|
|
11
|
+
},
|
|
12
|
+
async webpDecode(e) {
|
|
13
|
+
throw new Error("WebP decode not implemented in worker");
|
|
14
|
+
},
|
|
15
|
+
async wp2Decode(e) {
|
|
16
|
+
throw new Error("WebP2 decode not implemented in worker");
|
|
17
|
+
},
|
|
18
|
+
// Encoders
|
|
19
|
+
async avifEncode(e, o) {
|
|
20
|
+
throw new Error("AVIF encode not implemented in worker");
|
|
21
|
+
},
|
|
22
|
+
async jxlEncode(e, o) {
|
|
23
|
+
throw new Error("JXL encode not implemented in worker");
|
|
24
|
+
},
|
|
25
|
+
async mozjpegEncode(e, o) {
|
|
26
|
+
throw new Error("MozJPEG encode not implemented in worker");
|
|
27
|
+
},
|
|
28
|
+
async oxipngEncode(e, o) {
|
|
29
|
+
throw new Error("OxiPNG encode not implemented in worker");
|
|
30
|
+
},
|
|
31
|
+
async qoiEncode(e, o) {
|
|
32
|
+
throw new Error("QOI encode not implemented in worker");
|
|
33
|
+
},
|
|
34
|
+
async webpEncode(e, o) {
|
|
35
|
+
throw new Error("WebP encode not implemented in worker");
|
|
36
|
+
},
|
|
37
|
+
async wp2Encode(e, o) {
|
|
38
|
+
throw new Error("WebP2 encode not implemented in worker");
|
|
39
|
+
},
|
|
40
|
+
// Preprocessors
|
|
41
|
+
async rotate(e, o) {
|
|
42
|
+
throw new Error("Rotate not implemented in worker");
|
|
43
|
+
},
|
|
44
|
+
// Processors
|
|
45
|
+
async quantize(e, o) {
|
|
46
|
+
throw new Error("Quantize not implemented in worker");
|
|
47
|
+
},
|
|
48
|
+
async resize(e, o) {
|
|
49
|
+
throw new Error("Resize not implemented in worker");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
r.addEventListener("message", async (e) => {
|
|
53
|
+
const { id: o, method: t, args: i } = e.data;
|
|
54
|
+
try {
|
|
55
|
+
const n = d[t];
|
|
56
|
+
if (!n)
|
|
57
|
+
throw new Error(`Unknown method: ${t}`);
|
|
58
|
+
const a = await n.apply(d, i);
|
|
59
|
+
r.postMessage({ id: o, result: a, error: null });
|
|
60
|
+
} catch (n) {
|
|
61
|
+
r.postMessage({
|
|
62
|
+
id: o,
|
|
63
|
+
result: null,
|
|
64
|
+
error: n instanceof Error ? n.message : String(n)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
declare function __VLS_template(): {
|
|
2
|
+
"upload-prompt"?(_: {}): any;
|
|
3
|
+
"encoder-settings"?(_: {
|
|
4
|
+
encoder: import('..').EncoderType;
|
|
5
|
+
options: Record<string, any>;
|
|
6
|
+
}): any;
|
|
7
|
+
};
|
|
8
|
+
declare const __VLS_component: import('vue').DefineComponent<import('vue').ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
|
|
9
|
+
defaultEncoder?: string;
|
|
10
|
+
defaultOptions?: Record<string, any>;
|
|
11
|
+
}>>, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
12
|
+
success: (result: any) => void;
|
|
13
|
+
error: (err: Error) => void;
|
|
14
|
+
cancel: () => void;
|
|
15
|
+
}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
|
|
16
|
+
defaultEncoder?: string;
|
|
17
|
+
defaultOptions?: Record<string, any>;
|
|
18
|
+
}>>> & Readonly<{
|
|
19
|
+
onSuccess?: ((result: any) => any) | undefined;
|
|
20
|
+
onError?: ((err: Error) => any) | undefined;
|
|
21
|
+
onCancel?: (() => any) | undefined;
|
|
22
|
+
}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
23
|
+
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, ReturnType<typeof __VLS_template>>;
|
|
24
|
+
export default _default;
|
|
25
|
+
type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
|
|
26
|
+
type __VLS_TypePropsToRuntimeProps<T> = {
|
|
27
|
+
[K in keyof T]-?: {} extends Pick<T, K> ? {
|
|
28
|
+
type: import('vue').PropType<__VLS_NonUndefinedable<T[K]>>;
|
|
29
|
+
} : {
|
|
30
|
+
type: import('vue').PropType<T[K]>;
|
|
31
|
+
required: true;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
type __VLS_WithTemplateSlots<T, S> = T & {
|
|
35
|
+
new (): {
|
|
36
|
+
$slots: S;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { CompressionOptions, CompressionResult } from '../types/compression';
|
|
2
|
+
|
|
3
|
+
export declare function useCompression(): {
|
|
4
|
+
compress: (file: File, options: CompressionOptions) => Promise<CompressionResult>;
|
|
5
|
+
cancel: () => void;
|
|
6
|
+
downloadResult: () => void;
|
|
7
|
+
isCompressing: import('vue').Ref<boolean, boolean>;
|
|
8
|
+
progress: import('vue').Ref<number, number>;
|
|
9
|
+
error: import('vue').Ref<string | null, string | null>;
|
|
10
|
+
result: import('vue').Ref<{
|
|
11
|
+
original: {
|
|
12
|
+
file: {
|
|
13
|
+
readonly lastModified: number;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly webkitRelativePath: string;
|
|
16
|
+
readonly size: number;
|
|
17
|
+
readonly type: string;
|
|
18
|
+
arrayBuffer: {
|
|
19
|
+
(): Promise<ArrayBuffer>;
|
|
20
|
+
(): Promise<ArrayBuffer>;
|
|
21
|
+
(): Promise<ArrayBuffer>;
|
|
22
|
+
};
|
|
23
|
+
bytes: {
|
|
24
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
25
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
26
|
+
};
|
|
27
|
+
slice: {
|
|
28
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
29
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
30
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
31
|
+
};
|
|
32
|
+
stream: {
|
|
33
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
34
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
35
|
+
(): NodeJS.ReadableStream;
|
|
36
|
+
};
|
|
37
|
+
text: {
|
|
38
|
+
(): Promise<string>;
|
|
39
|
+
(): Promise<string>;
|
|
40
|
+
(): Promise<string>;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
size: number;
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
blobUrl: string;
|
|
47
|
+
};
|
|
48
|
+
compressed: {
|
|
49
|
+
file: {
|
|
50
|
+
readonly lastModified: number;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly webkitRelativePath: string;
|
|
53
|
+
readonly size: number;
|
|
54
|
+
readonly type: string;
|
|
55
|
+
arrayBuffer: {
|
|
56
|
+
(): Promise<ArrayBuffer>;
|
|
57
|
+
(): Promise<ArrayBuffer>;
|
|
58
|
+
(): Promise<ArrayBuffer>;
|
|
59
|
+
};
|
|
60
|
+
bytes: {
|
|
61
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
62
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
63
|
+
};
|
|
64
|
+
slice: {
|
|
65
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
66
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
67
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
68
|
+
};
|
|
69
|
+
stream: {
|
|
70
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
71
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
72
|
+
(): NodeJS.ReadableStream;
|
|
73
|
+
};
|
|
74
|
+
text: {
|
|
75
|
+
(): Promise<string>;
|
|
76
|
+
(): Promise<string>;
|
|
77
|
+
(): Promise<string>;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
size: number;
|
|
81
|
+
width: number;
|
|
82
|
+
height: number;
|
|
83
|
+
blobUrl: string;
|
|
84
|
+
};
|
|
85
|
+
savingsBytes: number;
|
|
86
|
+
savingsPercent: number;
|
|
87
|
+
encoderType: string;
|
|
88
|
+
encoderOptions: Record<string, any>;
|
|
89
|
+
} | null, CompressionResult | {
|
|
90
|
+
original: {
|
|
91
|
+
file: {
|
|
92
|
+
readonly lastModified: number;
|
|
93
|
+
readonly name: string;
|
|
94
|
+
readonly webkitRelativePath: string;
|
|
95
|
+
readonly size: number;
|
|
96
|
+
readonly type: string;
|
|
97
|
+
arrayBuffer: {
|
|
98
|
+
(): Promise<ArrayBuffer>;
|
|
99
|
+
(): Promise<ArrayBuffer>;
|
|
100
|
+
(): Promise<ArrayBuffer>;
|
|
101
|
+
};
|
|
102
|
+
bytes: {
|
|
103
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
104
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
105
|
+
};
|
|
106
|
+
slice: {
|
|
107
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
108
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
109
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
110
|
+
};
|
|
111
|
+
stream: {
|
|
112
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
113
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
114
|
+
(): NodeJS.ReadableStream;
|
|
115
|
+
};
|
|
116
|
+
text: {
|
|
117
|
+
(): Promise<string>;
|
|
118
|
+
(): Promise<string>;
|
|
119
|
+
(): Promise<string>;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
size: number;
|
|
123
|
+
width: number;
|
|
124
|
+
height: number;
|
|
125
|
+
blobUrl: string;
|
|
126
|
+
};
|
|
127
|
+
compressed: {
|
|
128
|
+
file: {
|
|
129
|
+
readonly lastModified: number;
|
|
130
|
+
readonly name: string;
|
|
131
|
+
readonly webkitRelativePath: string;
|
|
132
|
+
readonly size: number;
|
|
133
|
+
readonly type: string;
|
|
134
|
+
arrayBuffer: {
|
|
135
|
+
(): Promise<ArrayBuffer>;
|
|
136
|
+
(): Promise<ArrayBuffer>;
|
|
137
|
+
(): Promise<ArrayBuffer>;
|
|
138
|
+
};
|
|
139
|
+
bytes: {
|
|
140
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
141
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
142
|
+
};
|
|
143
|
+
slice: {
|
|
144
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
145
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
146
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
147
|
+
};
|
|
148
|
+
stream: {
|
|
149
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
150
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
151
|
+
(): NodeJS.ReadableStream;
|
|
152
|
+
};
|
|
153
|
+
text: {
|
|
154
|
+
(): Promise<string>;
|
|
155
|
+
(): Promise<string>;
|
|
156
|
+
(): Promise<string>;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
size: number;
|
|
160
|
+
width: number;
|
|
161
|
+
height: number;
|
|
162
|
+
blobUrl: string;
|
|
163
|
+
};
|
|
164
|
+
savingsBytes: number;
|
|
165
|
+
savingsPercent: number;
|
|
166
|
+
encoderType: string;
|
|
167
|
+
encoderOptions: Record<string, any>;
|
|
168
|
+
} | null>;
|
|
169
|
+
canCancel: import('vue').ComputedRef<boolean>;
|
|
170
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EncoderType } from '../types/encoder';
|
|
2
|
+
|
|
3
|
+
export declare function useEncoderRegistry(): {
|
|
4
|
+
selectedEncoder: import('vue').Ref<EncoderType, EncoderType>;
|
|
5
|
+
encoderOptions: import('vue').Ref<Record<string, any>, Record<string, any>>;
|
|
6
|
+
currentMeta: import('vue').ComputedRef<import('..').EncoderMeta>;
|
|
7
|
+
currentDefaults: import('vue').ComputedRef<Record<string, any>>;
|
|
8
|
+
availableEncoders: import('vue').ComputedRef<{
|
|
9
|
+
type: EncoderType;
|
|
10
|
+
label: string;
|
|
11
|
+
mimeType: string;
|
|
12
|
+
extension: string;
|
|
13
|
+
}[]>;
|
|
14
|
+
selectEncoder: (type: EncoderType) => void;
|
|
15
|
+
updateOption: (key: string, value: any) => void;
|
|
16
|
+
resetOptions: () => void;
|
|
17
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { WorkerApi } from '../types/worker';
|
|
2
|
+
|
|
3
|
+
export declare function useWorker(): {
|
|
4
|
+
isReady: import('vue').Ref<boolean, boolean>;
|
|
5
|
+
isLoading: import('vue').Ref<boolean, boolean>;
|
|
6
|
+
error: import('vue').Ref<Error | null, Error | null>;
|
|
7
|
+
worker: null;
|
|
8
|
+
getWorkerApi: () => Promise<WorkerApi>;
|
|
9
|
+
executeTask: <T>(signal: AbortSignal, task: () => Promise<T>) => Promise<T>;
|
|
10
|
+
terminateWorker: () => void;
|
|
11
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { EncoderMeta, EncoderType } from '../types/encoder';
|
|
2
|
+
|
|
3
|
+
export declare const ENCODER_REGISTRY: Record<EncoderType, EncoderMeta>;
|
|
4
|
+
export declare const DEFAULT_ENCODER_OPTIONS: Record<EncoderType, Record<string, any>>;
|
|
5
|
+
export declare const ENCODER_LIST: EncoderType[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 重采样算法常量
|
|
3
|
+
*/
|
|
4
|
+
export declare const RESIZE_METHODS: readonly [{
|
|
5
|
+
readonly value: "lanczos3";
|
|
6
|
+
readonly label: "Lanczos3";
|
|
7
|
+
}, {
|
|
8
|
+
readonly value: "catrom";
|
|
9
|
+
readonly label: "Catrom";
|
|
10
|
+
}, {
|
|
11
|
+
readonly value: "mitchell";
|
|
12
|
+
readonly label: "Mitchell";
|
|
13
|
+
}, {
|
|
14
|
+
readonly value: "triangle";
|
|
15
|
+
readonly label: "Triangle";
|
|
16
|
+
}, {
|
|
17
|
+
readonly value: "vector";
|
|
18
|
+
readonly label: "Vector";
|
|
19
|
+
}];
|
|
20
|
+
export declare const FIT_METHODS: readonly [{
|
|
21
|
+
readonly value: "stretch";
|
|
22
|
+
readonly label: "拉伸";
|
|
23
|
+
}, {
|
|
24
|
+
readonly value: "contain";
|
|
25
|
+
readonly label: "适应";
|
|
26
|
+
}];
|
|
27
|
+
export declare const ROTATE_OPTIONS: readonly [{
|
|
28
|
+
readonly value: 0;
|
|
29
|
+
readonly label: "不旋转";
|
|
30
|
+
}, {
|
|
31
|
+
readonly value: 90;
|
|
32
|
+
readonly label: "顺时针 90°";
|
|
33
|
+
}, {
|
|
34
|
+
readonly value: 180;
|
|
35
|
+
readonly label: "旋转 180°";
|
|
36
|
+
}, {
|
|
37
|
+
readonly value: 270;
|
|
38
|
+
readonly label: "逆时针 90°";
|
|
39
|
+
}];
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue3 Image Compressor 组件库入口
|
|
3
|
+
*/
|
|
4
|
+
export { default as ImageCompressor } from './components/ImageCompressor.vue';
|
|
5
|
+
export { useCompression } from './composables/useCompression';
|
|
6
|
+
export { useEncoderRegistry } from './composables/useEncoderRegistry';
|
|
7
|
+
export { useWorker } from './composables/useWorker';
|
|
8
|
+
export type { EncoderType, EncoderMeta, EncoderOptions, EncoderState, MozJpegEncodeOptions, WebPEncodeOptions, AvifEncodeOptions, JxlEncodeOptions, OxiPngEncodeOptions, BrowserJpegEncodeOptions, Wp2EncodeOptions, } from './types/encoder';
|
|
9
|
+
export type { ResizeOptions, QuantizeOptions, RotateOptions, ProcessorState, } from './types/processor';
|
|
10
|
+
export type { CompressionOptions, CompressionResult, ImageInfo, } from './types/compression';
|
|
11
|
+
export type { WorkerApi } from './types/worker';
|
|
12
|
+
export { ENCODER_REGISTRY, DEFAULT_ENCODER_OPTIONS, ENCODER_LIST } from './constants/encoders';
|
|
13
|
+
export { RESIZE_METHODS, FIT_METHODS, ROTATE_OPTIONS } from './constants/resizeMethods';
|
|
14
|
+
export { blobToImageData, imageDataToBlob, getImageDimensions, sniffMimeType, canDecodeImageType, arrayBufferToFile, } from './utils/image';
|
|
15
|
+
export { formatBytes, calculateSavings, generateCompressedFilename, createBlobUrl, revokeBlobUrl, } from './utils/file';
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.image-compressor[data-v-86f2b482]{max-width:800px;margin:0 auto}.upload-zone[data-v-86f2b482]{border:2px dashed #ccc;border-radius:8px;padding:40px;text-align:center;cursor:pointer;transition:border-color .2s}.upload-zone[data-v-86f2b482]:hover{border-color:#409eff}.file-input[data-v-86f2b482]{display:none}.preview-area[data-v-86f2b482]{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin:20px 0}.preview-item img[data-v-86f2b482]{max-width:100%;border-radius:4px;box-shadow:0 2px 8px #0000001a}.compressing[data-v-86f2b482]{padding:40px;text-align:center}.actions[data-v-86f2b482]{display:flex;gap:10px;margin-top:20px}.btn-primary[data-v-86f2b482],.btn-secondary[data-v-86f2b482],.btn-success[data-v-86f2b482]{padding:10px 24px;border:none;border-radius:4px;cursor:pointer;font-size:14px}.btn-primary[data-v-86f2b482]{background:#409eff;color:#fff}.btn-primary[data-v-86f2b482]:disabled{background:#a0cfff}.btn-secondary[data-v-86f2b482]{background:#f56c6c;color:#fff}.btn-success[data-v-86f2b482]{background:#67c23a;color:#fff}.error[data-v-86f2b482]{color:#f56c6c;margin-top:10px}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 压缩结果类型定义
|
|
3
|
+
*/
|
|
4
|
+
export interface ImageInfo {
|
|
5
|
+
file: File;
|
|
6
|
+
size: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
blobUrl: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CompressionResult {
|
|
12
|
+
original: ImageInfo;
|
|
13
|
+
compressed: ImageInfo;
|
|
14
|
+
savingsBytes: number;
|
|
15
|
+
savingsPercent: number;
|
|
16
|
+
encoderType: string;
|
|
17
|
+
encoderOptions: Record<string, any>;
|
|
18
|
+
}
|
|
19
|
+
export interface CompressionOptions {
|
|
20
|
+
encoder: string;
|
|
21
|
+
encoderOptions: Record<string, any>;
|
|
22
|
+
resize?: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
width?: number;
|
|
25
|
+
height?: number;
|
|
26
|
+
method?: string;
|
|
27
|
+
fitMethod?: string;
|
|
28
|
+
};
|
|
29
|
+
quantize?: {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
numColors?: number;
|
|
32
|
+
dither?: number;
|
|
33
|
+
};
|
|
34
|
+
rotate?: number;
|
|
35
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编码器类型定义
|
|
3
|
+
* 对标 Squoosh: src/client/lazy-app/feature-meta/index.ts
|
|
4
|
+
*/
|
|
5
|
+
export type EncoderType = 'mozJPEG' | 'webP' | 'avif' | 'jxl' | 'oxiPNG' | 'browserJPEG' | 'browserPNG' | 'browserGIF' | 'qoi' | 'wp2';
|
|
6
|
+
export interface EncoderMeta {
|
|
7
|
+
label: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
extension: string;
|
|
10
|
+
}
|
|
11
|
+
export interface MozJpegColorSpace {
|
|
12
|
+
GRAYSCALE: 1;
|
|
13
|
+
RGB: 2;
|
|
14
|
+
YCbCr: 3;
|
|
15
|
+
}
|
|
16
|
+
export interface MozJpegEncodeOptions {
|
|
17
|
+
quality: number;
|
|
18
|
+
baseline: boolean;
|
|
19
|
+
arithmetic: boolean;
|
|
20
|
+
progressive: boolean;
|
|
21
|
+
optimize_coding: boolean;
|
|
22
|
+
smoothing: number;
|
|
23
|
+
color_space: number;
|
|
24
|
+
quant_table: number;
|
|
25
|
+
trellis_multipass: boolean;
|
|
26
|
+
trellis_opt_zero: boolean;
|
|
27
|
+
trellis_opt_table: boolean;
|
|
28
|
+
trellis_loops: number;
|
|
29
|
+
auto_subsample: boolean;
|
|
30
|
+
chroma_subsample: number;
|
|
31
|
+
separate_chroma_quality: boolean;
|
|
32
|
+
chroma_quality: number;
|
|
33
|
+
}
|
|
34
|
+
export interface WebPEncodeOptions {
|
|
35
|
+
quality: number;
|
|
36
|
+
target_size: number;
|
|
37
|
+
target_PSNR: number;
|
|
38
|
+
method: number;
|
|
39
|
+
sns_strength: number;
|
|
40
|
+
filter_strength: number;
|
|
41
|
+
filter_sharpness: number;
|
|
42
|
+
filter_type: number;
|
|
43
|
+
partitions: number;
|
|
44
|
+
segments: number;
|
|
45
|
+
pass: number;
|
|
46
|
+
show_compressed: number;
|
|
47
|
+
preprocessing: number;
|
|
48
|
+
autofilter: number;
|
|
49
|
+
partition_limit: number;
|
|
50
|
+
alpha_compression: number;
|
|
51
|
+
alpha_filtering: number;
|
|
52
|
+
alpha_quality: number;
|
|
53
|
+
lossless: number;
|
|
54
|
+
exact: number;
|
|
55
|
+
use_delta_palette: number;
|
|
56
|
+
vlnr: number;
|
|
57
|
+
near_lossless: number;
|
|
58
|
+
}
|
|
59
|
+
export interface AvifEncodeOptions {
|
|
60
|
+
cqLevel: number;
|
|
61
|
+
denoiseLevel: number;
|
|
62
|
+
cqAlphaLevel: number;
|
|
63
|
+
tileRows: number;
|
|
64
|
+
tileCols: number;
|
|
65
|
+
speed: number;
|
|
66
|
+
subsample: number;
|
|
67
|
+
chromaDeltaQ: boolean;
|
|
68
|
+
sharpness: number;
|
|
69
|
+
tune: number;
|
|
70
|
+
}
|
|
71
|
+
export interface JxlEncodeOptions {
|
|
72
|
+
effort: number;
|
|
73
|
+
quality: number;
|
|
74
|
+
progressive: boolean;
|
|
75
|
+
targetPsize: number;
|
|
76
|
+
}
|
|
77
|
+
export interface OxiPngEncodeOptions {
|
|
78
|
+
level: number;
|
|
79
|
+
}
|
|
80
|
+
export interface BrowserJpegEncodeOptions {
|
|
81
|
+
quality: number;
|
|
82
|
+
}
|
|
83
|
+
export interface BrowserPngEncodeOptions {
|
|
84
|
+
}
|
|
85
|
+
export interface BrowserGifEncodeOptions {
|
|
86
|
+
}
|
|
87
|
+
export interface QoiEncodeOptions {
|
|
88
|
+
}
|
|
89
|
+
export interface Wp2EncodeOptions {
|
|
90
|
+
quality: number;
|
|
91
|
+
}
|
|
92
|
+
export type EncoderOptions = MozJpegEncodeOptions | WebPEncodeOptions | AvifEncodeOptions | JxlEncodeOptions | OxiPngEncodeOptions | BrowserJpegEncodeOptions | BrowserPngEncodeOptions | BrowserGifEncodeOptions | QoiEncodeOptions | Wp2EncodeOptions;
|
|
93
|
+
export interface EncoderState<T extends EncoderType = EncoderType> {
|
|
94
|
+
type: T;
|
|
95
|
+
options: EncoderOptions;
|
|
96
|
+
}
|
|
97
|
+
export type EncoderEntry = {
|
|
98
|
+
meta: EncoderMeta;
|
|
99
|
+
encode: (signal: AbortSignal, workerApi: any, imageData: ImageData, options: EncoderOptions) => Promise<ArrayBuffer>;
|
|
100
|
+
};
|