Improve PDF importing and introduce rendering
This commit is contained in:
parent
89d9efdec7
commit
a19693fa7a
8 changed files with 439 additions and 163 deletions
|
@ -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 })));
|
||||
|
|
|
@ -253,8 +253,6 @@ class EditorInstance {
|
|||
html += `<p><span class="citation" data-citation="${encodeURIComponent(JSON.stringify(citation))}">(${formatted})</span></p>`;
|
||||
}
|
||||
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);
|
||||
|
|
|
@ -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<Integer>} 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<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();
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -308,10 +308,6 @@
|
|||
<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-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()"/>
|
||||
</menupopup>
|
||||
|
||||
|
|
10
resource/pdf-renderer/renderer.html
Normal file
10
resource/pdf-renderer/renderer.html
Normal 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>
|
118
resource/pdf-renderer/renderer.js
Normal file
118
resource/pdf-renderer/renderer.js
Normal 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);
|
Loading…
Reference in a new issue