h5单页预览PDF文件模糊问题解决

这是h5单页预览PDF文件的补充

https://blog.csdn.net/motoudi/article/details/147362883?spm=1011.2415.3001.10575&sharefrom=mp_manage_link

目的:

在小程序进行文件预览,使用的是H5单页写的预览页面。

之所以采用这种方式是因为需要在预览页面上放置其他功能按钮。

该单页预览能实现基本的拖拽、缩放功能。

问题:

在预览PDF文件的时候是使用的pdf.min.js和pdf.worker.min.js,将PDF文件使用canvas绘制出来,转为普通图片进行预览。但是绘制出来的图片文字看不清楚、失真。

解决办法:

在绘制的时候按照比例将其放大然后再绘制,这样绘制出一副放大的图片,在显示的时候再将其缩放回原来的比例尺寸。这样肉眼看到的PDF文件中的文字就能清晰。

异步加载PDF文件

复制代码
pdfjsLib.getDocument(url).promise.then(function(pdfDoc_){})

分页渲染每一页的PDF页面

page.getViewport() 是 PDF.js 库中的一个重要方法,用于获取页面的**视口(viewport)**信息。

视口(Viewport)的作用:

视口定义了如何将PDF页面映射到canvas上的一个矩形区域,它包含了:

  • 尺寸信息:页面的宽度和高度

  • 缩放比例:页面的显示缩放级别

  • 变换矩阵:坐标系的转换信息

    pdfDoc.getPage(num).then(function(page) {
    // 在获取pdf窗口信息的时候,我们直接去获取放大后的pdf尺寸,按照这个尺寸绘制
    const viewport = page.getViewport({ scale: scale });
    }

绘制:

复制代码
// 创建容器div
                    const pageDiv = document.createElement('div');
                    pageDiv.className = 'pdf-page';
                    pageDiv.id = `page-${num}`;

                    // 移除旧的页面元素
                    const oldPage = document.getElementById(`page-${num}`);
                    if (oldPage) oldPage.remove();

                    // 创建canvas元素
                    const canvas = document.createElement('canvas');
                    const context = canvas.getContext('2d');

                    // 设置canvas尺寸
                    canvas.height = viewport.height;
                    canvas.width = viewport.width;

                    // 将canvas添加到页面容器
                    pageDiv.appendChild(canvas);

                    // 将页面容器添加到PDF容器
                    pdfContainer.appendChild(pageDiv);

                    // 渲染PDF页面到canvas上
                    const renderContext = {
                        canvasContext: context,
                        viewport: viewport
                    };

                    const renderTask = page.render(renderContext);

预览页面:

复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
    <title></title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            background-color: #f5f5f5;
            color: #333;
            height: 100vh;
        }
        .modal-content {
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
        }
        #pdf-container {
            width: 100%;
            flex-grow: 1;
        }
        #video-container {
            width: 100%;
            margin: auto;
            flex-grow: 1;
            display: flex;
            align-items: center;
            position: relative;
        }
        #image-container {
            width: 100%;
            min-height: 100vh;
            flex-grow: 1;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        #image-container img {
            width: 100%;
        }
        .loading {
            width: 100%;
            height: 100vh;
            background: rgba(0, 0, 0, 0.5);
            display: flex;
            justify-content: center;
            align-items: center;
            color: white;
        }

        /* 如果你想要一个旋转动画 */
        .loading::after {
            content: "";
            width: 30px;
            height: 30px;
            border: 3px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            border-top-color: white;
            animation: spin 1s linear infinite;
        }

        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        .error {
            color: white;
            text-align: center;
            margin-top: 50%;
            padding: 20px;
        }
        .video-js .vjs-control-bar{
            height: 5em !important;
            padding-bottom: 2em;
        }
        .video-js .vjs-big-play-button{
            top: 50% !important;
            left: 50% !important;
            transform: translate(-50%, -50%);
        }
        /* 视频暂停时显示大播放按钮 */
        .video-js.vjs-paused .vjs-big-play-button,
        .video-js.vjs-ended .vjs-big-play-button {
            display: flex !important;
            opacity: 1 !important;
            visibility: visible !important;
        }
        /* PDF 控制栏样式 */
        .pdf-controls {
            position: fixed;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0, 0, 0, 0.7);
            padding: 10px 15px;
            border-radius: 20px;
            display: flex;
            align-items: center;
            color: white;
            z-index: 100;
        }

        .pdf-controls button {
            background: none;
            border: none;
            color: white;
            font-size: 16px;
            padding: 5px 10px;
            cursor: pointer;
        }

        .pdf-controls span {
            margin: 0 10px;
        }

        .pdf-page {
            margin-bottom: 20px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            width: 100% !important;
            height: auto !important;
        }

        .pdf-page canvas {
            width: 100% !important;
            height: auto !important;
            display: block;
        }
        /* 添加点击区域覆盖层 */
        .video-click-area {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: calc(100% - 4em);
            z-index: 10;
        }
    </style>
    <!-- Video.js 视频播放器 -->
