diff --git a/package.json b/package.json index 34d5ed2..ca3bb2c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@rolemodel/spider", "description": "Shared high level web components for RoleModel Software and beyond", "packageManager": "yarn@4.12.0", - "version": "0.0.7", + "version": "0.0.8", "author": "RoleModel Software", "license": "MIT", "type": "module", diff --git a/src/components/pdf-viewer/page/pdf-page.js b/src/components/pdf-viewer/page/pdf-page.js index d776d37..ba77ad5 100644 --- a/src/components/pdf-viewer/page/pdf-page.js +++ b/src/components/pdf-viewer/page/pdf-page.js @@ -1,4 +1,4 @@ -import { html } from 'lit' +import { html, render } from 'lit' import * as pdfjsLib from 'pdfjs-dist' import { PDFViewerComponent } from '../pdf-viewer-component.js' import { normalizeText } from '../helpers/text-helper.js' @@ -80,6 +80,13 @@ export default class PDFPage extends PDFViewerComponent { textLayerDiv.innerHTML = '' await this.renderTextLayer(viewport, textLayerDiv) + + const annotationLayerDiv = this.shadowRoot.querySelector('.annotation-layer') + annotationLayerDiv.style.width = `${viewport.width}px` + annotationLayerDiv.style.height = `${viewport.height}px` + annotationLayerDiv.innerHTML = '' + + await this.renderLinkAnnotationLayer(viewport, annotationLayerDiv) } catch (error) { if (error.name !== 'RenderingCancelledException') { console.error('Error rendering page:', error) @@ -143,6 +150,57 @@ export default class PDFPage extends PDFViewerComponent { } } + async renderLinkAnnotationLayer(viewport, annotationLayerDiv) { + try { + const annotations = await this.page.getAnnotations({ intent: 'display' }) + const linkAnnotations = annotations.filter(annotation => + annotation?.subtype === 'Link' && + Array.isArray(annotation.rect) && + (annotation.url || annotation.unsafeUrl) + ) + + const links = linkAnnotations.map(annotation => { + const linkUrl = annotation.url || annotation.unsafeUrl + const viewportRect = this._getViewportRect(annotation.rect, viewport) + if (!viewportRect) return html`` + + const [left, top, width, height] = viewportRect + return html` + + ` + }) + render(html`${links}`, annotationLayerDiv) + } catch (error) { + console.error('Error rendering annotation layer:', error) + } + } + + _getViewportRect(annotationRect, viewport) { + let coords + if (typeof viewport.convertToViewportRectangle === 'function') { + coords = viewport.convertToViewportRectangle(annotationRect) + } else { + const [x1, y1, x2, y2] = annotationRect + const scale = viewport.scale || 1 + coords = [ + Math.min(x1, x2) * scale, + viewport.height - (Math.max(y1, y2) * scale), + Math.max(x1, x2) * scale, + viewport.height - (Math.min(y1, y2) * scale) + ] + } + + const [x1, y1, x2, y2] = coords + return [Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1)] + } + highlightText(element, text, itemStart, matches) { element.innerHTML = '' @@ -197,6 +255,7 @@ export default class PDFPage extends PDFViewerComponent {
+
` } diff --git a/src/components/pdf-viewer/page/pdf-page.styles.js b/src/components/pdf-viewer/page/pdf-page.styles.js index dd9401b..814b086 100644 --- a/src/components/pdf-viewer/page/pdf-page.styles.js +++ b/src/components/pdf-viewer/page/pdf-page.styles.js @@ -13,6 +13,8 @@ export default css` canvas { display: block; box-shadow: 0 var(--theme-spacing-xs, 0.25rem) var(--theme-spacing-sm, 0.5rem) var(--theme-shadow, rgba(0, 0, 0, 0.1)); + position: relative; + z-index: 0; } .text-layer { @@ -24,6 +26,7 @@ export default css` line-height: 1; text-align: initial; pointer-events: auto; + z-index: 1; } .text-layer > div { @@ -38,4 +41,22 @@ export default css` .text-layer > div::selection { background-color: var(--theme-primary-transparent); } + + .annotation-layer { + position: absolute; + left: 0; + top: 0; + z-index: 2; + pointer-events: auto; + } + + .annotation-layer a { + text-decoration: none; + } + + .annotation-link { + position: absolute; + display: block; + pointer-events: auto; + } ` diff --git a/test/components/pdf-viewer.test.js b/test/components/pdf-viewer.test.js index 41db421..8fd2325 100644 --- a/test/components/pdf-viewer.test.js +++ b/test/components/pdf-viewer.test.js @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import * as pdfjsLib from 'pdfjs-dist' import '../../src/components/pdf-viewer/pdf-viewer.js' -import { createMockPDFDocument, waitForCondition } from '../helpers/test-utils.js' +import { createMockPDFDocument, createMockPage, waitForCondition } from '../helpers/test-utils.js' vi.mock('pdfjs-dist', async () => { const actual = await vi.importActual('pdfjs-dist') @@ -254,6 +254,42 @@ describe('PDFViewer Component', () => { }) }) + + describe('Link annotations', () => { + beforeEach(async () => { + const pageWithLink = createMockPage(1) + pageWithLink.getAnnotations = vi.fn().mockResolvedValue([ + { + subtype: 'Link', + url: 'https://example.com/docs', + rect: [100, 100, 220, 140] + } + ]) + + pdfjsLib.getDocument.mockReturnValue({ + promise: Promise.resolve({ + numPages: 1, + getPage: vi.fn().mockResolvedValue(pageWithLink) + }) + }) + + element = await createViewer({ src: '/test.pdf', open: true }) + await waitForCondition(() => element.pdfDoc !== null) + }) + + it('should render clickable links from PDF annotations', async () => { + const canvas = element.shadowRoot.querySelector('rm-pdf-canvas') + await waitForCondition(() => canvas?.shadowRoot?.querySelector('rm-pdf-page')) + + const page = canvas.shadowRoot.querySelector('rm-pdf-page') + await waitForCondition(() => page?.shadowRoot?.querySelector('.annotation-layer a')) + + const link = page.shadowRoot.querySelector('.annotation-layer a') + expect(link).toBeDefined() + expect(link.getAttribute('href')).toBe('https://example.com/docs') + }) + }) + describe('Toolbar', () => { beforeEach(async () => { element = await createViewer({ src: '/test.pdf', open: true })