Improve PDF importing and introduce rendering

This commit is contained in:
Martynas Bagdonas 2021-01-22 16:28:00 +02:00 committed by Dan Stillman
parent 89d9efdec7
commit a19693fa7a
8 changed files with 439 additions and 163 deletions

View file

@ -36,10 +36,15 @@ Zotero.Annotations = new function () {
var file = this._getLibraryCacheDirectory(libraryID); var file = this._getLibraryCacheDirectory(libraryID);
return OS.Path.join(file, key + '.png'); 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) { 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) { if (!item) {
throw new Error(`Item not found`); throw new Error(`Item not found`);
} }
@ -96,6 +101,12 @@ Zotero.Annotations = new function () {
} }
return OS.Path.join(...parts); 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) { this.toJSON = async function (item) {
@ -103,6 +114,7 @@ Zotero.Annotations = new function () {
o.libraryID = item.libraryID; o.libraryID = item.libraryID;
o.key = item.key; o.key = item.key;
o.type = item.annotationType; o.type = item.annotationType;
o.isExternal = item.annotationIsExternal;
o.isAuthor = !item.createdByUserID || item.createdByUserID == Zotero.Users.getCurrentUserID(); o.isAuthor = !item.createdByUserID || item.createdByUserID == Zotero.Users.getCurrentUserID();
if (!o.isAuthor) { if (!o.isAuthor) {
o.authorName = Zotero.Users.getName(item.createdByUserID); o.authorName = Zotero.Users.getName(item.createdByUserID);
@ -185,10 +197,19 @@ Zotero.Annotations = new function () {
if (json.type == 'highlight') { if (json.type == 'highlight') {
item.annotationText = json.text; item.annotationText = json.text;
} }
item.annotationIsExternal = !!json.isExternal;
item.annotationComment = json.comment; item.annotationComment = json.comment;
item.annotationColor = json.color; item.annotationColor = json.color;
item.annotationPageLabel = json.pageLabel; item.annotationPageLabel = json.pageLabel;
item.annotationSortIndex = json.sortIndex; 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)); item.annotationPosition = JSON.stringify(Object.assign({}, json.position));
// TODO: Can colors be set? // TODO: Can colors be set?
item.setTags((json.tags || []).map(t => ({ tag: t.name }))); item.setTags((json.tags || []).map(t => ({ tag: t.name })));

View file

@ -253,8 +253,6 @@ class EditorInstance {
html += `<p><span class="citation" data-citation="${encodeURIComponent(JSON.stringify(citation))}">(${formatted})</span></p>`; html += `<p><span class="citation" data-citation="${encodeURIComponent(JSON.stringify(citation))}">(${formatted})</span></p>`;
} }
else if (item.isNote()) { else if (item.isNote()) {
// TODO: Remove when fixed
item._loaded.childItems = true;
let note = item.note; let note = item.note;
let attachments = await Zotero.Items.getAsync(item.getAttachments()); let attachments = await Zotero.Items.getAsync(item.getAttachments());
for (let attachment of attachments) { for (let attachment of attachments) {
@ -417,8 +415,6 @@ class EditorInstance {
if (this._isAttachment) { if (this._isAttachment) {
return; return;
} }
// TODO: Remove when fixed
this._item._loaded.childItems = true;
let attachmentItems = this._item.getAttachments().map(id => Zotero.Items.get(id)); let attachmentItems = this._item.getAttachments().map(id => Zotero.Items.get(id));
let abandonedItems = attachmentItems.filter(item => !attachmentKeys.includes(item.key)); let abandonedItems = attachmentItems.filter(item => !attachmentKeys.includes(item.key));
for (let item of abandonedItems) { for (let item of abandonedItems) {
@ -895,7 +891,6 @@ class EditorInstance {
editorInstance._item = note; editorInstance._item = note;
let jsonAnnotations = []; let jsonAnnotations = [];
for (let annotation of annotations) { for (let annotation of annotations) {
annotation._loaded.childItems = true;
let jsonAnnotation = await Zotero.Annotations.toJSON(annotation); let jsonAnnotation = await Zotero.Annotations.toJSON(annotation);
jsonAnnotation.itemId = attachmentItem.id; jsonAnnotation.itemId = attachmentItem.id;
jsonAnnotations.push(jsonAnnotation); jsonAnnotations.push(jsonAnnotation);

View file

@ -25,6 +25,7 @@
const WORKER_URL = 'chrome://zotero/content/xpcom/pdfWorker/worker.js'; const WORKER_URL = 'chrome://zotero/content/xpcom/pdfWorker/worker.js';
const CMAPS_URL = 'resource://zotero/pdf-reader/cmaps/'; const CMAPS_URL = 'resource://zotero/pdf-reader/cmaps/';
const RENDERER_URL = 'resource://zotero/pdf-renderer/renderer.html';
class PDFWorker { class PDFWorker {
constructor() { constructor() {
@ -122,20 +123,16 @@ class PDFWorker {
Zotero.debug(event); Zotero.debug(event);
}); });
} }
isPDFAttachment(item) {
return item.isAttachment() && item.attachmentContentType === 'application/pdf';
}
canImport(item) { canImport(item) {
if (this.isPDFAttachment(item)) { if (item.isPDFAttachment()) {
return true; return true;
} }
else if (item.isRegularItem()) { else if (item.isRegularItem()) {
let ids = item.getAttachments(); let ids = item.getAttachments();
for (let id of ids) { for (let id of ids) {
let attachment = Zotero.Items.get(id); let attachment = Zotero.Items.get(id);
if (this.isPDFAttachment(attachment)) { if (attachment.isPDFAttachment()) {
return true; return true;
} }
} }
@ -154,8 +151,8 @@ class PDFWorker {
async export(itemID, path, isPriority, password) { async export(itemID, path, isPriority, password) {
return this._enqueue(async () => { return this._enqueue(async () => {
let attachment = await Zotero.Items.getAsync(itemID); let attachment = await Zotero.Items.getAsync(itemID);
if (!this.isPDFAttachment(attachment)) { if (!attachment.isPDFAttachment()) {
throw new Error('not a valid attachment'); throw new Error('Item must be a PDF attachment');
} }
let items = attachment.getAnnotations(); let items = attachment.getAnnotations();
let annotations = []; let annotations = [];
@ -163,7 +160,7 @@ class PDFWorker {
annotations.push({ annotations.push({
id: item.key, id: item.key,
type: item.annotationType, type: item.annotationType,
authorName: Zotero.Users.getName(item.createdByUserID) || '', authorName: Zotero.Users.getName(item.createdByUserID) || Zotero.Users.getCurrentUsername() || '',
comment: item.annotationComment || '', comment: item.annotationComment || '',
color: item.annotationColor, color: item.annotationColor,
position: JSON.parse(item.annotationPosition), position: JSON.parse(item.annotationPosition),
@ -187,7 +184,7 @@ class PDFWorker {
*/ */
async exportParent(item, directory) { async exportParent(item, directory) {
if (!item.isRegularItem()) { if (!item.isRegularItem()) {
throw new Error('regular item not provided'); throw new Error('Item must be a regular item');
} }
if (!directory) { if (!directory) {
throw new Error('\'directory\' not provided'); throw new Error('\'directory\' not provided');
@ -196,7 +193,7 @@ class PDFWorker {
let ids = item.getAttachments(); let ids = item.getAttachments();
for (let id of ids) { for (let id of ids) {
let attachment = Zotero.Items.get(id); let attachment = Zotero.Items.get(id);
if (this.isPDFAttachment(attachment)) { if (attachment.isPDFAttachment()) {
let path = OS.Path.join(directory, attachment.attachmentFilename); let path = OS.Path.join(directory, attachment.attachmentFilename);
promises.push(this.export(id, path)); promises.push(this.export(id, path));
} }
@ -207,57 +204,56 @@ class PDFWorker {
/** /**
* Import annotations from PDF attachment * Import annotations from PDF attachment
* *
* @param {Integer} itemID * @param {Integer} itemID Attachment item id
* @param {Boolean} save Save imported annotations, or otherwise just return the number of importable annotations
* @param {Boolean} isPriority * @param {Boolean} isPriority
* @param {String} password * @param {String} password
* @returns {Promise<Integer>} Number of annotations * @returns {Promise<Integer>} Number of annotations
*/ */
async import(itemID, save, isPriority, password) { async import(itemID, isPriority, password) {
return this._enqueue(async () => { return this._enqueue(async () => {
let attachment = await Zotero.Items.getAsync(itemID); let attachment = await Zotero.Items.getAsync(itemID);
if (!this.isPDFAttachment(attachment)) { if (!attachment.isPDFAttachment()) {
throw new Error('not a valid PDF attachment'); throw new Error('Item must be a PDF attachment');
} }
// TODO: Remove when fixed
attachment._loaded.childItems = true; let mtime = Math.floor(await attachment.attachmentModificationTime / 1000);
let items = attachment.getAnnotations(); if (attachment.attachmentLastProcessedModificationTime === mtime) {
let existingAnnotations = []; return false;
for (let item of items) {
existingAnnotations.push({
id: item.key,
type: item.annotationType,
comment: item.annotationComment || '',
position: JSON.parse(item.annotationPosition)
});
} }
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 path = await attachment.getFilePath();
let buf = await OS.File.read(path, {}); let buf = await OS.File.read(path, {});
buf = new Uint8Array(buf).buffer; buf = new Uint8Array(buf).buffer;
let res = await this._query('import', { buf, existingAnnotations, password }, [buf]); let { imported, deleted } = await this._query('import', {
let annotations = res.annotations; buf, existingAnnotations, password
if (save) { }, [buf]);
for (let annotation of annotations) {
// TODO: Utilize the saved Zotero item key for deduplication. Newer annotation modificaiton date wins for (let annotation of imported) {
annotation.key = Zotero.DataObjectUtilities.generateKey(); annotation.key = Zotero.DataObjectUtilities.generateKey();
await Zotero.Annotations.saveFromJSON(attachment, annotation); annotation.isExternal = true;
} await Zotero.Annotations.saveFromJSON(attachment, annotation);
attachment.attachmentHasUnimportedAnnotations = false;
} }
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) { attachment.attachmentLastProcessedModificationTime = mtime;
reader.toggleImportPrompt(attachment.attachmentHasUnimportedAnnotations); await attachment.saveTx({ skipDateModifiedUpdate: true });
}
} return !!(imported.length || deleted.length);
attachment.attachmentLastProcessedModificationTime = Math.floor( }, isPriority);
await attachment.attachmentModificationTime / 1000
);
await attachment.saveTx();
return annotations.length;
});
} }
/** /**
@ -267,14 +263,14 @@ class PDFWorker {
*/ */
async importParent(item) { async importParent(item) {
if (!item.isRegularItem()) { if (!item.isRegularItem()) {
throw new Error('regular item not provided'); throw new Error('Item must be a regular item');
} }
let promises = []; let promises = [];
let ids = item.getAttachments(); let ids = item.getAttachments();
for (let id of ids) { for (let id of ids) {
let attachment = Zotero.Items.get(id); let attachment = Zotero.Items.get(id);
if (this.isPDFAttachment(attachment)) { if (attachment.isPDFAttachment()) {
promises.push(this.import(id, true)); promises.push(this.import({ itemID: id, isPriority: true }));
} }
} }
await Promise.all(promises); await Promise.all(promises);
@ -282,3 +278,169 @@ class PDFWorker {
} }
Zotero.PDFWorker = new 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<Integer>}
*/
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<Boolean>}
*/
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();

View file

@ -1,8 +1,27 @@
// Temporary stuff /*
Zotero.PDF = { ***** BEGIN LICENSE BLOCK *****
dateChecked: {},
hasUnmachedAnnotations: {} 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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
class ReaderInstance { class ReaderInstance {
constructor() { constructor() {
@ -46,7 +65,7 @@ class ReaderInstance {
annotations, annotations,
state, state,
location, location,
promptImport: item.attachmentHasUnimportedAnnotations, promptImport: false,
showItemPaneToggle: this._showItemPaneToggle, showItemPaneToggle: this._showItemPaneToggle,
sidebarWidth: this._sidebarWidth, sidebarWidth: this._sidebarWidth,
sidebarOpen: this._sidebarOpen, sidebarOpen: this._sidebarOpen,
@ -68,10 +87,10 @@ class ReaderInstance {
this._setTitleValue(title); this._setTitleValue(title);
} }
async setAnnotations(ids) { async setAnnotations(items) {
let annotations = []; let annotations = [];
for (let id of ids) { for (let item of items) {
let annotation = await this._getAnnotation(id); let annotation = await this._getAnnotation(item);
if (annotation) { if (annotation) {
annotations.push(annotation); annotations.push(annotation);
} }
@ -90,10 +109,6 @@ class ReaderInstance {
async navigate(location) { async navigate(location) {
this._postMessage({ action: 'navigate', location }); this._postMessage({ action: 'navigate', location });
} }
toggleImportPrompt(enable) {
this._postMessage({ action: 'toggleImportPrompt', enable });
}
enableAddToNote(enable) { enableAddToNote(enable) {
this._postMessage({ action: 'enableAddToNote', enable }); this._postMessage({ action: 'enableAddToNote', enable });
@ -292,23 +307,10 @@ class ReaderInstance {
} }
}; };
let savedAnnotation = await Zotero.Annotations.saveFromJSON(attachment, annotation, saveOptions); 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 blob = this._dataURLtoBlob(annotation.image);
let attachmentIds = savedAnnotation.getAttachments(); await Zotero.Annotations.saveCacheImage(savedAnnotation, blob);
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
});
}
} }
return; return;
} }
@ -360,11 +362,11 @@ class ReaderInstance {
} }
return; return;
} }
case 'import': { // case 'import': {
Zotero.debug('Importing PDF annotations'); // Zotero.debug('Importing PDF annotations');
Zotero.PDFWorker.import(this._itemID, true, true); // Zotero.PDFWorker.import(this._itemID, true, true);
return; // return;
} // }
case 'importDismiss': { case 'importDismiss': {
Zotero.debug('Dismiss PDF annotations'); Zotero.debug('Dismiss PDF annotations');
return; return;
@ -434,7 +436,8 @@ class ReaderInstance {
/** /**
* Return item JSON in the pdf-reader ready format * Return item JSON in the pdf-reader ready format
* @param itemID *
* @param {Zotero.Item} item
* @returns {Object|null} * @returns {Object|null}
*/ */
async _getAnnotation(item) { async _getAnnotation(item) {
@ -442,10 +445,9 @@ class ReaderInstance {
if (!item || !item.isAnnotation()) { if (!item || !item.isAnnotation()) {
return null; return null;
} }
// TODO: Remve when fixed
item._loaded.childItems = true;
let json = await Zotero.Annotations.toJSON(item); let json = await Zotero.Annotations.toJSON(item);
json.id = item.key; json.id = item.key;
json.readOnly = !json.isAuthor || json.isExternal;
delete json.key; delete json.key;
for (let key in json) { for (let key in json) {
json[key] = json[key] || ''; json[key] = json[key] || '';
@ -622,7 +624,7 @@ class Reader {
this._sidebarOpen = false; this._sidebarOpen = false;
this._bottomPlaceholderHeight = 800; this._bottomPlaceholderHeight = 800;
this._readers = []; this._readers = [];
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'reader'); this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'reader');
this.onChangeSidebarWidth = null; this.onChangeSidebarWidth = null;
this.onChangeSidebarOpen = null; this.onChangeSidebarOpen = null;
@ -682,47 +684,42 @@ class Reader {
reader.setBottomPlaceholderHeight(height); reader.setBottomPlaceholderHeight(height);
} }
} }
notify(event, type, ids, extraData) { notify(event, type, ids, extraData) {
// Listen for the parent item, PDF attachment and its annotation items updates if (type === 'tab') {
for (let readerWindow of this._readers) { var reader = Zotero.Reader.getByTabID(ids[0]);
if (event === 'delete') { if (reader) {
let disappearedIds = readerWindow.annotationItemIDs.filter(x => ids.includes(x)); this.triggerAnnotationsImportCheck(reader._itemID);
if (disappearedIds.length) {
let keys = disappearedIds.map(id => extraData[id].key);
readerWindow.unsetAnnotations(keys);
}
if (ids.includes(readerWindow._itemID)) {
readerWindow.close();
}
} }
else { }
let item = Zotero.Items.get(readerWindow._itemID); else if (type === 'item') {
// TODO: Remove when fixed // Listen for the parent item, PDF attachment and its annotations updates
item._loaded.childItems = true; for (let reader of this._readers) {
let annotationItems = item.getAnnotations(); if (event === 'delete') {
readerWindow.annotationItemIDs = annotationItems.map(x => x.id); let disappearedIds = reader.annotationItemIDs.filter(x => ids.includes(x));
let affectedAnnotationIds = annotationItems.filter(annotation => { if (disappearedIds.length) {
let annotationID = annotation.id; let keys = disappearedIds.map(id => extraData[id].key);
let imageAttachmentID = null; reader.unsetAnnotations(keys);
annotation._loaded.childItems = true; }
let annotationAttachments = annotation.getAttachments(); if (ids.includes(reader._itemID)) {
if (annotationAttachments.length) { reader.close();
imageAttachmentID = annotationAttachments[0];
} }
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 else {
if (ids.includes(readerWindow._itemID) || ids.includes(item.parentItemID)) { let item = Zotero.Items.get(reader._itemID);
readerWindow.updateTitle(); 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 item = await Zotero.Items.getAsync(itemID);
let mtime = await item.attachmentModificationTime; let mtime = await item.attachmentModificationTime;
if (item.attachmentLastProcessedModificationTime < Math.floor(mtime / 1000)) { if (item.attachmentLastProcessedModificationTime < Math.floor(mtime / 1000)) {
await Zotero.PDFWorker.import(itemID, false); await Zotero.PDFWorker.import(itemID, true);
} }
} }
} }

View file

@ -2745,7 +2745,6 @@ var ZoteroPane = new function()
'createParent', 'createParent',
'renameAttachments', 'renameAttachments',
'reindexItem', 'reindexItem',
'importAnnotations',
'createNoteFromAnnotations' 'createNoteFromAnnotations'
]; ];
@ -2793,8 +2792,7 @@ var ZoteroPane = new function()
canIndex = true, canIndex = true,
canRecognize = true, canRecognize = true,
canUnrecognize = true, canUnrecognize = true,
canRename = true, canRename = true;
canImportAnnotations = true;
var canMarkRead = collectionTreeRow.isFeed(); var canMarkRead = collectionTreeRow.isFeed();
var markUnread = true; var markUnread = true;
@ -2816,10 +2814,6 @@ var ZoteroPane = new function()
canUnrecognize = false; canUnrecognize = false;
} }
if (canImportAnnotations && !Zotero.PDFWorker.canImport(item)) {
canImportAnnotations = false;
}
// Show rename option only if all items are child attachments // Show rename option only if all items are child attachments
if (canRename && (!item.isAttachment() || item.isTopLevelItem() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL)) { if (canRename && (!item.isAttachment() || item.isTopLevelItem() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL)) {
canRename = false; canRename = false;
@ -2899,9 +2893,6 @@ var ZoteroPane = new function()
} }
} }
if (canImportAnnotations) {
show.push(m.importAnnotations);
}
} }
// Single item selected // Single item selected
@ -2971,9 +2962,6 @@ var ZoteroPane = new function()
show.push(m.duplicateItem); show.push(m.duplicateItem);
} }
if (Zotero.PDFWorker.canImport(item)) {
show.push(m.importAnnotations);
}
if (Zotero.EditorInstance.canCreateNoteFromAnnotations(item)) { if (Zotero.EditorInstance.canCreateNoteFromAnnotations(item)) {
show.push(m.createNoteFromAnnotations); 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 () { this.reportMetadataForSelected = async function () {
let items = ZoteroPane.getSelectedItems(); let items = ZoteroPane.getSelectedItems();

View file

@ -308,10 +308,6 @@
<menuitem class="menuitem-iconic zotero-menuitem-create-parent" oncommand="ZoteroPane_Local.createParentItemsFromSelected();"/> <menuitem class="menuitem-iconic zotero-menuitem-create-parent" oncommand="ZoteroPane_Local.createParentItemsFromSelected();"/>
<menuitem class="menuitem-iconic zotero-menuitem-rename-from-parent" oncommand="ZoteroPane_Local.renameSelectedAttachmentsFromParents()"/> <menuitem class="menuitem-iconic zotero-menuitem-rename-from-parent" oncommand="ZoteroPane_Local.renameSelectedAttachmentsFromParents()"/>
<menuitem class="menuitem-iconic zotero-menuitem-reindex" oncommand="ZoteroPane_Local.reindexItem();"/> <menuitem class="menuitem-iconic zotero-menuitem-reindex" oncommand="ZoteroPane_Local.reindexItem();"/>
<!-- <menuitem class="menuitem-iconic zotero-menuitem-import-annotations" label="&zotero.items.menu.importAnnotations;" oncommand="ZoteroPane.importAnnotationsForSelected()"/>-->
<menuitem class="menuitem-iconic zotero-menuitem-import-annotations" label="Import annotations" oncommand="ZoteroPane.importAnnotationsForSelected()"/>
<!-- <menuitem class="menuitem-iconic zotero-menuitem-export-annotations" label="&zotero.items.menu.exportAnnotations;" oncommand="ZoteroPane.exportAnnotationsForSelected()"/>-->
<menuitem class="menuitem-iconic zotero-menuitem-export-annotations" label="Export Annotations" oncommand="ZoteroPane.exportAnnotationsForSelected()"/>
<menuitem class="menuitem-iconic zotero-menuitem-create-note-from-annotations" label="&zotero.items.menu.createNoteFromAnnotations;" oncommand="ZoteroPane.createNoteFromSelected()"/> <menuitem class="menuitem-iconic zotero-menuitem-create-note-from-annotations" label="&zotero.items.menu.createNoteFromAnnotations;" oncommand="ZoteroPane.createNoteFromSelected()"/>
</menupopup> </menupopup>

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>PDF Renderer</title>
<script src="resource://zotero/pdf-reader/pdf.js"></script>
<script src="renderer.js"></script>
</head>
<body></body>
</html>

View file

@ -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 <http://www.gnu.org/licenses/>.
***** 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);