Vue3 在线 PDF 编辑 1.0 预览图生成逻辑

大家好!今天给大家拆解下 基于 Vue3 的在线 PDF 编辑 1.0 项目诞生记 里预览图模块的核心代码。不绕弯子,直接带大家 "扒一扒" 这些藏着小惊喜的技术实现!

基于 pdfjs-dist 与 pdf-lib 展示 PDF

在技术选型上,试过 N 种方案后,最终敲定了 pdfjs-dist 与 pdf-lib 组合。先看获取 PDF 地址的核心代码:

ini 复制代码
/**
 * @description: 获取 pdfUrl
 * @param {string} url
 * @return {*}
 */
const getPdfUrlFunc = async (url: string) => {
    const existingPdfBytes = await fetch(url).then((res) => res.arrayBuffer());
    pdfDoc = await PDFDocument.load(existingPdfBytes);
    const pdfBytes = await pdfDoc.save();
    const blob = new Blob([pdfBytes], { type: "application/pdf" });
    pdfUrl.value = URL.createObjectURL(blob);
}

这里先把 PDF 转成 Buffer,再捣鼓成 blob 喂给 pdfjsLib 生成预览。我用本地文件测试的,在线 PDF 直接用 pdfjsLib 应该也能搞定,感兴趣的小伙伴可以当 "技术探险家" 试试!

再看渲染 PDF 的关键逻辑:

ini 复制代码
const rederPdfFunc = async (
        scale: number,
        canvasRefs: any,
        istThumbnail: boolean = false,
        startLine: Function,
        drawLine: Function,
        stopDrwa: Function,
        addText: Function
    ) => {
        if (!pdfUrl.value) return;
        const loadingTask = pdfjsLib.getDocument(pdfUrl.value);
        const pdf = await loadingTask.promise;
        const thumbnailArr: string[] = []; // 缩略图
        const thumbnailInfoArr: { imgUrl: string; pageIndex: number }[] = [];
        pagesCount.value = pdf.numPages;
        for (let i = 1; i <= pagesCount.value; i++) {
            const page = await pdf.getPage(i);
            const viewport = page.getViewport({ scale }); // 设置合适的缩放比例
            const canvas = canvasRefs["canvas" + (i - 1)]; // 获取对应的canvas元素
            if (!canvas) break;
            const context = canvas.getContext("2d");
            canvas.height = viewport.height;
            canvas.width = viewport.width;
            const fabricCanvas = new fabric.Canvas(`annotation-canvas_${i - 1}`, {
                width: viewport.width,
                height: viewport.height,
                isDrawingMode: false,
                // backgroundColor: 'transparent' 
            })
            fabricCanvas.selectionColor = 'transparent'
            fabricCanvas.selectionBorderColor = 'transparent'
            // fabricCanvas.skipTargetFind = true // 禁止选中
            fabricCanvas.on('mouse:down', startLine.bind(fabricCanvas, {
                page: i - 1,
                canvas: fabricCanvas,
            })) // 鼠标在画布上按下
            fabricCanvas.on('mouse:move', drawLine.bind(fabricCanvas, {
                page: i - 1,
                canvas: fabricCanvas,
            })) // 鼠标在画布上移动
            fabricCanvas.on('mouse:up', stopDrwa.bind(fabricCanvas, {
                page: i - 1,
                canvas: fabricCanvas,
            })) // 鼠标在画布上移动
            // fabricCanvas.on('mouse:dblclick', addText.bind(fabricCanvas, {
            //     page: i - 1,
            //     canvas: fabricCanvas,
            // }))
            fabricCanvasObj.value[`annotation-canvas_${i - 1}`] = fabricCanvas
            const wrapper = canvas.parentElement;
            wrapper.style.width = `${viewport.width}px`;
            wrapper.style.height = `${viewport.height}px`;
            const renderContext = {
                canvasContext: context,
                viewport: viewport,
            };
            await page.render(renderContext).promise;
            if (istThumbnail) {
                const imageUrl = canvas.toDataURL("image/png");
                thumbnailArr.push(imageUrl);
                thumbnailInfoArr.push({
                    imgUrl: imageUrl,
                    pageIndex: i,
                });
            }
        }
        if (istThumbnail) {
            thumbnailObj.value = {
                thumbnail: thumbnailArr,
                thumbnailInfo: thumbnailInfoArr,
            }
        }
    }

