add highlight annotations

This commit is contained in:
j 2024-06-10 14:05:17 +01:00
parent 7e1a282ad6
commit 4df34b28e5
9 changed files with 359 additions and 34 deletions

View file

@ -10,6 +10,10 @@
font-size: 14px; font-size: 14px;
line-height: 21px; line-height: 21px;
} }
.OMLQuote img {
max-width: 100%;
margin: auto;
}
.OMLAnnotation .OMLQuoteBackground { .OMLAnnotation .OMLQuoteBackground {
position: absolute; position: absolute;

View file

@ -1,9 +1,18 @@
'use strict'; 'use strict';
oml.SELECTION = 0
oml.HIGHLIGHT = 1
oml.ui.annotation = function(annotation, $iframe) { oml.ui.annotation = function(annotation, $iframe) {
var value = Ox.encodeHTMLEntities(annotation.text).replace(/\n/g, '<br/>')
if (annotation.type == oml.HIGHLIGHT) {
let coord = annotation.coords[0].map(p => parseInt(p)).join(',')
let image = `/${oml.user.ui.item}/2048p${parseInt(annotation.page)},${coord}.jpg`
value = `<img src="${image}">`
}
var $quoteText = Ox.Element() var $quoteText = Ox.Element()
.addClass('OxSelectable OMLQuote') .addClass('OxSelectable OMLQuote')
.html(Ox.encodeHTMLEntities(annotation.text).replace(/\n/g, '<br/>')) .html(value)
.on({ .on({
click: function(event) { click: function(event) {
var id var id

View file

@ -151,7 +151,7 @@ oml.ui.viewer = function() {
height: '100%', height: '100%',
border: 0 border: 0
}).onMessage(function(data, event) { }).onMessage(function(data, event) {
console.log('got', event, data) // console.log('got', event, data, data.page)
if (event == 'addAnnotation') { if (event == 'addAnnotation') {
addAnnotation(data); addAnnotation(data);
var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents) var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents)
@ -217,7 +217,7 @@ oml.ui.viewer = function() {
} }
var map = {} var map = {}
map[sortKey] = function(value) { map[sortKey] = function(value) {
return value.toString(); return value ? value.toString() : '';
} }
annotations = Ox.sortBy(annotations, sortKey, map) annotations = Ox.sortBy(annotations, sortKey, map)
oml.$ui.annotationFolder.empty(); oml.$ui.annotationFolder.empty();

62
static/reader/pdf.css Normal file
View file

@ -0,0 +1,62 @@
.toolbarButton.cropFile::before,
.secondaryToolbarButton.cropFile::before {
mask-image: url(pdf/toolbarButton-crop.png);
}
.toolbarButton.embedPage::before,
.secondaryToolbarButton.embedPage::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGxpbmUgeDE9Ijg4IiB5MT0iNTYiIHgyPSIyNCIgeTI9IjEyOCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMjQiIHkxPSIxMjgiIHgyPSI4OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMTY4IiB5MT0iNTYiIHgyPSIyMzIiIHkyPSIxMjgiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjQ4Ii8+PGxpbmUgeDE9IjIzMiIgeTE9IjEyOCIgeDI9IjE2OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48L3N2Zz48IS0teyJjb2xvciI6ImRlZmF1bHQiLCJuYW1lIjoic3ltYm9sRW1iZWQiLCJ0aGVtZSI6Im94bWVkaXVtIn0tLT4=);
}
@media screen and (min-resolution: 2dppx) {
.toolbarButton.cropFile::before,
.secondaryToolbarButton.cropFile::before {
mask-image: url(pdf/toolbarButton-crop@2x.png);
}
.toolbarButton.embedPage::before,
.secondaryToolbarButton.embedPage::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGxpbmUgeDE9Ijg4IiB5MT0iNTYiIHgyPSIyNCIgeTI9IjEyOCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMjQiIHkxPSIxMjgiIHgyPSI4OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMTY4IiB5MT0iNTYiIHgyPSIyMzIiIHkyPSIxMjgiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjQ4Ii8+PGxpbmUgeDE9IjIzMiIgeTE9IjEyOCIgeDI9IjE2OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48L3N2Zz48IS0teyJjb2xvciI6ImRlZmF1bHQiLCJuYW1lIjoic3ltYm9sRW1iZWQiLCJ0aGVtZSI6Im94bWVkaXVtIn0tLT4=);
}
}
.verticalToolbarSeparator.hiddenMediumView,
#print,
#secondaryPrint,
#openFile,
#secondaryOpenFile,
#editorModeButtons {
display: none !important;
}
.page .crop-overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
//background: rgba(0,0,0,0.5);
cursor: crosshair;
z-index: 100;
}
.page .crop-overlay.inactive {
pointer-events: none;
cursor: default;
}
.page .crop-overlay canvas {
width: 100%;
height: 100%;
}
.page .highlights {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
//background: rgba(0,0,0,0.5);
z-index: 101;
pointer-events: none;
}
.page .highlights canvas {
width: 100%;
height: 100%;
}

View file

@ -1,6 +1,97 @@
var id = document.location.pathname.split('/')[1]; var id = document.location.pathname.split('/')[1];
var annotations = []; var annotations = [];
var currentPage = 1, rendered = false var currentPage = 1, rendered = false
var highlightInactive = true
var selectedAnnotation
const SELECTION = 0
const HIGHLIGHT = 1
var div = document.createElement("div")
div.innerHTML = `
<button id="cropFile" class="toolbarButton cropFile hiddenLargeView" title="Highlight" tabindex="30" data-l10n-id="crop_file">
<span data-l10n-id="crop_file_label">Highlight</span>
</button>
`
var cropFile = div.querySelector("#cropFile")
document.querySelector('#toolbarViewerRight').insertBefore(cropFile, document.querySelector('#toolbarViewerRight').firstChild)
// secondary menu
div.innerHTML = `
<button id="secondaryCropFile" class="secondaryToolbarButton visibleMediumView cropFile" title="Highlight" tabindex="50" data-l10n-id="crop">
<span data-l10n-id="crop_label">Highlight</span>
</button>
`
var secondaryCropFile = div.querySelector("#secondaryCropFile")
document.querySelector('#secondaryToolbarButtonContainer').insertBefore(
secondaryCropFile,
document.querySelector('#secondaryToolbarButtonContainer').firstChild
)
function initOverlay() {
document.querySelectorAll('#cropFile,.secondaryToolbarButton.cropFile').forEach(btn => {
btn.addEventListener('click', event=> {
if (highlightInactive) {
event.target.style.background = 'red'
highlightInactive = false
document.querySelectorAll('.crop-overlay.inactive').forEach(element => {
element.classList.remove('inactive')
})
} else {
event.target.style.background = ''
highlightInactive = true
document.querySelectorAll('.crop-overlay').forEach(element => {
element.classList.add('inactive')
})
}
})
})
PDFViewerApplication.initializedPromise.then(function() {
PDFViewerApplication.pdfViewer.eventBus.on("pagesinit", function(event) {
/*
document.querySelector('#viewerContainer').addEventListener('scroll', event => {
if (window.parent && window.parent.postMessage) {
if (first) {
first = false
} else {
window.parent.postMessage({event: 'scrolled', top: event.target.scrollTop})
}
}
})
*/
})
PDFViewerApplication.pdfViewer.eventBus.on("pagerender", function(event) {
var page = event.pageNumber.toString()
var div = event.source.div
var overlay = document.createElement('div')
overlay.classList.add('crop-overlay')
overlay.id = 'overlay' + page
if (highlightInactive) {
overlay.classList.add('inactive')
}
div.appendChild(overlay)
renderHighlightSelectionOverlay(overlay, id, page, event.source)
var highlights = document.createElement('div')
highlights.classList.add('highlights')
highlights.id = 'highlights' + page
var canvas = document.createElement('canvas')
highlights.appendChild(canvas)
div.appendChild(highlights)
renderHighlights(page)
})
PDFViewerApplication.eventBus.on('pagerendered', function(event) {
loadAnnotations(event.pageNumber)
})
})
}
document.addEventListener('DOMContentLoaded', function() {
window.PDFViewerApplication ? initOverlay() : document.addEventListener("webviewerloaded", initOverlay)
})
Ox.load({ Ox.load({
'UI': { 'UI': {
@ -25,7 +116,15 @@ Ox.load({
document.querySelector('#viewerContainer').scrollTop = el.offsetTop + el.parentElement.offsetTop - 64; document.querySelector('#viewerContainer').scrollTop = el.offsetTop + el.parentElement.offsetTop - 64;
} }
}, delay) }, delay)
var oldSelection = selectedAnnotation
selectedAnnotation = data.id
selectAnnotation(data.id) selectAnnotation(data.id)
if (oldSelection) {
var old = annotations.filter(function(a) { return a.id == oldSelection })[0]
if (old && old.type == HIGHLIGHT) {
renderHighlights(old.page)
}
}
} else if (event == 'addAnnotation') { } else if (event == 'addAnnotation') {
createAnnotation() createAnnotation()
} else if (event == 'addAnnotations') { } else if (event == 'addAnnotations') {
@ -37,7 +136,11 @@ Ox.load({
} }
data.annotations.forEach(function(annotation) { data.annotations.forEach(function(annotation) {
annotations.push(annotation) annotations.push(annotation)
renderAnnotation(annotation) if (annotation.type == HIGHLIGHT) {
renderHighlights(annotation.page)
} else {
renderAnnotation(annotation)
}
}) })
} else if (event == 'removeAnnotation') { } else if (event == 'removeAnnotation') {
removeAnnotation(data.id) removeAnnotation(data.id)
@ -75,18 +178,6 @@ window.addEventListener('mouseup', function(event) {
} }
}) })
function bindEvents() {
if (!window.PDFViewerApplication || !window.PDFViewerApplication.eventBus) {
setTimeout(bindEvents, 10)
return
}
PDFViewerApplication.eventBus.on('pagerendered', function(event) {
loadAnnotations(event.pageNumber)
})
}
bindEvents()
function getHighlight() { function getHighlight() {
var pageNumber = PDFViewerApplication.pdfViewer.currentPageNumber; var pageNumber = PDFViewerApplication.pdfViewer.currentPageNumber;
var pageIndex = pageNumber - 1; var pageIndex = pageNumber - 1;
@ -102,6 +193,7 @@ function getHighlight() {
var text = selection.toString(); var text = selection.toString();
var position = [pageNumber].concat(Ox.sort(selected.map(function(c) { return [c[1], c[0]]}))[0]); var position = [pageNumber].concat(Ox.sort(selected.map(function(c) { return [c[1], c[0]]}))[0]);
return { return {
type: SELECTION,
page: pageNumber, page: pageNumber,
pageLabel: PDFViewerApplication.pdfViewer.currentPageLabel, pageLabel: PDFViewerApplication.pdfViewer.currentPageLabel,
position: position, position: position,
@ -134,25 +226,32 @@ function renderAnnotation(annotation) {
} }
var pageElement = page.canvas.parentElement.parentElement; var pageElement = page.canvas.parentElement.parentElement;
var viewport = page.viewport; var viewport = page.viewport;
annotation.coords.forEach(function (rect) { if (annotation.type == HIGHLIGHT) {
var bounds = viewport.convertToViewportRectangle(rect); renderHighlights(annotation.page)
var el = document.createElement('div'); } else if (annotation.coords) {
el.classList.add('oml-annotation') pageElement.querySelectorAll('.oml-annotation').forEach(el => el.remove())
el.classList.add('a' + annotation.id) annotation.coords.forEach(function (rect) {
el.classList.add('page' + annotation.page) var bounds = viewport.convertToViewportRectangle(rect);
el.dataset.id = annotation.id var el = document.createElement('div');
el.setAttribute('style', 'position: absolute; background-color: yellow;opacity:0.3;' + el.classList.add('oml-annotation')
'left:' + Math.min(bounds[0], bounds[2]) + 'px; top:' + Math.min(bounds[1], bounds[3]) + 'px;' + el.classList.add('a' + annotation.id)
'width:' + Math.abs(bounds[0] - bounds[2]) + 'px; height:' + Math.abs(bounds[1] - bounds[3]) + 'px;'); el.classList.add('page' + annotation.page)
el.dataset.id = annotation.id
el.setAttribute('style', 'position: absolute; background-color: yellow;opacity:0.3;' +
'left:' + Math.min(bounds[0], bounds[2]) + 'px; top:' + Math.min(bounds[1], bounds[3]) + 'px;' +
'width:' + Math.abs(bounds[0] - bounds[2]) + 'px; height:' + Math.abs(bounds[1] - bounds[3]) + 'px;');
el.addEventListener('click', function() { el.addEventListener('click', function() {
if (!el.classList.contains('selected')) { if (!el.classList.contains('selected')) {
selectAnnotation(annotation.id) selectAnnotation(annotation.id)
Ox.$parent.postMessage('selectAnnotation', {id: annotation.id}) Ox.$parent.postMessage('selectAnnotation', {id: annotation.id})
} }
});
pageElement.appendChild(el);
}); });
pageElement.appendChild(el); } else {
}); // console.log("annotation without position", annotation)
}
} }
function addAnnotation(annotation) { function addAnnotation(annotation) {
@ -169,6 +268,11 @@ function selectAnnotation(id) {
g.classList.add('selected') g.classList.add('selected')
g.style.backgroundColor = 'blue' g.style.backgroundColor = 'blue'
}) })
annotations.forEach(a => {
if (a.id == id && a.type == HIGHLIGHT) {
renderHighlights(a.page)
}
})
} }
function deselectAnnotation(id) { function deselectAnnotation(id) {
@ -207,7 +311,7 @@ function loadAnnotations(page) {
e.remove() e.remove()
}) })
annotations.filter(function(a) { annotations.filter(function(a) {
return a.page == page return a.page == page && !a.type == HIGHLIGHT
}).forEach(function(annot) { }).forEach(function(annot) {
renderAnnotation(annot) renderAnnotation(annot)
}) })
@ -220,3 +324,145 @@ function isInView(element) {
var elementBottom = elementTop + $(element).height(); var elementBottom = elementTop + $(element).height();
return elementTop < docViewBottom && elementBottom > docViewTop; return elementTop < docViewBottom && elementBottom > docViewTop;
} }
function renderHighlightSelectionOverlay(root, documentId, page, source) {
var canvas = document.createElement('canvas')
root.appendChild(canvas)
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight
var ctx = canvas.getContext('2d');
var viewerContainer = document.querySelector('#viewerContainer')
var bounds = root.getBoundingClientRect();
var base = 2048
var scale = Math.max(bounds.height, bounds.width) / base
var last_mousex = last_mousey = 0;
var mousex = mousey = 0;
var mousedown = false;
var p = {
top: 0,
left: 0,
bottom: 0,
right: 0
}
var inside = false
canvas.addEventListener('mousedown', function(e) {
if (inside) {
const coords = [
[p.left, p.top, p.right, p.bottom]
]
addAnnotation({
type: HIGHLIGHT,
id: Ox.SHA1(pageNumber.toString() + JSON.stringify(p)),
text: "",
page: parseInt(page),
pageLabel: source.pageLabel,
coords: coords,
})
return
}
let bounds = root.getBoundingClientRect();
last_mousex = e.clientX - bounds.left;
last_mousey = e.clientY - bounds.top;
p.top = parseInt(last_mousey / scale)
p.left = parseInt(last_mousex / scale)
mousedown = true;
});
document.addEventListener('mouseup', function(e) {
if (mousedown) {
mousedown = false;
p.bottom = parseInt(mousey / scale)
p.right = parseInt(mousex / scale)
if (p.top > p.bottom) {
var t = p.top
p.top = p.bottom
p.bottom = t
}
if (p.left > p.right) {
var t = p.left
p.left = p.right
p.right = t
}
/*
var url = `${baseUrl}/documents/${documentId}/2048p${page},${p.left},${p.top},${p.right},${p.bottom}.jpg`
info.url = `${baseUrl}/document/${documentId}/${page}`
info.page = page
if (p.left != p.right && p.top != p.bottom) {
var context = formatOutput(info, url)
copyToClipboard(context)
addToRecent({
document: documentId,
page: parseInt(page),
title: info.title,
type: 'fragment',
link: `${baseUrl}/documents/${documentId}/${page}`,
src: url
})
}
*/
}
});
canvas.addEventListener('mousemove', function(e) {
let bounds = root.getBoundingClientRect();
mousex = e.clientX - bounds.left;
mousey = e.clientY - bounds.top;
if(mousedown) {
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight)
ctx.beginPath()
var width = mousex - last_mousex
var height = mousey - last_mousey
ctx.rect(last_mousex, last_mousey, width, height)
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
} else {
let py = parseInt(mousey / scale)
let px = parseInt(mousex / scale)
if (py > p.top && py < p.bottom && px > p.left && px < p.right) {
inside = true
canvas.style.cursor = 'pointer'
canvas.title = 'Click to add highlight'
} else {
inside = false
canvas.style.cursor = ''
canvas.title = ''
}
}
});
}
function renderHighlights(page) {
var pageAnnotations = annotations.filter(annotation => {
return annotation.type == HIGHLIGHT && (!page || (annotation.page == page))
})
pageAnnotations.forEach(annotation => {
let page = annotation.page
var canvas = document.querySelector(`#highlights${page} canvas`)
if (canvas) {
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight
var ctx = canvas.getContext('2d');
var viewerContainer = document.querySelector('#viewerContainer')
var bounds = canvas.parentElement.getBoundingClientRect();
var base = 2048
var scale = Math.max(bounds.height, bounds.width) / base
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight)
pageAnnotations.forEach(annotation => {
ctx.beginPath()
ctx.strokeStyle = annotation.id == selectedAnnotation ? 'blue' : 'yellow';
ctx.lineWidth = 2;
annotation.coords.forEach(coord => {
const width = coord[2] - coord[0],
height = coord[3] - coord[1];
ctx.rect(coord[0] * scale, coord[1] * scale, width * scale, height * scale)
})
ctx.stroke();
})
}
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" height="54" width="54">
<path stroke-linejoin="round" stroke="#333" stroke-linecap="round" stroke-width="5" fill="none" d="m13.2 39 35-34.6m-45 8.1h36.6v38m-27-47v36.6h38"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB