diff --git a/playground/src/App.vue b/playground/src/App.vue
index 0c06d65..eede6ae 100644
--- a/playground/src/App.vue
+++ b/playground/src/App.vue
@@ -8,7 +8,7 @@
-
+
diff --git a/playground/vite.config.ts b/playground/vite.config.ts
index 010f3af..3c83967 100644
--- a/playground/vite.config.ts
+++ b/playground/vite.config.ts
@@ -18,28 +18,5 @@ export default defineConfig({
port: 8451,
},
// publicDir: 'base/public',
- plugins: [
- vue(),
- imagemin({
- compress: {
- jpg: {
- quality: 10,
- },
- jpeg: {
- quality: 10,
- },
- png: {
- quality: 10,
- },
- webp: {
- quality: 0,
- },
- },
- conversion: [
- { from: 'jpeg', to: 'webp' },
- { from: 'jpg', to: 'webp' },
- { from: 'png', to: 'webp' },
- ],
- }),
- ],
+ plugins: [vue(), imagemin()],
});
diff --git a/src/core/context.ts b/src/core/context.ts
index 45f998f..6401d23 100644
--- a/src/core/context.ts
+++ b/src/core/context.ts
@@ -1,10 +1,11 @@
+/* eslint-disable no-return-await */
/* eslint-disable no-await-in-loop */
import fs from 'node:fs/promises';
import { Buffer } from 'node:buffer';
import { cpus } from 'node:os';
import { performance } from 'node:perf_hooks';
-import { basename, extname, isAbsolute, join, relative, resolve } from 'pathe';
+import { basename, extname, join, resolve } from 'pathe';
import { createFilter } from '@rollup/pluginutils';
import { optimize } from 'svgo';
import chalk from 'chalk';
@@ -14,11 +15,13 @@ import { encodeMap, encodeMapBack } from './encodeMap';
import {
exists,
filterFile,
- generateImageID,
+ getBundleImageSrc,
hasImageFiles,
+ isSvgFile,
isTurnImageType,
parseId,
transformFileName,
+ updateCssReferences,
} from './utils';
import { defaultOptions } from './compressOptions';
import devalue from './devalue';
@@ -156,17 +159,16 @@ export default class Context {
continue;
}
- // 4. 创建处理任务
const task = async () => {
if (!isSvgFile(path)) {
return {
originFileName: asset.fileName,
- result: await this.generateSquooshBundle(imagePool, path),
+ result: await this.processRasterImage(imagePool, path),
};
}
return {
originFileName: asset.fileName,
- result: await this.generateSvgBundle(path),
+ result: await this.processSvg(path),
};
};
@@ -249,7 +251,7 @@ export default class Context {
}
// squoosh
- async generateSquooshBundle(imagePool, item) {
+ async processRasterImage(imagePool, item) {
const start = performance.now();
const size = await fs.lstat(item);
const oldSize = size.size;
@@ -279,6 +281,8 @@ export default class Context {
[type!]: defaultSquooshOptions[type!],
};
+ console.log(currentType);
+
try {
await image.encode(currentType);
} catch (error) {
@@ -341,7 +345,8 @@ export default class Context {
spinner.stop();
}
- async generateSvgBundle(item) {
+ async processSvg(item) {
+ const { assetsDir, outDir,base: configBase } = this.config;
const svgCode = await fs.readFile(item, 'utf8');
const result = optimize(svgCode, {
@@ -352,7 +357,6 @@ export default class Context {
const generateSrc = getBundleImageSrc(item, this.config.options);
const base = basename(item, extname(item));
- const { assetsDir, outDir } = this.config;
const imageName = `${base}-${generateSrc}`;
const start = performance.now();
const size = await fs.lstat(item);
@@ -368,7 +372,7 @@ export default class Context {
};
compressSuccess(
- join(this.config.base, outDir, svgResult.fileName),
+ join(configBase, outDir, svgResult.fileName),
newSize,
oldSize,
start,
@@ -377,18 +381,6 @@ export default class Context {
}
}
-function getBundleImageSrc(filename: string, options: any) {
- const currentType =
- options.conversion.find(
- (item) => item.from === extname(filename).slice(1),
- ) ?? extname(filename).slice(1);
- const id = generateImageID(
- filename,
- currentType.to ?? extname(filename).slice(1),
- );
- return id;
-}
-
export function resolveOptions(
options: any,
configOption: any,
@@ -448,24 +440,4 @@ export async function transformCode(
);
await fs.writeFile(finallyPath, item[sourceCode]);
});
-}
-
-export function isSvgFile(filename) {
- return extname(filename) === '.svg';
-}
-
-function updateCssReferences(
- cssContent: string,
- fileNameMap: Map,
-): string {
- try {
- return Array.from(fileNameMap).reduce(
- (updatedCssContent, [oldFileName, newFileName]) =>
- updatedCssContent.replace(new RegExp(oldFileName, 'g'), newFileName),
- cssContent,
- );
- } catch (error) {
- console.error('[unplugin-imagemin] Error processing CSS:', error);
- return cssContent;
- }
-}
+}
\ No newline at end of file
diff --git a/src/core/fileHandler.ts b/src/core/fileHandler.ts
new file mode 100644
index 0000000..32a3789
--- /dev/null
+++ b/src/core/fileHandler.ts
@@ -0,0 +1,2 @@
+export default class FileHandler {
+}
\ No newline at end of file
diff --git a/src/core/imageProcess.ts b/src/core/imageProcess.ts
new file mode 100644
index 0000000..acf4915
--- /dev/null
+++ b/src/core/imageProcess.ts
@@ -0,0 +1,155 @@
+// ImageProcessor.ts
+import fs from 'node:fs/promises';
+import { cpus } from 'node:os';
+import { extname, basename, join } from 'pathe';
+import { ImagePool } from 'squoosh';
+import { optimize } from 'svgo';
+import { performance } from 'node:perf_hooks';
+import type { ResolvedOptions } from './types';
+import { isSvgFile, getBundleImageSrc } from './Utils';
+
+export default class ImageProcessor {
+ private imagePool: ImagePool;
+ private options: ResolvedOptions;
+ private cache: Cache;
+
+ constructor(options: ResolvedOptions, cache: Cache) {
+ this.options = options;
+ this.cache = cache;
+ this.imagePool = new ImagePool(cpus().length);
+ }
+
+ // /**
+ // * 处理图像文件,支持常规图像和 SVG
+ // * @param filePath 图像文件的路径
+ // * @returns 处理后的图像信息
+ // */
+ async processImage(filePath: string): Promise<{ fileName: string; source: Buffer | string }> {
+ if (isSvgFile(filePath)) {
+ return this.processSvg(filePath);
+ } else {
+ return this.processRasterImage(filePath);
+ }
+ }
+
+ /**
+ * 处理常规光栅图像(如 PNG、JPEG、WEBP 等)
+ * @param filePath 图像文件的路径
+ * @returns 处理后的图像信息
+ */
+ private async processRasterImage(filePath: string): Promise<{ fileName: string; source: Buffer }> {
+ const startTime = performance.now();
+ const { ext } = this.getFileInfo(filePath);
+
+ const userConversion = this.options.conversion.find(
+ (item) => item.from === ext
+ );
+
+ const targetType = userConversion?.to ?? ext;
+ const imageId = getBundleImageSrc(filePath, this.options);
+ const imageName = `${basename(filePath, extname(filePath))}-${imageId}.${targetType}`;
+ const outputFilePath = join(this.options.assetsDir, imageName);
+
+ // const fileStat = await fs.stat(filePath);
+ // const cacheKey = `${filePath}-${targetType}`;
+ // const isCached = await this.cache.hasCachedAsset(cacheKey, fileStat.mtimeMs);
+
+ // if (isCached) {
+ // Logger.info(`Using cached image for ${filePath}`);
+ // const cachedData = await this.cache.getCachedAsset(cacheKey);
+ // return { fileName: outputFilePath, source: cachedData };
+ // }
+
+ const imageBuffer = await fs.readFile(filePath);
+ const image = this.imagePool.ingestImage(imageBuffer);
+
+ const encodeOptions = this.getEncodeOptions(targetType);
+ if (!encodeOptions) {
+ throw new Error(`Unsupported target image type: ${targetType}`);
+ }
+
+ try {
+ await image.encode({ [targetType]: encodeOptions });
+ } catch (error) {
+ Logger.error(`Error encoding image ${filePath}`, error as Error);
+ throw error;
+ }
+
+ const encodedImage = await image.encodedWith[targetType];
+ const newSize = encodedImage.size;
+ const oldSize = imageBuffer.length;
+
+ // 保存缓存
+ await this.cache.setCachedAsset(cacheKey, encodedImage.binary);
+
+ const timeSpent = (performance.now() - startTime).toFixed(2);
+ // Logger.success(
+ // `Compressed ${filePath} [${(oldSize / 1024).toFixed(2)} KB -> ${(newSize / 1024).toFixed(2)} KB] in ${timeSpent} ms`
+ // );
+
+ return {
+ fileName: outputFilePath,
+ source: encodedImage.binary,
+ };
+ }
+
+ private async processSvg(filePath: string): Promise<{ fileName: string; source: string }> {
+ const startTime = performance.now();
+
+ const svgContent = await fs.readFile(filePath, 'utf8');
+ const result = optimize(svgContent, {
+ multipass: true,
+ path: filePath,
+ });
+
+ if ('data' in result) {
+ const optimizedSvg = result.data;
+ const oldSize = Buffer.byteLength(svgContent, 'utf8');
+ const newSize = Buffer.byteLength(optimizedSvg, 'utf8');
+
+ // await this.cache.setCachedAsset(filePath, optimizedSvg);
+
+ const imageId = getBundleImageSrc(filePath, this.options);
+ const imageName = `${basename(filePath, '.svg')}-${imageId}.svg`;
+ const outputFilePath = join(this.options.assetsDir, imageName);
+
+ const timeSpent = (performance.now() - startTime).toFixed(2);
+ Logger.success(
+ `Optimized SVG ${filePath} [${(oldSize / 1024).toFixed(2)} KB -> ${(newSize / 1024).toFixed(2)} KB] in ${timeSpent} ms`
+ );
+
+ return {
+ fileName: outputFilePath,
+ source: optimizedSvg,
+ };
+ } else {
+ // log(`Error optimizing SVG ${filePath}`, result.error as Error);
+ throw new Error(`Failed to optimize SVG: ${result.error}`);
+ }
+ }
+
+ private getFileInfo(filePath: string): { base: string; ext: string } {
+ const base = basename(filePath);
+ const ext = extname(filePath).slice(1); // 去掉扩展名前的点
+ return { base, ext };
+ }
+
+ private getEncodeOptions(targetType: string): Record | null {
+ switch (targetType) {
+ case 'avif':
+ return this.options.compress.avif;
+ case 'webp':
+ return this.options.compress.webp;
+ case 'mozjpeg':
+ return this.options.compress.mozjpeg;
+ case 'oxipng':
+ return this.options.compress.oxipng;
+ default:
+ return null;
+ }
+ }
+
+ async close() {
+ await this.imagePool.close();
+ }
+}
\ No newline at end of file
diff --git a/src/core/utils.ts b/src/core/utils.ts
index 001e66d..b59b887 100644
--- a/src/core/utils.ts
+++ b/src/core/utils.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-await-in-loop */
import { partial } from 'filesize';
import { createHash } from 'node:crypto';
import fs, { constants, promises as fsPromise } from 'fs';
@@ -83,7 +84,6 @@ export function isTurnImageType(options) {
return Boolean(hasConversion && hasType && isReallyType);
}
-// 最后一个符号后的内容
export function lastSymbol(string: string, symbol: string) {
const arr = string.split(symbol);
return arr[arr.length - 1];
@@ -114,7 +114,7 @@ export function generateImageID(filename: string, format: string = 'jpeg') {
export function transformFileName(file) {
return file.substring(0, file.lastIndexOf('.') + 1);
}
-// 判断后缀名
+
export function filterExtension(name: string, ext: string): boolean {
const reg = new RegExp(`.${ext}`);
return Boolean(name.match(reg));
@@ -146,17 +146,9 @@ export function filterImageModule(filePath: string) {
}
// filter public dir path with excludeDir
-export function filterDirPath(path, dir, excludeDir?) {
- // let regex;
-
- // if (excludeDir) {
- // regex = new RegExp(`${dir}/(?!${excludeDir}/)[\\w.-]+`);
- // } else {
- // regex = new RegExp(`${dir}/[\\w.-]+`);
- // }
-
+export function filterDirPath(pathe: string, dir: any) {
// return regex.test(path);
- if (path.startsWith(dir)) {
+ if (pathe.startsWith(dir)) {
return true;
}
return false;
@@ -196,7 +188,7 @@ export async function readImageFiles(dir) {
const images = [];
try {
- const files: any = await fs.promises.readdir(dir);
+ const files: string[] = await fs.promises.readdir(dir);
for (let file of files) {
const path2 = `${dir}/${file}`;
@@ -213,3 +205,35 @@ export async function readImageFiles(dir) {
return images;
}
+
+export function isSvgFile(filename) {
+ return extname(filename) === '.svg';
+}
+
+export function getBundleImageSrc(filename: string, options: any) {
+ const currentType =
+ options.conversion.find(
+ (item) => item.from === extname(filename).slice(1),
+ ) ?? extname(filename).slice(1);
+ const id = generateImageID(
+ filename,
+ currentType.to ?? extname(filename).slice(1),
+ );
+ return id;
+}
+
+export function updateCssReferences(
+ cssContent: string,
+ fileNameMap: Map,
+): string {
+ try {
+ return Array.from(fileNameMap).reduce(
+ (updatedCssContent, [oldFileName, newFileName]) =>
+ updatedCssContent.replace(new RegExp(oldFileName, 'g'), newFileName),
+ cssContent,
+ );
+ } catch (error) {
+ console.error('[unplugin-imagemin] Error processing CSS:', error);
+ return cssContent;
+ }
+}