diff --git a/chrome/content/zotero/xpcom/pdfWorker/manager.js b/chrome/content/zotero/xpcom/pdfWorker/manager.js index e2a8a060bd..9e230895be 100644 --- a/chrome/content/zotero/xpcom/pdfWorker/manager.js +++ b/chrome/content/zotero/xpcom/pdfWorker/manager.js @@ -410,6 +410,162 @@ class PDFWorker { } await Promise.all(promises); } + + /** + * Delete pages from PDF attachment + * + * @param {Integer} itemID Attachment item id + * @param {Array} pageIndexes + * @param {Boolean} [isPriority] + * @param {String} [password] + * @returns {Promise} + */ + async deletePages(itemID, pageIndexes, isPriority, password) { + return this._enqueue(async () => { + let attachment = await Zotero.Items.getAsync(itemID); + + Zotero.debug(`Deleting [${pageIndexes.join(', ')}] pages for item ${attachment.libraryKey}`); + let t = new Date(); + + if (!attachment.isPDFAttachment()) { + throw new Error('Item must be a PDF attachment'); + } + + let annotations = attachment + .getAnnotations() + .map(annotation => ({ + id: annotation.id, + position: JSON.parse(annotation.annotationPosition) + })); + + let path = await attachment.getFilePathAsync(); + let buf = await OS.File.read(path, {}); + buf = new Uint8Array(buf).buffer; + + try { + var { buf: modifiedBuf } = await this._query('deletePages', { + buf, pageIndexes, password + }, [buf]); + } + catch (e) { + let error = new Error(`Worker 'deletePages' failed: ${JSON.stringify({ error: e.message })}`); + try { + error.name = JSON.parse(e.message).name; + } + catch (e) { + Zotero.logError(e); + } + Zotero.logError(error); + throw error; + } + + // Delete annotations from deleted pages + let ids = []; + for (let i = annotations.length - 1; i >= 0; i--) { + let { id, position } = annotations[i]; + if (pageIndexes.includes(position.pageIndex)) { + ids.push(id); + annotations.splice(i, 1); + } + } + if (ids.length) { + await Zotero.Items.erase(ids); + } + + // Shift page index for other annotations + ids = []; + await Zotero.DB.executeTransaction(async function () { + let rows = await Zotero.DB.queryAsync('SELECT itemID, position FROM itemAnnotations WHERE parentItemID=?', itemID); + for (let { itemID, position } of rows) { + try { + position = JSON.parse(position); + } + catch (e) { + Zotero.logError(e); + continue; + } + // Find the count of deleted pages before the current annotation page + let shift = pageIndexes.reduce((prev, cur) => cur < position.pageIndex ? prev + 1 : prev, 0); + if (shift > 0) { + position.pageIndex -= shift; + position = JSON.stringify(position); + await Zotero.DB.queryAsync('UPDATE itemAnnotations SET position=? WHERE itemID=?', [position, itemID]); + ids.push(itemID); + } + } + }); + let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType('item'); + let loadedObjects = objectsClass.getLoaded(); + for (let object of loadedObjects) { + if (ids.includes(object.id)) { + await object.reload(null, true); + } + } + await Zotero.Notifier.trigger('modify', 'item', ids, {}); + + await OS.File.writeAtomic(path, new Uint8Array(modifiedBuf)); + let mtime = Math.floor(await attachment.attachmentModificationTime / 1000); + attachment.attachmentLastProcessedModificationTime = mtime; + await attachment.saveTx({ + skipAll: true + }); + + Zotero.debug(`Deleted pages for item ${attachment.libraryKey} in ${new Date() - t} ms`); + }, isPriority); + } + + /** + * Rotate pages in PDF attachment + * + * @param {Integer} itemID Attachment item id + * @param {Array} pageIndexes + * @param {Integer} degrees 90, 180, 270 + * @param {Boolean} [isPriority] + * @param {String} [password] + * @returns {Promise} + */ + async rotatePages(itemID, pageIndexes, degrees, isPriority, password) { + return this._enqueue(async () => { + let attachment = await Zotero.Items.getAsync(itemID); + + Zotero.debug(`Rotating [${pageIndexes.join(', ')}] pages for item ${attachment.libraryKey}`); + let t = new Date(); + + if (!attachment.isPDFAttachment()) { + throw new Error('Item must be a PDF attachment'); + } + + let path = await attachment.getFilePathAsync(); + let buf = await OS.File.read(path, {}); + buf = new Uint8Array(buf).buffer; + + try { + var { buf: modifiedBuf } = await this._query('rotatePages', { + buf, pageIndexes, degrees, password + }, [buf]); + } + catch (e) { + let error = new Error(`Worker 'rotatePages' failed: ${JSON.stringify({ error: e.message })}`); + try { + error.name = JSON.parse(e.message).name; + } + catch (e) { + Zotero.logError(e); + } + Zotero.logError(error); + throw error; + } + + await OS.File.writeAtomic(path, new Uint8Array(modifiedBuf)); + let mtime = Math.floor(await attachment.attachmentModificationTime / 1000); + attachment.attachmentLastProcessedModificationTime = mtime; + await attachment.saveTx({ + skipAll: true + }); + + Zotero.debug(`Rotated pages for item ${attachment.libraryKey} in ${new Date() - t} ms`); + }, isPriority); + } } Zotero.PDFWorker = new PDFWorker(); diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js index aed2e14829..d20c8d07ca 100644 --- a/chrome/content/zotero/xpcom/reader.js +++ b/chrome/content/zotero/xpcom/reader.js @@ -249,6 +249,33 @@ class ReaderInstance { ); return !index; } + + promptToDeletePages(num) { + let ps = Services.prompt; + let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING + + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL; + let index = ps.confirmEx( + null, + Zotero.getString('pdfReader.promptDeletePages.title'), + Zotero.getString( + 'pdfReader.promptDeletePages.text', + new Intl.NumberFormat().format(num), + num + ), + buttonFlags, + Zotero.getString('general.continue'), + null, null, null, {} + ); + return !index; + } + + async reload() { + let item = Zotero.Items.get(this._itemID); + let path = await item.getFilePathAsync(); + let buf = await OS.File.read(path, {}); + buf = new Uint8Array(buf).buffer; + this._postMessage({ action: 'reload', buf, }, [buf]); + } async menuCmd(cmd) { if (cmd === 'transferFromPDF') { @@ -668,6 +695,60 @@ class ReaderInstance { popup.openPopup(element, 'after_start', 0, 0, true); } + _openThumbnailPopup(data) { + let popup = this._window.document.createElement('menupopup'); + this._popupset.appendChild(popup); + popup.addEventListener('popuphidden', function () { + popup.remove(); + }); + let menuitem; + // Rotate 90 + menuitem = this._window.document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('pdfReader.rotate90')); + menuitem.addEventListener('command', async () => { + this._postMessage({ action: 'reloading' }); + await Zotero.PDFWorker.rotatePages(this._itemID, data.pageIndexes, 90, true); + await this.reload(); + }); + popup.appendChild(menuitem); + // Rotate 180 + menuitem = this._window.document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('pdfReader.rotate180')); + menuitem.addEventListener('command', async () => { + this._postMessage({ action: 'reloading' }); + await Zotero.PDFWorker.rotatePages(this._itemID, data.pageIndexes, 180, true); + await this.reload(); + }); + popup.appendChild(menuitem); + // Rotate 270 + menuitem = this._window.document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('pdfReader.rotate270')); + menuitem.addEventListener('command', async () => { + this._postMessage({ action: 'reloading' }); + await Zotero.PDFWorker.rotatePages(this._itemID, data.pageIndexes, 270, true); + await this.reload(); + }); + popup.appendChild(menuitem); + // Separator + popup.appendChild(this._window.document.createElement('menuseparator')); + // Delete + menuitem = this._window.document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('general.delete')); + menuitem.addEventListener('command', async () => { + if (this.promptToDeletePages(data.pageIndexes.length)) { + this._postMessage({ action: 'reloading' }); + try { + await Zotero.PDFWorker.deletePages(this._itemID, data.pageIndexes, true); + } + catch (e) { + } + await this.reload(); + } + }); + popup.appendChild(menuitem); + popup.openPopupAtScreen(data.x, data.y, true); + } + _openSelectorPopup(data) { let popup = this._window.document.createXULElement('menupopup'); this._popupset.appendChild(popup); @@ -806,6 +887,10 @@ class ReaderInstance { this._openColorPopup(message.data); return; } + case 'openThumbnailPopup': { + this._openThumbnailPopup(message.data); + return; + } case 'closePopup': { // Note: This currently only closes tags popup when annotations are // disappearing from pdf-reader sidebar diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index 0efd483365..ba91df0a19 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -1379,6 +1379,11 @@ pdfReader.promptTransferFromPDF.text = Annotations stored in the PDF file will b pdfReader.promptTransferToPDF.title = Store Annotations in File pdfReader.promptTransferToPDF.text = Annotations will be transferred to the PDF file and will no longer be editable in %S. pdfReader.promptPasswordProtected = The operation is not supported for password-protected PDF files. +pdfReader.promptDeletePages.title = Delete Pages +pdfReader.promptDeletePages.text = Are you sure you want to delete %1$S page from the PDF file?;Are you sure you want to delete %1$S pages from the PDF file? +pdfReader.rotate90 = Rotate 90° +pdfReader.rotate180 = Rotate 180° +pdfReader.rotate270 = Rotate 270° pdfReader.editPageNumber = Edit Page Number… pdfReader.editHighlightedText = Edit Highlighted Text pdfReader.pageNumberPopupHeader = Change page number for: diff --git a/pdf-reader b/pdf-reader index 4d7ce02e92..ac7cae37ae 160000 --- a/pdf-reader +++ b/pdf-reader @@ -1 +1 @@ -Subproject commit 4d7ce02e92de6a888ee35bd837e056a76a270cdb +Subproject commit ac7cae37ae8ba5ea8fc5e0b2a3faebd2d432e65a diff --git a/pdf-worker b/pdf-worker index 03e80435d3..eca1523779 160000 --- a/pdf-worker +++ b/pdf-worker @@ -1 +1 @@ -Subproject commit 03e80435d3390d8a61a67244dc0279bc6b64f134 +Subproject commit eca15237791a0d16c407dc397e0e53459e951464