Improve PDF reader

This commit is contained in:
Martynas Bagdonas 2020-09-08 16:28:06 +03:00 committed by Dan Stillman
parent 875e9f674f
commit a89f7e8ec7
12 changed files with 668 additions and 636 deletions

View file

@ -11,13 +11,17 @@
]> ]>
<window <window
windowtype="zotero:viewer" id="pdf-reader"
windowtype="zotero:reader"
orient="vertical" orient="vertical"
width="1300" width="1300"
height="800" height="800"
persist="screenX screenY width height" persist="screenX screenY width height"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
> >
<script type="application/javascript">
Components.utils.import('resource://gre/modules/Services.jsm');
</script>
<!-- TODO: Localize --> <!-- TODO: Localize -->
<menubar> <menubar>
<menu label="File"> <menu label="File">
@ -56,11 +60,16 @@
</menubar> </menubar>
<hbox flex="1"> <hbox flex="1">
<vbox id="zotero-viewer" flex="3"> <vbox id="zotero-reader" flex="3">
<browser tooltip="viewerTooltip" type="content" primary="true" transparent="transparent" src="" id="viewer" <browser id="reader"
tooltip="readerTooltip"
type="content"
primary="true"
transparent="transparent"
src="resource://zotero/pdf-reader/viewer.html"
flex="1"/> flex="1"/>
<popupset> <popupset>
<tooltip id="viewerTooltip" onpopupshowing="return fillTooltip(this);"/> <tooltip id="readerTooltip" onpopupshowing="return fillTooltip(this);"/>
<menupopup id="tagsPopup" ignorekeys="true" <menupopup id="tagsPopup" ignorekeys="true"
style="min-width: 300px;" style="min-width: 300px;"
onpopupshown="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'true'); }" onpopupshown="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'true'); }"
@ -71,28 +80,27 @@
<menupopup id="colorPopup"/> <menupopup id="colorPopup"/>
</popupset> </popupset>
</vbox> </vbox>
<splitter id="zotero-viewer-splitter" <splitter id="zotero-reader-splitter"
hidden="true" hidden="true"
resizebefore="closest" resizebefore="closest"
resizeafter="closest" resizeafter="closest"
collapse="after" collapse="after"
orient="horizontal" orient="horizontal"
zotero-persist="state orient" /> zotero-persist="state orient" />
<vbox flex="0" id="zotero-viewer-note-sidebar" width="350" hidden="true"> <vbox flex="0" id="zotero-reader-note-sidebar" width="350" hidden="true">
<vbox id="zotero-viewer-sidebar-cover" flex="1"> <vbox id="zotero-reader-sidebar-cover" flex="1">
<label>Drag a note here…</label> <label>Drag a note here…</label>
</vbox> </vbox>
<vbox id="zotero-viewer-sidebar-container" flex="1" style="overflow:auto;" hidden="true"> <vbox id="zotero-reader-sidebar-container" flex="1" style="overflow:auto;" hidden="true">
<zoteronoteeditor id="zotero-viewer-editor" flex="1" notitle="1" <zoteronoteeditor id="zotero-reader-editor" flex="1" notitle="1"
previousfocus="zotero-items-tree" previousfocus="zotero-items-tree"
onerror="/*this.mode = 'view'*/" onerror="/*this.mode = 'view'*/"
/> />
<button id="zotero-view-note-button" label="Close" <button id="zotero-view-note-button" label="Close"
oncommand="document.getElementById('zotero-viewer-sidebar-container').hidden = true;document.getElementById('zotero-viewer-sidebar-cover').hidden = false;"/> oncommand="document.getElementById('zotero-reader-sidebar-container').hidden = true;document.getElementById('zotero-reader-sidebar-cover').hidden = false;"/>
</vbox> </vbox>
</vbox> </vbox>
</hbox> </hbox>
<script src="include.js"/> <script src="include.js"/>
<script src="viewer.js"/>
</window> </window>

View file

@ -1,66 +0,0 @@
Components.utils.import('resource://gre/modules/Services.jsm');
function handleDragOver(event) {
if (event.dataTransfer.getData('zotero/item')) {
event.preventDefault();
event.stopPropagation();
}
}
function handleDrop(event) {
let data;
if (!(data = event.dataTransfer.getData('zotero/item'))) {
return;
}
let ids = data.split(',').map(id => parseInt(id));
let item = Zotero.Items.get(ids[0]);
if (!item) {
return;
}
if (item.isNote()) {
event.preventDefault();
event.stopPropagation();
let cover = document.getElementById('zotero-viewer-sidebar-cover');
let container = document.getElementById('zotero-viewer-sidebar-container');
cover.hidden = true;
container.hidden = false;
let editor = document.getElementById('zotero-viewer-editor');
let notebox = document.getElementById('zotero-viewer-note-sidebar');
editor.mode = 'edit';
notebox.hidden = false;
editor.item = item;
}
else if (item.isAttachment() && item.attachmentContentType === 'application/pdf') {
event.preventDefault();
event.stopPropagation();
let iframeWindow = document.getElementById('viewer').contentWindow;
let url = 'zotero://pdf.js/viewer.html?libraryID=' + item.libraryID + '&key=' + item.key;
if (url !== iframeWindow.location.href) {
iframeWindow.location = url;
}
}
else if (item.isRegularItem()) {
let attachments = item.getAttachments();
if (attachments.length === 1) {
let id = attachments[0];
let attachment = Zotero.Items.get(id);
if (attachment.attachmentContentType === 'application/pdf') {
event.preventDefault();
event.stopPropagation();
let iframeWindow = document.getElementById('viewer').contentWindow;
let url = 'zotero://pdf.js/viewer.html?libraryID=' + attachment.libraryID + '&key=' + attachment.key;
if (url !== iframeWindow.location.href) {
iframeWindow.location = url;
}
}
}
}
}
window.addEventListener('dragover', handleDragOver, true);
window.addEventListener('drop', handleDrop, true);

