Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
61 changes: 60 additions & 1 deletion src/components/pdf-viewer/page/pdf-page.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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`
<a
class="annotation-link"
href=${linkUrl}
target="_blank"
rel="noopener noreferrer"
aria-label="Open link: ${linkUrl}"
style="left:${left}px;top:${top}px;width:${width}px;height:${height}px"
></a>
`
})
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 = ''

Expand Down Expand Up @@ -197,6 +255,7 @@ export default class PDFPage extends PDFViewerComponent {
<div class="page-wrapper">
<canvas data-page-number="${this.pageNumber}"></canvas>
<div class="text-layer"></div>
<div class="annotation-layer"></div>
</div>
`
}
Expand Down
21 changes: 21 additions & 0 deletions src/components/pdf-viewer/page/pdf-page.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -24,6 +26,7 @@ export default css`
line-height: 1;
text-align: initial;
pointer-events: auto;
z-index: 1;
}

.text-layer > div {
Expand All @@ -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;
}
`
38 changes: 37 additions & 1 deletion test/components/pdf-viewer.test.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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 })
Expand Down
Loading