Improve PDF import/export

This commit is contained in:
Martynas Bagdonas 2020-09-21 14:33:11 +03:00 committed by Dan Stillman
parent e99f76b40c
commit 76e77d4422
12 changed files with 328 additions and 511 deletions

View file

@ -1,168 +0,0 @@
class PDFExport {
constructor() {
this._queue = [];
this._queueProcessing = false;
this._processingItemID = null;
this._progressQueue = Zotero.ProgressQueues.create({
id: 'pdf-export',
title: 'pdfExport.title',
columns: [
'recognizePDF.pdfName.label',
'pdfImport.annotations.label'
]
});
this._progressQueue.addListener('cancel', () => {
this._queue = [];
});
}
hasAnnotations(item) {
item._loaded.childItems = true;
return item.isAttachment() && item.getAnnotations().length;
}
canExport(item) {
if (this.hasAnnotations(item)) {
return true;
}
else if (item.isRegularItem()) {
let ids = item.getAttachments();
for (let id of ids) {
let attachment = Zotero.Items.get(id);
if (this.hasAnnotations(attachment)) {
return true;
}
}
}
return false;
}
/**
* Triggers queue processing and returns when all items in the queue are processed
* @return {Promise}
*/
async _processQueue() {
// await Zotero.Schema.schemaUpdatePromise;
if (this._queueProcessing) return;
this._queueProcessing = true;
while (1) {
let data = this._queue.pop();
if (!data) break;
let { itemID, path } = data;
this._processingItemID = itemID;
this._progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_PROCESSING, Zotero.getString('general.processing'));
try {
let item = await Zotero.Items.getAsync(itemID);
if (!item) {
throw new Error();
}
let num = await this._exportItemAnnotations(item, path);
this._progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, num);
}
catch (e) {
Zotero.logError(e);
this._progressQueue.updateRow(
itemID,
Zotero.ProgressQueue.ROW_FAILED,
e instanceof Zotero.Exception.Alert
? e.message
: Zotero.getString('general.error')
);
}
}
this._queueProcessing = false;
this._processingItemID = null;
}
/**
* Adds items to the queue and triggers processing
* @param {Zotero.Item[]} items
*/
async export(items) {
let pdfItems = [];
if (!Array.isArray(items)) {
items = [items];
}
for (let item of items) {
if (this.hasAnnotations(item)) {
pdfItems.push(item);
}
else if (item.isRegularItem()) {
let ids = item.getAttachments();
for (let id of ids) {
let attachment = Zotero.Items.get(id);
if (this.hasAnnotations(attachment)) {
pdfItems.push(attachment);
}
}
}
}
for (let item of pdfItems) {
if (
this._processingItemID === item.id ||
this._queue.find(x => x.itemID === item.id)
) {
continue;
}
this._queue.unshift({ itemID: item.id });
this._progressQueue.addRow(item);
}
await this._processQueue();
}
async exportToPath(item, path, isPriority) {
if (isPriority) {
this._queue.push({ itemID: item.id, path });
}
else {
this._queue.unshift({ itemID: item.id, path });
}
this._progressQueue.addRow(item);
await this._processQueue();
}
async _exportItemAnnotations(item, path) {
let ids = item.getAnnotations();
let annotations = [];
for (let id of ids) {
try {
annotations.push(Zotero.Annotations.toJSON(Zotero.Items.get(id)));
} catch (e) {
Zotero.logError(e);
}
}
annotations.id = annotations.key;
// annotations.image = annotations.imageURL;
for (let annotation of annotations) {
delete annotation.key;
for (let key in annotation) {
annotation[key] = annotation[key] || '';
}
annotation.authorName = '';
}
await Zotero.PDFWorker.writeAnnotations(item.id, annotations, path);
return annotations.length;
}
}
Zotero.PDFExport = new PDFExport();

View file