<!--    <link href="https://vjs.zencdn.net/7.20.3/video-js.css" rel="stylesheet">-->
<!--    <script src="https://vjs.zencdn.net/7.20.3/video.min.js"></script>-->
<!--    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.10.377/pdf.min.js"></script>-->
<!--    <script>-->
<!--        // 设置PDF.js worker路径-->
<!--        pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.10.377/pdf.worker.min.js';-->
<!--    </script>-->
    <!-- Video.js 视频播放器 -->
    <link href="./static/preview/video-js.css" rel="stylesheet">
    <script src="./static/preview/video.min.js"></script>
    <script src="./static/preview/pdf.min.js"></script>
    <script src="./static/dayjs.js"></script>
    <script src="./static/md5.js"></script>
    <script src="./static/wechat.env.js"></script>
    <script>
      // 设置PDF.js worker路径
      pdfjsLib.GlobalWorkerOptions.workerSrc = './static/preview/pdf.worker.min.js';
    </script>
</head>
<body>
<!-- 预览模态框 -->
<div id="preview-modal" class="modal">
    <div class="modal-content">
        <div id="image-container" style="display: none;">
            <img id="preview-image" src="" class="img-box">
        </div>
        <div id="video-container" style="display: none;">
            <video id="preview-video" class="video-js" controls playsinline style="width: 100%;height: 100vh;"></video>
            <div class="video-click-area" id="video-click-area" style="display: none;"></div>
        </div>
        <div id="pdf-container" style="display: none;">
        </div>
        <div id="loading" class="loading">
        </div>
        <div id="error" class="error" style="display: none;"></div>
        <!-- PDF 控制栏 -->
        <div id="pdf-controls" class="pdf-controls" style="display: none;">
            <button id="prev-page">上一页</button>
            <span id="page-num">1 / 1</span>
            <button id="next-page">下一页</button>
        </div>
    </div>
</div>

