#!/usr/bin/env python # -*- coding: utf-8 -*- from anki.hooks import wrap from anki.media import MediaManager from aqt.utils import tooltip, showWarning from aqt.editor import Editor, EditorWebView from aqt import mw from PyQt4.QtGui import * from PyQt4.QtCore import * from PyQt4.QtWebKit import * import os import pickle import logging import copy import shutil import tempfile import urllib2 # Get log file irFolder = os.path.join(mw.pm.addonFolder(), 'ImageResizer') logFile = os.path.join(irFolder, 'imageResizer.log') # if ImageResizer's folder doesn't exist, create one if not os.path.exists(irFolder): os.makedirs(irFolder) # create the logFile open(logFile, 'w').close() # setup logger logging.basicConfig(format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s', filename = logFile, level = logging.DEBUG) logger = logging.getLogger(__name__) # settings main window, Qt won't show the window if # we don't assign a global variable to Settings() class Setup(object): """Do all the necessary initialization when anki loads the addon """ config = dict( isUpScalingDisabled = False, auto = True, keys = dict(Ctrl = True, Alt = False, Shift = True, Extra = 'V'), width = '400', height = '400', ratioKeep = 'height', scalingMode = 'fast' ) defaultConfig = copy.deepcopy(config) settingsMw = None addonDir = mw.pm.addonFolder() irFolder = os.path.join(addonDir, 'ImageResizer') pickleFile = os.path.join(irFolder, 'config.pickle') def __init__(self, imageResizer): self.checkConfigAndLoad() self.setupMenu() self.setupFunctions(imageResizer) logger.debug('Setup init completed') def checkConfigAndLoad(self): """Check if the ImageResizer folder exists Create one if not, then load the configuration """ if not os.path.exists(Setup.irFolder): logger.debug("config folder doesn't exist, creating a new one...") os.makedirs(Setup.irFolder) if not os.path.exists(Setup.pickleFile): # dump the default config if config.pickle doesn't exist logger.debug("config.pickle doesn't exist, creating one with the default settings: {}".format(Setup.defaultConfig)) with open(Setup.pickleFile, 'wb') as f: pickle.dump(Setup.config, f) # load config.pickle logger.debug("loading config.pickle...") with open(self.pickleFile, 'rb') as f: Setup.config = pickle.load(f) logger.debug("loaded config: {}".format(Setup.config)) def setupMenu(self): """ setup menu in anki """ action = QAction("Image Resizer", mw) mw.connect(action, SIGNAL("triggered()"), self._settings) mw.form.menuTools.addAction(action) def setupFunctions(self, imageResizer): """Replace functions in anki """ # setup button Editor.setupButtons = wrap(Editor.setupButtons, ImageResizerButton, 'after') Editor.imageResizer = imageResizer EditorWebView._processMime = wrap(EditorWebView._processMime, _processMime_around, 'around') def _settings(self): """ Show the settings dialog if the user clicked on the menu """ self.settingsMw = Settings(self, Setup.config) def resize(im): """Resize the image :im: QImage to be resized :returns: resized QImage """ logger.debug('resizing images...') logger.debug('image before resizing, width: {}, height: {}'.format(im.width(), im.height())) option = Setup.config['ratioKeep'] isUpScalingDisabled = Setup.config['isUpScalingDisabled'] heightInConfig = int(Setup.config['height']) widthInConfig = int(Setup.config['width']) transformationMode = Qt.FastTransformation if (Setup.config['scalingMode'] == 'fast') else Qt.SmoothTransformation if option == 'height' or option == 'either' and im.width() <= im.height(): if im.height() <= heightInConfig and not isUpScalingDisabled or im.height() > heightInConfig: logger.debug('scale according to height: {}'.format(int(Setup.config['height']))) im = im.scaledToHeight(heightInConfig, transformationMode) elif option == 'width' or option == 'either' and im.height() < im.width(): if im.width() <= widthInConfig and not isUpScalingDisabled or im.width() > widthInConfig: logger.debug('scale according to width: {}'.format(int(Setup.config['width']))) im = im.scaledToWidth(widthInConfig, transformationMode) logger.debug('image after resizing, width: {}, height: {}'.format(im.width(), im.height())) return im def imageResizer(self, paste = True, mime = None): """resize the image contained in the clipboard paste: paste the resized image in the currently focused widget if the parameter is set True returns: QMimeData""" if mime == None: mime = mw.app.clipboard().mimeData() # check if mime contains any image related urls, and put the image data in the clipboard if it contains it mime = checkAndResize(mime, self) # check if mime contains images or any image file urls if mime.hasImage(): logger.debug('mime contains images relative data in it') if paste: # paste it in the currently focused widget logger.debug('paste is True, paste it') clip = self.mw.app.clipboard() clip.setMimeData(mime, mode = QClipboard.Clipboard) QApplication.focusWidget().onPaste() return mime def ImageResizerButton(self): shortcut = '+' .join([k for k, v in Setup.config['keys'].items() if v == True]) shortcut += '+' + Setup.config['keys']['Extra'] self._addButton("Image Resizer", lambda s = self: imageResizer(self), _(shortcut), text="Image Resizer", size=True) def _processMime_around(self, mime, _old): """I found that anki dealt with html, urls, text first before dealing with image, I didn't find any advantages of it. If the user wants to copy an image from the web broweser, it will make anki fetch the image again, which is a waste of time. the function will try to deal with image data first if mime contains it""" if Setup.config['auto'] == False: logger.debug("Setup.config['auto'] is False, run the original _processMime directly") return _old(self, mime) logger.debug('getting the resized QImage...') mime = self.editor.imageResizer(paste = False, mime = mime) if mime.hasImage(): logger.debug('let anki handle the resized image') return self._processImage(mime) else: logger.debug("image data isn't detected, run the old _processMime function") return _old(self, mime) def checkAndResize(mime, editor): """check if mime contains url and if the url represents a picture file path, fetch the url and put the image in the clipboard if the url represents an image file the function will resize the image if it finds that mime contains it :mime: QMimeData to be checked :editor: an instance of Editor :returns: image filled QMimeData if the contained url represents an image file, the original QMimeData otherwise """ logger.debug('checking if url contained in mime is a pic file...') pic = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp") if mime.hasImage(): logger.debug('find image in mime, resize and return the mime') im = resize(mime.imageData()) mime = QMimeData() mime.setImageData(im) elif mime.hasUrls(): url = mime.urls()[0].toString() # check prefix prefix = url[:url.find(':')] # check suffix for suffix in pic: if url.lower().endswith(suffix): logger.debug('suffix {} meet requirements'.format(suffix)) # fetch the image, put it in the mime and return it im = QImage() req = urllib2.Request(url, None, { 'User-Agent': 'Mozilla/5.0 (compatible; Anki)'}) filecontents = urllib2.urlopen(req).read() im.loadFromData(filecontents) # resize it im = resize(im) mime = QMimeData() mime.setImageData(im) return mime return mime class GrabKey(QWidget): """ Grab the key combination to paste the resized image """ def __init__(self, parent): super(GrabKey, self).__init__() self.parent = parent self.setupUI() # self.active is used to trace whether there's any key held now self.active = 0 self.ctrl = False self.alt = False self.shift = False self.extra = None def setupUI(self): mainLayout = QVBoxLayout() self.setLayout(mainLayout) label = QLabel('Please press the new key combination') mainLayout.addWidget(label) self.setWindowTitle('Grab key combination') self.show() def keyPressEvent(self, evt): self.active += 1 if evt.key() >0 and evt.key() < 127: self.extra = chr(evt.key()) elif evt.key() == Qt.Key_Control: self.ctrl = True elif evt.key() == Qt.Key_Alt: self.alt = True elif evt.key() == Qt.Key_Shift: self.shift = True def keyReleaseEvent(self, evt): self.active -= 1 if self.active == 0: if not (self.ctrl or self.alt or self.shift): msg = QMessageBox() msg.setText('Please press at least one of these keys: Ctrl/Alt/Shift') msg.exec_() return Setup.config['keys'] = dict(Ctrl = self.ctrl, Alt = self.alt, Shift = self.shift, Extra = self.extra) self.parent.updateKeyCombinations() self.close() class Settings(QWidget): """ Image Resizer Settings Window """ def __init__(self, setup, config): super(Settings, self).__init__() self.setup = setup self.pickleFile = Setup.pickleFile self.setupUI() self.checkPickle() def checkPickle(self): """if the config file exists, load it, or continue to use the default setting if the config file doesn't exist """ if os.path.exists(self.pickleFile): self.loadFromDisk() def saveToDisk(self): """save settings to the current directory where the plugin lies, then close the settings window """ Setup.config['isUpScalingDisabled'] = self.disableUpScalingCb.isChecked(); Setup.config['auto'] = self.enableCb.isChecked() Setup.config['width'] = self.widthEdit.text() Setup.config['height'] = self.heightEdit.text() if self.ratioCb.currentIndex() == 0: Setup.config['ratioKeep'] = 'height' elif self.ratioCb.currentIndex() == 1: Setup.config['ratioKeep'] = 'width' elif self.ratioCb.currentIndex() == 2: Setup.config['ratioKeep'] = 'either' if self.scalingCb.currentIndex() == 0: Setup.config['scalingMode'] = 'fast' elif self.scalingCb.currentIndex() == 1: Setup.config['scalingMode'] = 'smooth' with open(self.pickleFile, 'wb') as f: pickle.dump(Setup.config, f) logger.debug('saved config to config.pickle: {}'.format(Setup.config)) self.close() def fillInMissedKeys(self, previousConfig, newConfig): for key in previousConfig: if key not in newConfig: newConfig[key] = previousConfig[key] def loadFromDisk(self): """Load settings from disk """ with open(self.pickleFile, 'rb') as f: Setup.config = pickle.load(f) self.fillInMissedKeys(Setup.defaultConfig, Setup.config) # reflect the settings on the window self.enableCb.setChecked(Setup.config['auto']) self.disableUpScalingCb.setChecked(Setup.config['isUpScalingDisabled']) self.updateKeyCombinations() self.widthEdit.setText(Setup.config['width']) self.heightEdit.setText(Setup.config['height']) if Setup.config['ratioKeep'] == 'height': self.ratioCb.setCurrentIndex(0) elif Setup.config['ratioKeep'] == 'width': self.ratioCb.setCurrentIndex(1) elif Setup.config['ratioKeep'] == 'either': self.ratioCb.setCurrentIndex(2) self.setLineEditState() if Setup.config['scalingMode'] == 'fast': self.scalingCb.setCurrentIndex(0) elif Setup.config['scalingMode'] == 'smooth': self.scalingCb.setCurrentIndex(1) logger.debug('config is loaded from config.pickle: {}'.format(Setup.config)) def reset(self): """reset all configurations to default""" if os.path.exists(Setup.irFolder): logger.debug('removing ImageResizer\'s folder') shutil.rmtree(Setup.irFolder) logger.debug('set config to the default one: {}'.format(Setup.defaultConfig)) Setup.config = copy.deepcopy(Setup.defaultConfig) self.setup.checkConfigAndLoad() self.checkPickle() def updateKeyCombinations(self): """ update the key combination label in the settings window according to Setup.config """ label = self.grabKeyLabel label.setText('Shortcut to paste the resized image: ') # add ctrl/shift/alt [label.setText(label.text() + k + '+') for k, v in Setup.config['keys'].items() if k != 'Extra' and v == True] # add the extra key if Setup.config['keys'].get('Extra'): label.setText(label.text() + Setup.config['keys'].get('Extra')) logger.debug('shortcut is updated: {}'.format(Setup.config['keys'])) def showGrabKey(self): self.GrabKeyWindow = GrabKey(self) def setupUI(self): # main layout mainLayout = QVBoxLayout() self.setLayout(mainLayout) # add widgets to set shortcut self.enableCb = QCheckBox('Resize on pasting', self) self.disableUpScalingCb = QCheckBox('Disable upscaling', self) self.grabKeyLabel = QLabel('Shortcut to paste the resized image: Ctrl+Shift+V') grabKeyBtn = QPushButton('Grab the shortcut', self) grabKeyBtn.clicked.connect(self.showGrabKey) self.scalingCb = QComboBox(self) self.scalingCb.addItem('use fast resizing') self.scalingCb.addItem('use smooth resizing') keyHBox = QHBoxLayout() keyHBox.addWidget(self.grabKeyLabel) keyHBox.addWidget(grabKeyBtn) mainLayout.addWidget(self.enableCb) mainLayout.addWidget(self.disableUpScalingCb) mainLayout.addWidget(self.scalingCb) mainLayout.addLayout(keyHBox) # add widgets to set height and width widthLable = QLabel('width') heightLable = QLabel('height') self.widthEdit = QLineEdit(self) self.heightEdit = QLineEdit(self) self.ratioCb = QComboBox(self) self.ratioCb.addItem('scale to height and keep ratio') self.ratioCb.addItem('scale to width and keep ratio') self.ratioCb.addItem('scale to the maximum dimension and keep ratio') QObject.connect(self.ratioCb, SIGNAL("currentIndexChanged(int)"), self.setLineEditState) sizeLayout = QHBoxLayout() sizeLayout.addWidget(widthLable) sizeLayout.addWidget(self.widthEdit) sizeLayout.addWidget(heightLable) sizeLayout.addWidget(self.heightEdit) sizeLayout.addWidget(self.ratioCb) mainLayout.addLayout(sizeLayout) # add an horizontal line mainLayout.addWidget(self.hLine()) # add OK and Cancel buttons okButton = QPushButton("OK") okButton.clicked.connect(self.saveToDisk) cancelButton = QPushButton("Cancel") cancelButton.clicked.connect(self.close) resetButton = QPushButton("Reset") resetButton.clicked.connect(self.reset) btnLayout = QHBoxLayout() btnLayout.addStretch(1) btnLayout.addWidget(okButton) btnLayout.addWidget(cancelButton) btnLayout.addWidget(resetButton) mainLayout.addLayout(btnLayout) # center the window self.move(QDesktopWidget().availableGeometry().center() - self.frameGeometry().center()) self.setWindowTitle('Image Resizer Settings') self.show() def disableLineEdit(self, lineEdit): lineEdit.setReadOnly(True) # change color palette = QPalette() palette.setColor(QPalette.Base, Qt.gray) palette.setColor(QPalette.Text, Qt.darkGray) lineEdit.setPalette(palette) def enableLineEdit(self, lineEdit): lineEdit.setReadOnly(False) # change color palette = QPalette() palette.setColor(QPalette.Base, Qt.white) palette.setColor(QPalette.Text, Qt.black) lineEdit.setPalette(palette) def setLineEditState(self): if self.ratioCb.currentIndex() == 0: self.disableLineEdit(self.widthEdit) self.enableLineEdit(self.heightEdit) elif self.ratioCb.currentIndex() == 1: self.disableLineEdit(self.heightEdit) self.enableLineEdit(self.widthEdit) elif self.ratioCb.currentIndex() == 2: self.enableLineEdit(self.heightEdit) self.enableLineEdit(self.widthEdit) def hLine(self): line = QFrame() line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) return line s = Setup(imageResizer)