@ -1,167 +0,0 @@
// TODO: Import ToC
class PdfImport {
constructor() {
this._queue = [];
this._queueProcessing = false;
this._processingItemID = null;
this._progressQueue = Zotero.ProgressQueues.create({
id: 'pdf-import',
title: 'pdfImport.title',
columns: [
'recognizePDF.pdfName.label',
'pdfImport.annotations.label'
]
});
this._progressQueue.addListener('cancel', () => {
this._queue = [];
});
}
isPDFAttachment(item) {
return item.isAttachment() && item.attachmentContentType === 'application/pdf';
}
canImport(item) {
if (this.isPDFAttachment(item)) {
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)) {
return true;
}
}
}
};
/**
* Triggers queue processing and returns when all items in the queue are processed
* @return {Promise}
*/
async _processQueue() {
// await Zotero.Schema.schemaUpdatePromise;
if (this._queueProcessing) return;
this._queueProcessing = true;
while (1) {
let itemID = this._queue.pop();
if (!itemID) break;
this._processingItemID = itemID;
this._progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_PROCESSING, Zotero.getString('general.processing'));
try {
let item = await Zotero.Items.getAsync(itemID);
if (!item) {
throw new Error();
}
let num = await this._importItemAnnotations(item);
this._progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, num);
}
catch (e) {
Zotero.logError(e);
this._progressQueue.updateRow(
itemID,
Zotero.ProgressQueue.ROW_FAILED,
e instanceof Zotero.Exception.Alert
? e.message
: Zotero.getString('general.error')
);
}
}
this._queueProcessing = false;
this._processingItemID = null;
}
/**
* Adds items to the queue and triggers processing
* @param {Zotero.Item[]} items
*/
async import(items, isPriority) {
let pdfItems = [];
if (!Array.isArray(items)) {
items = [items];
}
for (let item of items) {
if (this.isPDFAttachment(item)) {
pdfItems.push(item);
}
else if (item.isRegularItem()) {
let ids = item.getAttachments();
for (let id of ids) {
let attachment = Zotero.Items.get(id);
if (this.isPDFAttachment(attachment)) {
pdfItems.push(attachment);
}
}
}
}
for (let item of pdfItems) {
if (
this._processingItemID === item.id ||
this._queue.includes(item.id)
) {
continue;
}
this._queue.unshift(item.id);
this._progressQueue.addRow(item);
}
await this._processQueue();
}
similarAnnotions(annotation1, annotation2) {
return (annotation1.position.pageIndex === annotation2.position.pageIndex &&
JSON.stringify(annotation1.position.rects) === JSON.stringify(annotation2.position.rects));
}
async _importItemAnnotations(item) {
if (!item.isAttachment() || item.attachmentContentType !== 'application/pdf') {
throw new Error('Not a valid PDF attachment');
}
// TODO: Remove when fixed
item._loaded.childItems = true;
let ids = item.getAnnotations();
let existingAnnotations = [];
for (let id of ids) {
try {
existingAnnotations.push(Zotero.Annotations.toJSON(Zotero.Items.get(id)));
} catch (e) {
Zotero.logError(e);
}
}
let annotations = await Zotero.PDFWorker.readAnnotations(item.id);
annotations = annotations.filter(x => ['highlight', 'note'].includes(x.type));
let num = 0;
for (let annotation of annotations) {
annotation.comment = annotation.comment || '';
if (existingAnnotations.some(existingAnnotation => this.similarAnnotions(existingAnnotation, annotation))) {
continue;
}
// TODO: Utilize the saved Zotero item key for deduplication
annotation.key = Zotero.DataObjectUtilities.generateKey();
let annotationItem = await Zotero.Annotations.saveFromJSON(item, annotation);
num++;
}
return num;
}
}
Zotero.PDFImport = new PdfImport();

View file

