Add zotero://open-pdf handler to open PDF at a given page
This is loosely based on the same functionality in ZotFile, but it tries to do the right thing based on existing Zotero settings: either the new PDF handler setting in the prefs or the system-default app. The latter can only reliably be determined on Windows (and this uses ZotFile's function to read that from the registry), but this tries to figure it out on macOS and Linux too using the Mozilla handler service. (The handler service only gets you an app name, not a path, so on Linux we can try reading mimetypes.list and the like in case someone is using a system-default okular or evince not in /usr/bin, but that's not yet implemented.) This uses the new 5.0 URL format, and a 'page' query parameter instead of a path component: zotero://open-pdf/library/items/[itemKey]?page=[page] zotero://open-pdf/groups/[groupID]/items/[itemKey]?page=[page] It also accepts ZotFile-style URLs, though, so if you uninstall ZotFile you should still be able to open those links. ZotFile will need to accept the new format for new links to work when ZotFile is installed, since it will override this handler. This functionality will be necessary for annotation extraction (#1018) and for imported annotations from Mendeley (#1451).
This commit is contained in:
parent
c0a4fa43f0
commit
609657a8e4
3 changed files with 387 additions and 0 deletions
272
chrome/content/zotero/xpcom/openPDF.js
Normal file
272
chrome/content/zotero/xpcom/openPDF.js
Normal file
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2018 Center for History and New Media
|
||||
George Mason University, Fairfax, Virginia, USA
|
||||
https://zotero.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 *****
|
||||
*/
|
||||
|
||||
Zotero.OpenPDF = {
|
||||
openToPage: async function (path, page) {
|
||||
var handler = Zotero.Prefs.get("fileHandler.pdf");
|
||||
var opened = false;
|
||||
if (Zotero.isMac) {
|
||||
if (handler.includes('Preview')) {
|
||||
this._openWithPreview(path, page);
|
||||
opened = true;
|
||||
}
|
||||
else if (handler.includes('Skim')) {
|
||||
this._openWithSkim(path, page);
|
||||
opened = true;
|
||||
}
|
||||
else {
|
||||
// Try to detect default app
|
||||
handler = this._getPDFHandlerName();
|
||||
Zotero.debug(`Handler is ${handler}`);
|
||||
if (handler && handler == 'Skim') {
|
||||
this._openWithSkim(path, page);
|
||||
}
|
||||
// Fall back to Preview
|
||||
else {
|
||||
this._openWithPreview(path, page);
|
||||
}
|
||||
}
|
||||
opened = true;
|
||||
}
|
||||
else if (Zotero.isWin) {
|
||||
handler = handler || this._getPDFHandlerWindows();
|
||||
// Include flags to open the PDF on a given page in various apps
|
||||
//
|
||||
// Adobe Acrobat: http://partners.adobe.com/public/developer/en/acrobat/PDFOpenParameters.pdf
|
||||
// PDF-XChange: http://help.tracker-software.com/eu/default.aspx?pageid=PDFXView25:command_line_options
|
||||
let args = ['/A', 'page=' + page, path];
|
||||
Zotero.Utilities.Internal.exec(handler, args);
|
||||
opened = true;
|
||||
}
|
||||
else if (Zotero.isLinux) {
|
||||
if (handler.includes('evince') || handler.includes('okular')) {
|
||||
this._openWithEvinceOrOkular(handler, path, page);
|
||||
opened = true;
|
||||
}
|
||||
else {
|
||||
let handler = await this._getPDFHandlerLinux();
|
||||
if (handler.includes('evince') || handler.includes('okular')) {
|
||||
this._openWithEvinceOrOkular(handler, path, page);
|
||||
opened = true;
|
||||
}
|
||||
// Fall back to okular and then evince if unknown handler
|
||||
else if (await OS.File.exists('/usr/bin/okular')) {
|
||||
this._openWithEvinceOrOkular('/usr/bin/okular', path, page);
|
||||
opened = true;
|
||||
}
|
||||
else if (await OS.File.exists('/usr/bin/evince')) {
|
||||
this._openWithEvinceOrOkular('/usr/bin/evince', path, page);
|
||||
opened = true;
|
||||
}
|
||||
else {
|
||||
Zotero.debug("No handler found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return opened;
|
||||
},
|
||||
|
||||
_getPDFHandlerName: function () {
|
||||
var handlerService = Cc["@mozilla.org/uriloader/handler-service;1"]
|
||||
.getService(Ci.nsIHandlerService);
|
||||
var handlers = handlerService.enumerate();
|
||||
var handler;
|
||||
while (handlers.hasMoreElements()) {
|
||||
let handlerInfo = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo);
|
||||
if (handlerInfo.type == 'application/pdf') {
|
||||
handler = handlerInfo;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!handler) {
|
||||
// We can't get the name of the system default handler unless we add an entry
|
||||
Zotero.debug("Default handler not found -- adding default entry");
|
||||
let mimeService = Components.classes["@mozilla.org/mime;1"]
|
||||
.getService(Components.interfaces.nsIMIMEService);
|
||||
let mimeInfo = mimeService.getFromTypeAndExtension("application/pdf", "");
|
||||
mimeInfo.preferredAction = 4;
|
||||
mimeInfo.alwaysAskBeforeHandling = false;
|
||||
handlerService.store(mimeInfo);
|
||||
|
||||
// And once we do that, we can get the name (but not the path, unfortunately)
|
||||
let handlers = handlerService.enumerate();
|
||||
while (handlers.hasMoreElements()) {
|
||||
let handlerInfo = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo);
|
||||
if (handlerInfo.type == 'application/pdf') {
|
||||
handler = handlerInfo;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (handler) {
|
||||
Zotero.debug(`Default handler is ${handler.defaultDescription}`);
|
||||
return handler.defaultDescription;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
//
|
||||
// Mac
|
||||
//
|
||||
_openWithPreview: async function (filePath, page) {
|
||||
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', 'Preview', filePath]);
|
||||
// Go to page using AppleScript
|
||||
let args = [
|
||||
'-e', 'tell app "Preview" to activate',
|
||||
'-e', 'tell app "System Events" to keystroke "g" using {option down, command down}',
|
||||
'-e', `tell app "System Events" to keystroke "${page}"`,
|
||||
'-e', 'tell app "System Events" to keystroke return'
|
||||
];
|
||||
await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
|
||||
},
|
||||
|
||||
_openWithSkim: async function (filePath, page) {
|
||||
// Escape double-quotes in path
|
||||
var quoteRE = /"/g;
|
||||
filePath = filePath.replace(quoteRE, '\\"');
|
||||
let filename = OS.Path.basename(filePath).replace(quoteRE, '\\"');
|
||||
let args = [
|
||||
'-e', 'tell app "Skim" to activate',
|
||||
'-e', `tell app "Skim" to open "${filePath}"`
|
||||
];
|
||||
args.push('-e', `tell document "${filename}" of application "Skim" to go to page ${page}`);
|
||||
await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
|
||||
},
|
||||
|
||||
//
|
||||
// Windows
|
||||
//
|
||||
/**
|
||||
* Get path to default pdf reader application on windows
|
||||
* @return {string} Path to default pdf reader application
|
||||
*
|
||||
* From getPDFReader() in ZotFile (GPL)
|
||||
* https://github.com/jlegewie/zotfile/blob/master/chrome/content/zotfile/utils.js
|
||||
*/
|
||||
_getPDFHandlerWindows: function () {
|
||||
var wrk = Components.classes["@mozilla.org/windows-registry-key;1"]
|
||||
.createInstance(Components.interfaces.nsIWindowsRegKey);
|
||||
// Get handler for PDFs
|
||||
var tryKeys = [
|
||||
{
|
||||
root: wrk.ROOT_KEY_CURRENT_USER,
|
||||
path: 'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.pdf\\UserChoice',
|
||||
value: 'Progid'
|
||||
},
|
||||
{
|
||||
root: wrk.ROOT_KEY_CLASSES_ROOT,
|
||||
path: '.pdf',
|
||||
value: ''
|
||||
}
|
||||
];
|
||||
var progId;
|
||||
for (let i = 0; !progId && i < tryKeys.length; i++) {
|
||||
try {
|
||||
wrk.open(
|
||||
tryKeys[i].root,
|
||||
tryKeys[i].path,
|
||||
wrk.ACCESS_READ
|
||||
);
|
||||
progId = wrk.readStringValue(tryKeys[i].value);
|
||||
}
|
||||
catch (e) {}
|
||||
}
|
||||
|
||||
if (!progId) {
|
||||
wrk.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get version specific handler, if it exists
|
||||
try {
|
||||
wrk.open(
|
||||
wrk.ROOT_KEY_CLASSES_ROOT,
|
||||
progId + '\\CurVer',
|
||||
wrk.ACCESS_READ
|
||||
);
|
||||
progId = wrk.readStringValue('') || progId;
|
||||
}
|
||||
catch (e) {}
|
||||
|
||||
// Get command
|
||||
var success = false;
|
||||
tryKeys = [
|
||||
progId + '\\shell\\Read\\command',
|
||||
progId + '\\shell\\Open\\command'
|
||||
];
|
||||
for (let i = 0; !success && i < tryKeys.length; i++) {
|
||||
try {
|
||||
wrk.open(
|
||||
wrk.ROOT_KEY_CLASSES_ROOT,
|
||||
tryKeys[i],
|
||||
wrk.ACCESS_READ
|
||||
);
|
||||
success = true;
|
||||
}
|
||||
catch (e) {}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
wrk.close();
|
||||
return;
|
||||
}
|
||||
|
||||
var command = wrk.readStringValue('').match(/^(?:".+?"|[^"]\S+)/);
|
||||
|
||||
wrk.close();
|
||||
|
||||
if (!command) return;
|
||||
return command[0].replace(/"/g, '');
|
||||
},
|
||||
|
||||
//
|
||||
// Linux
|
||||
//
|
||||
_getPDFHandlerLinux: async function () {
|
||||
var name = this._getPDFHandlerName();
|
||||
switch (name.toLowerCase()) {
|
||||
case 'okular':
|
||||
return `/usr/bin/${name}`;
|
||||
|
||||
// It's "Document Viewer" on stock Ubuntu
|
||||
case 'document viewer':
|
||||
case 'evince':
|
||||
return `/usr/bin/evince`;
|
||||
}
|
||||
|
||||
// TODO: Try to get default from mimeapps.list, etc., in case system default is okular
|
||||
// or evince somewhere other than /usr/bin
|
||||
var homeDir = OS.Constants.Path.homeDir;
|
||||
|
||||
return false;
|
||||
|
||||
},
|
||||
|
||||
_openWithEvinceOrOkular: function (appPath, filePath, page) {
|
||||
var args = ['-p', page, filePath];
|
||||
Zotero.Utilities.Internal.exec(appPath, args);
|
||||
}
|
||||
}
|
|
@ -1000,6 +1000,119 @@ function ZoteroProtocolHandler() {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Open a PDF at a given page (or try to)
|
||||
*
|
||||
* zotero://open-pdf/library/items/[itemKey]?page=[page]
|
||||
* zotero://open-pdf/groups/[groupID]/items/[itemKey]?page=[page]
|
||||
*
|
||||
* Also supports ZotFile format:
|
||||
* zotero://open-pdf/[libraryID]_[key]/[page]
|
||||
*/
|
||||
var OpenPDFExtension = {
|
||||
noContent: true,
|
||||
|
||||
doAction: async function (uri) {
|
||||
var userLibraryID = Zotero.Libraries.userLibraryID;
|
||||
|
||||
var uriPath = uri.path;
|
||||
if (!uriPath) {
|
||||
return 'Invalid URL';
|
||||
}
|
||||
// Strip leading '/'
|
||||
uriPath = uriPath.substr(1);
|
||||
var mimeType, content = '';
|
||||
|
||||
var params = {
|
||||
objectType: 'item'
|
||||
};
|
||||
var router = new Zotero.Router(params);
|
||||
|
||||
// All items
|
||||
router.add('library/items/:objectKey/:pathPage', function () {
|
||||
params.libraryID = userLibraryID;
|
||||
});
|
||||
router.add('groups/:groupID/items/:objectKey/:pathPage');
|
||||
|
||||
// ZotFile URLs
|
||||
router.add(':id/:pathPage', function () {
|
||||
var lkh = Zotero.Items.parseLibraryKeyHash(params.id);
|
||||
if (!lkh) {
|
||||
Zotero.warn(`Invalid URL ${url}`);
|
||||
return;
|
||||
}
|
||||
params.libraryID = lkh.libraryID || userLibraryID;
|
||||
params.objectKey = lkh.key;
|
||||
delete params.id;
|
||||
});
|
||||
router.run(uriPath);
|
||||
|
||||
Zotero.API.parseParams(params);
|
||||
var results = await Zotero.API.getResultsFromParams(params);
|
||||
var page = params.pathPage || params.page;
|
||||
if (parseInt(page) != page) {
|
||||
page = null;
|
||||
}
|
||||
|
||||
if (!results.length) {
|
||||
Zotero.warn(`No item found for ${uriPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
var item = results[0];
|
||||
|
||||
if (!item.isFileAttachment()) {
|
||||
Zotero.warn(`Item for ${uriPath} is not a file attachment`);
|
||||
return;
|
||||
}
|
||||
|
||||
var path = await item.getFilePathAsync();
|
||||
if (!path) {
|
||||
Zotero.warn(`${path} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!path.toLowerCase().endsWith('.pdf')
|
||||
&& Zotero.MIME.sniffForMIMEType(await Zotero.File.getSample(path)) != 'application/pdf') {
|
||||
Zotero.warn(`${path} is not a PDF`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no page number, just open normally
|
||||
if (!page) {
|
||||
let zp = Zotero.getActiveZoteroPane();
|
||||
// TODO: Open pane if closed (macOS)
|
||||
if (zp) {
|
||||
zp.viewAttachment([item.id]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var opened = Zotero.OpenPDF.openToPage(path, page);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
// If something went wrong, just open PDF without page
|
||||
if (!opened) {
|
||||
let zp = Zotero.getActiveZoteroPane();
|
||||
// TODO: Open pane if closed (macOS)
|
||||
if (zp) {
|
||||
zp.viewAttachment([item.id]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Zotero.Notifier.trigger('open', 'file', item.id);
|
||||
},
|
||||
|
||||
|
||||
newChannel: function (uri) {
|
||||
this.doAction(uri);
|
||||
}
|
||||
};
|
||||
|
||||
this._extensions[ZOTERO_SCHEME + "://data"] = DataExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://report"] = ReportExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://timeline"] = TimelineExtension;
|
||||
|
@ -1008,6 +1121,7 @@ function ZoteroProtocolHandler() {
|
|||
this._extensions[ZOTERO_SCHEME + "://fullscreen"] = FullscreenExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://debug"] = DebugExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://connector"] = ConnectorExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://open-pdf"] = OpenPDFExtension;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -100,6 +100,7 @@ const xpcomFilesLocal = [
|
|||
'locateManager',
|
||||
'mime',
|
||||
'notifier',
|
||||
'openPDF',
|
||||
'quickCopy',
|
||||
'recognizePDF',
|
||||
'report',
|
||||
|
|
Loading…
Add table
Reference in a new issue