开篇
本篇文章是对于工具小站-图片转黑白功能的一个总结。该功能相对于之前实现的图片随机色功能来说简单了很多,所以文章的篇幅不会特别长。仅仅对其中重要的几个功能点,做一些讲解。
效果展示
技术栈
前端框架:Vue 3 + Element Plus
状态管理:组件内部状态
图片处理:Web Worker + Canvas API
文件处理:File API、Blob API
打包下载:JSZip
技术亮点
- 双模式处理:单张图片和批量图片转换
- 专业调节:可调节黑白、亮度、对比度
- 历史记录功能:可支持撤销和重做
- 性能优化设计:查找表优化、Web Worker异步处理
- 用户体验优化:图片拖曳上传、转换效果实时预览、支持Ctrl + Z快捷键撤销
核心功能实现
图片转黑白功能实现
- 通用函数
// 图片处理方法
const processImage = (image) => {
return new Promise((resolve, reject) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
worker.postMessage({
imageData,
threshold: settings.value.threshold,
brightness: settings.value.brightness,
contrast: settings.value.contrast,
});
resolve();
};
img.onerror = reject;
img.src = image.originalPreview;
});
};
这个函数是图片处理中的通用函数,目的是将图片在canvas上重新绘制,并获取到图片数据。在完成上面的操作后,将图片数据及调节的配置项一并传递给worker文件,以便后续处理。
- 图像转黑白相关函数
// 处理图片转黑白的 Worker
// 预计算亮度和对比度查找表
const brightnessContrastLookupTable = new Uint8Array(256);
// 初始化查找表
function initLookupTable(brightness, contrast) {
const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
for (let i = 0; i < 256; i++) {
let value = i;
// 应用亮度
value += brightness;
// 应用对比度
value = factor * (value - 128) + 128;
// 限制在有效范围内
brightnessContrastLookupTable[i] = Math.max(0, Math.min(255, value)) | 0;
}
}
// 使用查找表优化的黑白处理
function processBlackAndWhite(imageData, threshold, brightness, contrast) {
// 初始化亮度和对比度查找表
initLookupTable(brightness, contrast);
const data = imageData.data;
const len = data.length;
// 使用 Uint32Array 视图加速访问
const pixels = new Uint32Array(data.buffer);
const pixelCount = len >> 2;
for (let i = 0; i < pixelCount; i++) {
const offset = i << 2;
// 获取RGB值并应用亮度和对比度
const r = brightnessContrastLookupTable[data[offset]];
const g = brightnessContrastLookupTable[data[offset + 1]];
const b = brightnessContrastLookupTable[data[offset + 2]];
// 计算灰度值 (使用加权平均法)
const gray = (r * 0.299 + g * 0.587 + b * 0.114) | 0;
// 根据阈值决定黑白
const value = gray >= threshold ? 255 : 0;
// 一次性设置RGB值(保持Alpha不变)
pixels[i] = (data[offset + 3] << 24) | // Alpha
(value << 16) | // Red
(value << 8) | // Green
value; // Blue
}
return imageData;
}
// 接收主线程消息
self.onmessage = function(e) {
const { imageData, threshold, brightness, contrast } = e.data;
// 处理图片
const processedData = processBlackAndWhite(
imageData,
threshold,
brightness,
contrast
);
// 返回处理后的数据
self.postMessage(processedData);
}
上面是worker文件的所有逻辑。主要进行了一下的操作:
1.创建了一个用于存储预计算的亮度和对比度值的查找表,便于后面直接使用;
2.使用黑白处理函数processBlackAndWhite()来讲图片转换成黑白色,这里使用了刚刚的查找表,有效的优化了性能;
3.将处理好的图像数据返回给主线程;
撤销功能
这里可以理解为我设置了一个历史栈history,每次撤销都会从栈中弹出最新加入的状态数据,而每次更改状态,都会将该状态数据加入到栈中。
worker.onmessage = (e) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = e.data.width;
canvas.height = e.data.height;
ctx.putImageData(e.data, 0, 0);
const imageData = canvas.toDataURL("image/png");
if (activeTab.value === "single" && singleImage.value) {
singleImage.value.processedPreview = imageData;
addToHistory(imageData);
} else {
const image = images.value.find((img) => img.processing);
if (image) {
image.processedPreview = imageData;
image.processing = false;
}
}
processing.value = false;
ElMessage.success("转换完成");
};
const undo = () => {
if (!canUndo.value) return;
historyIndex.value--;
if (singleImage.value) {
if (historyIndex.value < 0) {
singleImage.value.processedPreview = "";
} else {
singleImage.value.processedPreview = history.value[historyIndex.value];
}
singleImage.value = { ...singleImage.value };
}
};
重做功能
重做功能比较简单粗暴,恢复初始化状态即可。
const redo = () => {
if (!canRedo.value) return;
historyIndex.value++;
if (singleImage.value) {
singleImage.value.processedPreview = history.value[historyIndex.value];
singleImage.value = { ...singleImage.value };
}
};
完整代码
BlackOrWhite.vue文件
<template>
<div class="app-container">
<header class="app-header">
<h1>图片转黑白</h1>
<p class="subtitle">专业的图片黑白处理工具,支持阈值调节</p>
</header>
<main class="main-content">
<el-tabs v-model="activeTab" class="image-tabs">
<!-- 单张处理 -->
<el-tab-pane label="单张处理" name="single">
<div class="upload-section" v-if="!singleImage">
<el-upload
class="upload-drop-zone"
drag
:auto-upload="false"
accept="image/*"
:show-file-list="false"
:multiple="false"
@change="handleSingleFileChange"
>
<el-icon class="upload-icon"><upload-filled /></el-icon>
<div class="upload-text">
<h3>将图片拖到此处,或点击上传</h3>
<p>支持 PNG、JPG、WebP 等常见格式</p>
</div>
</el-upload>
</div>
<div v-else class="process-section single-mode">
<div class="image-comparison">
<!-- 原图预览 -->
<div class="image-preview original">
<h3>原图</h3>
<div class="image-container">
<img
:src="singleImage.originalPreview"
:alt="singleImage.file.name"
/>
</div>
</div>
<!-- 黑白效果预览 -->
<div class="image-preview processed">
<h3>黑白效果</h3>
<div class="image-container">
<img
v-if="singleImage.processedPreview"
:src="singleImage.processedPreview"
:alt="singleImage.file.name + '(黑白)'"
/>
<div v-else class="placeholder">
<el-icon><picture-rounded /></el-icon>
<span>待处理</span>
</div>
</div>
</div>
</div>
</div>
</el-tab-pane>
<!-- 批量处理 -->
<el-tab-pane label="批量处理" name="batch">
<div class="upload-section" v-if="!images.length">
<el-upload
class="upload-drop-zone"
drag
:auto-upload="false"
accept="image/*"
:show-file-list="false"
:multiple="true"
@change="handleBatchFileChange"
>
<el-icon class="upload-icon"><upload-filled /></el-icon>
<div class="upload-text">
<h3>将图片拖到此处,或点击上传</h3>
<p>支持批量上传多张图片,PNG、JPG、WebP 等常见格式</p>
</div>
</el-upload>
</div>
<div v-else class="process-section batch-mode">