diff --git a/static/css/oml.css b/static/css/oml.css index 375341b..dabc0a9 100644 --- a/static/css/oml.css +++ b/static/css/oml.css @@ -10,6 +10,10 @@ font-size: 14px; line-height: 21px; } +.OMLQuote img { + max-width: 100%; + margin: auto; +} .OMLAnnotation .OMLQuoteBackground { position: absolute; diff --git a/static/js/annotation.js b/static/js/annotation.js index 6df2090..d82a584 100644 --- a/static/js/annotation.js +++ b/static/js/annotation.js @@ -1,9 +1,18 @@ 'use strict'; +oml.SELECTION = 0 +oml.HIGHLIGHT = 1 + oml.ui.annotation = function(annotation, $iframe) { + var value = Ox.encodeHTMLEntities(annotation.text).replace(/\n/g, '
') + 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 = `` + } var $quoteText = Ox.Element() .addClass('OxSelectable OMLQuote') - .html(Ox.encodeHTMLEntities(annotation.text).replace(/\n/g, '
')) + .html(value) .on({ click: function(event) { var id diff --git a/static/js/viewer.js b/static/js/viewer.js index d621087..89bc798 100644 --- a/static/js/viewer.js +++ b/static/js/viewer.js @@ -151,7 +151,7 @@ oml.ui.viewer = function() { height: '100%', border: 0 }).onMessage(function(data, event) { - console.log('got', event, data) + // console.log('got', event, data, data.page) if (event == 'addAnnotation') { addAnnotation(data); var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents) @@ -217,7 +217,7 @@ oml.ui.viewer = function() { } var map = {} map[sortKey] = function(value) { - return value.toString(); + return value ? value.toString() : ''; } annotations = Ox.sortBy(annotations, sortKey, map) oml.$ui.annotationFolder.empty(); diff --git a/static/reader/pdf.css b/static/reader/pdf.css new file mode 100644 index 0000000..467b305 --- /dev/null +++ b/static/reader/pdf.css @@ -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%; +} diff --git a/static/reader/pdf.js b/static/reader/pdf.js index 0a0b50b..8ac01eb 100644 --- a/static/reader/pdf.js +++ b/static/reader/pdf.js @@ -1,6 +1,97 @@ var id = document.location.pathname.split('/')[1]; var annotations = []; var currentPage = 1, rendered = false +var highlightInactive = true +var selectedAnnotation + +const SELECTION = 0 +const HIGHLIGHT = 1 + + +var div = document.createElement("div") +div.innerHTML = ` + +` +var cropFile = div.querySelector("#cropFile") + +document.querySelector('#toolbarViewerRight').insertBefore(cropFile, document.querySelector('#toolbarViewerRight').firstChild) + +// secondary menu +div.innerHTML = ` + +` +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({ 'UI': { @@ -25,7 +116,15 @@ Ox.load({ document.querySelector('#viewerContainer').scrollTop = el.offsetTop + el.parentElement.offsetTop - 64; } }, delay) + var oldSelection = selectedAnnotation + selectedAnnotation = 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') { createAnnotation() } else if (event == 'addAnnotations') { @@ -37,7 +136,11 @@ Ox.load({ } data.annotations.forEach(function(annotation) { annotations.push(annotation) - renderAnnotation(annotation) + if (annotation.type == HIGHLIGHT) { + renderHighlights(annotation.page) + } else { + renderAnnotation(annotation) + } }) } else if (event == 'removeAnnotation') { 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() { var pageNumber = PDFViewerApplication.pdfViewer.currentPageNumber; var pageIndex = pageNumber - 1; @@ -102,6 +193,7 @@ function getHighlight() { var text = selection.toString(); var position = [pageNumber].concat(Ox.sort(selected.map(function(c) { return [c[1], c[0]]}))[0]); return { + type: SELECTION, page: pageNumber, pageLabel: PDFViewerApplication.pdfViewer.currentPageLabel, position: position, @@ -134,25 +226,32 @@ function renderAnnotation(annotation) { } var pageElement = page.canvas.parentElement.parentElement; var viewport = page.viewport; - annotation.coords.forEach(function (rect) { - var bounds = viewport.convertToViewportRectangle(rect); - var el = document.createElement('div'); - el.classList.add('oml-annotation') - el.classList.add('a' + annotation.id) - 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;'); + if (annotation.type == HIGHLIGHT) { + renderHighlights(annotation.page) + } else if (annotation.coords) { + pageElement.querySelectorAll('.oml-annotation').forEach(el => el.remove()) + annotation.coords.forEach(function (rect) { + var bounds = viewport.convertToViewportRectangle(rect); + var el = document.createElement('div'); + el.classList.add('oml-annotation') + el.classList.add('a' + annotation.id) + 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() { - if (!el.classList.contains('selected')) { - selectAnnotation(annotation.id) - Ox.$parent.postMessage('selectAnnotation', {id: annotation.id}) - } + el.addEventListener('click', function() { + if (!el.classList.contains('selected')) { + selectAnnotation(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) { @@ -169,6 +268,11 @@ function selectAnnotation(id) { g.classList.add('selected') g.style.backgroundColor = 'blue' }) + annotations.forEach(a => { + if (a.id == id && a.type == HIGHLIGHT) { + renderHighlights(a.page) + } + }) } function deselectAnnotation(id) { @@ -207,7 +311,7 @@ function loadAnnotations(page) { e.remove() }) annotations.filter(function(a) { - return a.page == page + return a.page == page && !a.type == HIGHLIGHT }).forEach(function(annot) { renderAnnotation(annot) }) @@ -220,3 +324,145 @@ function isInView(element) { var elementBottom = elementTop + $(element).height(); 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(); + }) + + } + }) + +} diff --git a/static/reader/pdf/toolbarButton-crop.png b/static/reader/pdf/toolbarButton-crop.png new file mode 100644 index 0000000..00c56d5 Binary files /dev/null and b/static/reader/pdf/toolbarButton-crop.png differ diff --git a/static/reader/pdf/toolbarButton-crop.svg b/static/reader/pdf/toolbarButton-crop.svg new file mode 100644 index 0000000..618195a --- /dev/null +++ b/static/reader/pdf/toolbarButton-crop.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/reader/pdf/toolbarButton-crop@10.png b/static/reader/pdf/toolbarButton-crop@10.png new file mode 100644 index 0000000..7cc05e5 Binary files /dev/null and b/static/reader/pdf/toolbarButton-crop@10.png differ diff --git a/static/reader/pdf/toolbarButton-crop@2x.png b/static/reader/pdf/toolbarButton-crop@2x.png new file mode 100644 index 0000000..950d275 Binary files /dev/null and b/static/reader/pdf/toolbarButton-crop@2x.png differ