Add support for PDF page deletion and rotation (#2595)
* Add support for PDF page deletion and rotation Fixes #2561
This commit is contained in:
parent
44e8a372e5
commit
bfc61a69ba
5 changed files with 248 additions and 2 deletions
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 4d7ce02e92de6a888ee35bd837e056a76a270cdb
|
||||
Subproject commit ac7cae37ae8ba5ea8fc5e0b2a3faebd2d432e65a
|
|
@ -1 +1 @@
|
|||
Subproject commit 03e80435d3390d8a61a67244dc0279bc6b64f134
|
||||
Subproject commit eca15237791a0d16c407dc397e0e53459e951464
|
Loading…
Add table
Reference in a new issue