View file

@ -32,7 +32,7 @@ Zotero.Annotations = new function () {
Zotero.defineProperty(this, 'ANNOTATION_TYPE_IMAGE', { value: 3 }); Zotero.defineProperty(this, 'ANNOTATION_TYPE_IMAGE', { value: 3 });
this.toJSON = function (item) { this.toJSON = async function (item) {
var o = {}; var o = {};
o.key = item.key; o.key = item.key;
o.type = item.annotationType; o.type = item.annotationType;
@ -44,7 +44,13 @@ Zotero.Annotations = new function () {
o.text = item.annotationText; o.text = item.annotationText;
} }
else if (o.type == 'image') { else if (o.type == 'image') {
o.imageURL = item.annotationImageURL; var attachments = item.getAttachments();
if (attachments.length) {
let imageAttachment = Zotero.Items.get(attachments[0]);
if (imageAttachment) {
o.image = await imageAttachment.attachmentDataURI;
}
}
} }
o.comment = item.annotationComment; o.comment = item.annotationComment;
o.pageLabel = item.annotationPageLabel; o.pageLabel = item.annotationPageLabel;

View file

@ -3332,6 +3332,31 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentText', {
}); });
/**
* Return dataURI of attachment content
*
* @return {Promise<String>} - A promise for attachment dataURI or empty string if unavailable
*/
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentDataURI', {
get: async function () {
if (!this.isAttachment()) {
throw new Error("'attachmentDataURI' is only valid for attachments");
}
let path = await this.getFilePathAsync();
if (!path || !(await OS.File.exists(path))) {
return '';
}
let buf = await OS.File.read(path, {});
let bytes = new Uint8Array(buf);
let binary = '';
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return 'data:' + this.attachmentContentType + ';base64,' + btoa(binary);
}
});
/** /**
* Returns child attachments of this item * Returns child attachments of this item
@ -3549,34 +3574,6 @@ Zotero.defineProperty(Zotero.Item.prototype, 'annotationImageAttachment', {
}); });
/**
* @property {String} annotationImageURL
*/
Zotero.defineProperty(Zotero.Item.prototype, 'annotationImageURL', {
get: function () {
if (!this.isImageAnnotation()) {
throw new Error("'annotationImageURL' is only valid for image annotations");
}
var attachments = this.getAttachments();
if (!attachments.length) {
throw new Error("No attachments found for image annotation");
}
var { libraryID, key } = Zotero.Items.getLibraryAndKeyFromID(attachments[0]);
var url = 'zotero://attachment/';
if (libraryID == Zotero.Libraries.userLibraryID) {
url += 'library';
}
else {
url += Zotero.URI.getLibraryPath(libraryID);
}
url += '/items/' + key;
return url;
}
});
/** /**
* Determine if an item is an annotation * Determine if an item is an annotation
* *

View file

@ -190,7 +190,7 @@ class EditorInstance {
this.onNavigate(uri, { position }); this.onNavigate(uri, { position });
} }
else { else {
await Zotero.Viewer.openURI(uri, { position }); await Zotero.Reader.openURI(uri, { position });
} }
return; return;
} }

View file

@ -1,5 +1,5 @@
const CMAPS_URL = 'resource://zotero/pdf.js/cmaps/'; const CMAPS_URL = 'resource://zotero/pdf-reader/cmaps/';
class PDFWorker { class PDFWorker {
constructor() { constructor() {

View file

@ -0,0 +1,610 @@
let PDFStates = {};
class ReaderWindow {
constructor() {
this.annotationItemIds = [];
this._instanceID = Zotero.Utilities.randomString();
this._window = null;
this._iframeWindow = null;
this._itemID = null;
this._state = null;
this._prevHistory = [];
this._nextHistory = [];
this._isReaderInitialized = false;
}
init() {
let win = Services.wm.getMostRecentWindow('navigator:browser');
if (!win) return;
this._window = win.open(
'chrome://zotero/content/reader.xul', '', 'chrome,resizable'
);
this._window.addEventListener('DOMContentLoaded', (event) => {
this._window.addEventListener('dragover', this._handleDragOver, true);
this._window.addEventListener('drop', this._handleDrop, true);
this._window.addEventListener('keypress', this._handleKeyPress);
this._window.fillTooltip = (tooltip) => {
let node = this._window.document.tooltipNode.closest('*[title]');
if (!node) {
return false;
}
tooltip.setAttribute('label', node.getAttribute('title'));
return true;
};
this._window.menuCmd = (cmd) => {
if (cmd === 'export') {
let zp = Zotero.getActiveZoteroPane();
zp.exportPDF(this._itemID);
return;
}
let data = {
action: 'menuCmd',
cmd
};
this._postMessage(data);
};
let readerIframe = this._window.document.getElementById('reader');
if (!(readerIframe && readerIframe.contentWindow && readerIframe.contentWindow.document === event.target)) {
return;
}
let editor = this._window.document.getElementById('zotero-reader-editor');
editor.navigateHandler = async (uri, location) => {
let item = await Zotero.URI.getURIItem(uri);
if (!item) {
return;
}
if (item.id === this._itemID) {
this.navigate(location);
}
else {
await this.open({
itemID: item.id,
location
});
}
};
this._iframeWindow = this._window.document.getElementById('reader').contentWindow;
this._iframeWindow.addEventListener('message', this._handleMessage);
});
}
async open({ itemID, state, location, skipHistory }) {
if (itemID === this._itemID) {
return false;
}
let item = await Zotero.Items.getAsync(itemID);
if (!item) {
return false;
}
let path = await item.getFilePathAsync();
let buf = await OS.File.read(path, {});
buf = new Uint8Array(buf).buffer;
if (this._itemID && !skipHistory) {
this._prevHistory.push({
itemID: this._itemID,
state: this._state
});
this._nextHistory = [];
}
this._itemID = item.id;
let title = item.getField('title');
let parentItemID = item.parentItemID;
if (parentItemID) {
let parentItem = await Zotero.Items.getAsync(parentItemID);
if (parentItem) {
title = parentItem.getField('title');
}
}
this._window.document.title = title;
// TODO: Remove when fixed
item._loaded.childItems = true;
let ids = item.getAnnotations();
let annotations = (await Promise.all(ids.map(id => this._getAnnotation(id)))).filter(x => x);
this.annotationItemIds = ids;
state = state || PDFStates[this._itemID];
this._state = state;
this._postMessage({
action: 'open',
buf,
annotations,
state,
location,
enablePrev: !!this._prevHistory.length,
enableNext: !!this._nextHistory.length
}, [buf]);
return true;
}
updateTitle() {
let item = Zotero.Items.get(this._itemID);
let title = item.getField('title');
let parentItemID = item.parentItemID;
if (parentItemID) {
let parentItem = Zotero.Items.get(parentItemID);
if (parentItem) {
title = parentItem.getField('title');
}
}
this._window.document.title = title;
}
async setAnnotations(ids) {
let annotations = [];
for (let id of ids) {
let annotation = await this._getAnnotation(id);
if (annotation) {
annotations.push(annotation);
}
}
if (annotations.length) {
let data = { action: 'setAnnotations', annotations };
this._postMessage(data);
}
}
unsetAnnotations(keys) {
let data = { action: 'unsetAnnotations', ids: keys };
this._postMessage(data);
}
async navigate(location) {
this._postMessage({ action: 'navigate', location });
}
close() {
this._window.close();
}
_dataURLtoBlob(dataurl) {
let parts = dataurl.split(',');
let mime = parts[0].match(/:(.*?);/)[1];
if (parts[0].indexOf('base64') !== -1) {
let bstr = atob(parts[1]);
let n = bstr.length;
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new this._iframeWindow.Blob([u8arr], { type: mime });
}
}
// TODO: Pass sidebar state to the responsible pdf-reader button
_toggleNoteSidebar(isToggled) {
let splitter = this._window.document.getElementById('zotero-reader-splitter');
let sidebar = this._window.document.getElementById('zotero-reader-note-sidebar');
if (isToggled) {
splitter.hidden = false;
sidebar.hidden = false;
}
else {
splitter.hidden = true;
sidebar.hidden = true;
}
}
_getColorIcon(color, selected) {
let stroke = selected ? 'lightgray' : 'transparent';
let fill = '%23' + color.slice(1);
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect shape-rendering="geometricPrecision" fill="${fill}" stroke-width="2" x="2" y="2" stroke="${stroke}" width="12" height="12" rx="3"/></svg>`;
}
_openAnnotationPopup(x, y, annotationId, colors, selectedColor) {
let popup = this._window.document.getElementById('annotationPopup');
popup.hidePopup();
while (popup.firstChild) {
popup.removeChild(popup.firstChild);
}
let menuitem = this._window.document.createElement('menuitem');
menuitem.setAttribute('label', 'Delete');
menuitem.addEventListener('command', () => {
let data = {
action: 'popupCmd',
cmd: 'deleteAnnotation',
id: annotationId
};
this._postMessage(data);
});
popup.appendChild(menuitem);
popup.appendChild(this._window.document.createElement('menuseparator'));
for (let color of colors) {
menuitem = this._window.document.createElement('menuitem');
menuitem.setAttribute('label', color[0]);
menuitem.className = 'menuitem-iconic';
menuitem.setAttribute('image', this._getColorIcon(color[1], color[1] === selectedColor));
menuitem.addEventListener('command', () => {
let data = {
action: 'popupCmd',
cmd: 'setAnnotationColor',
id: annotationId,
color: color[1]
};
this._postMessage(data);
});
popup.appendChild(menuitem);
}
popup.openPopupAtScreen(x, y, true);
}
_openColorPopup(x, y, colors, selectedColor) {
let popup = this._window.document.getElementById('colorPopup');
popup.hidePopup();
while (popup.firstChild) {
popup.removeChild(popup.firstChild);
}
let menuitem;
for (let color of colors) {
menuitem = this._window.document.createElement('menuitem');
menuitem.setAttribute('label', color[0]);
menuitem.className = 'menuitem-iconic';
menuitem.setAttribute('image', this._getColorIcon(color[1], color[1] === selectedColor));
menuitem.addEventListener('command', () => {
let data = {
action: 'popupCmd',
cmd: 'setColor',
color: color[1]
};
this._postMessage(data);
});
popup.appendChild(menuitem);
}
popup.openPopupAtScreen(x, y, true);
}
async _postMessage(message, transfer) {
await this._waitForReader();
this._iframeWindow.postMessage({ itemId: this._itemID, message }, this._iframeWindow.origin, transfer);
}
_handleMessage = async (event) => {
let message;
try {
if (event.source !== this._iframeWindow) {
return;
}
// Clone data to avoid the dead object error when the window is closed
let data = JSON.parse(JSON.stringify(event.data));
// Filter messages coming from previous reader instances,
// except for `setAnnotation` to still allow saving it
if (data.itemId !== this._itemID && data.message.action !== 'setAnnotation') {
return;
}
Zotero.debug('Received message from pdf-reader iframe: ' + JSON.stringify(data));
message = data.message;
switch (message.action) {
case 'navigatePrev': {
let prev = this._prevHistory.pop();
if (prev) {
this._nextHistory.push({
itemID: this._itemID,
state: this._state
});
this.open({ itemID: prev.itemID, state: prev.state, skipHistory: true });
}
return;
}
case 'navigateNext': {
let next = this._nextHistory.pop();
if (next) {
this._prevHistory.push({
itemID: this._itemID,
state: this._state
});
this.open({ itemID: next.itemID, state: next.state, skipHistory: true });
}
return;
}
case 'setAnnotation': {
let attachment = Zotero.Items.get(data.itemId);
let { annotation } = message;
annotation.key = annotation.id;
let saveOptions = {
notifierData: {
instanceID: this._instanceID
}
};
let savedAnnotation = await Zotero.Annotations.saveFromJSON(attachment, annotation, saveOptions);
if (annotation.image) {
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
});
}
}
return;
}
case 'deleteAnnotations': {
let { ids: keys } = message;
let attachment = Zotero.Items.get(this._itemID);
let libraryID = attachment.libraryID;
for (let key of keys) {
let annotation = Zotero.Items.getByLibraryAndKey(libraryID, key);
// A small check, as we are receiving a list of item keys from a less secure code
if (annotation && annotation.isAnnotation() && annotation.parentID === this._itemID) {
this.annotationItemIds = this.annotationItemIds.filter(id => id !== annotation.id);
await annotation.eraseTx();
}
}
return;
}
case 'setState': {
let { state } = message;
PDFStates[this._itemID] = state;
this._state = state;
return;
}
case 'openTagsPopup': {
let { id: key, x, y } = message;
let attachment = Zotero.Items.get(this._itemID);
let libraryID = attachment.libraryID;
let annotation = Zotero.Items.getByLibraryAndKey(libraryID, key);
if (annotation) {
this._window.document.getElementById('tags').item = annotation;
this._window.document.getElementById('tagsPopup').openPopupAtScreen(x, y, false);
}
return;
}
case 'openAnnotationPopup': {
let { x, y, id, colors, selectedColor } = message;
this._openAnnotationPopup(x, y, id, colors, selectedColor);
return;
}
case 'openColorPopup': {
let { x, y, colors, selectedColor } = message;
this._openColorPopup(x, y, colors, selectedColor);
return;
}
case 'openUrl': {
let { url } = message;
let win = Services.wm.getMostRecentWindow('navigator:browser');
if (win) {
win.ZoteroPane.loadURI(url);
}
return;
}
case 'import': {
Zotero.debug('Importing PDF annotations');
let item = Zotero.Items.get(this._itemID);
Zotero.PDFImport.import(item);
return;
}
case 'importDismiss': {
Zotero.debug('Dismiss PDF annotations');
return;
}
case 'save': {
Zotero.debug('Exporting PDF');
let zp = Zotero.getActiveZoteroPane();
zp.exportPDF(this._itemID);
return;
}
case 'toggleNoteSidebar': {
let { isToggled } = message;
this._toggleNoteSidebar(isToggled);
return;
}
}
}
catch (e) {
this._postMessage({
action: 'error',
message: `An error occured during '${message ? message.action : ''}'`,
moreInfo: {
message: e.message,
stack: e.stack,
fileName: e.fileName,
lineNumber: e.lineNumber
}
});
throw e;
}
}
_handleDragOver = (event) => {
if (event.dataTransfer.getData('zotero/item')) {
event.preventDefault();
event.stopPropagation();
}
}
_handleDrop = (event) => {
let data;
if (!(data = event.dataTransfer.getData('zotero/item'))) {
return;
}
let ids = data.split(',').map(id => parseInt(id));
let item = Zotero.Items.get(ids[0]);
if (!item) {
return;
}
if (item.isNote()) {
event.preventDefault();
event.stopPropagation();
let cover = this._window.document.getElementById('zotero-reader-sidebar-cover');
let container = this._window.document.getElementById('zotero-reader-sidebar-container');
let splitter = this._window.document.getElementById('zotero-reader-splitter');
cover.hidden = true;
container.hidden = false;
splitter.hidden = false;
let editor = this._window.document.getElementById('zotero-reader-editor');
let notebox = this._window.document.getElementById('zotero-reader-note-sidebar');
editor.mode = 'edit';
notebox.hidden = false;
editor.item = item;
}
else if (item.isAttachment() && item.attachmentContentType === 'application/pdf') {
event.preventDefault();
event.stopPropagation();
this.open({ itemID: item.id });
}
else if (item.isRegularItem()) {
let attachments = item.getAttachments();
if (attachments.length === 1) {
let id = attachments[0];
let attachment = Zotero.Items.get(id);
if (attachment.attachmentContentType === 'application/pdf') {
event.preventDefault();
event.stopPropagation();
this.open({ itemID: attachment.id });
}
}
}
}
_handleKeyPress = (event) => {
if ((Zotero.isMac && event.metaKey || event.ctrlKey)
&& !event.shiftKey && !event.altKey && event.key === 'w') {
this._window.close();
}
}
async _waitForReader() {
if (this._isReaderInitialized) {
return;
}
let n = 0;
while (!this._iframeWindow || !this._iframeWindow.eval('window.isReady')) {
if (n >= 500) {
throw new Error('Waiting for reader failed');
}
await Zotero.Promise.delay(10);
n++;
}
this._isReaderInitialized = true;
}
/**
* Return item JSON in the pdf-reader ready format
* @param itemID
* @returns {Object|null}
*/
async _getAnnotation(itemID) {
try {
let item = Zotero.Items.get(itemID);
if (!item || !item.isAnnotation()) {
return null;
}
// TODO: Remve when fixed
item._loaded.childItems = true;
item = await Zotero.Annotations.toJSON(item);
item.id = item.key;
item.image = item.image;
delete item.key;
for (let key in item) {
item[key] = item[key] || '';
}
item.tags = item.tags || [];
return item;
}
catch (e) {
Zotero.logError(e);
return null;
}
}
}
class Reader {
constructor() {
this._readerWindows = [];
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'reader');
}
notify(event, type, ids, extraData) {
// Listen for the parent item, PDF attachment and its annotation items updates
for (let readerWindow of this._readerWindows) {
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();
}
}
else {
let item = Zotero.Items.get(readerWindow._itemID);
// TODO: Remove when fixed
item._loaded.childItems = true;
let annotationItemIds = item.getAnnotations();
readerWindow.annotationItemIds = annotationItemIds;
let affectedAnnotationIds = annotationItemIds.filter(annotationID => {
let annotation = Zotero.Items.get(annotationID);
let imageAttachmentID = null;
annotation._loaded.childItems = true;
let annotationAttachments = annotation.getAttachments();
if (annotationAttachments.length) {
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
if (ids.includes(readerWindow._itemID) || ids.includes(item.parentItemID)) {
readerWindow.updateTitle();
}
}
}
}
_getReaderWindow(itemID) {
return this._readerWindows.find(v => v._itemID === itemID);
}
async openURI(itemURI, location) {
let item = await Zotero.URI.getURIItem(itemURI);
if (!item) return;
this.open(item.id, location);
}
async open(itemID, location) {
let reader = this._getReaderWindow(itemID);
if (reader) {
if (location) {
reader.navigate(location);
}
}
else {
reader = new ReaderWindow();
reader.init();
if (!(await reader.open({ itemID, location }))) {
return;
}
this._readerWindows.push(reader);
reader._window.addEventListener('unload', () => {
this._readerWindows.splice(this._readerWindows.indexOf(reader), 1);
});
}
reader._window.focus();
}
}
Zotero.Reader = new Reader();

