各位开发者朋友们!昨天为大家介绍了 PDF 批注的撤回与反撤回功能,今天咱们接着深入剖析本次更新的另外几个实用特性 ------ 图片批注、批注清空以及批注记录的实现细节。相较于撤回与反撤回功能,今天这些内容相对容易理解。废话不多说,让我们直接开始技术揭秘!
图片批注:轻松添加可视化元素
图片批注的添加方式和文字批注类似,都是依据当前页码,借助 fabric.Image
将图片添加到页面的左上或左下位置,用户可以自由拖动图片进行批注。
typescript
const addImage = (event: { page: string | number, canvas: any, canvasRefs: any }, imageUrl: string) => {
if (!event || !event.canvas || !event.canvasRefs) return
fabricCanvas = event.canvas
const pointer = getPointer(event.canvasRefs);
// 使用 fabric.Image.fromURL 加载图片
fabric.Image.fromURL(imageUrl, (img: any) => {
// 设置图片的位置
img.set({
left: pointer.x,
top: pointer.y,
id: `image_${new Date().getTime()}`,
});
// 设置图片的缩放比例
img.scale(0.5);
// 将图片添加到画布
event.canvas.add(img);
// 渲染画布
event.canvas.renderAll();
saveState({ ...event, type: 'add' })
});
}
这里要提醒一下,我在项目里用的是本地图片,保存后无法回显。如果大家在项目中使用,建议先把图片上传到服务器,或者直接转成 base64 格式(不过我还没测试过这种方式),如果是下载的话就不需要考虑那么多了。
功能预览

批注清空:一键清理画布
批注清空操作非常简单,只需要遍历所有的 canvas
,然后调用 clear
方法即可。
scss
const clearActiveObjectAll = (canvasObj: any) => {
for (let key in canvasObj) {
canvasObj[key].clear()
canvasObj[key].renderAll();
}
}
功能预览

批注记录:精准管理批注信息
批注记录功能是在昨天提到的记录批注信息的 saveState
方法基础上实现的。无论 canvas
进行何种操作,都要更新 queueStack
记录。
ini
export const useStack = () => {
const undoStack: any = [];
const redoStack: any = [];
const queueStack = ref<any>(new Map()); // 存储当前操作的画布
let isUndo = false; // 是否撤回
// 记录当前状态
const saveState = (event: any) => {
if (!isUndo) {
const state = event.canvas.toJSON();
undoStack.push({
state: state,
canvas: event.canvas,
page: event.page,
type: event.type,
});
// 清空反撤回栈
redoStack.length = 0;
storeQueue(event.page, event.canvas); // 存储当前操作的画布
}
}
// 撤回操作
const undo = () => {
if (undoStack.length > 0) {
// 取出上一个状态
isUndo = true; // 设置为撤回状态
const previousState = undoStack.pop();
redoStack.push(previousState); // 保存当前状态到反撤回栈
if (previousState.type === 'modify' || previousState.type === 'remove') {
const stateToRestore = undoStack[undoStack.length - 1]; // 获取上一个状态
stateToRestore.canvas.loadFromJSON(stateToRestore.state, () => {
stateToRestore.canvas.renderAll();
isUndo = false; // 设置为撤回状态
});
storeQueue(previousState.page, previousState.canvas); // 存储当前操作的画布
} else if (previousState?.state.objects.length === 1) {
// 如果只有一个对象,直接清空画布
previousState.canvas.clear();
storeQueue(previousState.page, previousState.canvas); // 存储当前操作的画布
isUndo = false; // 设置为撤回状态
} else {
const objects = previousState.state.objects.slice(0, previousState.state.objects.length - 1); // 删除最后一个对象
const stateToRestore = previousState
previousState && previousState.canvas.loadFromJSON({ ...stateToRestore.state, objects }, () => {
previousState.canvas.renderAll();
isUndo = false; // 设置为撤回状态
});
storeQueue(previousState.page, previousState.canvas); // 存储当前操作的画布
}
}
}
// 反撤回操作
const redo = () => {
if (redoStack.length > 0) {
// 取出反撤回栈中的状态
isUndo = true; // 设置为撤回状态
const stateToRestore = redoStack.pop();
// 保存当前状态到撤回栈
undoStack.push(stateToRestore);
stateToRestore.canvas.loadFromJSON(stateToRestore.state, () => {
stateToRestore.canvas.renderAll();
isUndo = false; // 设置为撤回状态
});
storeQueue(stateToRestore.page, stateToRestore.canvas); // 存储当前操作的画布
}
}
const storeQueue = (page: number, canvas: any) => {
const dataArr = canvas.getObjects()
if (dataArr.length > 0) {
queueStack.value.set(page + 1, canvas.getObjects()); // 存储当前操作的画布
} else {
queueStack.value.delete(page + 1); // 删除当前操作的画布
}
}
return {
saveState,
undo,
redo,
queueStack: queueStack,
}
}
这里我使用了 Map
数据格式来存储批注记录,因为 Map
在进行属性的增删改查操作时,比 Object
的开销要小很多。
在 storeQueue
方法中,还需要判断当前画布上是否还有批注信息,如果没有,就需要清空该画布的记录。
对于批注记录功能,我还进行了扩展,实现了点击记录即可回滚到当前批注所在页并选中该批注,同时也可以直接在批注记录中删除任何一条批注。前面的文章已经介绍过跳转页码的实现,这里就不再赘述,下面给大家展示一下选中批注和删除批注的实现代码。
ini
const setActiveObject = (canvas: any, targetId: string, type = "setActive") => {
const objects = canvas.getObjects();
for (let i = 0; i < objects.length; i++) {
const object = objects[i];
if (object.id === targetId) {
// 将具有指定 ID 的元素设置为活动对象
type === "setActive" ? canvas.setActiveObject(object) : canvas.remove(object);
// 重新渲染画布
canvas.renderAll();
break;
}
}
}
这段代码会根据传入的 id
和类型,判断当前 canvas
中的批注信息,从而实现选中或删除操作。
功能预览

总结与展望
本次新增的撤回与反撤回功能,让 PDF 批注操作更加容错和灵活,目前已经形成了 "批注编辑→保存→回显→撤回 / 反撤回" 的完整闭环。此次更新的图片批注、批注清空和批注记录功能,为 PDF 批注操作增添了更多元素。
后续,我们会对现有功能进行优化,比如增加画线选项,像箭头、曲线等。虽然目前缩放功能还没有特别好的实现方案,但请大家放心,我一定会在后续完成这个功能。
对于源码感兴趣的小伙伴,欢迎前往 项目仓库 或者 gitee 仓库 体验完整功能。同时,也期待大家在评论区分享对功能扩展的想法,你的建议很可能会成为下一个版本的核心特性! 🚀
本次新增的图片批注、批注清空、批注记录,让 PDF 批注操作增加了更多的元素,后期的话可能就是对于现有的功能进行一些优化,比如增加画线选项,如箭头、曲线等等,至于缩放,目前还没有特别好的实现方案,不过不用担心,小编后面也一定会做出来。
对于源码感兴趣的小伙伴,欢迎前往 项目仓库 或者 gitee仓库 体验完整功能,也期待你在评论区分享对功能扩展的想法 ------ 你的建议可能成为下一个版本的核心特性!