<script>
    // 获取URL参数
    function getQueryParam(name) {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get(name);
    }

    // 获取文件类型
    function getFileType(url) {
        if (!url) return null;

        // 提取文件扩展名
        const extension = url.split('.').pop().toLowerCase().split('?')[0];

        // 图片类型
        const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
        if (imageTypes.includes(extension)) return 'image';

        // 视频类型
        const videoTypes = ['mp4', 'webm', 'ogg', 'mov'];
        if (videoTypes.includes(extension)) return 'video';

        // PDF类型
        if (extension === 'pdf') return 'pdf';

        return null;
    }

    // 预览文件函数
    function previewFile(url) {
        const modal = document.getElementById('preview-modal');
        const loading = document.getElementById('loading');
        const errorDiv = document.getElementById('error');
        const imageContainer = document.getElementById('image-container');
        const videoContainer = document.getElementById('video-container');
        const pdfContainer = document.getElementById('pdf-container');
        const videoClickArea = document.getElementById('video-click-area');

        // 显示加载中和模态框
        loading.style.display = 'flex';
        errorDiv.style.display = 'none';
        imageContainer.style.display = 'none';
        videoContainer.style.display = 'none';
        pdfContainer.style.display = 'none';
        videoClickArea.style.display = 'none';
        modal.style.display = 'block';
        console.log('加载开始');


        // 获取文件类型
        const fileType = getFileType(url);

        if (!fileType) {
            loading.style.display = 'none';
            errorDiv.style.display = 'block';
            errorDiv.textContent = '不支持的文件类型或URL格式不正确';
            return;
        }

        // 根据文件类型处理
        if (fileType === 'image') {
            const img = document.getElementById('preview-image');
            img.alt = '图片预览';
            img.onload = function() {
                loading.style.display = 'none';
                imageContainer.style.display = 'flex';
            };
            img.onerror = function() {
                showError('图片加载失败');
            };
            img.src = url;
        }
        else if (fileType === 'video') {
            const metaViewport = document.querySelector('meta[name="viewport"]');
            if (metaViewport) {
                metaViewport.setAttribute(
                    'content',
                    'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
                );
            }

            videoContainer.style.display = 'flex';
            videoClickArea.style.display = 'block';

            // 初始化视频播放器
            const player = videojs('preview-video', {
                controls: true,
                autoplay: true,
                preload: 'auto',
                playsinline: true,
                poster: url + '?vframe/png/offset/1',
                sources: [{
                    src: url,
                    type: getVideoMimeType(url)
                }]
            });
            loading.style.display = 'none';

            player.on('error', function() {
                showError('视频加载失败');
            });
            // 添加点击事件处理
            videoClickArea.addEventListener('click', function() {
                if (player.paused()) {
                    player.play();
                } else {
                    player.pause();
                }
            });
            // 监听播放状态变化,确保暂停时显示大播放按钮
            player.on('pause', function() {
                const bigPlayButton = player.bigPlayButton;
                bigPlayButton.show();
            });

            player.on('play', function() {
                const bigPlayButton = player.bigPlayButton;
                bigPlayButton.hide();
            });
            // 存储播放器实例以便关闭时销毁
            modal.dataset.player = player;
        }
        else if (fileType === 'pdf') {
            pdfContainer.style.display = 'block';
            const previewContainer = document.getElementById('pdf-container');

            // 存储PDF相关变量
            let pdfDoc = null;
            let currentPage = 1;
            let pageRendering = false;
            let pageNumPending = null;
            const scale = 1.0;

            // 获取DOM元素
            const prevPageBtn = document.getElementById('prev-page');
            const nextPageBtn = document.getElementById('next-page');
            const pageNumSpan = document.getElementById('page-num');

            // 渲染PDF页面
            function renderPage(num) {
                pageRendering = true;

                // 使用promise获取页面
                pdfDoc.getPage(num).then(function(page) {
                    const viewport = page.getViewport({ scale: scale });

                    // 创建容器div
                    const pageDiv = document.createElement('div');
                    pageDiv.className = 'pdf-page';
                    pageDiv.id = `page-${num}`;

                    // 移除旧的页面元素
                    const oldPage = document.getElementById(`page-${num}`);
                    if (oldPage) oldPage.remove();

                    // 创建canvas元素
                    const canvas = document.createElement('canvas');
                    const context = canvas.getContext('2d');

                    // 设置canvas尺寸
                    canvas.height = viewport.height;
                    canvas.width = viewport.width;

                    // 将canvas添加到页面容器
                    pageDiv.appendChild(canvas);

                    // 将页面容器添加到PDF容器
                    pdfContainer.appendChild(pageDiv);

                    // 渲染PDF页面到canvas上
                    const renderContext = {
                        canvasContext: context,
                        viewport: viewport
                    };

                    const renderTask = page.render(renderContext);

                    // 渲染完成
                    renderTask.promise.then(function() {
                        pageRendering = false;
                        if (pageNumPending !== null) {
                            // 有新页面要渲染
                            renderPage(pageNumPending);
                            pageNumPending = null;
                        }

                        // 更新页面显示
                        pageNumSpan.textContent = `${currentPage} / ${pdfDoc.numPages}`;

                        // 加载完成
                        if (currentPage === 1) {
                            loading.style.display = 'none';
                        }
                    });
                });
            }

            // 跳转到指定页面
            function gotoPage(num) {
                if (pageRendering) {
                    pageNumPending = num;
                } else if (num !== currentPage && num > 0 && num <= pdfDoc.numPages) {
                    currentPage = num;
                    renderPage(currentPage);

                    // 滚动到页面顶部
                    const pageElement = document.getElementById(`page-${currentPage}`);
                    if (pageElement) {
                        pageElement.scrollIntoView();
                    }
                }
            }

            // 上一页
            prevPageBtn.onclick = function() {
                if (currentPage <= 1) return;
                gotoPage(currentPage - 1);
            };

            // 下一页
            nextPageBtn.onclick = function() {
                if (currentPage >= pdfDoc.numPages) return;
                gotoPage(currentPage + 1);
            };

            // 异步加载PDF文档
            pdfjsLib.getDocument(url).promise.then(function(pdfDoc_) {
                pdfDoc = pdfDoc_;

                // 更新页面总数显示
                pageNumSpan.textContent = `1 / ${pdfDoc.numPages}`;

                // 初始渲染第一页
                renderPage(1);

                // 预加载后续页面
                for (let i = 2; i <= Math.min(999999999999, pdfDoc.numPages); i++) {
                    renderPage(i);
                }
            }).catch(function(error) {
                console.error('Error loading PDF:', error);
                showError('PDF加载失败');
            });
        }
    }

    // 获取视频MIME类型
    function getVideoMimeType(url) {
        const extension = url.split('.').pop().toLowerCase().split('?')[0];
        switch(extension) {
            case 'mp4': return 'video/mp4';
            case 'webm': return 'video/webm';
            case 'ogg': return 'video/ogg';
            default: return 'video/mp4';
        }
    }

    // 显示错误信息
    function showError(message) {
        document.getElementById('loading').style.display = 'none';
        const errorDiv = document.getElementById('error');
        errorDiv.style.display = 'block';
        errorDiv.textContent = message;
    }

    // 关闭模态框
    function closeModal() {
        const modal = document.getElementById('preview-modal');
        modal.style.display = 'none';

        // 如果有视频播放器实例,则销毁它
        if (modal.dataset.player) {
            modal.dataset.player.dispose();
            delete modal.dataset.player;
        }

        // 如果有图片查看器实例,则销毁它
        if (modal.dataset.viewer) {
            modal.dataset.viewer.destroy();
            delete modal.dataset.viewer;
        }
    }
    // 初始化请求字段
    function initParams() {
        const salt = '81ad0be7fd53914f8cf8193c1886f635'
        const timestamp = dayjs().unix();
        const sign = hex_md5(timestamp + salt);
        const channel = 3;
        const reqConfig = {
            timestamp,
            sign,
            channel,
        };
        return reqConfig;
    }

    // post请求方法
    function postRequest(url, data, additionalHeaders = {}) {
        const headers = {
            'Content-Type': 'application/json',
            ...additionalHeaders // 允许传入额外的头部信息
        };
        const pageUrl = window.location.origin
        const reqUrl = pageUrl + url
        const reqData = {
            ...initParams(),
            ...data,
        }
        return fetch(reqUrl, {
            method: 'POST', // 指定请求方法为POST
            headers: headers,
            body: JSON.stringify(reqData) // 将JavaScript对象转换为JSON字符串
        })
            .then(response => {
                if (!response.ok) { // 检查响应是否成功
                    throw new Error('Network response was not ok ' + response.statusText);
                }
                return response.json(); // 假设服务器返回的是JSON格式的数据
            })
            .catch(error => {
                console.error('There has been a problem with your fetch operation:', error);
                throw error; // 重新抛出错误以便调用者处理
            });
    }
    // 点击模态框背景关闭
    document.getElementById('preview-modal').addEventListener('click', function(e) {
        if (e.target === this) {
            closeModal();
        }
    });

    // 页面加载完成后自动处理
    document.addEventListener('DOMContentLoaded', function() {
      document.getElementById('loading').style.display = 'flex';
        const fileUrl = getQueryParam('url');
        const fileId = getQueryParam('fileId')
        if (fileId) {
            postRequest('/api/net_disk_file/no_login_detail', { id: fileId }).then(async  res => {
                if (res.success) {
                    previewFile(res.data.url);
                } else {
                    showError('未提供文件URL参数');
                    document.getElementById('preview-modal').style.display = 'block';
                    document.getElementById('loading').style.display = 'none';
                }
            }).catch(err => {
                showError('未提供文件URL参数');
                document.getElementById('preview-modal').style.display = 'block';
                document.getElementById('loading').style.display = 'none';
            })
        } else {
            previewFile(fileUrl);
        }
    });