@ -0,0 +1,283 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2020 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 WORKER_URL = 'chrome://zotero/content/xpcom/pdfWorker/worker.js';
const CMAPS_URL = 'resource://zotero/pdf-reader/cmaps/';
class PDFWorker {
constructor() {
this._worker = null;
this._lastPromiseID = 0;
this._waitingPromises = {};
this._queue = [];
this._processingQueue = false;
}
async _processQueue() {
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._worker.postMessage({ id: this._lastPromiseID, action, data }, transfer);
});
}
_init() {
if (this._worker) return;
this._worker = new Worker(WORKER_URL);
this._worker.addEventListener('message', async (event) => {
let message = event.data;
// console.log(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.id) {
let respData = null;
try {
if (message.action === 'FetchBuiltInCMap') {
let response = await Zotero.HTTP.request(
'GET',
CMAPS_URL + event.data.data.name + '.bcmap',
{ responseType: 'arraybuffer' }
);
respData = {
compressionType: 1,
cMapData: new Uint8Array(response.response)
};
}
}
catch (e) {
Zotero.debug('Failed to fetch CMap data:');
Zotero.debug(e);
}
this._worker.postMessage({ responseId: event.data.id, data: respData });
}
});
this._worker.addEventListener('error', (event) => {
Zotero.debug('PDF Web Worker error:');
Zotero.debug(event);
});
}
isPDFAttachment(item) {
return item.isAttachment() && item.attachmentContentType === 'application/pdf';
}
canImport(item) {
if (this.isPDFAttachment(item)) {
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)) {
return true;
}
}
}
}
/**
* Export attachment with annotations to specified path
*
* @param {Integer} itemID
* @param {String} path
* @param {Boolean} isPriority
* @param {String} password
* @returns {Promise<Integer>} Number of written annotations
*/
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');
}
let ids = attachment.getAnnotations();
let annotations = [];
for (let id of ids) {
let item = await Zotero.Items.getAsync(id);
annotations.push({
id: item.key,
type: item.annotationType,
authorName: Zotero.Users.getName(item.createdByUserID) || '',
comment: item.annotationComment || '',
color: item.annotationColor,
position: JSON.parse(item.annotationPosition),
dateModified: item.dateModified
});
}
let attachmentPath = await attachment.getFilePath();
let buf = await OS.File.read(attachmentPath, {});
buf = new Uint8Array(buf).buffer;
let res = await this._query('export', { buf, annotations, password }, [buf]);
await OS.File.writeAtomic(path, new Uint8Array(res.buf));
return annotations.length;
});
}
/**
* Export children PDF attachments with annotations
*
* @param {Zotero.Item} item
* @param {String} directory
*/
async exportParent(item, directory) {
if (!item.isRegularItem()) {
throw new Error('regular item not provided');
}
if (!directory) {
throw new Error('\'directory\' not provided');
}
let promises = [];
let ids = item.getAttachments();
for (let id of ids) {
let attachment = Zotero.Items.get(id);
if (this.isPDFAttachment(attachment)) {
let path = OS.Path.join(directory, attachment.attachmentFilename);
promises.push(this.export(id, path));
}
}
await Promise.all(promises);
}
/**
* Import annotations from PDF attachment
*
* @param {Integer} itemID
* @param {Boolean} save Save imported annotations, or otherwise just return the number of importable annotations
* @param {Boolean} isPriority
* @param {String} password
* @returns {Promise<Integer>} Number of annotations
*/
async import(itemID, save, 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');
}
// TODO: Remove when fixed
attachment._loaded.childItems = true;
let ids = attachment.getAnnotations();
let existingAnnotations = [];
for (let id of ids) {
let item = await Zotero.Items.getAsync(id);
existingAnnotations.push({
id: item.key,
type: item.annotationType,
comment: item.annotationComment || '',
position: JSON.parse(item.annotationPosition)
});
}
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);
}
Zotero.PDF.hasUnmachedAnnotations[itemID] = false;
}
else {
Zotero.PDF.hasUnmachedAnnotations[itemID] = !!annotations.length;
}
for (let readerWindow of Zotero.Reader._readerWindows) {
if (readerWindow._itemID === itemID) {
readerWindow.toggleImportPrompt(!!Zotero.PDF.hasUnmachedAnnotations[itemID]);
}
}
Zotero.PDF.dateChecked[itemID] = Zotero.Date.dateToISO(new Date());
return annotations.length;
});
}
/**
* Import children PDF attachment annotations
*
* @param {Zotero.Item} item
*/
async importParent(item) {
if (!item.isRegularItem()) {
throw new Error('regular item not provided');
}
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));
}
}
await Promise.all(promises);
}
}
Zotero.PDFWorker = new PDFWorker();

View file

@ -1,106 +0,0 @@
const CMAPS_URL = 'resource://zotero/pdf-reader/cmaps/';
class PDFWorker {
constructor() {
this.worker = null;
this.promiseId = 0;
this.waitingPromises = {};
}
async query(op, data, transfer) {
return new Promise((resolve, reject) => {
this.promiseId++;
this.waitingPromises[this.promiseId] = {resolve, reject};
this.worker.postMessage({id: this.promiseId, op, data}, transfer);
});
}
init() {
if (this.worker) return;
this.worker = new Worker('chrome://zotero/content/xpcom/pdfWorker/worker.js');
this.worker.addEventListener('message', async e => {
let message = e.data;
// console.log(e.data)
if (message.responseId) {
let { resolve, reject } = this.waitingPromises[message.responseId];
if (message.data) {
resolve(message.data);
}
else {
reject(message.error);
}
return;
}
if (message.id) {
let respData = null;
try {
if (message.op === 'FetchBuiltInCMap') {
let response = await Zotero.HTTP.request(
"GET",
CMAPS_URL + e.data.data.name + '.bcmap',
{responseType: 'arraybuffer'}
);
respData = {
compressionType: 1,
cMapData: new Uint8Array(response.response)
};
}
}
catch (e) {
Zotero.debug('Failed to fetch CMap data:');
Zotero.debug(e);
}
this.worker.postMessage({responseId: e.data.id, data: respData});
}
});
this.worker.addEventListener('error', e => {
Zotero.debug('PDF Web Worker error:');
Zotero.debug(e);
});
}
async writeAnnotations(itemID, annotations, path) {
Zotero.debug("Writing annotations");
this.init();
let password = '';
let item = await Zotero.Items.getAsync(itemID);
let itemFilePath = await item.getFilePath();
let buf = await OS.File.read(itemFilePath, {});
buf = new Uint8Array(buf).buffer;
let res = await this.query('write', {buf, annotations, password}, [buf]);
if (!path) {
path = itemFilePath;
}
await OS.File.writeAtomic(path, new Uint8Array(res.buf));
}
async readAnnotations(itemID) {
this.init();
let password = '';
let item = await Zotero.Items.getAsync(itemID);
let path = await item.getFilePath();
let buf = await OS.File.read(path, {});
buf = new Uint8Array(buf).buffer;
let res = await this.query('read', {buf, password}, [buf]);
return res.annotations;
}
}
Zotero.PDFWorker = new PDFWorker();

