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);
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 })));

View file

@ -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);

View file

@ -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();

View file

@ -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);
}
}
}

View file

@ -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();

View file

@ -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>

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);