</script>
</body>
</html>
相关推荐
星光一影5 小时前
PDF工具箱/合并拆分pdf/提取图片
pdf·c#
michaelzhouh5 小时前
php项目ueditor上传pdf文件,防止XSS攻击
pdf·php·xss·ueditor
#麻辣小龙虾#6 小时前
网页Iframe读取PDF文件的参数设置
pdf
有过~1 天前
多功能电脑PDF转换工具Icecream PDFv3.15 中文绿色便携版
经验分享·科技·pdf·办公软件
喝凉白开都长肉的大胖子2 天前
比较 main.tex 的两个不同版本(例如旧版和新版),并生成一个带有修改标记(如删除线、高亮、修订注释)的 PDF 或文本输出。
pdf·latex
weixin_441003642 天前
2025教资面试真题电子版|科目试讲+结构化真题解析|完整PDF
面试·职场和发展·pdf
小兜全糖(xdqt)2 天前
python ppt转pdf以及图片提取
python·pdf·powerpoint
番石榴AI2 天前
视频转ppt/pdf V2.0版(新增转为可编辑PPT功能)
人工智能·pdf·powerpoint
_深巷的猫2 天前
python爬虫自动库DrissionPage保存网页快照mhtml/pdf/全局截图/打印机另存pdf
pdf