View file

@ -1,8 +1,12 @@
let PDFStates = {};
// Temporary stuff
Zotero.PDF = {
dateChecked: {},
hasUnmachedAnnotations: {}
};
class ReaderWindow {
constructor() {
this.annotationItemIds = [];
this.annotationItemIDs = [];
this._instanceID = Zotero.Utilities.randomString();
this._window = null;
this._iframeWindow = null;
@ -115,7 +119,8 @@ class ReaderWindow {
state,
location,
enablePrev: !!this._prevHistory.length,
enableNext: !!this._nextHistory.length
enableNext: !!this._nextHistory.length,
promptImport: !!Zotero.PDF.hasUnmachedAnnotations[this._itemID]
}, [buf]);
return true;
}
@ -156,6 +161,10 @@ class ReaderWindow {
this._postMessage({ action: 'navigate', location });
}
toggleImportPrompt(enable) {
this._postMessage({ action: 'toggleImportPrompt', enable });
}
close() {
this._window.close();
}
@ -337,7 +346,7 @@ class ReaderWindow {
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);
this.annotationItemIDs = this.annotationItemIDs.filter(id => id !== annotation.id);
await annotation.eraseTx();
}
}
@ -380,8 +389,7 @@ class ReaderWindow {
}
case 'import': {
Zotero.debug('Importing PDF annotations');
let item = Zotero.Items.get(this._itemID);
Zotero.PDFImport.import(item);
Zotero.PDFWorker.import(this._itemID, true, true);
return;
}
case 'importDismiss': {
@ -534,7 +542,7 @@ class Reader {
// 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));
let disappearedIds = readerWindow.annotationItemIDs.filter(x => ids.includes(x));
if (disappearedIds.length) {
let keys = disappearedIds.map(id => extraData[id].key);
readerWindow.unsetAnnotations(keys);
@ -547,9 +555,9 @@ class Reader {
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 annotationItemIDs = item.getAnnotations();
readerWindow.annotationItemIDs = annotationItemIDs;
let affectedAnnotationIds = annotationItemIDs.filter(annotationID => {
let annotation = Zotero.Items.get(annotationID);
let imageAttachmentID = null;
annotation._loaded.childItems = true;
@ -586,6 +594,7 @@ class Reader {
}
async open(itemID, location) {
this.triggerAnnotationsImportCheck(itemID);
let reader = this._getReaderWindow(itemID);
if (reader) {
if (location) {
@ -605,6 +614,15 @@ class Reader {
}
reader._window.focus();
}
async triggerAnnotationsImportCheck(itemID) {
let item = await Zotero.Items.getAsync(itemID);
let mtime = await item.attachmentModificationTime;
let dateModified = Zotero.Date.dateToISO(new Date(mtime));
if (!Zotero.PDF.dateChecked[itemID] || Zotero.PDF.dateChecked[itemID] < dateModified) {
await Zotero.PDFWorker.import(itemID, false);
}
}
}
Zotero.Reader = new Reader();

View file

@ -2746,8 +2746,7 @@ var ZoteroPane = new function()
'createParent',
'renameAttachments',
'reindexItem',
'importAnnotations',
'exportAnnotations'
'importAnnotations'
];
var m = {};
@ -2795,8 +2794,7 @@ var ZoteroPane = new function()
canRecognize = true,
canUnrecognize = true,
canRename = true,
canImportAnnotations = true,
canExportAnnotations = true;
canImportAnnotations = true;
var canMarkRead = collectionTreeRow.isFeed();
var markUnread = true;
@ -2818,14 +2816,10 @@ var ZoteroPane = new function()
canUnrecognize = false;
}
if (canImportAnnotations && !Zotero.PDFImport.canImport(item)) {
if (canImportAnnotations && !Zotero.PDFWorker.canImport(item)) {
canImportAnnotations = false;
}
if (canExportAnnotations && !Zotero.PDFExport.canExport(item)) {
canExportAnnotations = 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;
@ -2908,10 +2902,6 @@ var ZoteroPane = new function()
if (canImportAnnotations) {
show.push(m.importAnnotations);
}
if (canExportAnnotations) {
show.push(m.exportAnnotations);
}
}
// Single item selected
@ -2981,13 +2971,9 @@ var ZoteroPane = new function()
show.push(m.duplicateItem);
}
if (Zotero.PDFImport.canImport(item)) {
if (Zotero.PDFWorker.canImport(item)) {
show.push(m.importAnnotations);
}
if (Zotero.PDFExport.canExport(item)) {
show.push(m.exportAnnotations);
}
}
// Update attachment submenu
@ -4578,16 +4564,16 @@ var ZoteroPane = new function()
}
};
this.exportAnnotationsForSelected = async function () {
var items = ZoteroPane.getSelectedItems();
Zotero.PDFExport.export(items);
Zotero.ProgressQueues.get('pdf-export').getDialog().open();
};
this.importAnnotationsForSelected = async function () {
var items = ZoteroPane.getSelectedItems();
Zotero.PDFImport.import(items);
Zotero.ProgressQueues.get('pdf-import').getDialog().open();
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 () {
@ -4709,7 +4695,7 @@ var ZoteroPane = new function()
var rv = await fp.show();
if (rv === fp.returnOK || rv === fp.returnReplace) {
let outputFile = fp.file;
Zotero.PDFExport.exportToPath(item, outputFile, true);
await Zotero.PDFWorker.export(item.id, outputFile, true);
}
};

View file

@ -292,10 +292,7 @@
<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-import-annotations" label="&zotero.items.menu.importAnnotations;" oncommand="ZoteroPane.importAnnotationsForSelected()"/>
</menupopup>
<tooltip id="fake-tooltip"/>

View file

@ -100,7 +100,6 @@
<!ENTITY zotero.items.menu.mergeItems "Merge Items…">
<!ENTITY zotero.items.menu.unrecognize "Undo Retrieve Metadata">
<!ENTITY zotero.items.menu.reportMetadata "Report Incorrect Metadata">
<!ENTITY zotero.items.menu.exportAnnotations "Export Annotations">
<!ENTITY zotero.items.menu.importAnnotations "Import Annotations">
<!ENTITY zotero.duplicatesMerge.versionSelect "Choose the version of the item to use as the master item:">

View file

@ -1131,11 +1131,6 @@ recognizePDF.reportMetadata = Report Incorrect Metadata
recognizePDF.pdfName.label = PDF Name
recognizePDF.itemName.label = Item Name
pdfExport.title = PDF Annotations Export
pdfImport.title = PDF Annotations Import
pdfImport.annotations.label = Annotations
rtfScan.openTitle = Select a file to scan
rtfScan.scanning.label = Scanning RTF Document…
rtfScan.saving.label = Formatting RTF Document…

View file

@ -620,24 +620,6 @@
margin-top: 1px;
}
#zotero-tb-pq-pdf-export {
list-style-image: url(chrome://zotero/skin/pdf-search.png);
}
#zotero-tb-pq-pdf-export .toolbarbutton-icon {
width: 18px;
margin-top: 1px;
}
#zotero-tb-pq-pdf-import {
list-style-image: url(chrome://zotero/skin/pdf-search.png);
}
#zotero-tb-pq-pdf-import .toolbarbutton-icon {
width: 18px;
margin-top: 1px;
}
/* Sync error icon */
#zotero-tb-sync-error {
list-style-image: url(chrome://zotero/skin/error.png);

View file

@ -47,7 +47,7 @@ const xpcomFilesAll = [
'http',
'mimeTypeHandler',
'openurl',
'pdfWorker/transport',
'pdfWorker/manager',
'ipc',
'profile',
'progressWindow',
@ -113,8 +113,6 @@ const xpcomFilesLocal = [
'progressQueueDialog',
'quickCopy',
'recognizePDF',
'pdfExport',
'pdfImport',
'report',
'retractions',
'router',

@ -1 +1 @@
Subproject commit 20b9f8f7a197b809c310db0c94bc7a5d22ed9bd0
Subproject commit 9067fc6a9245019b0a4670f8a2b5d81f9f36ad0f