项目功能
- 可批量上传、拖拽上传
- 暂时只支持文字水印,图片水印开发中
- 支持水印的不同字体、颜色、字号、旋转
- 支持多个文字水印
- 支持平铺水印
- 右键复制或下载添加了水印的图片
- 可批量导出结果到指定目录
web端
项目地址可访问这个查看,www.songweisuo.com/water/
不过由于受浏览器限制,没法访问本地文件系统,底部的输出目录和导出是无法使用的,无法批量操作,只能对每一张图片使用右键下载图片
创建web项目
使用vite创建基于vue3的项目,官网cn.vitejs.dev/guide/
引入fabric
- 首先创建fabric的canvas对象
- 根据当前图片大小来设置canvas画布大小
// 创建fabric的canvas对象
const canvas = new fabric.Canvas('canvas', {
fireRightClick: true, // 启用右键,button的数字为3
stopContextMenu: true, // 禁止默认右键菜单
});
// 设置成全局对象,这玩意放在pinia里有问题
global.canvas = canvas;
// 根据当前图片大小来设置canvas画布的大小
watch(currentPic, () => {
if(currentPic.value?.base64) {
fabric.Image.fromURL(currentPic.value.base64, (img) => {
// 设置画布和图片一样大
const [imgWidth, imgHeight] = [img.width!, img.height!];
const [canvasWidth, canvasHeight] = getContainerSize(imgWidth, imgHeight);
canvas.setWidth(canvasWidth);
canvas.setHeight(canvasHeight);
img.set({
scaleX: canvasWidth / imgWidth,
scaleY: canvasHeight / imgHeight,
});
// 设置背景
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
canvas.renderAll();
});
}
});
创建水印对象
这里要在代码中创建两个水印对象,一个是fabric的fabric.Textbox对象,另一个是textProps对象,textProps是reactive对象,控制着右侧水印属性的变化
然后再通过监听fabric.Textbox的旋转和编辑事件和watchtextProps的各种属性来关联fabric.Textbox和textProps,相当于双向绑定了
在图片上操作水印,右侧属性面板会跟着变化,变化右侧属性面板,图片上的水印也会跟着变化
export const addWater = () => {
const canvas = global.canvas;
const { waterList } = waterStore;
const textProps = waterList[waterList.length - 1];
// 获取上一个添加的canvas元素
let lastObject;
if (lastTextProps) {
const allObjects = canvas.getObjects();
lastObject = allObjects.find(obj => obj.name === lastTextProps.name);
}
// 计算当前新增的canvas元素的位置
const { left, top } = getTopLeft(lastObject);
const text = new fabric.Textbox(textProps.text, {
name: newName,
top: top,
left: left,
angle: 0,
fontFamily: textProps.fontFamily,
fontSize: textProps.fontSize,
fill: textProps.color, // 填充色:橙色
fontWeight: textProps.bold ? 600 : 400,
fontStyle: textProps.italic ? 'italic' : '',
lineHeight: textProps.lineHeight,
opacity: textProps.opacity / 100,
originX: 'center',
originY: 'center',
transparentCorners: false, // 选中时,角是被填充了。true 空心;false 实心
borderColor: '#16f1fc', // 选中时,边框颜色:天蓝
borderScaleFactor: 1, // 选中时,边的粗细:5px
borderDashArray: [4, 2], // 选中时,虚线边的规则
cornerColor: '#a1de93', // 选中时,角的颜色是 青色
cornerStyle: 'circle', // 选中时,叫的属性。默认rect 矩形;circle 圆形
cornerSize: 10, // 选中时,角的大小为20
cornerDashArray: [10, 2, 6], // 选中时,虚线角的规则
borderOpacityWhenMoving: 0.6, // 当对象活动和移动时,对象控制边界的不透明度
});
text.lockScalingX = true;
text.lockScalingY = true;
text.on('rotating', e => {
console.log(text.name);
textProps.rotate = parseInt((e as any).transform.target.angle);
});
text.on('modified', e => {
if (textProps.position !== PositionType.duplicate) {
textProps.text = text.text || '';
}
});
canvas.add(text);
canvas.setActiveObject(text);
watch(() => textProps.text, () => {
if (textProps.position === PositionType.duplicate) {
renderDuplicate(textProps, text);
} else {
text.text = textProps.text;
}
canvas.renderAll();
});
watch(() => textProps.fontFamily, () => {
text.fontFamily = textProps.fontFamily;
canvas.renderAll();
});
watch(() => textProps.fontSize, () => {
text.fontSize = textProps.fontSize;
canvas.renderAll();
});
watch(() => textProps.color, () => {
// fill修改不生效,但改动fontSize加1减1后就生效了
text.fontSize = textProps.fontSize + 1;
text.fontSize = textProps.fontSize - 1;
text.fill = textProps.color;
canvas.renderAll();
});
watch(() => textProps.bold, () => {
text.fontWeight = textProps.bold ? 600 : 400;
canvas.renderAll();
});
watch(() => textProps.italic, () => {
text.fontStyle = textProps.italic ? 'italic' : '';
canvas.renderAll();
});
watch(() => textProps.rotate, () => {
text.angle = textProps.rotate;
canvas.renderAll();
});
watch(() => textProps.opacity, () => {
text.opacity = textProps.opacity / 100;
canvas.renderAll();
});
watch(() => textProps.lineHeight, () => {
text.lineHeight = textProps.lineHeight;
canvas.renderAll();
});
watch(() => textProps.position, () => {
// 一些操作
});
};
需要注意的是,mac端和windows端系统自带的字体不太相同,这里通过font-list这个npm包来事先获取系统自带的字体,筛选其中几个供选择
右键功能
- 右键点击图片有复制图片和下载图片功能
- 右键点击水印出上述功能外,还有有复制水印和删除水印功能
onMounted(() => {
canvas.on('mouse:up', opt => {
if (opt.button === 3) {
// 当前鼠标位置
let pointX = opt.pointer!.x;
let pointY = opt.pointer!.y;
const absoluteStyle = `
position: absolute;
left: ${pointX}px;
top: ${pointY}px;
`;
// 点击水印
if (opt.target) {
// console.log(opt.target);
rightClickObj = opt.target;
objRightMenuData.show = true;
objRightMenuData.style = absoluteStyle;
}
// 点击canvas
else {
rightMenuData.show = true;
rightMenuData.style = absoluteStyle;
}
} else {
rightClickObj = undefined;
objRightMenuData.show = false;
rightMenuData.show = false;
}
});
});
导出
导出选择的方案是图片列表中挨个切换图片,然后调用electron端提供的下载图片接口,保存完成后打开目录
async function save() {
const canvas = global.canvas;
canvas.discardActiveObject();
canvas.renderAll();
for (let i = 0; i < imgList.length; i++) {
const imgBase64 = imgList[i].base64;
fabric.Image.fromURL(imgBase64, (img) => {
const [imgWidth, imgHeight] = [img.width!, img.height!];
const [canvasWidth, canvasHeight] = getContainerSize(imgWidth, imgHeight);
canvas.setWidth(canvasWidth);
canvas.setHeight(canvasHeight);
img.set({
scaleX: canvasWidth / imgWidth,
scaleY: canvasHeight / imgHeight,
});
// 设置背景
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
canvas.renderAll();
});
currentPic.value = imgList[i];
// fabric的canvas.toDataURL会压缩图片质量
const htmlCanvas = document.querySelector('canvas');
const canvasBase64 = htmlCanvas!.toDataURL('image/jpeg', 1);
await window.electronAPI.saveFile(
outputPath.value + '/watermark-' + imgList[i].name,
canvasBase64,
);
}
ElMessage.success('导出成功,已打开导出目录!');
window.electronAPI.openDirectory(outputPath.value + '/watermark-' + imgList[0].name);
}
Electron端
electron端添加了3个方法供web端调用
选择保存目录
ipcMain.handle('openDirectoryDialog', function (event, defaultPath) {
return dialog.showOpenDialog({
defaultPath,
properties: ['openDirectory'],
title: '请选择保存目录',
buttonLabel: '选择'
}).then(result => {
if (!result.canceled) {
return result.filePaths[0];
}
});
});
保存图片
ipcMain.handle('saveFile', function (event, filePath, data) {
let base64Data = data.replace(/^data:image/\w+;base64,/, '');
let dataBuffer = new Buffer(base64Data, 'base64');
fs.writeFile(filePath, dataBuffer, function (err) {
return Promise.reject(err);
});
return Promise.resolve();
});
保存完图片后打开指定目录
ipcMain.on('openDirectory', function (event, directoryPath) {
shell.showItemInFolder(directoryPath);
});
项目地址
欢迎讨论,下载使用