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... 拉取源码 "把玩"!也期待和各位技术小伙伴交流,一起把这个项目打磨得更酷炫!

相关推荐
掘金酱2 分钟前
😊 酱酱宝的推荐:做任务赢积分“拿”华为MatePad Air、雷蛇机械键盘、 热门APP会员卡...
前端·后端·trae
热爱编程的小曾13 分钟前
sqli-labs靶场 less 11
前端·css·less
丁总学Java19 分钟前
wget(World Wide Web Tool) 教程:Mac ARM 架构下安装与使用指南!!!
前端·arm开发·macos
总之就是非常可爱24 分钟前
🚀 使用 ReadableStream 优雅地处理 SSE(Server-Sent Events)
前端·javascript·后端
shoa_top35 分钟前
Cookie、sessionStorage、localStorage、IndexedDB介绍
前端
鸿蒙场景化示例代码技术工程师40 分钟前
实现文本场景化鸿蒙示例代码
前端
ᖰ・◡・ᖳ42 分钟前
Web APIs阶段
开发语言·前端·javascript·学习
stoneSkySpace1 小时前
算法——BFS
前端·javascript·算法
H5开发新纪元1 小时前
基于 Vue3 + TypeScript + Vite 的现代化移动端应用架构实践
前端·javascript
云原生应用市场1 小时前
一键私有化部署Dify,轻松搞定 AI 智能客服机器人
运维·前端·后端