This commit is contained in:
2026-01-28 16:46:07 +00:00
parent b97601a876
commit 7e50b8cb3f
3 changed files with 208 additions and 2 deletions

View File

@@ -52,7 +52,7 @@ export class DeesPdfViewer extends DeesElement {
accessor thumbnailData: Array<{page: number, rendered: boolean}> = [];
@property({ type: Array })
accessor pageData: Array<{page: number, rendered: boolean, rendering: boolean}> = [];
accessor pageData: Array<{page: number, rendered: boolean, rendering: boolean, textLayerRendered: boolean}> = [];
private pdfDocument: any;
private renderState: RenderState = 'idle';
@@ -63,6 +63,7 @@ export class DeesPdfViewer extends DeesElement {
private currentRenderPromise: Promise<void> | null = null;
private thumbnailRenderTasks: any[] = [];
private pageRenderTasks: Map<number, any> = new Map();
private textLayerRenderTasks: Map<number, any> = new Map();
private canvas: HTMLCanvasElement | undefined;
private ctx: CanvasRenderingContext2D | undefined;
private viewerMain: HTMLElement | null = null;
@@ -230,6 +231,7 @@ export class DeesPdfViewer extends DeesElement {
<div class="page-wrapper" data-page="${item.page}">
<div class="canvas-container">
<canvas class="page-canvas" data-page="${item.page}"></canvas>
<div class="text-layer" data-page="${item.page}"></div>
</div>
</div>
`
@@ -330,7 +332,8 @@ export class DeesPdfViewer extends DeesElement {
this.pageData = Array.from({length: this.totalPages}, (_, i) => ({
page: i + 1,
rendered: false,
rendering: false
rendering: false,
textLayerRendered: false,
}));
// Set loading to false to render the pages
@@ -476,6 +479,9 @@ export class DeesPdfViewer extends DeesElement {
pageInfo.rendering = false;
this.pageRenderTasks.delete(pageNum);
// Render text layer for selection
await this.renderTextLayer(pageNum);
// Update page data to reflect rendered state
this.requestUpdate('pageData');
} catch (error: any) {
@@ -487,6 +493,63 @@ export class DeesPdfViewer extends DeesElement {
}
}
private async renderTextLayer(pageNum: number): Promise<void> {
const pageInfo = this.pageData.find(p => p.page === pageNum);
if (!pageInfo || pageInfo.textLayerRendered) return;
try {
const textLayerDiv = this.shadowRoot?.querySelector(
`.text-layer[data-page="${pageNum}"]`
) as HTMLElement;
if (!textLayerDiv) return;
textLayerDiv.innerHTML = '';
const page = await this.pdfDocument.getPage(pageNum);
const textContent = await page.getTextContent();
const viewport = this.computeViewport(page);
// @ts-ignore - Dynamic import of pdfjs
const pdfjs = await import('https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/+esm');
textLayerDiv.style.width = `${viewport.width}px`;
textLayerDiv.style.height = `${viewport.height}px`;
const textLayerRenderTask = pdfjs.renderTextLayer({
textContentSource: textContent,
container: textLayerDiv,
viewport: viewport,
});
this.textLayerRenderTasks.set(pageNum, textLayerRenderTask);
await textLayerRenderTask.promise;
// Add endOfContent for selection boundary
const endOfContent = document.createElement('div');
endOfContent.className = 'endOfContent';
textLayerDiv.appendChild(endOfContent);
// Selection class handling
textLayerDiv.addEventListener('mousedown', () => {
textLayerDiv.classList.add('selecting');
const onMouseUp = () => {
textLayerDiv.classList.remove('selecting');
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mouseup', onMouseUp);
});
pageInfo.textLayerRendered = true;
page.cleanup?.();
this.textLayerRenderTasks.delete(pageNum);
} catch (error: any) {
if (error?.name !== 'RenderingCancelledException') {
console.error(`Error rendering text layer for page ${pageNum}:`, error);
}
this.textLayerRenderTasks.delete(pageNum);
}
}
private handleScroll = () => {
// Throttle scroll events
if (this.scrollThrottleTimeout) {
@@ -771,6 +834,7 @@ export class DeesPdfViewer extends DeesElement {
this.pageData.forEach(page => {
page.rendered = false;
page.rendering = false;
page.textLayerRendered = false;
});
// Cancel any ongoing render tasks
@@ -783,6 +847,16 @@ export class DeesPdfViewer extends DeesElement {
});
this.pageRenderTasks.clear();
// Cancel text layer render tasks
this.textLayerRenderTasks.forEach(task => {
try {
task.cancel?.();
} catch (error) {
// Ignore cancellation errors
}
});
this.textLayerRenderTasks.clear();
// Request update to re-render pages
this.requestUpdate();
@@ -996,6 +1070,16 @@ export class DeesPdfViewer extends DeesElement {
});
this.pageRenderTasks.clear();
// Cancel text layer render tasks
this.textLayerRenderTasks.forEach(task => {
try {
task.cancel?.();
} catch (error) {
// Ignore cancellation errors
}
});
this.textLayerRenderTasks.clear();
// Cancel any thumbnail render tasks
for (const task of (this.thumbnailRenderTasks || [])) {
try {

View File

@@ -276,6 +276,7 @@ export const viewerStyles = [
border-radius: 4px;
overflow: hidden;
display: inline-block;
position: relative;
}
.page-canvas {
@@ -284,6 +285,48 @@ export const viewerStyles = [
image-rendering: crisp-edges;
}
/* Text layer for selection */
.text-layer {
position: absolute;
inset: 0;
overflow: clip;
line-height: 1;
text-size-adjust: none;
forced-color-adjust: none;
transform-origin: 0 0;
z-index: 1;
}
.text-layer span,
.text-layer br {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}
.text-layer ::selection {
background: rgba(0, 100, 200, 0.3);
}
.text-layer br::selection {
background: transparent;
}
.text-layer .endOfContent {
display: block;
position: absolute;
inset: 100% 0 0;
z-index: 0;
cursor: default;
user-select: none;
}
.text-layer.selecting .endOfContent {
top: 0;
}
.pdf-viewer.with-sidebar .viewer-main {
margin-left: 0;
}