View file

@ -1,523 +0,0 @@
let PDFStates = {};
const COLORS = [
['Red', '#ff6666'],
['Orange', '#ff8c19'],
['Green', '#5fb236'],
['Blue', '#2ea8e5'],
['Purple', '#a28ae5']
];
class ViewerWindow {
constructor() {
this._window = null;
this._iframeWindow = null;
this.popupData = null;
}
dataURLtoBlob(dataurl) {
let parts = dataurl.split(',');
let mime = parts[0].match(/:(.*?);/)[1];
if (parts[0].indexOf('base64') !== -1) {
let bstr = atob(parts[1]);
let n = bstr.length;
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new this._iframeWindow.Blob([u8arr], { type: mime });
}
}
toggleNoteSidebar(isToggled) {
let splitter = this._window.document.getElementById('zotero-viewer-splitter');
let sidebar = this._window.document.getElementById('zotero-viewer-note-sidebar');
if (isToggled) {
splitter.hidden = false;
sidebar.hidden = false;
}
else {
splitter.hidden = true;
sidebar.hidden = true;
}
}
openAnnotationPopup(x, y, annotationId, selectedColor) {
let popup = this._window.document.getElementById('annotationPopup');
popup.hidePopup();
while (popup.firstChild) {
popup.removeChild(popup.firstChild);
}
let menuitem = this._window.document.createElement('menuitem');
menuitem.setAttribute('label', 'Delete');
menuitem.addEventListener('command', () => {
let data = {
action: 'popupCmd',
cmd: 'deleteAnnotation',
id: this.popupData.id
};
this._iframeWindow.postMessage(data, '*');
});
popup.appendChild(menuitem);
popup.appendChild(this._window.document.createElement('menuseparator'));
for (let color of COLORS) {
menuitem = this._window.document.createElement('menuitem');
menuitem.setAttribute('label', color[0]);
menuitem.className = 'menuitem-iconic';
let stroke = color[1] === selectedColor ? 'lightgray' : 'transparent';
let fill = '%23' + color[1].slice(1);
menuitem.setAttribute('image', 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle shape-rendering="geometricPrecision" fill="' + fill + '" stroke-width="2" stroke="' + stroke + '" cx="8" cy="8" r="6"/></svg>');
menuitem.addEventListener('command', () => {
let data = {
action: 'popupCmd',
cmd: 'setAnnotationColor',
id: this.popupData.id,
color: color[1]
};
this._iframeWindow.postMessage(data, '*');
});
popup.appendChild(menuitem);
}
popup.openPopupAtScreen(x, y, true);
}
openColorPopup(x, y, selectedColor) {
let popup = this._window.document.getElementById('colorPopup');
popup.hidePopup();
while (popup.firstChild) {
popup.removeChild(popup.firstChild);
}
let menuitem;
for (let color of COLORS) {
menuitem = this._window.document.createElement('menuitem');
menuitem.setAttribute('label', color[0]);
menuitem.className = 'menuitem-iconic';
let stroke = color[1] === selectedColor ? 'lightgray' : 'transparent';
let fill = '%23' + color[1].slice(1);
menuitem.setAttribute('image', 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle shape-rendering="geometricPrecision" fill="' + fill + '" stroke-width="2" stroke="' + stroke + '" cx="8" cy="8" r="6"/></svg>');
menuitem.addEventListener('command', () => {
let data = {
action: 'popupCmd',
cmd: 'setColor',
color: color[1]
};
this._iframeWindow.postMessage(data, '*');
});
popup.appendChild(menuitem);
}
popup.openPopupAtScreen(x, y, true);
}
init() {
let win = Services.wm.getMostRecentWindow('navigator:browser');
if (!win) return;
this._window = win.open(
'chrome://zotero/content/viewer.xul', '', 'chrome,resizable,centerscreen'
);
this._window.addEventListener('DOMContentLoaded', (e) => {
this._window.fillTooltip = (tooltip) => {
let node = this._window.document.tooltipNode.closest('*[title]');
if (!node) {
return false;
}
tooltip.setAttribute('label', node.getAttribute('title'));
return true;
}
this._window.menuCmd = (cmd) => {
if (cmd === 'export') {
let zp = Zotero.getActiveZoteroPane();
zp.exportPDF(this.itemID);
return;
}
let data = {
action: 'menuCmd',
cmd
};
this._iframeWindow.postMessage(data, '*');
}
let viewerIframe = this._window.document.getElementById('viewer');
if (!(viewerIframe && viewerIframe.contentWindow && viewerIframe.contentWindow.document === e.target)) return;
let that = this;
let editor = this._window.document.getElementById('zotero-viewer-editor');
editor.navigateHandler = async function (uri, annotation) {
let item = await Zotero.URI.getURIItem(uri);
if (!item) return;
that.open(item.id, annotation);
}
this._iframeWindow = this._window.document.getElementById('viewer').contentWindow;
// In the iframe `window.performance` is null which firstly makes React to fail,
// because it expects `undefined` or `object` type, and secondly pdf.js is hardcoded
// to always use performance API
// By using the method below the performance API in the iframe appears not immediately,
// which can cause problems for scipts trying to access it too early
this._iframeWindow.performance = this._window.performance;
this._iframeWindow.addEventListener('message', async (e) => {
// Clone data to avoid the dead object error when the window is closed
let data = JSON.parse(JSON.stringify(e.data));
switch (data.action) {
case 'load':
this.load(data.libraryID, data.key);
break;
case 'setAnnotation':
var item = await Zotero.Items.getAsync(this.itemID);
data.annotation.key = data.annotation.id;
var annotation = await Zotero.Annotations.saveFromJSON(item, data.annotation);
if (data.annotation.image) {
let blob = this.dataURLtoBlob(data.annotation.image);
let attachmentIds = annotation.getAttachments();
if (attachmentIds.length) {
let attachment = Zotero.Items.get(attachmentIds[0]);
var path = await attachment.getFilePathAsync();
await Zotero.File.putContentsAsync(path, blob);
await Zotero.Sync.Storage.Local.updateSyncStates([attachment], 'to_upload');
}
else {
let imageAttachment = await Zotero.Attachments.importEmbeddedImage({
blob,
parentItemID: annotation.id
});
}
}
break;
case 'deleteAnnotations':
for (let id of data.ids) {
let item = Zotero.Items.getByLibraryAndKey(this.libraryID, id);
if (item) {
await Zotero.Items.trashTx([item.id]);
}
}
break;
case 'setState':
PDFStates[this.itemID] = data.state;
break;
case 'openTagsPopup':
var item = Zotero.Items.getByLibraryAndKey(this.libraryID, data.id);
if (item) {
this._window.document.getElementById('tags').item = item;
this._window.document.getElementById('tagsPopup').openPopupAtScreen(data.x, data.y, false);
}
break;
case 'openAnnotationPopup':
this.popupData = data;
this.openAnnotationPopup(data.x, data.y, data.id, data.selectedColor);
break;
case 'openColorPopup':
this.popupData = data;
this.openColorPopup(data.x, data.y, data.selectedColor);
break;
case 'openURL':
let win = Services.wm.getMostRecentWindow('navigator:browser');
if (win) {
win.ZoteroPane.loadURI(data.url);
}
break;
case 'import':
Zotero.debug('Importing PDF annotations');
let item1 = Zotero.Items.get(this.itemID);
Zotero.PDFImport.import(item1);
break;
case 'importDismiss':
Zotero.debug('Dismiss PDF annotations');
break;
case 'save':
Zotero.debug('Exporting PDF');
var zp = Zotero.getActiveZoteroPane();
zp.exportPDF(this.itemID);
break;
case 'toggleNoteSidebar':
this.toggleNoteSidebar(data.isToggled);
break;
}
});
});
return true;
};
async waitForViewer() {
await Zotero.Promise.delay(100);
let n = 0;
while (!this._iframeWindow || !this._iframeWindow.eval('window.isDocumentReady')) {
if (n >= 500) {
throw new Error('Waiting for viewer failed');
}
await Zotero.Promise.delay(100);
n++;
}
};
async waitForViewer2() {
let n = 0;
while (!this._iframeWindow) {
if (n >= 50) {
throw new Error('Waiting for viewer failed');
}
await Zotero.Promise.delay(10);
n++;
}
};
async open(itemID, annotation) {
await this.waitForViewer2();
let item = await Zotero.Items.getAsync(itemID);
if (!item) return;
let url = 'zotero://pdf.js/viewer.html?libraryID=' + item.libraryID + '&key=' + item.key;
if (url !== this._iframeWindow.location.href) {
this._iframeWindow.location = url;
}
this.navigate(annotation);
return true;
};
async load(libraryID, key) {
let item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
if (!item) return;
this.itemID = item.id;
this.libraryID = item.libraryID;
let title = item.getField('title');
let parentItemID = item.parentItemID;
if (parentItemID) {
let parentItem = await Zotero.Items.getAsync(parentItemID);
if (parentItem) {
title = parentItem.getField('title');
}
}
this._window.document.title = title;
Zotero.debug('Annots');
// TODO: Remove when fixed
item._loaded.childItems = true;
let ids = item.getAnnotations();
let annotations = ids.map(id => this.getAnnotation(id)).filter(x => x);
this.annotationIds = ids;
Zotero.debug(annotations);
let state = PDFStates[this.itemID];
let data = {
action: 'open',
libraryID,
key,
itemId: item.itemID,
annotations,
state
};
this._iframeWindow.postMessage(data, '*');
return true;
}
updateTitle() {
let item = Zotero.Items.get(this.itemID);
let title = item.getField('title');
let parentItemID = item.parentItemID;
if (parentItemID) {
let parentItem = Zotero.Items.get(parentItemID);
if (parentItem) {
title = parentItem.getField('title');
}
}
this._window.document.title = title;
}
/**
* Return item JSON in pdf-reader ready format
* @param itemID
* @returns {Object|null}
*/
getAnnotation(itemID) {
try {
let item = Zotero.Items.get(itemID);
if (!item || !item.isAnnotation()) {
return null;
}
item = Zotero.Annotations.toJSON(item);
item.id = item.key;
item.image = item.imageURL;
delete item.key;
for (let key in item) {
item[key] = item[key] || '';
}
item.tags = item.tags || [];
return item;
}
catch (e) {
Zotero.logError(e);
return null;
}
}
setAnnotations(ids) {
Zotero.debug('set annots')
Zotero.debug(ids);
let annotations = [];
for (let id of ids) {
let annotation = this.getAnnotation(id);
if (annotation) {
annotations.push(annotation);
}
}
if (annotations.length) {
let data = { action: 'setAnnotations', annotations };
this._iframeWindow.postMessage(data, '*');
}
}
unsetAnnotations(keys) {
Zotero.debug('unset annots')
Zotero.debug(keys)
let data = { action: 'unsetAnnotations', ids: keys };
this._iframeWindow.postMessage(data, '*');
}
async navigate(annotation) {
if (!annotation) return;
await this.waitForViewer();
// TODO: Wait until the document is loaded
let data = {
action: 'navigate',
annotationId: annotation.id,
position: annotation.position,
to: annotation
};
this._iframeWindow.postMessage(data, '*');
};
close() {
this._window.close();
}
}
class Viewer {
constructor() {
this._viewerWindows = [];
this.instanceID = Zotero.Utilities.randomString();
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'viewer');
}
notify(event, type, ids, extraData) {
// Listen for the parent item, PDF attachment and its annotation items updates
// TODO: Skip events that emerge in the current pdf-reader window
Zotero.debug('notification received')
Zotero.debug(event)
Zotero.debug(type)
Zotero.debug(ids)
Zotero.debug(extraData)
for (let viewerWindow of this._viewerWindows) {
if (event === 'delete') {
let disappearedIds = viewerWindow.annotationIds.filter(x => ids.includes(x));
if (disappearedIds.length) {
let keys = disappearedIds.map(id => extraData[id].itemKey);
viewerWindow.unsetAnnotations(keys);
}
if (ids.includes(viewerWindow.itemID)) {
viewerWindow.close();
}
}
else {
// Check if any annotation is involved
let item = Zotero.Items.get(viewerWindow.itemID);
// TODO: Remove when fixed
item._loaded.childItems = true;
let annotationIds = item.getAnnotations();
viewerWindow.annotationIds = annotationIds;
let affectedAnnotationIds = annotationIds.filter(x => ids.includes(x));
if (affectedAnnotationIds.length) {
viewerWindow.setAnnotations(ids);
}
// Update title if the PDF attachment or the parent item changes
if (ids.includes(viewerWindow.itemID) || ids.includes(item.parentItemID)) {
viewerWindow.updateTitle();
}
}
}
}
_getViewerWindow(itemID) {
return this._viewerWindows.find(v => v.itemID === itemID);
}
async openURI(itemURI, annotation) {
let item = await Zotero.URI.getURIItem(itemURI);
if (!item) return;
this.open(item.id, annotation);
}
async open(itemID, annotation) {
let viewer = this._getViewerWindow(itemID);
if (viewer) {
if (annotation) {
viewer.navigate(annotation);
}
}
else {
viewer = new ViewerWindow();
viewer.init();
if (!(await viewer.open(itemID))) return;
this._viewerWindows.push(viewer);
viewer._window.addEventListener('close', () => {
this._viewerWindows.splice(this._viewerWindows.indexOf(viewer), 1);
});
viewer.navigate(annotation);
}
viewer._window.focus();
}
}
Zotero.Viewer = new Viewer();

View file

@ -4232,7 +4232,7 @@ var ZoteroPane = new function()
}); });
this.viewPDF = function (itemID) { this.viewPDF = function (itemID) {
Zotero.Viewer.open(itemID); Zotero.Reader.open(itemID);
}; };

View file

@ -108,7 +108,7 @@ const xpcomFilesLocal = [
'noteBackups', 'noteBackups',
'notifier', 'notifier',
'openPDF', 'openPDF',
'viewer', 'reader',
'progressQueue', 'progressQueue',
'progressQueueDialog', 'progressQueueDialog',
'quickCopy', 'quickCopy',

@ -1 +1 @@
Subproject commit 7aefe43059843f2b065f1ca9630d1bb48e08d4c3 Subproject commit acd34e852d38768c8f31e59aa7238d4d934081eb

View file

@ -30,7 +30,7 @@ async function getPDFReader(signatures) {
} }
if (updated) { if (updated) {
await fs.copy('./pdf-reader/build/zotero', './build/resource/pdf.js'); await fs.copy('./pdf-reader/build/zotero', './build/resource/pdf-reader');
} }
const t2 = Date.now(); const t2 = Date.now();