主体预览用 canvas 绘制,左侧预览默认生成图片。不过从性能角度 "唠一唠",左侧换成 canvas 更香哦!

左侧预览图选中,主题预览图跳转

想实现点击左侧跳转到对应页面?这段代码就是 "幕后推手":

ini 复制代码
const setPageFunc = (pageRefs: HTMLElement | null, canvasRefs: Record<string, HTMLElement>, currenPage: number) => {
        if (!pageRefs || !canvasRefs[`canvas${currenPage - 1}`]) return;
        const targetScrollTop = canvasRefs[`canvas${currenPage - 1}`].offsetHeight * (currenPage - 1);
        const startScrollTop = pageRefs.scrollTop;
        const duration = 300; // 动画持续时间,单位毫秒
        const startTime = performance.now();
        const animateScroll = (currentTime: number) => {
            const elapsedTime = currentTime - startTime;
            if (elapsedTime < duration) {
                const progress = elapsedTime / duration;
                pageRefs.scrollTop = startScrollTop + (targetScrollTop - startScrollTop) * progress;
                requestAnimationFrame(animateScroll);
            } else {
                pageRefs.scrollTop = targetScrollTop;
            }
        };
        requestAnimationFrame(animateScroll);
    };

通过计算 canvas 高度,配合丝滑的动画效果,点击左侧预览图,主体页面就能 "嗖" 地一下精准跳转!

主体预览图滚动,选中左侧预览图

反过来,主体滚动时左侧自动选中的功能,靠 IntersectionObserver 这位 "小帮手":

typescript 复制代码
export const useMountObserve = (pageRefs: HTMLElement,
    canvasRefs: any, pagesCount: number,
    callback: (arg: string | number) => void) => {
    let canvasIndex: string | number = 0
    let observer: IntersectionObserver | null = null; // 当前可视窗口最大得canvas页码
    /**
     * @description: 初始化方法
     * @return {*}
     */
    const initFunc = () => {
        observer = new IntersectionObserver(handleIntersection, {
            root: pageRefs,
            threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],
        });
        for (let i = 0; i < pagesCount; i++) {
            const canvas = canvasRefs[`canvas${i}`];
            if (canvas) {
                observer.observe(canvas);
            }
        }
    }
    /**
     * @description: 检测当前可视窗口占比最大得canvas
     * @param {IntersectionObserverEntry} entries
     * @return {*}
     */
    const handleIntersection = (entries: IntersectionObserverEntry[]) => {
        entries.forEach((entry) => {
            const ratio = entry.intersectionRatio;
            if (ratio > 0.5) {
                canvasIndex = entry.target.getAttribute("data-index") || 0;
            }
        });
        callback((+canvasIndex + 1))
    };
    initFunc()
    onUnmounted(() => {
        if (observer) {
            observer.disconnect();
        }
    });
}

它实时监测每个 canvas 的可见比例,一旦超过 50%,就立刻通知左侧预览图 "对号入座"!

结语

以上就是预览图生成、翻页交互的核心源码解析啦!后续还会解锁更多功能的技术细节。想深入研究的同学,欢迎到 github.com/xknk/costom... 拉取源码 "把玩"!也期待和各位技术小伙伴交流,一起把这个项目打磨得更酷炫!

相关推荐
2501_9153738819 分钟前
Vue 3零基础入门:从环境搭建到第一个组件
前端·javascript·vue.js
沙振宇3 小时前
【Web】使用Vue3开发鸿蒙的HelloWorld!
前端·华为·harmonyos
运维@小兵4 小时前
vue开发用户注册功能
前端·javascript·vue.js
蓝婷儿4 小时前
前端面试每日三题 - Day 30
前端·面试·职场和发展
oMMh4 小时前
使用C# ASP.NET创建一个可以由服务端推送信息至客户端的WEB应用(2)
前端·c#·asp.net
一口一个橘子4 小时前
[ctfshow web入门] web69
前端·web安全·网络安全
读心悦5 小时前
CSS:盒子阴影与渐变完全解析:从基础语法到创意应用
前端·css
香蕉可乐荷包蛋6 小时前
vue数据可视化开发echarts等组件、插件的使用及建议-浅看一下就行
vue.js·信息可视化·echarts
老马啸西风6 小时前
sensitive-word-admin v2.0.0 全新 ui 版本发布!vue+前后端分离
vue.js·ui·ai·nlp·github·word
湛海不过深蓝6 小时前
【ts】defineProps数组的类型声明
前端·javascript·vue.js