提交 8b4b0a55 authored 作者: gzcnkilys_admin's avatar gzcnkilys_admin

1、pic reader增加pdf文件加载,支持文本渲染

上级 06968f64
This source diff could not be displayed because it is too large. You can view the blob instead.
<template> <template>
<div <div
class="book-reader" class="book-reader"
v-loading="loading && initialLoading" v-loading="(loading && initialLoading) || pdfLoading"
element-loading-text="正在加载图书..." :element-loading-text="props.isPdf ? '正在加载PDF...' : '正在加载图书...'"
> >
<div class="magazine-viewport"> <div class="magazine-viewport">
<div ref="magazine" class="magazine"> <div ref="magazine" class="magazine">
...@@ -20,7 +20,17 @@ ...@@ -20,7 +20,17 @@
}, },
]" ]"
> >
<!-- PDF页面使用canvas -->
<canvas
v-if="page.isPdf"
:data-page-index="index"
:class="['page-canvas', { 'wide-canvas': page.isWide }]"
:ref="`canvas${index}`"
></canvas>
<!-- 图片页面使用img -->
<img <img
v-else
:data-page-index="index" :data-page-index="index"
:src="page.src" :src="page.src"
:alt="`第 ${page.page_num} 页`" :alt="`第 ${page.page_num} 页`"
...@@ -145,6 +155,33 @@ ...@@ -145,6 +155,33 @@
</el-tooltip> </el-tooltip>
</el-col> </el-col>
<el-col class="right" :span="8"> <el-col class="right" :span="8">
<!-- PDF文字层开关 -->
<el-tooltip
v-if="props.isPdf"
:content="textLayerEnabled ? '关闭文字层' : '开启文字层'"
placement="top"
effect="dark"
>
<svg-icon
:name="textLayerEnabled ? 'list' : 'menu'"
:size="20"
@click="toggleTextLayer"
></svg-icon>
</el-tooltip>
<!-- PDF清晰度开关 -->
<el-tooltip
v-if="props.isPdf"
:content="props.highQualityRendering ? '降低清晰度' : '提高清晰度'"
placement="top"
effect="dark"
>
<svg-icon
:name="props.highQualityRendering ? 'zoom-in' : 'zoom-out'"
:size="20"
@click="toggleHighQuality"
></svg-icon>
</el-tooltip>
<!-- <svg-icon name="download" size="20"></svg-icon> --> <!-- <svg-icon name="download" size="20"></svg-icon> -->
<!-- <svg-icon name="fullscreen" size="20"></svg-icon> --> <!-- <svg-icon name="fullscreen" size="20"></svg-icon> -->
</el-col> </el-col>
...@@ -155,11 +192,12 @@ ...@@ -155,11 +192,12 @@
<script setup> <script setup>
import $ from "jquery"; import $ from "jquery";
import "@/assets/js/turn.js"; // 直接导入执行,扩展 jQuery 对象 import "@/assets/js/turn.js"; // 直接导入执行,扩展 jQuery 对象
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from "vue"; import { ref, onMounted, onUnmounted, watch, nextTick, computed, toRaw } from "vue";
import VueEasyLightbox from "vue-easy-lightbox"; import VueEasyLightbox from "vue-easy-lightbox";
import GuideMobile from "../GuideMobile/index.vue"; import GuideMobile from "../GuideMobile/index.vue";
import audioFlip from "@/assets/audio/flip.mp3"; import audioFlip from "@/assets/audio/flip.mp3";
import throttle from "lodash/throttle"; import throttle from "lodash/throttle";
import * as pdfjsLib from "pdfjs-dist";
const props = defineProps({ const props = defineProps({
pages: { pages: {
type: Array, type: Array,
...@@ -170,6 +208,23 @@ const props = defineProps({ ...@@ -170,6 +208,23 @@ const props = defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
// 新增PDF相关属性
pdfUrl: {
type: String,
default: "",
},
isPdf: {
type: Boolean,
default: false,
},
enableTextLayer: {
type: Boolean,
default: true,
},
highQualityRendering: {
type: Boolean,
default: true,
},
}); });
const magazine = ref(null); const magazine = ref(null);
...@@ -198,6 +253,15 @@ const previewImages = ref([]); ...@@ -198,6 +253,15 @@ const previewImages = ref([]);
// 将 processedPages 改为普通数据属性 // 将 processedPages 改为普通数据属性
const processedPages = ref([]); const processedPages = ref([]);
// PDF相关状态
const pdfDocument = ref(null);
const totalPdfPages = ref(0);
const pdfLoading = ref(false);
const textLayerEnabled = ref(props.enableTextLayer); // 文字层开关状态
// const textLayerEnabled = ref(false); // 文字层开关状态
const renderedPages = ref(new Set()); // 跟踪已渲染的页面
const pdfInitialized = ref(false); // 跟踪PDF是否已完成初始化渲染
// Add new refs for sound and directory // Add new refs for sound and directory
const pageTurnSound = ref(null); const pageTurnSound = ref(null);
const audioInitialized = ref(false); const audioInitialized = ref(false);
...@@ -229,6 +293,21 @@ const processPages = (pages) => { ...@@ -229,6 +293,21 @@ const processPages = (pages) => {
})); }));
}; };
// 处理PDF页面
const processPdfPages = (totalPages) => {
return Array.from({ length: totalPages }, (_, index) => ({
src: "",
originalIndex: index,
page_num: index + 1,
images: [],
isWide: false,
isLoaded: false,
url: "", // PDF页面不需要URL
isPdf: true,
pageIndex: index,
}));
};
const loadImage = async (pageIndex) => { const loadImage = async (pageIndex) => {
const page = processedPages.value[pageIndex]; const page = processedPages.value[pageIndex];
if (!page || page.isLoaded || preloadingPages.value.has(pageIndex)) { if (!page || page.isLoaded || preloadingPages.value.has(pageIndex)) {
...@@ -327,6 +406,48 @@ const loadVisiblePages = async (currentPageNum) => { ...@@ -327,6 +406,48 @@ const loadVisiblePages = async (currentPageNum) => {
// 完全移除finally块中的loading状态管理 // 完全移除finally块中的loading状态管理
}; };
// 渲染可见的PDF页面
const renderVisiblePdfPages = async (currentPageNum) => {
if (!magazine.value || !pdfDocument.value) return;
// 避免在PDF加载过程中重复渲染
if (pdfLoading.value) {
console.log(`PDF正在加载中,跳过渲染页面: ${currentPageNum}`);
return;
}
const $magazine = $(magazine.value);
const totalPages = $magazine.turn("pages");
// 渲染当前页、上一页、下一页
const startPage = Math.max(1, currentPageNum - 1);
const endPage = Math.min(totalPages, currentPageNum + 1);
const highPriority = [currentPageNum, currentPageNum + 1]; // 优先渲染当前和下一页
console.log(`渲染PDF页面: ${currentPageNum}, 范围: ${startPage}-${endPage}`);
try {
console.log(`开始渲染高优先级页面: ${highPriority.join(', ')}`);
// 优先渲染高优先级页面
await Promise.allSettled(
highPriority.map((pageIndex) => renderPdfPage(pageIndex - 1))
);
// 后台渲染其他页面,不阻塞UI
const otherPages = Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i)
.filter((page) => !highPriority.includes(page));
if (otherPages.length > 0) {
console.log(`后台渲染其他页面: ${otherPages.join(', ')}`);
Promise.allSettled(
otherPages.map((pageIndex) => renderPdfPage(pageIndex - 1))
);
}
} catch (error) {
console.error("渲染PDF页面失败:", error);
}
};
// 修改初始化函数 // 修改初始化函数
const initBook = async () => { const initBook = async () => {
if (!magazine.value || !processedPages.value.length) return; if (!magazine.value || !processedPages.value.length) return;
...@@ -387,6 +508,26 @@ const initBook = async () => { ...@@ -387,6 +508,26 @@ const initBook = async () => {
objectFit: "contain", objectFit: "contain",
}); });
} }
// 处理PDF页面的canvas
const $canvas = $page.find("canvas");
if ($canvas.length) {
$canvas.css({
width: "100%",
height: "100%",
display: "block",
});
}
// 确保PDF页面容器有正确的尺寸
if ($page.find("canvas").length) {
$page.css({
width: pageWidth,
height: pageHeight,
minWidth: pageWidth,
minHeight: pageHeight,
});
}
}); });
// 移动端使用单页显示 // 移动端使用单页显示
...@@ -396,7 +537,7 @@ const initBook = async () => { ...@@ -396,7 +537,7 @@ const initBook = async () => {
display: isMobile.value ? "single" : "double", display: isMobile.value ? "single" : "double",
acceleration: true, acceleration: true,
// 启用非corner区域翻页 // 启用非corner区域翻页
enableNonCorner: true, enableNonCorner: false,
// 最大折叠距离比例 (0.1 - 1.0) // 最大折叠距离比例 (0.1 - 1.0)
maxFoldingDistance: 2, maxFoldingDistance: 2,
// 最小拖拽距离触发翻页 // 最小拖拽距离触发翻页
...@@ -414,9 +555,22 @@ const initBook = async () => { ...@@ -414,9 +555,22 @@ const initBook = async () => {
}, },
turned: (event, page) => { turned: (event, page) => {
currentPage.value = page; currentPage.value = page;
console.log(`turn.js翻页到第${page}页`);
// 翻页完成后异步预加载,不影响动画,不设置翻页锁 // 翻页完成后异步预加载,不影响动画,不设置翻页锁
setTimeout(() => { setTimeout(() => {
if (props.isPdf && pdfDocument.value) {
// 避免在初始化时重复渲染
if (isInitialized.value && !pdfLoading.value && pdfInitialized.value) {
console.log(`开始渲染第${page}页的PDF内容`);
renderVisiblePdfPages(page);
}
} else {
loadVisiblePages(page); loadVisiblePages(page);
}
// 调试:检查页面状态
setTimeout(() => debugPageStatus(), 100);
}, 50); }, 50);
// 播放声音 // 播放声音
...@@ -429,7 +583,11 @@ const initBook = async () => { ...@@ -429,7 +583,11 @@ const initBook = async () => {
// 初始化完成后关闭loading // 初始化完成后关闭loading
loading.value = false; loading.value = false;
initialLoading.value = false; initialLoading.value = false;
// 只处理图片页面,PDF页面在loadPdf中处理
if (!props.isPdf) {
await loadVisiblePages(1); await loadVisiblePages(1);
}
} catch (error) { } catch (error) {
console.error("Turn.js initialization error:", error); console.error("Turn.js initialization error:", error);
loading.value = false; loading.value = false;
...@@ -440,7 +598,10 @@ const initBook = async () => { ...@@ -440,7 +598,10 @@ const initBook = async () => {
const loadImages = async () => { const loadImages = async () => {
// loading.value = true; // loading.value = true;
try { try {
// 只处理图片页面,不初始化turn.js(PDF模式下已经初始化)
if (!props.isPdf) {
await initBook(); await initBook();
}
} catch (error) { } catch (error) {
console.error("加载图片失败:", error); console.error("加载图片失败:", error);
} finally { } finally {
...@@ -448,11 +609,62 @@ const loadImages = async () => { ...@@ -448,11 +609,62 @@ const loadImages = async () => {
} }
}; };
// 加载PDF
const loadPdf = async () => {
if (!props.pdfUrl) {
console.error("PDF URL 未提供");
return;
}
try {
pdfLoading.value = true;
console.log("开始加载PDF:", props.pdfUrl);
// 加载PDF文档
const loadingTask = pdfjsLib.getDocument(props.pdfUrl);
const rawPdfDocument = await loadingTask.promise;
pdfDocument.value = rawPdfDocument;
totalPdfPages.value = rawPdfDocument.numPages;
console.log("PDF加载成功,总页数:", totalPdfPages.value);
// 处理PDF页面
processedPages.value = processPdfPages(totalPdfPages.value);
// 清除之前的渲染状态
renderedPages.value.clear();
pdfInitialized.value = false; // 重置PDF初始化状态
// 初始化turn.js
await initBook();
// 先关闭loading状态,再渲染第一页PDF
pdfLoading.value = false;
// 等待turn.js完全初始化完成
await new Promise(resolve => setTimeout(resolve, 200));
// 渲染第一页PDF
await renderVisiblePdfPages(1);
// 标记PDF已完成初始化渲染
pdfInitialized.value = true;
// 等待一小段时间,确保turn.js事件处理完成
await new Promise(resolve => setTimeout(resolve, 100));
console.log("PDF渲染完成");
} catch (error) {
console.error("PDF加载失败:", error);
pdfLoading.value = false;
}
};
// 监听 props.pages 的变化 // 监听 props.pages 的变化
watch( watch(
() => props.pages, () => props.pages,
(newPages) => { (newPages) => {
if (newPages && newPages.length > 0) { if (newPages && newPages.length > 0 && !props.isPdf) {
processedPages.value = processPages(newPages); processedPages.value = processPages(newPages);
loadImages(); loadImages();
} }
...@@ -460,6 +672,17 @@ watch( ...@@ -460,6 +672,17 @@ watch(
{ immediate: true } { immediate: true }
); );
// 监听PDF URL的变化
watch(
() => props.pdfUrl,
async (newUrl) => {
if (newUrl && props.isPdf) {
console.log("PDF URL 变化,重新加载:", newUrl);
await loadPdf();
}
}
);
const destroyTurn = () => { const destroyTurn = () => {
if (magazine.value && isInitialized.value) { if (magazine.value && isInitialized.value) {
try { try {
...@@ -476,6 +699,350 @@ const destroyTurn = () => { ...@@ -476,6 +699,350 @@ const destroyTurn = () => {
// 初始化 Intersection Observer // 初始化 Intersection Observer
// 渲染单个PDF页面
const renderPdfPage = async (pageIndex) => {
if (!pdfDocument.value || !magazine.value) return;
let pageNum = pageIndex + 1
// 检查页面是否已经渲染过
if (renderedPages.value.has(pageIndex)) {
console.log(`页面 ${pageNum} 已经渲染过,跳过`);
return;
}
console.log(`开始渲染PDF页面 ${pageNum}`);
try {
// 使用 toRaw 将 Vue3 响应式对象转换为原始对象,避免 Proxy 兼容性问题
const rawPdfDocument = toRaw(pdfDocument.value);
const page = await rawPdfDocument.getPage(pageNum);
// 获取页面容器
const pageContainer = magazine.value.querySelector(`.p${pageNum}`);
if (!pageContainer) {
console.warn(`Page container not found for page ${pageNum}`);
return;
}
// 检查是否已经渲染过
let canvas = pageContainer.querySelector('canvas');
let isNewCanvas = false;
// 如果canvas已经渲染过,创建新的canvas
if (canvas && canvas.dataset.rendered === 'true') {
const newCanvas = document.createElement('canvas');
newCanvas.className = canvas.className;
newCanvas.style.cssText = canvas.style.cssText;
pageContainer.replaceChild(newCanvas, canvas);
canvas = newCanvas;
isNewCanvas = true;
}
if (!canvas) {
console.warn(`Canvas not found for page ${pageNum}`);
return;
}
// 计算缩放比例(考虑设备像素比,提高清晰度)
const viewport = page.getViewport({ scale: 1.0 });
// 获取容器尺寸,如果容器尺寸为0,则使用turn.js设置的页面尺寸
let containerWidth = canvas.clientWidth || pageContainer.clientWidth;
let containerHeight = canvas.clientHeight || pageContainer.clientHeight;
// 如果容器尺寸为0,尝试从turn.js获取页面尺寸
if (containerWidth === 0 || containerHeight === 0) {
const $magazine = $(magazine.value);
if ($magazine.length && $magazine.turn) {
const turnSize = $magazine.turn('size');
if (turnSize && turnSize.width && turnSize.height) {
// 移动端使用单页尺寸,桌面端使用双页尺寸的一半
if (isMobile.value) {
containerWidth = turnSize.width;
containerHeight = turnSize.height;
} else {
containerWidth = turnSize.width / 2;
containerHeight = turnSize.height;
}
}
}
}
// 如果仍然无法获取尺寸,使用默认尺寸
if (containerWidth === 0 || containerHeight === 0) {
console.warn(`页面${pageNum}容器尺寸为0,使用默认尺寸`);
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
if (isMobile.value) {
containerWidth = viewportWidth - 20;
containerHeight = (containerWidth * 4) / 3;
if (containerHeight > viewportHeight - 100) {
containerHeight = viewportHeight - 92;
containerWidth = (containerHeight * 3) / 4;
}
} else {
containerWidth = Math.min(600, viewportWidth / 2 - 50);
containerHeight = Math.min(800, viewportHeight - 100);
}
}
console.log(`页面${pageNum}容器尺寸: ${containerWidth}x${containerHeight}`);
// 获取设备像素比,提高高分辨率屏幕的显示质量
const devicePixelRatio = window.devicePixelRatio || 1;
const scaleX = containerWidth / viewport.width;
const scaleY = containerHeight / viewport.height;
const baseScale = Math.min(scaleX, scaleY);
// 根据清晰度设置决定是否使用高分辨率
const scale = props.highQualityRendering ? baseScale * devicePixelRatio : baseScale;
// 设置canvas尺寸(使用高分辨率)
const scaledViewport = page.getViewport({ scale });
canvas.width = Math.floor(scaledViewport.width);
canvas.height = Math.floor(scaledViewport.height);
// 设置CSS尺寸(保持显示大小不变)
canvas.style.width = `${containerWidth}px`;
canvas.style.height = `${containerHeight}px`;
console.log(`页面${pageIndex + 1}缩放信息: 基础缩放=${baseScale.toFixed(3)}, 设备像素比=${devicePixelRatio}, 最终缩放=${scale.toFixed(3)}, canvas尺寸=${canvas.width}x${canvas.height}, 显示尺寸=${containerWidth}x${containerHeight}`);
// 渲染PDF页面
const context = canvas.getContext("2d");
const renderContext = {
canvasContext: context,
viewport: scaledViewport,
};
await page.render(renderContext).promise;
// 标记canvas已渲染
canvas.dataset.rendered = 'true';
// 标记页面已渲染
renderedPages.value.add(pageIndex);
// 渲染文字层(如果启用)
if (textLayerEnabled.value) {
try {
const textContent = await page.getTextContent();
// 创建文字层容器
const textLayer = document.createElement('div');
textLayer.className = 'text-layer';
textLayer.id = `text-layer-${pageNum}`; // 添加唯一ID便于调试
textLayer.style.position = 'absolute';
textLayer.style.left = '0';
textLayer.style.top = '0';
// 文字层尺寸应该和turn.js的页面尺寸一致,这样就能完全覆盖左右分页
// 从turn.js获取实际的页面尺寸
const $magazine = $(magazine.value);
let turnPageWidth, turnPageHeight;
if ($magazine.length && $magazine.turn) {
const turnSize = $magazine.turn('size');
if (turnSize && turnSize.width && turnSize.height) {
// 移动端使用单页尺寸,桌面端使用双页尺寸的一半
if (isMobile.value) {
turnPageWidth = turnSize.width;
turnPageHeight = turnSize.height;
} else {
turnPageWidth = turnSize.width / 2;
turnPageHeight = turnSize.height;
}
}
}
// 如果无法从turn.js获取尺寸,使用containerWidth和containerHeight
if (!turnPageWidth || !turnPageHeight) {
turnPageWidth = containerWidth;
turnPageHeight = containerHeight;
}
textLayer.style.width = `${turnPageWidth}px`;
textLayer.style.height = `${turnPageHeight}px`;
textLayer.style.pointerEvents = 'auto';
textLayer.style.userSelect = 'text';
textLayer.style.zIndex = '999'; // 使用更高的z-index
textLayer.style.backgroundColor = 'transparent'; // 文字层背景透明
// 添加详细的尺寸调试信息
console.log(`=== 页面${pageNum}尺寸调试 ===`);
console.log(`containerWidth: ${containerWidth}, containerHeight: ${containerHeight}`);
console.log(`turn.js页面尺寸: ${turnPageWidth}x${turnPageHeight}`);
console.log(`canvas.style.width: ${canvas.style.width}, canvas.style.height: ${canvas.style.height}`);
console.log(`canvas.clientWidth: ${canvas.clientWidth}, canvas.clientHeight: ${canvas.clientHeight}`);
console.log(`canvas.offsetWidth: ${canvas.offsetWidth}, canvas.offsetHeight: ${canvas.offsetHeight}`);
console.log(`pageContainer.clientWidth: ${pageContainer.clientWidth}, pageContainer.clientHeight: ${pageContainer.clientHeight}`);
console.log(`PDF原始尺寸: ${viewport.width}x${viewport.height}`);
console.log(`缩放比例: ${baseScale}`);
console.log(`文字层尺寸: ${turnPageWidth}x${turnPageHeight}`);
console.log(`========================`);
// 专门打印文字层和PDF分页的尺寸对比
console.log(`🔍 尺寸对比检查:`);
console.log(` 文字层尺寸: ${turnPageWidth}x${turnPageHeight}`);
console.log(` PDF分页尺寸: ${pageContainer.clientWidth}x${pageContainer.clientHeight}`);
console.log(` Canvas显示尺寸: ${canvas.style.width}x${canvas.style.height}`);
console.log(` 是否一致: ${turnPageWidth === pageContainer.clientWidth && turnPageHeight === pageContainer.clientHeight ? '✅ 一致' : '❌ 不一致'}`);
if (turnPageWidth !== pageContainer.clientWidth || turnPageHeight !== pageContainer.clientHeight) {
console.log(` 差异: 宽度差${Math.abs(turnPageWidth - pageContainer.clientWidth)}px, 高度差${Math.abs(turnPageHeight - pageContainer.clientHeight)}px`);
}
// 不需要scale变换,直接使用turn.js的页面尺寸
textLayer.style.transform = 'none';
textLayer.style.transformOrigin = '0 0';
console.log(`文字层直接使用turn.js页面尺寸,无变换`);
// 添加文字项
textContent.items.forEach((item, index) => {
// 简化文字定位,先确保能看到文字
const style = textContent.styles[item.fontName];
const fontSize = item.height * baseScale; // 应用缩放
const fontFamily = style ? style.fontFamily : 'sans-serif';
// 创建文字span元素
const textSpan = document.createElement('span');
textSpan.textContent = item.str;
textSpan.id = `text-${pageNum}-${index}`; // 添加唯一ID便于调试
textSpan.style.position = 'absolute';
// 使用百分比方法计算文字在turn.js页面中的位置
// 先计算PDF中的百分比位置,再乘以分页尺寸
// X坐标:计算PDF中的水平百分比
const xPercent = item.transform[4] / viewport.width;
const xPos = xPercent * turnPageWidth;
// Y坐标:计算PDF中的垂直百分比(需要翻转)
// PDF中的Y坐标是文字框的顶部位置
// 我们需要考虑文字框的高度来调整位置
const textHeight = item.height;
const pdfY = item.transform[5];
// 方法1:如果PDF的Y是文字框顶部,我们需要调整到文字基线位置
// 文字基线通常在文字框高度的80%位置
const baselineOffset = textHeight * 0.8; // 文字基线偏移
const adjustedPdfY = pdfY + baselineOffset;
// 计算调整后的百分比位置
const yPercent = 1 - (adjustedPdfY / viewport.height);
let yPos = yPercent * turnPageHeight;
// 调试百分比计算
if (index < 3) {
console.log(`文字"${item.str}" 百分比计算:`);
console.log(` PDF原始坐标: x=${item.transform[4]}, y=${item.transform[5]}`);
console.log(` PDF原始Y: ${pdfY}, 文字高度: ${textHeight}`);
console.log(` 基线偏移: ${baselineOffset.toFixed(2)} (文字高度的80%)`);
console.log(` 调整后PDF Y: ${adjustedPdfY.toFixed(2)}`);
console.log(` PDF百分比: x=${(xPercent * 100).toFixed(2)}%, y=${(yPercent * 100).toFixed(2)}%`);
console.log(` turn.js尺寸: ${turnPageWidth}x${turnPageHeight}`);
console.log(` 最终位置: x=${xPos.toFixed(2)}, y=${yPos.toFixed(2)}`);
console.log(` Y坐标说明: 考虑了文字框高度和基线偏移`);
}
textSpan.style.left = `${xPos}px`;
textSpan.style.top = `${yPos}px`;
textSpan.style.fontSize = `${fontSize}px`;
textSpan.style.fontFamily = fontFamily;
textSpan.style.fontWeight = style?.fontWeight || 'normal';
textSpan.style.fontStyle = style?.fontStyle || 'normal';
// 设置文字尺寸
textSpan.style.width = `${item.width * baseScale}px`;
textSpan.style.height = `${item.height * baseScale}px`;
textSpan.style.lineHeight = `${item.height * baseScale}px`;
// 文字颜色设为透明,但可以选中和复制
textSpan.style.color = 'transparent';
textSpan.style.cursor = 'text';
textSpan.style.whiteSpace = 'pre';
textSpan.style.overflow = 'hidden';
textSpan.style.backgroundColor = 'transparent'; // 文字背景透明
// 添加选择事件支持
textSpan.addEventListener('mouseup', () => {
const selection = window.getSelection();
if (selection.toString()) {
console.log('选中文本:', selection.toString());
}
});
textLayer.appendChild(textSpan);
// 调试信息
if (index < 5) { // 只显示前5个文字项的信息
console.log(`文字项${index}: "${item.str}" 位置=(${xPos.toFixed(2)}, ${yPos.toFixed(2)}) 尺寸=${item.width * baseScale}x${item.height * baseScale}`);
console.log(` - 原始transform: [${item.transform.join(', ')}]`);
console.log(` - 百分比位置: x=${(xPercent * 100).toFixed(2)}%, y=${(yPercent * 100).toFixed(2)}%`);
}
});
// 将文字层添加到页面容器中
if (pageContainer) {
// 移除已存在的文字层
const existingTextLayer = pageContainer.querySelector('.text-layer');
if (existingTextLayer) {
existingTextLayer.remove();
}
// 确保文字层在canvas之上
pageContainer.appendChild(textLayer);
// 设置文字层的z-index,确保在canvas之上
textLayer.style.zIndex = '20';
}
console.log(`文字层渲染完成,共 ${textContent.items.length} 个文字项`);
console.log(`文字层位置: left=0, top=0, width=100%, height=100%`);
console.log(`文字层z-index: ${textLayer.style.zIndex}`);
console.log(`文字层ID: ${textLayer.id}`);
// 验证文字层是否真的添加到DOM中
const addedTextLayer = pageContainer.querySelector(`#text-layer-${pageNum}`);
if (addedTextLayer) {
console.log(`✅ 文字层已成功添加到DOM,ID: ${addedTextLayer.id}`);
console.log(`文字层子元素数量: ${addedTextLayer.children.length}`);
// 最终验证文字层的实际尺寸
const textLayerRect = addedTextLayer.getBoundingClientRect();
const pageContainerRect = pageContainer.getBoundingClientRect();
console.log(`🔍 最终尺寸验证:`);
console.log(` 文字层实际尺寸: ${textLayerRect.width.toFixed(2)}x${textLayerRect.height.toFixed(2)}`);
console.log(` 页面容器实际尺寸: ${pageContainerRect.width.toFixed(2)}x${pageContainerRect.height.toFixed(2)}`);
console.log(` 是否完全一致: ${Math.abs(textLayerRect.width - pageContainerRect.width) < 1 && Math.abs(textLayerRect.height - pageContainerRect.height) < 1 ? '✅ 完全一致' : '❌ 有差异'}`);
} else {
console.error(`❌ 文字层未能添加到DOM中!`);
}
} catch (textError) {
console.warn(`文字层渲染失败:`, textError);
}
}
// 标记页面已渲染
if (processedPages.value[pageIndex]) {
processedPages.value[pageIndex].isLoaded = true;
}
// 检查canvas是否正确显示
const renderedCanvas = pageContainer.querySelector('canvas');
if (renderedCanvas) {
const rect = renderedCanvas.getBoundingClientRect();
console.log(`PDF页面 ${pageNum} 渲染完成,canvas尺寸: ${rect.width}x${rect.height}, 位置: (${rect.left}, ${rect.top}), 可见性: ${renderedCanvas.style.visibility || 'visible'}, 显示: ${renderedCanvas.style.display || 'block'}`);
} else {
console.warn(`PDF页面 ${pageNum} 渲染完成,但找不到canvas元素`);
}
} catch (error) {
console.error(`渲染PDF页面 ${pageNum} 失败:`, error);
}
};
// 修改图片错误处理函数 // 修改图片错误处理函数
const handleImageError = async (index) => { const handleImageError = async (index) => {
// 清除超时 // 清除超时
...@@ -722,6 +1289,84 @@ const toggleMute = () => { ...@@ -722,6 +1289,84 @@ const toggleMute = () => {
localStorage.setItem("bookReaderMuted", isMuted.value); localStorage.setItem("bookReaderMuted", isMuted.value);
}; };
// 添加文字层开关函数
const toggleTextLayer = () => {
textLayerEnabled.value = !textLayerEnabled.value;
if (textLayerEnabled.value) {
// 清除渲染状态,重新渲染所有可见页面
renderedPages.value.clear();
const $magazine = $(magazine.value);
if ($magazine.length) {
const currentPage = $magazine.turn("page");
renderVisiblePdfPages(currentPage);
}
} else {
// 移除所有文字层
const textLayers = document.querySelectorAll('.text-layer');
textLayers.forEach(layer => layer.remove());
}
// 保存设置到本地存储
localStorage.setItem("bookReaderTextLayer", textLayerEnabled.value);
};
// 添加清晰度切换函数
const toggleHighQuality = () => {
// 切换清晰度设置
const newQuality = !props.highQualityRendering;
// 清除渲染状态,重新渲染所有可见页面
renderedPages.value.clear();
// 重新渲染当前可见页面
const $magazine = $(magazine.value);
if ($magazine.length) {
const currentPage = $magazine.turn("page");
console.log(`切换清晰度设置: ${newQuality ? '高清晰度' : '标准清晰度'}`);
renderVisiblePdfPages(currentPage);
}
// 保存设置到本地存储
localStorage.setItem("bookReaderHighQuality", newQuality);
};
// 添加调试函数:检查页面状态
const debugPageStatus = () => {
if (!magazine.value) return;
const $magazine = $(magazine.value);
const currentPage = $magazine.turn("page");
const totalPages = $magazine.turn("pages");
console.log(`=== 页面状态调试 ===`);
console.log(`当前页码: ${currentPage}, 总页数: ${totalPages}`);
console.log(`已渲染页面:`, Array.from(renderedPages.value).map(i => i + 1));
// 检查当前页面的canvas状态
const currentPageContainer = magazine.value.querySelector(`.p${currentPage}`);
if (currentPageContainer) {
const canvas = currentPageContainer.querySelector('canvas');
if (canvas) {
const rect = canvas.getBoundingClientRect();
console.log(`当前页面canvas状态: 尺寸=${rect.width}x${rect.height}, 位置=(${rect.left}, ${rect.top}), 可见性=${canvas.style.visibility || 'visible'}, 显示=${canvas.style.display || 'block'}`);
} else {
console.log(`当前页面没有canvas元素`);
}
}
// 检查所有页面的状态
for (let i = 1; i <= totalPages; i++) {
const pageContainer = magazine.value.querySelector(`.p${i}`);
if (pageContainer) {
const canvas = pageContainer.querySelector('canvas');
const isRendered = renderedPages.value.has(i - 1);
console.log(`页面${i}: 容器存在=${!!pageContainer}, canvas存在=${!!canvas}, 已渲染=${isRendered}`);
}
}
console.log(`====================`);
};
// 添加防抖处理 // 添加防抖处理
let resizeTimeout; let resizeTimeout;
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
...@@ -732,12 +1377,28 @@ window.addEventListener("resize", () => { ...@@ -732,12 +1377,28 @@ window.addEventListener("resize", () => {
}); });
onMounted(async () => { onMounted(async () => {
// 配置PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs";
// 从本地存储读取静音状态 // 从本地存储读取静音状态
const savedMuted = localStorage.getItem("bookReaderMuted"); const savedMuted = localStorage.getItem("bookReaderMuted");
if (savedMuted !== null) { if (savedMuted !== null) {
isMuted.value = savedMuted === "true"; isMuted.value = savedMuted === "true";
} }
// 从本地存储读取文字层设置
const savedTextLayer = localStorage.getItem("bookReaderTextLayer");
if (savedTextLayer !== null) {
textLayerEnabled.value = savedTextLayer === "true";
}
// 根据类型选择加载方式
if (props.isPdf && props.pdfUrl) {
await loadPdf();
} else if (props.pages && props.pages.length > 0) {
await loadImages(); await loadImages();
}
initAudio(); initAudio();
window.addEventListener("keydown", handleKeyDown); window.addEventListener("keydown", handleKeyDown);
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
...@@ -754,6 +1415,8 @@ onUnmounted(() => { ...@@ -754,6 +1415,8 @@ onUnmounted(() => {
imageCache.value.clear(); imageCache.value.clear();
loadingQueue.value.clear(); loadingQueue.value.clear();
preloadingPages.value.clear(); preloadingPages.value.clear();
renderedPages.value.clear(); // 清除PDF渲染状态
pdfInitialized.value = false; // 清除PDF初始化状态
// 重置状态 // 重置状态
initialLoading.value = true; initialLoading.value = true;
...@@ -917,6 +1580,81 @@ const showGuide = () => { ...@@ -917,6 +1580,81 @@ const showGuide = () => {
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.05); box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.05);
} }
/* PDF页面canvas样式 */
.page-canvas {
width: 100%;
height: 100%;
display: block;
background-color: white;
/* 确保canvas在高分辨率下清晰显示 */
image-rendering: -webkit-optimize-contrast;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
image-rendering: pixelated;
/* 防止浏览器自动缩放 */
image-rendering: auto;
}
/* PDF文字层样式 */
.text-layer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
opacity: 0.2;
line-height: 1.0;
pointer-events: auto;
user-select: text;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
z-index: 10;
}
.text-layer > span {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}
.text-layer .highlight {
margin: -1px;
padding: 1px;
background-color: rgb(180, 0, 170);
border-radius: 4px;
}
.text-layer .highlight.begin {
border-radius: 4px 0px 0px 4px;
}
.text-layer .highlight.end {
border-radius: 0px 4px 4px 0px;
}
.text-layer .highlight.middle {
border-radius: 0px;
}
.text-layer .highlight.selected {
background-color: rgb(0, 100, 0);
}
/* 确保文字层在turn.js翻页时保持可见 */
.page .text-layer {
transform-style: preserve-3d;
backface-visibility: visible;
}
.page-canvas.wide-canvas {
width: 100%;
height: 100%;
}
/* 缩放状态样式 */ /* 缩放状态样式 */
.magazine.zoomed { .magazine.zoomed {
cursor: move; cursor: move;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论