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.
Files changed (36) hide show
  1. package/README.md +174 -0
  2. package/dist/assets/imageWorker-DyeUTFOy.js +67 -0
  3. package/dist/components/ImageCompressor.vue.d.ts +38 -0
  4. package/dist/composables/useCompression.d.ts +170 -0
  5. package/dist/composables/useEncoderRegistry.d.ts +17 -0
  6. package/dist/composables/useWorker.d.ts +11 -0
  7. package/dist/constants/encoders.d.ts +5 -0
  8. package/dist/constants/resizeMethods.d.ts +39 -0
  9. package/dist/index.d.ts +15 -0
  10. package/dist/style.css +1 -0
  11. package/dist/types/compression.d.ts +35 -0
  12. package/dist/types/encoder.d.ts +100 -0
  13. package/dist/types/processor.d.ts +30 -0
  14. package/dist/types/worker.d.ts +21 -0
  15. package/dist/utils/file.d.ts +23 -0
  16. package/dist/utils/image.d.ts +31 -0
  17. package/dist/vue-image-compressor.js +660 -0
  18. package/dist/vue-image-compressor.umd.cjs +1 -0
  19. package/dist/workers/imageWorker.d.ts +4 -0
  20. package/dist/workers/utils/emscripten.d.ts +6 -0
  21. package/package.json +49 -0
  22. package/src/components/ImageCompressor.vue +304 -0
  23. package/src/composables/useCompression.ts +314 -0
  24. package/src/composables/useEncoderRegistry.ts +70 -0
  25. package/src/composables/useWorker.ts +132 -0
  26. package/src/constants/encoders.ts +137 -0
  27. package/src/constants/resizeMethods.ts +23 -0
  28. package/src/index.ts +63 -0
  29. package/src/types/compression.ts +38 -0
  30. package/src/types/encoder.ts +144 -0
  31. package/src/types/processor.ts +36 -0
  32. package/src/types/worker.ts +29 -0
  33. package/src/utils/file.ts +48 -0
  34. package/src/utils/image.ts +90 -0
  35. package/src/workers/imageWorker.ts +107 -0
  36. package/src/workers/utils/emscripten.ts +16 -0
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 编码器注册表 Composable
3
+ * 管理编码器列表、默认选项和当前选项
4
+ */
5
+
6
+ import { ref, computed } from 'vue';
7
+ import type { EncoderType, EncoderOptions } from '../types/encoder';
8
+ import { ENCODER_REGISTRY, DEFAULT_ENCODER_OPTIONS, ENCODER_LIST } from '../constants/encoders';
9
+
10
+ export function useEncoderRegistry() {
11
+ const selectedEncoder = ref<EncoderType>('mozJPEG');
12
+ const encoderOptions = ref<Record<string, any>>({});
13
+
14
+ // 当前编码器元数据
15
+ const currentMeta = computed(() => ENCODER_REGISTRY[selectedEncoder.value]);
16
+
17
+ // 当前编码器默认选项
18
+ const currentDefaults = computed(() =>
19
+ DEFAULT_ENCODER_OPTIONS[selectedEncoder.value]
20
+ );
21
+
22
+ // 所有可用编码器列表
23
+ const availableEncoders = computed(() =>
24
+ ENCODER_LIST.map((type) => ({
25
+ type,
26
+ label: ENCODER_REGISTRY[type].label,
27
+ mimeType: ENCODER_REGISTRY[type].mimeType,
28
+ extension: ENCODER_REGISTRY[type].extension,
29
+ }))
30
+ );
31
+
32
+ /**
33
+ * 切换编码器时重置选项
34
+ */
35
+ function selectEncoder(type: EncoderType) {
36
+ selectedEncoder.value = type;
37
+ encoderOptions.value = { ...DEFAULT_ENCODER_OPTIONS[type] };
38
+ }
39
+
40
+ /**
41
+ * 更新编码器选项
42
+ */
43
+ function updateOption(key: string, value: any) {
44
+ encoderOptions.value = {
45
+ ...encoderOptions.value,
46
+ [key]: value,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * 重置为默认选项
52
+ */
53
+ function resetOptions() {
54
+ encoderOptions.value = { ...currentDefaults.value };
55
+ }
56
+
57
+ // 初始化
58
+ resetOptions();
59
+
60
+ return {
61
+ selectedEncoder,
62
+ encoderOptions,
63
+ currentMeta,
64
+ currentDefaults,
65
+ availableEncoders,
66
+ selectEncoder,
67
+ updateOption,
68
+ resetOptions,
69
+ };
70
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Worker 封装 Composable
3
+ * 对标 Squoosh: src/client/lazy-app/worker-bridge/index.ts
4
+ * 使用 comlink 封装 Web Worker 通信
5
+ */
6
+
7
+ import { ref, onUnmounted } from 'vue';
8
+ import type { WorkerApi } from '../types/worker';
9
+
10
+ // Worker 空闲超时时间(ms)
11
+ const WORKER_TIMEOUT = 10000;
12
+
13
+ export function useWorker() {
14
+ const isReady = ref(false);
15
+ const isLoading = ref(false);
16
+ const error = ref<Error | null>(null);
17
+
18
+ let worker: Worker | null = null;
19
+ let workerApi: WorkerApi | null = null;
20
+ let timeoutId: number | null = null;
21
+
22
+ /**
23
+ * 启动 Worker
24
+ */
25
+ function startWorker() {
26
+ if (worker) return;
27
+
28
+ try {
29
+ isLoading.value = true;
30
+ // 使用 Vite 的 ?worker 导入方式
31
+ // 实际项目中需要配置 worker 文件路径
32
+ worker = new Worker(
33
+ new URL('../workers/imageWorker.ts', import.meta.url),
34
+ { type: 'module' }
35
+ );
36
+ isReady.value = true;
37
+ error.value = null;
38
+ } catch (err) {
39
+ error.value = err instanceof Error ? err : new Error('Failed to start worker');
40
+ console.error('Worker start failed:', err);
41
+ } finally {
42
+ isLoading.value = false;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 终止 Worker
48
+ */
49
+ function terminateWorker() {
50
+ if (timeoutId) {
51
+ clearTimeout(timeoutId);
52
+ timeoutId = null;
53
+ }
54
+ if (worker) {
55
+ worker.terminate();
56
+ worker = null;
57
+ workerApi = null;
58
+ isReady.value = false;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 设置 Worker 空闲超时回收
64
+ */
65
+ function scheduleTermination() {
66
+ if (timeoutId) clearTimeout(timeoutId);
67
+ timeoutId = window.setTimeout(() => {
68
+ terminateWorker();
69
+ }, WORKER_TIMEOUT);
70
+ }
71
+
72
+ /**
73
+ * 获取 Worker API(懒加载)
74
+ */
75
+ async function getWorkerApi(): Promise<WorkerApi> {
76
+ if (!worker) {
77
+ startWorker();
78
+ }
79
+ if (!worker) {
80
+ throw new Error('Worker failed to initialize');
81
+ }
82
+ // 简化版:直接返回 worker 的 postMessage API
83
+ // 实际使用 comlink 时:return wrap<WorkerApi>(worker);
84
+ return worker as any;
85
+ }
86
+
87
+ /**
88
+ * 执行 Worker 任务(带取消支持)
89
+ */
90
+ async function executeTask<T>(
91
+ signal: AbortSignal,
92
+ task: () => Promise<T>
93
+ ): Promise<T> {
94
+ if (signal.aborted) {
95
+ throw new DOMException('AbortError', 'AbortError');
96
+ }
97
+
98
+ clearTimeout(timeoutId!);
99
+
100
+ return new Promise((resolve, reject) => {
101
+ const onAbort = () => {
102
+ terminateWorker();
103
+ reject(new DOMException('AbortError', 'AbortError'));
104
+ };
105
+
106
+ signal.addEventListener('abort', onAbort);
107
+
108
+ task()
109
+ .then(resolve)
110
+ .catch(reject)
111
+ .finally(() => {
112
+ signal.removeEventListener('abort', onAbort);
113
+ scheduleTermination();
114
+ });
115
+ });
116
+ }
117
+
118
+ // 组件卸载时清理 Worker
119
+ onUnmounted(() => {
120
+ terminateWorker();
121
+ });
122
+
123
+ return {
124
+ isReady,
125
+ isLoading,
126
+ error,
127
+ worker,
128
+ getWorkerApi,
129
+ executeTask,
130
+ terminateWorker,
131
+ };
132
+ }
@@ -0,0 +1,137 @@
1
+ /*
2
+ * 编码器常量配置
3
+ * 对标 Squoosh: src/features/encoders/x/shared/meta.ts
4
+ */
5
+
6
+ import type { EncoderMeta, EncoderType } from '../types/encoder';
7
+
8
+ export const ENCODER_REGISTRY: Record<EncoderType, EncoderMeta> = {
9
+ mozJPEG: {
10
+ label: 'MozJPEG',
11
+ mimeType: 'image/jpeg',
12
+ extension: 'jpg',
13
+ },
14
+ webP: {
15
+ label: 'WebP',
16
+ mimeType: 'image/webp',
17
+ extension: 'webp',
18
+ },
19
+ avif: {
20
+ label: 'AVIF',
21
+ mimeType: 'image/avif',
22
+ extension: 'avif',
23
+ },
24
+ jxl: {
25
+ label: 'JPEG XL',
26
+ mimeType: 'image/jxl',
27
+ extension: 'jxl',
28
+ },
29
+ oxiPNG: {
30
+ label: 'OxiPNG',
31
+ mimeType: 'image/png',
32
+ extension: 'png',
33
+ },
34
+ browserJPEG: {
35
+ label: 'Browser JPEG',
36
+ mimeType: 'image/jpeg',
37
+ extension: 'jpg',
38
+ },
39
+ browserPNG: {
40
+ label: 'Browser PNG',
41
+ mimeType: 'image/png',
42
+ extension: 'png',
43
+ },
44
+ browserGIF: {
45
+ label: 'Browser GIF',
46
+ mimeType: 'image/gif',
47
+ extension: 'gif',
48
+ },
49
+ qoi: {
50
+ label: 'QOI',
51
+ mimeType: 'image/qoi',
52
+ extension: 'qoi',
53
+ },
54
+ wp2: {
55
+ label: 'WebP2',
56
+ mimeType: 'image/webp2',
57
+ extension: 'wp2',
58
+ },
59
+ };
60
+
61
+ export const DEFAULT_ENCODER_OPTIONS: Record<EncoderType, Record<string, any>> = {
62
+ mozJPEG: {
63
+ quality: 75,
64
+ baseline: false,
65
+ arithmetic: false,
66
+ progressive: true,
67
+ optimize_coding: true,
68
+ smoothing: 0,
69
+ color_space: 3,
70
+ quant_table: 3,
71
+ trellis_multipass: false,
72
+ trellis_opt_zero: false,
73
+ trellis_opt_table: false,
74
+ trellis_loops: 1,
75
+ auto_subsample: true,
76
+ chroma_subsample: 2,
77
+ separate_chroma_quality: false,
78
+ chroma_quality: 75,
79
+ },
80
+ webP: {
81
+ quality: 75,
82
+ target_size: 0,
83
+ target_PSNR: 0,
84
+ method: 4,
85
+ sns_strength: 50,
86
+ filter_strength: 60,
87
+ filter_sharpness: 0,
88
+ filter_type: 1,
89
+ partitions: 0,
90
+ segments: 4,
91
+ pass: 1,
92
+ show_compressed: 0,
93
+ preprocessing: 0,
94
+ autofilter: 0,
95
+ partition_limit: 0,
96
+ alpha_compression: 1,
97
+ alpha_filtering: 1,
98
+ alpha_quality: 100,
99
+ lossless: 0,
100
+ exact: 0,
101
+ use_delta_palette: 0,
102
+ vlnr: 0,
103
+ near_lossless: 60,
104
+ },
105
+ avif: {
106
+ cqLevel: 33,
107
+ denoiseLevel: 0,
108
+ cqAlphaLevel: -1,
109
+ tileRows: 0,
110
+ tileCols: 0,
111
+ speed: 6,
112
+ subsample: 1,
113
+ chromaDeltaQ: false,
114
+ sharpness: 0,
115
+ tune: 0,
116
+ },
117
+ jxl: {
118
+ effort: 7,
119
+ quality: 75,
120
+ progressive: false,
121
+ targetPsize: 0,
122
+ },
123
+ oxiPNG: {
124
+ level: 2,
125
+ },
126
+ browserJPEG: {
127
+ quality: 0.75,
128
+ },
129
+ browserPNG: {},
130
+ browserGIF: {},
131
+ qoi: {},
132
+ wp2: {
133
+ quality: 75,
134
+ },
135
+ };
136
+
137
+ export const ENCODER_LIST: EncoderType[] = Object.keys(ENCODER_REGISTRY) as EncoderType[];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 重采样算法常量
3
+ */
4
+
5
+ export const RESIZE_METHODS = [
6
+ { value: 'lanczos3', label: 'Lanczos3' },
7
+ { value: 'catrom', label: 'Catrom' },
8
+ { value: 'mitchell', label: 'Mitchell' },
9
+ { value: 'triangle', label: 'Triangle' },
10
+ { value: 'vector', label: 'Vector' },
11
+ ] as const;
12
+
13
+ export const FIT_METHODS = [
14
+ { value: 'stretch', label: '拉伸' },
15
+ { value: 'contain', label: '适应' },
16
+ ] as const;
17
+
18
+ export const ROTATE_OPTIONS = [
19
+ { value: 0, label: '不旋转' },
20
+ { value: 90, label: '顺时针 90°' },
21
+ { value: 180, label: '旋转 180°' },
22
+ { value: 270, label: '逆时针 90°' },
23
+ ] as const;
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Vue3 Image Compressor 组件库入口
3
+ */
4
+
5
+ // 组件
6
+ export { default as ImageCompressor } from './components/ImageCompressor.vue';
7
+
8
+ // Composables
9
+ export { useCompression } from './composables/useCompression';
10
+ export { useEncoderRegistry } from './composables/useEncoderRegistry';
11
+ export { useWorker } from './composables/useWorker';
12
+
13
+ // 类型
14
+ export type {
15
+ EncoderType,
16
+ EncoderMeta,
17
+ EncoderOptions,
18
+ EncoderState,
19
+ MozJpegEncodeOptions,
20
+ WebPEncodeOptions,
21
+ AvifEncodeOptions,
22
+ JxlEncodeOptions,
23
+ OxiPngEncodeOptions,
24
+ BrowserJpegEncodeOptions,
25
+ Wp2EncodeOptions,
26
+ } from './types/encoder';
27
+
28
+ export type {
29
+ ResizeOptions,
30
+ QuantizeOptions,
31
+ RotateOptions,
32
+ ProcessorState,
33
+ } from './types/processor';
34
+
35
+ export type {
36
+ CompressionOptions,
37
+ CompressionResult,
38
+ ImageInfo,
39
+ } from './types/compression';
40
+
41
+ export type { WorkerApi } from './types/worker';
42
+
43
+ // 常量
44
+ export { ENCODER_REGISTRY, DEFAULT_ENCODER_OPTIONS, ENCODER_LIST } from './constants/encoders';
45
+ export { RESIZE_METHODS, FIT_METHODS, ROTATE_OPTIONS } from './constants/resizeMethods';
46
+
47
+ // 工具
48
+ export {
49
+ blobToImageData,
50
+ imageDataToBlob,
51
+ getImageDimensions,
52
+ sniffMimeType,
53
+ canDecodeImageType,
54
+ arrayBufferToFile,
55
+ } from './utils/image';
56
+
57
+ export {
58
+ formatBytes,
59
+ calculateSavings,
60
+ generateCompressedFilename,
61
+ createBlobUrl,
62
+ revokeBlobUrl,
63
+ } from './utils/file';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 压缩结果类型定义
3
+ */
4
+
5
+ export interface ImageInfo {
6
+ file: File;
7
+ size: number;
8
+ width: number;
9
+ height: number;
10
+ blobUrl: string;
11
+ }
12
+
13
+ export interface CompressionResult {
14
+ original: ImageInfo;
15
+ compressed: ImageInfo;
16
+ savingsBytes: number;
17
+ savingsPercent: number;
18
+ encoderType: string;
19
+ encoderOptions: Record<string, any>;
20
+ }
21
+
22
+ export interface CompressionOptions {
23
+ encoder: string;
24
+ encoderOptions: Record<string, any>;
25
+ resize?: {
26
+ enabled: boolean;
27
+ width?: number;
28
+ height?: number;
29
+ method?: string;
30
+ fitMethod?: string;
31
+ };
32
+ quantize?: {
33
+ enabled: boolean;
34
+ numColors?: number;
35
+ dither?: number;
36
+ };
37
+ rotate?: number;
38
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * 编码器类型定义
3
+ * 对标 Squoosh: src/client/lazy-app/feature-meta/index.ts
4
+ */
5
+
6
+ export type EncoderType =
7
+ | 'mozJPEG'
8
+ | 'webP'
9
+ | 'avif'
10
+ | 'jxl'
11
+ | 'oxiPNG'
12
+ | 'browserJPEG'
13
+ | 'browserPNG'
14
+ | 'browserGIF'
15
+ | 'qoi'
16
+ | 'wp2';
17
+
18
+ export interface EncoderMeta {
19
+ label: string;
20
+ mimeType: string;
21
+ extension: string;
22
+ }
23
+
24
+ export interface MozJpegColorSpace {
25
+ GRAYSCALE: 1;
26
+ RGB: 2;
27
+ YCbCr: 3;
28
+ }
29
+
30
+ export interface MozJpegEncodeOptions {
31
+ quality: number;
32
+ baseline: boolean;
33
+ arithmetic: boolean;
34
+ progressive: boolean;
35
+ optimize_coding: boolean;
36
+ smoothing: number;
37
+ color_space: number;
38
+ quant_table: number;
39
+ trellis_multipass: boolean;
40
+ trellis_opt_zero: boolean;
41
+ trellis_opt_table: boolean;
42
+ trellis_loops: number;
43
+ auto_subsample: boolean;
44
+ chroma_subsample: number;
45
+ separate_chroma_quality: boolean;
46
+ chroma_quality: number;
47
+ }
48
+
49
+ export interface WebPEncodeOptions {
50
+ quality: number;
51
+ target_size: number;
52
+ target_PSNR: number;
53
+ method: number;
54
+ sns_strength: number;
55
+ filter_strength: number;
56
+ filter_sharpness: number;
57
+ filter_type: number;
58
+ partitions: number;
59
+ segments: number;
60
+ pass: number;
61
+ show_compressed: number;
62
+ preprocessing: number;
63
+ autofilter: number;
64
+ partition_limit: number;
65
+ alpha_compression: number;
66
+ alpha_filtering: number;
67
+ alpha_quality: number;
68
+ lossless: number;
69
+ exact: number;
70
+ use_delta_palette: number;
71
+ vlnr: number;
72
+ near_lossless: number;
73
+ }
74
+
75
+ export interface AvifEncodeOptions {
76
+ cqLevel: number;
77
+ denoiseLevel: number;
78
+ cqAlphaLevel: number;
79
+ tileRows: number;
80
+ tileCols: number;
81
+ speed: number;
82
+ subsample: number;
83
+ chromaDeltaQ: boolean;
84
+ sharpness: number;
85
+ tune: number;
86
+ }
87
+
88
+ export interface JxlEncodeOptions {
89
+ effort: number;
90
+ quality: number;
91
+ progressive: boolean;
92
+ targetPsize: number;
93
+ }
94
+
95
+ export interface OxiPngEncodeOptions {
96
+ level: number;
97
+ }
98
+
99
+ export interface BrowserJpegEncodeOptions {
100
+ quality: number;
101
+ }
102
+
103
+ export interface BrowserPngEncodeOptions {
104
+ // browser PNG has no options
105
+ }
106
+
107
+ export interface BrowserGifEncodeOptions {
108
+ // browser GIF has no options
109
+ }
110
+
111
+ export interface QoiEncodeOptions {
112
+ // QOI has no options
113
+ }
114
+
115
+ export interface Wp2EncodeOptions {
116
+ quality: number;
117
+ }
118
+
119
+ export type EncoderOptions =
120
+ | MozJpegEncodeOptions
121
+ | WebPEncodeOptions
122
+ | AvifEncodeOptions
123
+ | JxlEncodeOptions
124
+ | OxiPngEncodeOptions
125
+ | BrowserJpegEncodeOptions
126
+ | BrowserPngEncodeOptions
127
+ | BrowserGifEncodeOptions
128
+ | QoiEncodeOptions
129
+ | Wp2EncodeOptions;
130
+
131
+ export interface EncoderState<T extends EncoderType = EncoderType> {
132
+ type: T;
133
+ options: EncoderOptions;
134
+ }
135
+
136
+ export type EncoderEntry = {
137
+ meta: EncoderMeta;
138
+ encode: (
139
+ signal: AbortSignal,
140
+ workerApi: any,
141
+ imageData: ImageData,
142
+ options: EncoderOptions
143
+ ) => Promise<ArrayBuffer>;
144
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 处理器类型定义
3
+ * 对标 Squoosh: src/features/processors/resize/shared/meta.ts
4
+ */
5
+
6
+ export interface ResizeOptions {
7
+ enabled: boolean;
8
+ width: number;
9
+ height: number;
10
+ method: 'lanczos3' | 'catrom' | 'mitchell' | 'triangle' | 'vector';
11
+ fitMethod: 'stretch' | 'contain';
12
+ premultiply: boolean;
13
+ linearRGB: boolean;
14
+ }
15
+
16
+ export interface QuantizeOptions {
17
+ enabled: boolean;
18
+ numColors: number;
19
+ dither: number;
20
+ }
21
+
22
+ export interface RotateOptions {
23
+ rotate: number; // 0, 90, 180, 270
24
+ }
25
+
26
+ export interface ProcessorState {
27
+ resize: ResizeOptions;
28
+ quantize: QuantizeOptions;
29
+ }
30
+
31
+ export interface PreprocessorState {
32
+ rotate: RotateOptions;
33
+ }
34
+
35
+ export type ResizeMethod = 'lanczos3' | 'catrom' | 'mitchell' | 'triangle' | 'vector';
36
+ export type FitMethod = 'stretch' | 'contain';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Worker API 类型定义
3
+ * 对标 Squoosh: src/client/lazy-app/worker-bridge/meta.ts
4
+ */
5
+
6
+ export interface WorkerApi {
7
+ // Decoders
8
+ avifDecode(blob: Blob): Promise<ImageData>;
9
+ jxlDecode(blob: Blob): Promise<ImageData>;
10
+ qoiDecode(blob: Blob): Promise<ImageData>;
11
+ webpDecode(blob: Blob): Promise<ImageData>;
12
+ wp2Decode(blob: Blob): Promise<ImageData>;
13
+
14
+ // Encoders
15
+ avifEncode(data: ImageData, options: any): Promise<Uint8Array>;
16
+ jxlEncode(data: ImageData, options: any): Promise<Uint8Array>;
17
+ mozjpegEncode(data: ImageData, options: any): Promise<Uint8Array>;
18
+ oxipngEncode(data: ImageData, options: any): Promise<Uint8Array>;
19
+ qoiEncode(data: ImageData, options: any): Promise<Uint8Array>;
20
+ webpEncode(data: ImageData, options: any): Promise<Uint8Array>;
21
+ wp2Encode(data: ImageData, options: any): Promise<Uint8Array>;
22
+
23
+ // Preprocessors
24
+ rotate(data: ImageData, options: any): Promise<ImageData>;
25
+
26
+ // Processors
27
+ quantize(data: ImageData, options: any): Promise<ImageData>;
28
+ resize(data: ImageData, options: any): Promise<ImageData>;
29
+ }