feat: Update PDF components to improve rendering performance and manage document lifecycle without caching

This commit is contained in:
2025-09-20 21:28:43 +00:00
parent 7b5ba74d8b
commit d9703d3ce3
6 changed files with 583 additions and 206 deletions

View File

@@ -42,10 +42,16 @@ export class DeesPdfPreview extends DeesElement {
@property({ type: Boolean })
private error: boolean = false;
private renderPagesTask: Promise<void> | null = null;
private renderPagesQueued: boolean = false;
private observer: IntersectionObserver;
private pdfDocument: any;
private canvases: PooledCanvas[] = [];
private renderRequestId: number;
private resizeObserver?: ResizeObserver;
private previewContainer: HTMLElement | null = null;
private stackElement: HTMLElement | null = null;
private loadedPdfUrl: string | null = null;
constructor() {
super();
@@ -118,13 +124,21 @@ export class DeesPdfPreview extends DeesElement {
}
public async connectedCallback() {
super.connectedCallback();
await super.connectedCallback();
this.setupIntersectionObserver();
await this.updateComplete;
this.cacheElements();
this.setupResizeObserver();
}
public async disconnectedCallback() {
super.disconnectedCallback();
await super.disconnectedCallback();
this.cleanup();
if (this.observer) {
this.observer.disconnect();
}
this.resizeObserver?.disconnect();
this.resizeObserver = undefined;
}
private setupIntersectionObserver() {
@@ -161,9 +175,11 @@ export class DeesPdfPreview extends DeesElement {
try {
this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl);
this.pageCount = this.pdfDocument.numPages;
this.loadedPdfUrl = this.pdfUrl;
await this.updateComplete;
await this.renderPages();
this.cacheElements();
await this.scheduleRenderPages();
this.rendered = true;
@@ -177,17 +193,46 @@ export class DeesPdfPreview extends DeesElement {
}
}
private async renderPages() {
private scheduleRenderPages(): Promise<void> {
if (!this.pdfDocument) {
return Promise.resolve();
}
if (this.renderPagesTask) {
this.renderPagesQueued = true;
return this.renderPagesTask;
}
this.renderPagesTask = (async () => {
try {
await this.performRenderPages();
} catch (error) {
console.error('Failed to render PDF preview pages:', error);
}
})().finally(() => {
this.renderPagesTask = null;
if (this.renderPagesQueued) {
this.renderPagesQueued = false;
void this.scheduleRenderPages();
}
});
return this.renderPagesTask;
}
private async performRenderPages() {
if (!this.pdfDocument) return;
const canvasElements = this.shadowRoot?.querySelectorAll('.preview-canvas') as NodeListOf<HTMLCanvasElement>;
const pagesToRender = Math.min(this.pageCount, this.maxPages);
// Release old canvases
this.clearCanvases();
// Calculate available width for preview (container width minus padding and stacking offset)
const containerWidth = 160; // 200px container - 40px padding
const maxStackOffset = (pagesToRender - 1) * this.stackOffset;
const availableWidth = containerWidth - maxStackOffset;
this.cacheElements();
const { availableWidth, availableHeight } = this.getAvailableStackSize(maxStackOffset);
// Render pages in reverse order (back to front for stacking)
for (let i = 0; i < pagesToRender; i++) {
@@ -197,9 +242,15 @@ export class DeesPdfPreview extends DeesElement {
const pageNum = parseInt(canvas.dataset.page || '1');
const page = await this.pdfDocument.getPage(pageNum);
// Calculate scale to fit within available width
// Calculate scale to fit within available area while keeping aspect ratio
const initialViewport = page.getViewport({ scale: 1 });
const scale = Math.min(availableWidth / initialViewport.width, 0.5); // Cap at 0.5 for quality
const scaleX = availableWidth > 0 ? availableWidth / initialViewport.width : 0;
const scaleY = availableHeight > 0 ? availableHeight / initialViewport.height : 0;
const scale = Math.min(scaleX || 0.5, scaleY || scaleX || 0.5, 0.75);
if (!Number.isFinite(scale) || scale <= 0) {
page.cleanup?.();
continue;
}
const viewport = page.getViewport({ scale });
// Acquire canvas from pool
@@ -231,12 +282,6 @@ export class DeesPdfPreview extends DeesElement {
}
private clearCanvases() {
// Cancel any pending render
if (this.renderRequestId) {
cancelAnimationFrame(this.renderRequestId);
this.renderRequestId = 0;
}
// Release pooled canvases
for (const pooledCanvas of this.canvases) {
CanvasPool.release(pooledCanvas);
@@ -245,18 +290,22 @@ export class DeesPdfPreview extends DeesElement {
}
private cleanup() {
if (this.observer) {
this.observer.disconnect();
}
this.clearCanvases();
if (this.pdfUrl && this.pdfDocument) {
PdfManager.releaseDocument(this.pdfUrl);
if (this.pdfDocument) {
PdfManager.releaseDocument(this.loadedPdfUrl ?? this.pdfUrl);
this.pdfDocument = null;
}
this.renderPagesQueued = false;
this.pageCount = 0;
this.previewContainer = null;
this.stackElement = null;
this.loadedPdfUrl = null;
this.rendered = false;
this.loading = false;
this.error = false;
}
private handleClick() {
@@ -277,6 +326,10 @@ export class DeesPdfPreview extends DeesElement {
super.updated(changedProperties);
if (changedProperties.has('pdfUrl') && this.pdfUrl) {
const previousUrl = changedProperties.get('pdfUrl') as string | undefined;
if (previousUrl) {
PdfManager.releaseDocument(previousUrl);
}
this.cleanup();
this.rendered = false;
@@ -288,6 +341,10 @@ export class DeesPdfPreview extends DeesElement {
}
}
}
if ((changedProperties.has('maxPages') || changedProperties.has('stackOffset')) && this.rendered) {
await this.scheduleRenderPages();
}
}
/**
@@ -351,4 +408,40 @@ export class DeesPdfPreview extends DeesElement {
return items;
}
}
private cacheElements() {
if (!this.previewContainer) {
this.previewContainer = this.shadowRoot?.querySelector('.preview-container') as HTMLElement;
}
if (!this.stackElement) {
this.stackElement = this.shadowRoot?.querySelector('.preview-stack') as HTMLElement;
}
}
private setupResizeObserver() {
if (!this.previewContainer || this.resizeObserver) return;
this.resizeObserver = new ResizeObserver(() => {
if (this.rendered && this.pdfDocument && !this.loading) {
void this.scheduleRenderPages();
}
});
this.resizeObserver.observe(this);
}
private getAvailableStackSize(maxStackOffset: number) {
if (!this.stackElement) {
return {
availableWidth: 0,
availableHeight: 0,
};
}
const rect = this.stackElement.getBoundingClientRect();
const availableWidth = Math.max(rect.width - maxStackOffset, 0);
const availableHeight = Math.max(rect.height - maxStackOffset, 0);
return { availableWidth, availableHeight };
}
}