From a19693fa7ad95a01e668665ace4ed3ae99766ac8 Mon Sep 17 00:00:00 2001 From: Martynas Bagdonas Date: Fri, 22 Jan 2021 16:28:00 +0200 Subject: [PATCH] Improve PDF importing and introduce rendering --- chrome/content/zotero/xpcom/annotations.js | 23 +- chrome/content/zotero/xpcom/editorInstance.js | 5 - .../content/zotero/xpcom/pdfWorker/manager.js | 268 ++++++++++++++---- chrome/content/zotero/xpcom/reader.js | 149 +++++----- chrome/content/zotero/zoteroPane.js | 25 +- chrome/content/zotero/zoteroPane.xul | 4 - resource/pdf-renderer/renderer.html | 10 + resource/pdf-renderer/renderer.js | 118 ++++++++ 8 files changed, 439 insertions(+), 163 deletions(-) create mode 100644 resource/pdf-renderer/renderer.html create mode 100644 resource/pdf-renderer/renderer.js diff --git a/chrome/content/zotero/xpcom/annotations.js b/chrome/content/zotero/xpcom/annotations.js index 127543459b..9d0920f33b 100644 --- a/chrome/content/zotero/xpcom/annotations.js +++ b/chrome/content/zotero/xpcom/annotations.js @@ -36,10 +36,15 @@ Zotero.Annotations = new function () { var file = this._getLibraryCacheDirectory(libraryID); return OS.Path.join(file, key + '.png'); }; + + + this.hasCacheImage = async function (item) { + return OS.File.exists(this.getCacheImagePath(item)); + }; this.saveCacheImage = async function ({ libraryID, key }, blob) { - var item = await Zotero.Items.getByLibraryAndKey(libraryID, key); + var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key); if (!item) { throw new Error(`Item not found`); } @@ -96,6 +101,12 @@ Zotero.Annotations = new function () { } return OS.Path.join(...parts); }; + + + this.positionEquals = function (position1, position2) { + return position1.pageIndex == position2.pageIndex + && JSON.stringify(position1.rects) == JSON.stringify(position2.rects); + }; this.toJSON = async function (item) { @@ -103,6 +114,7 @@ Zotero.Annotations = new function () { o.libraryID = item.libraryID; o.key = item.key; o.type = item.annotationType; + o.isExternal = item.annotationIsExternal; o.isAuthor = !item.createdByUserID || item.createdByUserID == Zotero.Users.getCurrentUserID(); if (!o.isAuthor) { o.authorName = Zotero.Users.getName(item.createdByUserID); @@ -185,10 +197,19 @@ Zotero.Annotations = new function () { if (json.type == 'highlight') { item.annotationText = json.text; } + item.annotationIsExternal = !!json.isExternal; item.annotationComment = json.comment; item.annotationColor = json.color; item.annotationPageLabel = json.pageLabel; item.annotationSortIndex = json.sortIndex; + + if (item.annotationType == 'image' && item.annotationPosition) { + var currentPosition = JSON.parse(item.annotationPosition); + if (!this.positionEquals(currentPosition, json.position)) { + await this.removeCacheImage(item); + } + } + item.annotationPosition = JSON.stringify(Object.assign({}, json.position)); // TODO: Can colors be set? item.setTags((json.tags || []).map(t => ({ tag: t.name }))); diff --git a/chrome/content/zotero/xpcom/editorInstance.js b/chrome/content/zotero/xpcom/editorInstance.js index 958648d3d1..fdf34c425a 100644 --- a/chrome/content/zotero/xpcom/editorInstance.js +++ b/chrome/content/zotero/xpcom/editorInstance.js @@ -253,8 +253,6 @@ class EditorInstance { html += `

(${formatted})

`; } else if (item.isNote()) { - // TODO: Remove when fixed - item._loaded.childItems = true; let note = item.note; let attachments = await Zotero.Items.getAsync(item.getAttachments()); for (let attachment of attachments) { @@ -417,8 +415,6 @@ class EditorInstance { if (this._isAttachment) { return; } - // TODO: Remove when fixed - this._item._loaded.childItems = true; let attachmentItems = this._item.getAttachments().map(id => Zotero.Items.get(id)); let abandonedItems = attachmentItems.filter(item => !attachmentKeys.includes(item.key)); for (let item of abandonedItems) { @@ -895,7 +891,6 @@ class EditorInstance { editorInstance._item = note; let jsonAnnotations = []; for (let annotation of annotations) { - annotation._loaded.childItems = true; let jsonAnnotation = await Zotero.Annotations.toJSON(annotation); jsonAnnotation.itemId = attachmentItem.id; jsonAnnotations.push(jsonAnnotation); diff --git a/chrome/content/zotero/xpcom/pdfWorker/manager.js b/chrome/content/zotero/xpcom/pdfWorker/manager.js index a9e0693427..bab6552367 100644 --- a/chrome/content/zotero/xpcom/pdfWorker/manager.js +++ b/chrome/content/zotero/xpcom/pdfWorker/manager.js @@ -25,6 +25,7 @@ const WORKER_URL = 'chrome://zotero/content/xpcom/pdfWorker/worker.js'; const CMAPS_URL = 'resource://zotero/pdf-reader/cmaps/'; +const RENDERER_URL = 'resource://zotero/pdf-renderer/renderer.html'; class PDFWorker { constructor() { @@ -122,20 +123,16 @@ class PDFWorker { Zotero.debug(event); }); } - - isPDFAttachment(item) { - return item.isAttachment() && item.attachmentContentType === 'application/pdf'; - } - + canImport(item) { - if (this.isPDFAttachment(item)) { + if (item.isPDFAttachment()) { return true; } else if (item.isRegularItem()) { let ids = item.getAttachments(); for (let id of ids) { let attachment = Zotero.Items.get(id); - if (this.isPDFAttachment(attachment)) { + if (attachment.isPDFAttachment()) { return true; } } @@ -154,8 +151,8 @@ class PDFWorker { async export(itemID, path, isPriority, password) { return this._enqueue(async () => { let attachment = await Zotero.Items.getAsync(itemID); - if (!this.isPDFAttachment(attachment)) { - throw new Error('not a valid attachment'); + if (!attachment.isPDFAttachment()) { + throw new Error('Item must be a PDF attachment'); } let items = attachment.getAnnotations(); let annotations = []; @@ -163,7 +160,7 @@ class PDFWorker { annotations.push({ id: item.key, type: item.annotationType, - authorName: Zotero.Users.getName(item.createdByUserID) || '', + authorName: Zotero.Users.getName(item.createdByUserID) || Zotero.Users.getCurrentUsername() || '', comment: item.annotationComment || '', color: item.annotationColor, position: JSON.parse(item.annotationPosition), @@ -187,7 +184,7 @@ class PDFWorker { */ async exportParent(item, directory) { if (!item.isRegularItem()) { - throw new Error('regular item not provided'); + throw new Error('Item must be a regular item'); } if (!directory) { throw new Error('\'directory\' not provided'); @@ -196,7 +193,7 @@ class PDFWorker { let ids = item.getAttachments(); for (let id of ids) { let attachment = Zotero.Items.get(id); - if (this.isPDFAttachment(attachment)) { + if (attachment.isPDFAttachment()) { let path = OS.Path.join(directory, attachment.attachmentFilename); promises.push(this.export(id, path)); } @@ -207,57 +204,56 @@ class PDFWorker { /** * Import annotations from PDF attachment * - * @param {Integer} itemID - * @param {Boolean} save Save imported annotations, or otherwise just return the number of importable annotations + * @param {Integer} itemID Attachment item id * @param {Boolean} isPriority * @param {String} password * @returns {Promise} Number of annotations */ - async import(itemID, save, isPriority, password) { + async import(itemID, isPriority, password) { return this._enqueue(async () => { let attachment = await Zotero.Items.getAsync(itemID); - if (!this.isPDFAttachment(attachment)) { - throw new Error('not a valid PDF attachment'); + if (!attachment.isPDFAttachment()) { + throw new Error('Item must be a PDF attachment'); } - // TODO: Remove when fixed - attachment._loaded.childItems = true; - let items = attachment.getAnnotations(); - let existingAnnotations = []; - for (let item of items) { - existingAnnotations.push({ - id: item.key, - type: item.annotationType, - comment: item.annotationComment || '', - position: JSON.parse(item.annotationPosition) - }); + + let mtime = Math.floor(await attachment.attachmentModificationTime / 1000); + if (attachment.attachmentLastProcessedModificationTime === mtime) { + return false; } + + let existingAnnotations = attachment + .getAnnotations() + .filter(x => x.annotationIsExternal) + .map(annotation => ({ + id: annotation.key, + type: annotation.annotationType, + position: JSON.parse(annotation.annotationPosition), + comment: annotation.annotationComment || '' + })); + let path = await attachment.getFilePath(); let buf = await OS.File.read(path, {}); buf = new Uint8Array(buf).buffer; - let res = await this._query('import', { buf, existingAnnotations, password }, [buf]); - let annotations = res.annotations; - if (save) { - for (let annotation of annotations) { - // TODO: Utilize the saved Zotero item key for deduplication. Newer annotation modificaiton date wins - annotation.key = Zotero.DataObjectUtilities.generateKey(); - await Zotero.Annotations.saveFromJSON(attachment, annotation); - } - attachment.attachmentHasUnimportedAnnotations = false; + let { imported, deleted } = await this._query('import', { + buf, existingAnnotations, password + }, [buf]); + + for (let annotation of imported) { + annotation.key = Zotero.DataObjectUtilities.generateKey(); + annotation.isExternal = true; + await Zotero.Annotations.saveFromJSON(attachment, annotation); } - else { - attachment.attachmentHasUnimportedAnnotations = !!annotations.length; + + for (let key of deleted) { + let annotation = Zotero.Items.getByLibraryAndKey(attachment.libraryID, key); + await annotation.eraseTx(); } - for (let reader of Zotero.Reader._readers) { - if (reader._itemID === itemID) { - reader.toggleImportPrompt(attachment.attachmentHasUnimportedAnnotations); - } - } - attachment.attachmentLastProcessedModificationTime = Math.floor( - await attachment.attachmentModificationTime / 1000 - ); - await attachment.saveTx(); - return annotations.length; - }); + + attachment.attachmentLastProcessedModificationTime = mtime; + await attachment.saveTx({ skipDateModifiedUpdate: true }); + + return !!(imported.length || deleted.length); + }, isPriority); } /** @@ -267,14 +263,14 @@ class PDFWorker { */ async importParent(item) { if (!item.isRegularItem()) { - throw new Error('regular item not provided'); + throw new Error('Item must be a regular item'); } let promises = []; let ids = item.getAttachments(); for (let id of ids) { let attachment = Zotero.Items.get(id); - if (this.isPDFAttachment(attachment)) { - promises.push(this.import(id, true)); + if (attachment.isPDFAttachment()) { + promises.push(this.import({ itemID: id, isPriority: true })); } } await Promise.all(promises); @@ -282,3 +278,169 @@ class PDFWorker { } Zotero.PDFWorker = new PDFWorker(); + + +// PDF Renderer +class PDFRenderer { + constructor() { + this._browser = null; + this._lastPromiseID = 0; + this._waitingPromises = {}; + this._queue = []; + this._processingQueue = false; + } + + async _processQueue() { + await this._init(); + if (this._processingQueue) { + return; + } + this._processingQueue = true; + let item; + while ((item = this._queue.shift())) { + if (item) { + let [fn, resolve, reject] = item; + try { + resolve(await fn()); + } + catch (e) { + reject(e); + } + } + } + this._processingQueue = false; + } + + async _enqueue(fn, isPriority) { + return new Promise((resolve, reject) => { + if (isPriority) { + this._queue.unshift([fn, resolve, reject]); + } + else { + this._queue.push([fn, resolve, reject]); + } + this._processQueue(); + }); + } + + async _query(action, data, transfer) { + return new Promise((resolve, reject) => { + this._lastPromiseID++; + this._waitingPromises[this._lastPromiseID] = { resolve, reject }; + this._browser.contentWindow.postMessage({ + id: this._lastPromiseID, + action, + data + }, this._browser.contentWindow.origin, transfer); + }); + } + + async _init() { + if (this._browser) return; + return new Promise((resolve) => { + this._browser = Zotero.Browser.createHiddenBrowser(); + let doc = this._browser.ownerDocument; + let container = doc.createElement('hbox'); + container.style.position = 'fixed'; + container.style.zIndex = '-1'; + container.append(this._browser); + doc.documentElement.append(container); + this._browser.style.width = '1px'; + this._browser.style.height = '1px'; + this._browser.addEventListener('DOMContentLoaded', (event) => { + if (this._browser.contentWindow.location.href === 'about:blank') return; + this._browser.contentWindow.addEventListener('message', _handleMessage); + }); + this._browser.loadURI(RENDERER_URL); + + let _handleMessage = async (event) => { + let message = event.data; + if (message.responseId) { + let { resolve, reject } = this._waitingPromises[message.responseId]; + delete this._waitingPromises[message.responseId]; + if (message.data) { + resolve(message.data); + } + else { + let err = new Error(message.error.message); + Object.assign(err, message.error); + reject(err); + } + return; + } + + if (message.action === 'initialized') { + resolve(); + } + else if (message.action === 'renderedAnnotation') { + let { id, image } = message.data.annotation; + let item = await Zotero.Items.getAsync(id); + let win = Zotero.getMainWindow(); + if (!win) { + return; + } + let blob = new win.Blob([new Uint8Array(image)]); + await Zotero.Annotations.saveCacheImage(item, blob); + await Zotero.Notifier.trigger('modify', 'item', [item.id]); + } + }; + }); + } + + /** + * Render missing image annotation images for attachment + * + * @param {Integer} itemID Attachment item id + * @param {Boolean} isPriority + * @returns {Promise} + */ + async renderAttachmentAnnotations(itemID, isPriority) { + return this._enqueue(async () => { + let attachment = await Zotero.Items.getAsync(itemID); + let annotations = []; + for (let annotation of attachment.getAnnotations()) { + if (annotation.annotationType === 'image' + && !await Zotero.Annotations.hasCacheImage(annotation)) { + annotations.push({ + id: annotation.id, + position: JSON.parse(annotation.annotationPosition) + }); + } + } + if (!annotations.length) { + return 0; + } + let path = await attachment.getFilePath(); + let buf = await OS.File.read(path, {}); + buf = new Uint8Array(buf).buffer; + return this._query('renderAnnotations', { buf, annotations }, [buf]); + }, isPriority); + } + + /** + * Render image annotation image + * + * @param {Integer} itemID Attachment item id + * @param {Boolean} isPriority + * @returns {Promise} + */ + async renderAnnotation(itemID, isPriority) { + return this._enqueue(async () => { + let annotation = await Zotero.Items.getAsync(itemID); + if (await Zotero.Annotations.hasCacheImage(annotation)) { + return false; + } + let attachment = await Zotero.Items.getAsync(annotation.parentID); + let path = await attachment.getFilePath(); + let buf = await OS.File.read(path, {}); + buf = new Uint8Array(buf).buffer; + let annotations = [{ + id: annotation.id, + position: JSON.parse(annotation.annotationPosition) + }]; + return !!await this._query('renderAnnotations', { buf, annotations }, [buf]); + }, isPriority); + } +} + +Zotero.PDFRenderer = new PDFRenderer(); diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js index 0df7ee5d35..59e3f8deb8 100644 --- a/chrome/content/zotero/xpcom/reader.js +++ b/chrome/content/zotero/xpcom/reader.js @@ -1,8 +1,27 @@ -// Temporary stuff -Zotero.PDF = { - dateChecked: {}, - hasUnmachedAnnotations: {} -}; +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2021 Corporation for Digital Scholarship + Vienna, Virginia, USA + http://digitalscholar.org/ + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ class ReaderInstance { constructor() { @@ -46,7 +65,7 @@ class ReaderInstance { annotations, state, location, - promptImport: item.attachmentHasUnimportedAnnotations, + promptImport: false, showItemPaneToggle: this._showItemPaneToggle, sidebarWidth: this._sidebarWidth, sidebarOpen: this._sidebarOpen, @@ -68,10 +87,10 @@ class ReaderInstance { this._setTitleValue(title); } - async setAnnotations(ids) { + async setAnnotations(items) { let annotations = []; - for (let id of ids) { - let annotation = await this._getAnnotation(id); + for (let item of items) { + let annotation = await this._getAnnotation(item); if (annotation) { annotations.push(annotation); } @@ -90,10 +109,6 @@ class ReaderInstance { async navigate(location) { this._postMessage({ action: 'navigate', location }); } - - toggleImportPrompt(enable) { - this._postMessage({ action: 'toggleImportPrompt', enable }); - } enableAddToNote(enable) { this._postMessage({ action: 'enableAddToNote', enable }); @@ -292,23 +307,10 @@ class ReaderInstance { } }; let savedAnnotation = await Zotero.Annotations.saveFromJSON(attachment, annotation, saveOptions); - if (annotation.image) { + + if (annotation.image && !await Zotero.Annotations.hasCacheImage(savedAnnotation)) { let blob = this._dataURLtoBlob(annotation.image); - let attachmentIds = savedAnnotation.getAttachments(); - if (attachmentIds.length) { - let attachment = Zotero.Items.get(attachmentIds[0]); - let path = await attachment.getFilePathAsync(); - await Zotero.File.putContentsAsync(path, blob); - await Zotero.Sync.Storage.Local.updateSyncStates([attachment], 'to_upload'); - Zotero.Notifier.trigger('modify', 'item', attachment.id, { instanceID: this._instanceID }); - } - else { - await Zotero.Attachments.importEmbeddedImage({ - blob, - parentItemID: savedAnnotation.id, - saveOptions - }); - } + await Zotero.Annotations.saveCacheImage(savedAnnotation, blob); } return; } @@ -360,11 +362,11 @@ class ReaderInstance { } return; } - case 'import': { - Zotero.debug('Importing PDF annotations'); - Zotero.PDFWorker.import(this._itemID, true, true); - return; - } + // case 'import': { + // Zotero.debug('Importing PDF annotations'); + // Zotero.PDFWorker.import(this._itemID, true, true); + // return; + // } case 'importDismiss': { Zotero.debug('Dismiss PDF annotations'); return; @@ -434,7 +436,8 @@ class ReaderInstance { /** * Return item JSON in the pdf-reader ready format - * @param itemID + * + * @param {Zotero.Item} item * @returns {Object|null} */ async _getAnnotation(item) { @@ -442,10 +445,9 @@ class ReaderInstance { if (!item || !item.isAnnotation()) { return null; } - // TODO: Remve when fixed - item._loaded.childItems = true; let json = await Zotero.Annotations.toJSON(item); json.id = item.key; + json.readOnly = !json.isAuthor || json.isExternal; delete json.key; for (let key in json) { json[key] = json[key] || ''; @@ -622,7 +624,7 @@ class Reader { this._sidebarOpen = false; this._bottomPlaceholderHeight = 800; this._readers = []; - this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'reader'); + this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'reader'); this.onChangeSidebarWidth = null; this.onChangeSidebarOpen = null; @@ -682,47 +684,42 @@ class Reader { reader.setBottomPlaceholderHeight(height); } } - + notify(event, type, ids, extraData) { - // Listen for the parent item, PDF attachment and its annotation items updates - for (let readerWindow of this._readers) { - if (event === 'delete') { - let disappearedIds = readerWindow.annotationItemIDs.filter(x => ids.includes(x)); - if (disappearedIds.length) { - let keys = disappearedIds.map(id => extraData[id].key); - readerWindow.unsetAnnotations(keys); - } - if (ids.includes(readerWindow._itemID)) { - readerWindow.close(); - } + if (type === 'tab') { + var reader = Zotero.Reader.getByTabID(ids[0]); + if (reader) { + this.triggerAnnotationsImportCheck(reader._itemID); } - else { - let item = Zotero.Items.get(readerWindow._itemID); - // TODO: Remove when fixed - item._loaded.childItems = true; - let annotationItems = item.getAnnotations(); - readerWindow.annotationItemIDs = annotationItems.map(x => x.id); - let affectedAnnotationIds = annotationItems.filter(annotation => { - let annotationID = annotation.id; - let imageAttachmentID = null; - annotation._loaded.childItems = true; - let annotationAttachments = annotation.getAttachments(); - if (annotationAttachments.length) { - imageAttachmentID = annotationAttachments[0]; + } + else if (type === 'item') { + // Listen for the parent item, PDF attachment and its annotations updates + for (let reader of this._readers) { + if (event === 'delete') { + let disappearedIds = reader.annotationItemIDs.filter(x => ids.includes(x)); + if (disappearedIds.length) { + let keys = disappearedIds.map(id => extraData[id].key); + reader.unsetAnnotations(keys); + } + if (ids.includes(reader._itemID)) { + reader.close(); } - return ( - ids.includes(annotationID) && !(extraData[annotationID] - && extraData[annotationID].instanceID === readerWindow._instanceID) - || ids.includes(imageAttachmentID) && !(extraData[imageAttachmentID] - && extraData[imageAttachmentID].instanceID === readerWindow._instanceID) - ); - }); - if (affectedAnnotationIds.length) { - readerWindow.setAnnotations(affectedAnnotationIds); } - // Update title if the PDF attachment or the parent item changes - if (ids.includes(readerWindow._itemID) || ids.includes(item.parentItemID)) { - readerWindow.updateTitle(); + else { + let item = Zotero.Items.get(reader._itemID); + let annotationItems = item.getAnnotations(); + reader.annotationItemIDs = annotationItems.map(x => x.id); + let affectedAnnotations = annotationItems.filter(({ id }) => ( + ids.includes(id) + && !(extraData && extraData[id] && extraData[id].instanceID === reader._instanceID) + )); + if (affectedAnnotations.length) { + reader.setAnnotations(affectedAnnotations); + } + // Update title if the PDF attachment or the parent item changes + if (ids.includes(reader._itemID) || ids.includes(item.parentItemID)) { + reader.updateTitle(); + } } } } @@ -812,7 +809,7 @@ class Reader { let item = await Zotero.Items.getAsync(itemID); let mtime = await item.attachmentModificationTime; if (item.attachmentLastProcessedModificationTime < Math.floor(mtime / 1000)) { - await Zotero.PDFWorker.import(itemID, false); + await Zotero.PDFWorker.import(itemID, true); } } } diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index ece61940d9..d6e71fbf95 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -2745,7 +2745,6 @@ var ZoteroPane = new function() 'createParent', 'renameAttachments', 'reindexItem', - 'importAnnotations', 'createNoteFromAnnotations' ]; @@ -2793,8 +2792,7 @@ var ZoteroPane = new function() canIndex = true, canRecognize = true, canUnrecognize = true, - canRename = true, - canImportAnnotations = true; + canRename = true; var canMarkRead = collectionTreeRow.isFeed(); var markUnread = true; @@ -2816,10 +2814,6 @@ var ZoteroPane = new function() canUnrecognize = false; } - if (canImportAnnotations && !Zotero.PDFWorker.canImport(item)) { - canImportAnnotations = false; - } - // Show rename option only if all items are child attachments if (canRename && (!item.isAttachment() || item.isTopLevelItem() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL)) { canRename = false; @@ -2899,9 +2893,6 @@ var ZoteroPane = new function() } } - if (canImportAnnotations) { - show.push(m.importAnnotations); - } } // Single item selected @@ -2971,9 +2962,6 @@ var ZoteroPane = new function() show.push(m.duplicateItem); } - if (Zotero.PDFWorker.canImport(item)) { - show.push(m.importAnnotations); - } if (Zotero.EditorInstance.canCreateNoteFromAnnotations(item)) { show.push(m.createNoteFromAnnotations); @@ -4579,17 +4567,6 @@ var ZoteroPane = new function() } }; - this.importAnnotationsForSelected = async function () { - let items = ZoteroPane.getSelectedItems(); - for (let item of items) { - if (item.isRegularItem()) { - Zotero.PDFWorker.importParent(item); - } - else if (item.isAttachment()) { - Zotero.PDFWorker.import(item.id, true); - } - } - }; this.reportMetadataForSelected = async function () { let items = ZoteroPane.getSelectedItems(); diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul index 63704375ae..8468723041 100644 --- a/chrome/content/zotero/zoteroPane.xul +++ b/chrome/content/zotero/zoteroPane.xul @@ -308,10 +308,6 @@ - - - - diff --git a/resource/pdf-renderer/renderer.html b/resource/pdf-renderer/renderer.html new file mode 100644 index 0000000000..d9dc111448 --- /dev/null +++ b/resource/pdf-renderer/renderer.html @@ -0,0 +1,10 @@ + + + + + PDF Renderer + + + + + diff --git a/resource/pdf-renderer/renderer.js b/resource/pdf-renderer/renderer.js new file mode 100644 index 0000000000..f02bf90aec --- /dev/null +++ b/resource/pdf-renderer/renderer.js @@ -0,0 +1,118 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2021 Corporation for Digital Scholarship + Vienna, Virginia, USA + http://digitalscholar.org/ + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +const SCALE_FACTOR = 4; + +window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'resource://zotero/pdf-reader/pdf.worker.js'; + +function errObject(err) { + return JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))); +} + +async function renderAnnotations(buf, annotations, send) { + let num = 0; + let pdfDocument = await window.pdfjsLib.getDocument({ data: buf }).promise; + let pages = new Map(); + for (let annotation of annotations) { + let pageIndex = annotation.position.pageIndex; + let page = pages.get(pageIndex) || []; + page.push(annotation); + pages.set(pageIndex, page); + } + for (let [pageIndex, annotations] of pages) { + let { canvas, viewport } = await renderPage(pdfDocument, pageIndex); + for (let annotation of annotations) { + let position = p2v(annotation.position, viewport); + let rect = position.rects[0]; + let [left, top, right, bottom] = rect; + let width = right - left; + let height = bottom - top; + let newCanvas = document.createElement('canvas'); + newCanvas.width = width; + newCanvas.height = height; + let newCanvasContext = newCanvas.getContext('2d'); + newCanvasContext.drawImage(canvas, left, top, width, height, 0, 0, width, height); + newCanvas.toBlob(async (blob) => { + let image = await new Response(blob).arrayBuffer(); + send({ id: annotation.id, image }); + }, 'image/png'); + num++; + } + } + return num; +} + +function p2v(position, viewport) { + return { + pageIndex: position.pageIndex, + rects: position.rects.map(rect => { + let [x1, y2] = viewport.convertToViewportPoint(rect[0], rect[1]); + let [x2, y1] = viewport.convertToViewportPoint(rect[2], rect[3]); + return [Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2)]; + }) + }; +} + +async function renderPage(pdfDocument, pageIndex) { + let page = await pdfDocument.getPage(pageIndex + 1); + var canvas = document.createElement('canvas'); + var viewport = page.getViewport({ scale: SCALE_FACTOR }); + var context = canvas.getContext('2d', { alpha: false }); + canvas.height = viewport.height; + canvas.width = viewport.width; + await page.render({ canvasContext: context, viewport: viewport }).promise; + return { canvas, viewport }; +} + +window.addEventListener('message', async (event) => { + if (event.source === parent) { + return; + } + let message = event.data; + if (message.action === 'renderAnnotations') { + try { + let { buf, annotations } = message.data; + let num = await renderAnnotations( + buf, + annotations, + (annotation) => { + parent.postMessage({ action: 'renderedAnnotation', data: { annotation } }, parent.origin, [annotation.image]); + } + ); + parent.postMessage({ responseId: message.id, data: num }, parent.origin); + } + catch (e) { + console.log(e); + parent.postMessage({ + responseId: message.id, + error: errObject(e) + }, parent.origin); + } + } +}); + +setTimeout(() => { + parent.postMessage({ action: 'initialized' }, parent.origin); +}, 100);