这是h5单页预览PDF文件的补充
目的:
在小程序进行文件预览,使用的是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>