diff --git a/python/bootstrap.js b/python/bootstrap.js new file mode 100644 index 00000000..dbdd75c8 --- /dev/null +++ b/python/bootstrap.js @@ -0,0 +1,40 @@ +// This allows creating a new web page using the construct "new WebPage", +// which feels more natural than "phantom.createWebPage()". +window.WebPage = function() { + var page = phantom.createWebPage(); + + // deep copy + page.settings = JSON.parse(JSON.stringify(phantom.defaultPageSettings)); + + page.onAlert = function (msg) {}; + + page.onConsoleMessage = function (msg) {}; + + page.open = function () { + if (typeof this.onAlert === 'function') { + this.javaScriptAlertSent.connect(this.onAlert); + } + if (typeof this.onConsoleMessage === 'function') { + this.javaScriptConsoleMessageSent.connect(this.onConsoleMessage); + } + if (arguments.length === 2) { + this.loadStatusChanged.connect(arguments[1]); + this.openUrl(arguments[0], 'get', this.settings); + return; + } else if (arguments.length === 3) { + this.loadStatusChanged.connect(arguments[2]); + this.openUrl(arguments[0], arguments[1], this.settings); + return; + } else if (arguments.length === 4) { + this.loadStatusChanged.connect(arguments[3]); + this.openUrl(arguments[0], { + operation: arguments[1], + data: arguments[2] + }, this.settings); + return; + } + throw "Wrong use of WebPage#open"; + }; + + return page; +} diff --git a/python/csconverter.py b/python/csconverter.py index 1f56b44e..2c666556 100644 --- a/python/csconverter.py +++ b/python/csconverter.py @@ -17,16 +17,21 @@ along with this program. If not, see . ''' -from PyQt4.QtCore import QObject, QFile +import sys + +from PyQt4.QtCore import QObject, QFile, qCritical from PyQt4.QtWebKit import QWebPage + class CSConverter(QObject): def __init__(self, parent=None): QObject.__init__(self, parent) self.m_webPage = QWebPage(self) converter = QFile(':/resources/coffee-script.js') - converter.open(QFile.ReadOnly) + if not converter.open(QFile.ReadOnly): + qCritical('CoffeeScript compiler is not available!') + sys.exit(1) script = str(converter.readAll()) converter.close() @@ -36,6 +41,6 @@ class CSConverter(QObject): def convert(self, script): self.setProperty('source', script) result = self.m_webPage.mainFrame().evaluateJavaScript('this.CoffeeScript.compile(converter.source)') - if len(result): + if result: return result return '' diff --git a/python/networkaccessmanager.py b/python/networkaccessmanager.py index 09feb56a..c188b6da 100644 --- a/python/networkaccessmanager.py +++ b/python/networkaccessmanager.py @@ -24,6 +24,7 @@ from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkDiskCache, \ from plugincontroller import Bunch, do_action + class NetworkAccessManager(QNetworkAccessManager): def __init__(self, diskCacheEnabled, ignoreSslErrors, parent=None): QNetworkAccessManager.__init__(self, parent) diff --git a/python/phantom.py b/python/phantom.py index 69dd277a..3bd6df7b 100644 --- a/python/phantom.py +++ b/python/phantom.py @@ -17,82 +17,67 @@ along with this program. If not, see . ''' -import os -import codecs +import sys from utils import version_major, version_minor, version_patch from plugincontroller import Bunch, do_action from csconverter import CSConverter -from math import ceil, floor -from time import sleep as usleep from webpage import WebPage from networkaccessmanager import NetworkAccessManager -from PyQt4.QtCore import pyqtProperty, pyqtSlot, Qt, QObject, QRect, \ - SLOT, QTimer, QUrl, QFileInfo, QDir, QSize, \ - QSizeF, QTime, QEventLoop, qDebug -from PyQt4.QtGui import QPalette, QDesktopServices, qApp, QPrinter, \ - QImage, QPainter, QRegion, QApplication, qRgba -from PyQt4.QtWebKit import QWebSettings, QWebPage +from PyQt4.QtCore import pyqtProperty, pyqtSlot, QObject, \ + QFile, qCritical +from PyQt4.QtGui import QApplication from PyQt4.QtNetwork import QNetworkProxy, QNetworkProxyFactory -# Different defaults. -# OSX: 72, X11: 75(?), Windows: 96 -pdf_dpi = 72 class Phantom(QObject): def __init__(self, args, parent=None): QObject.__init__(self, parent) # variable declarations - self.m_loadStatus = self.m_state = '' - self.m_var = self.m_paperSize = self.m_loadScript_cache = {} + self.m_defaultPageSettings = {} self.m_verbose = args.verbose self.m_page = WebPage(self) - self.m_clipRect = QRect() # setup the values from args - self.m_script = args.script.read() - self.m_scriptFile = args.script.name - self.m_scriptDir = os.path.dirname(args.script.name) + '/' + self.m_script = args.script + self.m_scriptFile = args.script_name self.m_args = args.script_args - self.m_upload_file = args.upload_file autoLoadImages = False if args.load_images == 'no' else True pluginsEnabled = True if args.load_plugins == 'yes' else False - args.script.close() - do_action('PhantomInitPre', Bunch(locals())) - palette = self.m_page.palette() - palette.setBrush(QPalette.Base, Qt.transparent) - self.m_page.setPalette(palette) - if not args.proxy: QNetworkProxyFactory.setUseSystemConfiguration(True) else: proxy = QNetworkProxy(QNetworkProxy.HttpProxy, args.proxy[0], int(args.proxy[1])) QNetworkProxy.setApplicationProxy(proxy) - self.m_page.settings().setAttribute(QWebSettings.AutoLoadImages, autoLoadImages) - self.m_page.settings().setAttribute(QWebSettings.PluginsEnabled, pluginsEnabled) - self.m_page.settings().setAttribute(QWebSettings.FrameFlatteningEnabled, True) - self.m_page.settings().setAttribute(QWebSettings.OfflineStorageDatabaseEnabled, True) - self.m_page.settings().setAttribute(QWebSettings.LocalStorageEnabled, True) - self.m_page.settings().setLocalStoragePath(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) - self.m_page.settings().setOfflineStoragePath(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) + # Provide WebPage with a non-standard Network Access Manager + self.m_netAccessMan = NetworkAccessManager(args.disk_cache, args.ignore_ssl_errors, self) + self.m_page.setNetworkAccessManager(self.m_netAccessMan) - # Ensure we have a document.body. - self.m_page.mainFrame().setHtml('') + self.m_page.javaScriptConsoleMessageSent.connect(self.printConsoleMessage) - self.m_page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) - self.m_page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) - - m_netAccessMan = NetworkAccessManager(args.disk_cache, args.ignore_ssl_errors, self) - self.m_page.setNetworkAccessManager(m_netAccessMan) + self.m_defaultPageSettings['loadImages'] = autoLoadImages + self.m_defaultPageSettings['loadPlugins'] = pluginsEnabled + self.m_defaultPageSettings['userAgent'] = self.m_page.userAgent() + self.m_page.applySettings(self.m_defaultPageSettings) # inject our properties and slots into javascript - self.m_page.mainFrame().javaScriptWindowObjectCleared.connect(self.inject) - self.m_page.loadFinished.connect(self.finish) + self.m_page.mainFrame().addToJavaScriptWindowObject('phantom', self) + + bootstrap = QFile(':/bootstrap.js') + if not bootstrap.open(QFile.ReadOnly): + qCritical('Can not bootstrap!') + sys.exit(1) + bootstrapper = str(bootstrap.readAll()) + bootstrap.close() + if not bootstrapper: + qCritical('Can not bootstrap!') + sys.exit(1) + self.m_page.mainFrame().evaluateJavaScript(bootstrapper) do_action('PhantomInitPost', Bunch(locals())) @@ -106,76 +91,12 @@ class Phantom(QObject): self.m_page.mainFrame().evaluateJavaScript(self.m_script) - def finish(self, success): - self.m_loadStatus = 'success' if success else 'fail' - self.m_page.mainFrame().evaluateJavaScript(self.m_script) - - def inject(self): - self.m_page.mainFrame().addToJavaScriptWindowObject('phantom', self) - - def renderPdf(self, fileName): - p = QPrinter() - p.setOutputFormat(QPrinter.PdfFormat) - p.setOutputFileName(fileName) - p.setResolution(pdf_dpi) - paperSize = self.m_paperSize - - if not len(paperSize): - pageSize = QSize(self.m_page.mainFrame().contentsSize()) - paperSize['width'] = str(pageSize.width()) + 'px' - paperSize['height'] = str(pageSize.height()) + 'px' - paperSize['border'] = '0px' - - if paperSize.get('width') and paperSize.get('height'): - sizePt = QSizeF(ceil(self.stringToPointSize(paperSize['width'])), - ceil(self.stringToPointSize(paperSize['height']))) - p.setPaperSize(sizePt, QPrinter.Point) - elif 'format' in paperSize: - orientation = QPrinter.Landscape if paperSize.get('orientation') and paperSize['orientation'].lower() == 'landscape' else QPrinter.Portrait - orientation = QPrinter.Orientation(orientation) - p.setOrientation(orientation) - - formats = { - 'A3': QPrinter.A3, - 'A4': QPrinter.A4, - 'A5': QPrinter.A5, - 'Legal': QPrinter.Legal, - 'Letter': QPrinter.Letter, - 'Tabloid': QPrinter.Tabloid - } - - p.setPaperSize(QPrinter.A4) # fallback - for format, size in formats.items(): - if format.lower() == paperSize['format'].lower(): - p.setPaperSize(size) - break - else: - return False - - border = floor(self.stringToPointSize(paperSize['border'])) if paperSize.get('border') else 0 - p.setPageMargins(border, border, border, border, QPrinter.Point) - - self.m_page.mainFrame().print_(p) - return True + def printConsoleMessage(self, msg): + print msg def returnValue(self): return self.m_returnValue - def stringToPointSize(self, string): - units = ( - ('mm', 72 / 25.4), - ('cm', 72 / 2.54), - ('in', 72.0), - ('px', 72.0 / pdf_dpi / 2.54), - ('', 72.0 / pdf_dpi / 2.54) - ) - - for unit, format in units: - if string.endswith(unit): - value = string.rstrip(unit) - return float(value) * format - return 0 - ## # Properties and methods exposed to JavaScript ## @@ -184,171 +105,22 @@ class Phantom(QObject): def args(self): return self.m_args + @pyqtSlot(result=WebPage) + def createWebPage(self): + page = WebPage(self) + page.applySettings(self.m_defaultPageSettings) + page.setNetworkAccessManager(self.m_netAccessMan) + return page + @pyqtProperty('QVariantMap') - def clipRect(self): - result = { - 'width': self.m_clipRect.width(), - 'height': self.m_clipRect.height(), - 'top': self.m_clipRect.top(), - 'left': self.m_clipRect.left() - } - return result - - @clipRect.setter - def clipRect(self, size): - names = ('width', 'height', 'top', 'left') - for item in names: - try: - globals()[item] = int(size[item]) - if globals()[item] < 0: - if item not in ('top', 'left'): - globals()[item] = 0 - except KeyError: - globals()[item] = getattr(self.m_clipRect, item)() - - self.m_clipRect = QRect(left, top, width, height) - - @pyqtProperty(str) - def content(self): - return self.m_page.mainFrame().toHtml() - - @content.setter - def content(self, content): - self.m_page.mainFrame().setHtml(content) + def defaultPageSettings(self): + return self.m_defaultPageSettings @pyqtSlot() @pyqtSlot(int) def exit(self, code=0): self.m_returnValue = code - self.m_page.loadFinished.disconnect(self.finish) - QTimer.singleShot(0, qApp, SLOT('quit()')) - - @pyqtProperty(str) - def loadStatus(self): - return self.m_loadStatus - - @pyqtSlot(str, result=bool) - def loadScript(self, script): - if script in self.m_loadScript_cache: - self.m_page.mainFrame().evaluateJavaScript(self.m_loadScript_cache[script]) - return True - - scriptFile = script - try: - script = codecs.open(self.m_scriptDir + script, encoding='utf-8') - script = script.read() - except IOError: - return False - - if script.startswith('#!'): - script = '//' + script - - if scriptFile.lower().endswith('.coffee'): - coffee = CSConverter(self) - script = coffee.convert(script) - - self.m_loadScript_cache[scriptFile] = script - self.m_page.mainFrame().evaluateJavaScript(script) - return True - - @pyqtSlot(str, name='open') - def open_(self, address): - qDebug('Opening address %s' % address) - self.m_page.triggerAction(QWebPage.Stop) - self.m_loadStatus = 'loading' - self.m_page.mainFrame().setUrl(QUrl(address)) - - @pyqtProperty('QVariantMap') - def paperSize(self): - return self.m_paperSize - - @paperSize.setter - def paperSize(self, size): - self.m_paperSize = size - - @pyqtSlot(str, result=bool) - def render(self, fileName): - fileInfo = QFileInfo(fileName) - path = QDir() - path.mkpath(fileInfo.absolutePath()) - - if fileName.lower().endswith('.pdf'): - return self.renderPdf(fileName) - - viewportSize = QSize(self.m_page.viewportSize()) - pageSize = QSize(self.m_page.mainFrame().contentsSize()) - - bufferSize = QSize() - if not self.m_clipRect.isEmpty(): - bufferSize = self.m_clipRect.size() - else: - bufferSize = self.m_page.mainFrame().contentsSize() - - if pageSize == '': - return False - - image = QImage(bufferSize, QImage.Format_ARGB32) - image.fill(qRgba(255, 255, 255, 0)) - p = QPainter(image) - - p.setRenderHint(QPainter.Antialiasing, True) - p.setRenderHint(QPainter.TextAntialiasing, True) - p.setRenderHint(QPainter.SmoothPixmapTransform, True) - - self.m_page.setViewportSize(pageSize) - - if not self.m_clipRect.isEmpty(): - p.translate(-self.m_clipRect.left(), -self.m_clipRect.top()) - self.m_page.mainFrame().render(p, QRegion(self.m_clipRect)) - else: - self.m_page.mainFrame().render(p) - - p.end() - self.m_page.setViewportSize(viewportSize) - return image.save(fileName) - - @pyqtSlot('QWebElement', str) - def setFormInputFile(self, el, fileTag): - self.m_page.m_nextFileTag = fileTag - el.evaluateJavaScript('''(function(target){ - var evt = document.createEvent('MouseEvents'); - evt.initMouseEvent("click", true, true, window, - 0, 0, 0, 0, 0, false, false, false, false, 0, null); - target.dispatchEvent(evt);})(this);''') - - @pyqtSlot(int) - def sleep(self, ms): - startTime = QTime.currentTime() - while True: - QApplication.processEvents(QEventLoop.AllEvents, 25) - if startTime.msecsTo(QTime.currentTime()) > ms: - break - usleep(0.005) - - @pyqtProperty(str) - def state(self): - return self.m_state - - @state.setter - def state(self, value): - self.m_state = value - - @pyqtProperty(str) - def userAgent(self): - return self.m_page.m_userAgent - - @userAgent.setter - def userAgent(self, ua): - self.m_page.m_userAgent = ua - - @pyqtSlot(str, result='QVariant') - @pyqtSlot(int, result='QVariant') - @pyqtSlot(str, 'QVariant') - @pyqtSlot(int, 'QVariant') - def ctx(self, name, value=None): - if not value: - return self.m_var.get(name) - self.m_var[name] = value + QApplication.instance().exit(code) @pyqtProperty('QVariantMap') def version(self): @@ -359,26 +131,4 @@ class Phantom(QObject): } return version - @pyqtProperty('QVariantMap') - def viewportSize(self): - size = self.m_page.viewportSize() - result = { - 'width': size.width(), - 'height': size.height() - } - return result - - @viewportSize.setter - def viewportSize(self, size): - names = ('width', 'height') - for item in names: - try: - globals()[item] = int(size[item]) - if globals()[item] < 0: - globals()[item] = 0 - except KeyError: - globals()[item] = getattr(self.m_page.viewportSize(), item)() - - self.m_page.setViewportSize(QSize(width, height)) - do_action('Phantom', Bunch(locals())) diff --git a/python/plugincontroller.py b/python/plugincontroller.py index a97b7787..41c5dacb 100644 --- a/python/plugincontroller.py +++ b/python/plugincontroller.py @@ -24,6 +24,7 @@ from os.path import dirname, split, splitext plugins = defaultdict(list) hook_count = {} + class Bunch(object): ''' Simple class to bunch a dict into an object that with attributes @@ -31,9 +32,10 @@ class Bunch(object): def __init__(self, adict): self.__dict__ = adict + def add_action(*hooks): ''' Decorator to be used for registering a function to - a specific hook or list of hooks. + a specific hook or list of hooks. ''' def register(func): for hook in hooks: @@ -41,10 +43,12 @@ def add_action(*hooks): return func return register + def did_action(hook): '''Find out how many times a hook was fired''' return hook_count[hook] + def do_action(hook, *args, **kwargs): ''' Trigger a hook. It will run any functions that have registered themselves to the hook. Any additional arguments or keyword @@ -55,21 +59,37 @@ def do_action(hook, *args, **kwargs): hook_count[hook] += 1 plugin(*args, **kwargs) -def has_action(hook): - '''Check if any functions have been registered for a hook''' + +def has_action(hook, func=None): + '''Check if hook exists. If function is specified, + check if function has been registered for hook. + ''' if hook in plugins: - return True + if not func: + return True + else: + for f in plugins[hook]: + if f == func: + return True return False -def remove_action(hook, func): - '''Remove function that has been registered to hook''' + +def remove_action(hook, func=None): + '''Remove hook if hook exists. If function is specified, + remove function from hook. + ''' if hook in plugins: - for f in plugins[hook]: - if f == func: - del plugins[hook][plugins[hook].index(func)] - return True + if not func: + del plugins[hook] + return True + else: + for f in plugins[hook]: + if f == func: + del plugins[hook][plugins[hook].index(func)] + return True return False + def remove_all_actions(hook): '''Remove all functions that have been registered to hook''' if hook in plugins: @@ -77,6 +97,7 @@ def remove_all_actions(hook): return True return False + def load_plugins(): ''' Loads the plugins. diff --git a/python/pyphantomjs.py b/python/pyphantomjs.py index beb9175c..80267d76 100644 --- a/python/pyphantomjs.py +++ b/python/pyphantomjs.py @@ -47,6 +47,7 @@ from utils import SafeStreamFilter sys.stdout = SafeStreamFilter(sys.stdout) sys.stderr = SafeStreamFilter(sys.stderr) + def parseArgs(args): # Handle all command-line options p = argParser() @@ -119,12 +120,15 @@ def parseArgs(args): sys.exit(1) try: - args.script = codecs.open(args.script, encoding='utf-8') + with codecs.open(args.script, encoding='utf-8') as script: + args.script_name = script.name + args.script = script.read() except IOError as (errno, stderr): sys.exit('%s: \'%s\'' % (stderr, args.script)) return args + def main(): args = parseArgs(sys.argv[1:]) @@ -146,9 +150,11 @@ def main(): phantom.execute() app.exec_() - sys.exit(phantom.returnValue()) + return phantom.returnValue() + do_action('PyPhantomJS', Bunch(locals())) + if __name__ == '__main__': - main() + sys.exit(main()) diff --git a/python/resources.py b/python/resources.py index 91088fa9..3f0dfc6d 100644 --- a/python/resources.py +++ b/python/resources.py @@ -2,7 +2,7 @@ # Resource object code # -# Created: Fri May 13 01:08:54 2011 +# Created: Tue May 31 11:30:31 2011 # by: The Resource Compiler for PyQt (Qt v4.7.2) # # WARNING! All changes made in this file will be lost! @@ -10,6 +10,97 @@ from PyQt4 import QtCore qt_resource_data = "\ +\x00\x00\x05\x81\ +\x2f\ +\x2f\x20\x54\x68\x69\x73\x20\x61\x6c\x6c\x6f\x77\x73\x20\x63\x72\ +\x65\x61\x74\x69\x6e\x67\x20\x61\x20\x6e\x65\x77\x20\x77\x65\x62\ +\x20\x70\x61\x67\x65\x20\x75\x73\x69\x6e\x67\x20\x74\x68\x65\x20\ +\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x20\x22\x6e\x65\x77\x20\x57\ +\x65\x62\x50\x61\x67\x65\x22\x2c\x0a\x2f\x2f\x20\x77\x68\x69\x63\ +\x68\x20\x66\x65\x65\x6c\x73\x20\x6d\x6f\x72\x65\x20\x6e\x61\x74\ +\x75\x72\x61\x6c\x20\x74\x68\x61\x6e\x20\x22\x70\x68\x61\x6e\x74\ +\x6f\x6d\x2e\x63\x72\x65\x61\x74\x65\x57\x65\x62\x50\x61\x67\x65\ +\x28\x29\x22\x2e\x0a\x77\x69\x6e\x64\x6f\x77\x2e\x57\x65\x62\x50\ +\x61\x67\x65\x20\x3d\x20\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\ +\x20\x7b\x0a\x20\x20\x20\x20\x76\x61\x72\x20\x70\x61\x67\x65\x20\ +\x3d\x20\x70\x68\x61\x6e\x74\x6f\x6d\x2e\x63\x72\x65\x61\x74\x65\ +\x57\x65\x62\x50\x61\x67\x65\x28\x29\x3b\x0a\x0a\x20\x20\x20\x20\ +\x2f\x2f\x20\x64\x65\x65\x70\x20\x63\x6f\x70\x79\x0a\x20\x20\x20\ +\x20\x70\x61\x67\x65\x2e\x73\x65\x74\x74\x69\x6e\x67\x73\x20\x3d\ +\x20\x4a\x53\x4f\x4e\x2e\x70\x61\x72\x73\x65\x28\x4a\x53\x4f\x4e\ +\x2e\x73\x74\x72\x69\x6e\x67\x69\x66\x79\x28\x70\x68\x61\x6e\x74\ +\x6f\x6d\x2e\x64\x65\x66\x61\x75\x6c\x74\x50\x61\x67\x65\x53\x65\ +\x74\x74\x69\x6e\x67\x73\x29\x29\x3b\x0a\x0a\x20\x20\x20\x20\x70\ +\x61\x67\x65\x2e\x6f\x6e\x41\x6c\x65\x72\x74\x20\x3d\x20\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x20\x28\x6d\x73\x67\x29\x20\x7b\x7d\x3b\ +\x0a\x0a\x20\x20\x20\x20\x70\x61\x67\x65\x2e\x6f\x6e\x43\x6f\x6e\ +\x73\x6f\x6c\x65\x4d\x65\x73\x73\x61\x67\x65\x20\x3d\x20\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x20\x28\x6d\x73\x67\x29\x20\x7b\x7d\x3b\ +\x0a\x0a\x20\x20\x20\x20\x70\x61\x67\x65\x2e\x6f\x70\x65\x6e\x20\ +\x3d\x20\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x28\x29\x20\x7b\x0a\ +\x20\x20\x20\x20\x20\x20\x20\x20\x69\x66\x20\x28\x74\x79\x70\x65\ +\x6f\x66\x20\x74\x68\x69\x73\x2e\x6f\x6e\x41\x6c\x65\x72\x74\x20\ +\x3d\x3d\x3d\x20\x27\x66\x75\x6e\x63\x74\x69\x6f\x6e\x27\x29\x20\ +\x7b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x74\x68\ +\x69\x73\x2e\x6a\x61\x76\x61\x53\x63\x72\x69\x70\x74\x41\x6c\x65\ +\x72\x74\x53\x65\x6e\x74\x2e\x63\x6f\x6e\x6e\x65\x63\x74\x28\x74\ +\x68\x69\x73\x2e\x6f\x6e\x41\x6c\x65\x72\x74\x29\x3b\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\ +\x69\x66\x20\x28\x74\x79\x70\x65\x6f\x66\x20\x74\x68\x69\x73\x2e\ +\x6f\x6e\x43\x6f\x6e\x73\x6f\x6c\x65\x4d\x65\x73\x73\x61\x67\x65\ +\x20\x3d\x3d\x3d\x20\x27\x66\x75\x6e\x63\x74\x69\x6f\x6e\x27\x29\ +\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x74\ +\x68\x69\x73\x2e\x6a\x61\x76\x61\x53\x63\x72\x69\x70\x74\x43\x6f\ +\x6e\x73\x6f\x6c\x65\x4d\x65\x73\x73\x61\x67\x65\x53\x65\x6e\x74\ +\x2e\x63\x6f\x6e\x6e\x65\x63\x74\x28\x74\x68\x69\x73\x2e\x6f\x6e\ +\x43\x6f\x6e\x73\x6f\x6c\x65\x4d\x65\x73\x73\x61\x67\x65\x29\x3b\ +\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x20\x69\x66\x20\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\ +\x2e\x6c\x65\x6e\x67\x74\x68\x20\x3d\x3d\x3d\x20\x32\x29\x20\x7b\ +\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x74\x68\x69\ +\x73\x2e\x6c\x6f\x61\x64\x53\x74\x61\x74\x75\x73\x43\x68\x61\x6e\ +\x67\x65\x64\x2e\x63\x6f\x6e\x6e\x65\x63\x74\x28\x61\x72\x67\x75\ +\x6d\x65\x6e\x74\x73\x5b\x31\x5d\x29\x3b\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x74\x68\x69\x73\x2e\x6f\x70\x65\x6e\ +\x55\x72\x6c\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x5b\x30\x5d\ +\x2c\x20\x27\x67\x65\x74\x27\x2c\x20\x74\x68\x69\x73\x2e\x73\x65\ +\x74\x74\x69\x6e\x67\x73\x29\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x72\x65\x74\x75\x72\x6e\x3b\x0a\x20\x20\x20\ +\x20\x20\x20\x20\x20\x7d\x20\x65\x6c\x73\x65\x20\x69\x66\x20\x28\ +\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2e\x6c\x65\x6e\x67\x74\x68\ +\x20\x3d\x3d\x3d\x20\x33\x29\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x74\x68\x69\x73\x2e\x6c\x6f\x61\x64\x53\ +\x74\x61\x74\x75\x73\x43\x68\x61\x6e\x67\x65\x64\x2e\x63\x6f\x6e\ +\x6e\x65\x63\x74\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x5b\x32\ +\x5d\x29\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x74\x68\x69\x73\x2e\x6f\x70\x65\x6e\x55\x72\x6c\x28\x61\x72\x67\ +\x75\x6d\x65\x6e\x74\x73\x5b\x30\x5d\x2c\x20\x61\x72\x67\x75\x6d\ +\x65\x6e\x74\x73\x5b\x31\x5d\x2c\x20\x74\x68\x69\x73\x2e\x73\x65\ +\x74\x74\x69\x6e\x67\x73\x29\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x72\x65\x74\x75\x72\x6e\x3b\x0a\x20\x20\x20\ +\x20\x20\x20\x20\x20\x7d\x20\x65\x6c\x73\x65\x20\x69\x66\x20\x28\ +\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2e\x6c\x65\x6e\x67\x74\x68\ +\x20\x3d\x3d\x3d\x20\x34\x29\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x74\x68\x69\x73\x2e\x6c\x6f\x61\x64\x53\ +\x74\x61\x74\x75\x73\x43\x68\x61\x6e\x67\x65\x64\x2e\x63\x6f\x6e\ +\x6e\x65\x63\x74\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x5b\x33\ +\x5d\x29\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x74\x68\x69\x73\x2e\x6f\x70\x65\x6e\x55\x72\x6c\x28\x61\x72\x67\ +\x75\x6d\x65\x6e\x74\x73\x5b\x30\x5d\x2c\x20\x7b\x0a\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x6f\x70\x65\ +\x72\x61\x74\x69\x6f\x6e\x3a\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\ +\x73\x5b\x31\x5d\x2c\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x64\x61\x74\x61\x3a\x20\x61\x72\x67\x75\ +\x6d\x65\x6e\x74\x73\x5b\x32\x5d\x0a\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x7d\x2c\x20\x74\x68\x69\x73\ +\x2e\x73\x65\x74\x74\x69\x6e\x67\x73\x29\x3b\x0a\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x72\x65\x74\x75\x72\x6e\x3b\x0a\ +\x20\x20\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x74\x68\x72\x6f\x77\x20\x22\x57\x72\x6f\x6e\x67\x20\x75\ +\x73\x65\x20\x6f\x66\x20\x57\x65\x62\x50\x61\x67\x65\x23\x6f\x70\ +\x65\x6e\x22\x3b\x0a\x20\x20\x20\x20\x7d\x3b\x0a\x0a\x20\x20\x20\ +\x20\x72\x65\x74\x75\x72\x6e\x20\x70\x61\x67\x65\x3b\x0a\x7d\x0a\ +\ \x00\x00\x56\x27\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -4129,6 +4220,10 @@ qt_resource_data = "\ " qt_resource_name = "\ +\x00\x0c\ +\x06\xa4\xe2\x53\ +\x00\x62\ +\x00\x6f\x00\x6f\x00\x74\x00\x73\x00\x74\x00\x72\x00\x61\x00\x70\x00\x2e\x00\x6a\x00\x73\ \x00\x09\ \x0a\x6c\x78\x43\ \x00\x72\ @@ -4145,10 +4240,11 @@ qt_resource_name = "\ " qt_resource_struct = "\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\ -\x00\x00\x00\x46\x00\x01\x00\x00\x00\x01\x00\x00\x56\x2b\ -\x00\x00\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\x1e\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\ +\x00\x00\x00\x64\x00\x01\x00\x00\x00\x01\x00\x00\x5b\xb0\ +\x00\x00\x00\x36\x00\x00\x00\x00\x00\x01\x00\x00\x05\x85\ " def qInitResources(): diff --git a/python/resources.qrc b/python/resources.qrc index 8b069314..cf7c6368 100644 --- a/python/resources.qrc +++ b/python/resources.qrc @@ -1,5 +1,6 @@ + bootstrap.js resources/pyphantomjs-icon.png resources/coffee-script.js diff --git a/python/tools/setup.py b/python/tools/setup.py index babc40ca..bf6d055d 100644 --- a/python/tools/setup.py +++ b/python/tools/setup.py @@ -39,11 +39,13 @@ if sys.platform.startswith('win'): print 'install pywin32 extensions if you want the file stamped' sleep(2) + exe = Executable( script = '../pyphantomjs.py', icon = '../resources/pyphantomjs-icon.ico' ) + setup( name = 'PyPhantomJS', version = version, diff --git a/python/utils.py b/python/utils.py index 0b6b172d..e4097976 100644 --- a/python/utils.py +++ b/python/utils.py @@ -23,7 +23,7 @@ from plugincontroller import Bunch, do_action from PyQt4.QtCore import QDateTime, Qt, QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg -version_major, version_minor, version_patch = (1, 1, 0) +version_major, version_minor, version_patch = (1, 2, 0) version = '%d.%d.%d' % (version_major, version_minor, version_patch) license = ''' @@ -45,6 +45,7 @@ license = ''' along with this program. If not, see . ''' % version + def argParser(): parser = argparse.ArgumentParser( description='Minimalistic headless WebKit-based JavaScript-driven tool', @@ -90,6 +91,7 @@ def argParser(): return parser + class MessageHandler: def __init__(self, verbose): self.verbose = verbose @@ -107,6 +109,7 @@ class MessageHandler: elif msgType == QtFatalMsg: print >> sys.stderr, '%s [FATAL] %s' % (now, msg) + class SafeStreamFilter(object): '''Convert string to something safe''' def __init__(self, target): diff --git a/python/webpage.py b/python/webpage.py index 076f5432..a3622f0e 100644 --- a/python/webpage.py +++ b/python/webpage.py @@ -17,51 +17,340 @@ along with this program. If not, see . ''' -from PyQt4.QtCore import QUrl, QEventLoop, qDebug -from PyQt4.QtGui import QApplication -from PyQt4.QtWebKit import QWebPage +from math import ceil, floor + +from PyQt4.QtCore import pyqtProperty, pyqtSlot, pyqtSignal, Qt, QObject, \ + QRect, QPoint, QUrl, QFileInfo, QDir, QSize, \ + QSizeF, QByteArray, QEventLoop, QFile +from PyQt4.QtGui import QPalette, QDesktopServices, QPrinter, QImage, \ + QPainter, QRegion, QApplication, qRgba +from PyQt4.QtWebKit import QWebSettings, QWebPage +from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest from plugincontroller import Bunch, do_action -class WebPage(QWebPage): +# Different defaults. +# OSX: 72, X11: 75(?), Windows: 96 +pdf_dpi = 72 + + +class CustomPage(QWebPage): def __init__(self, parent=None): QWebPage.__init__(self, parent) self.parent = parent - self.m_nextFileTag = '' self.m_userAgent = QWebPage.userAgentForUrl(self, QUrl()) - if self.parent.m_verbose: - self.currentFrame().urlChanged.connect(self.handleFrameUrlChanged) - self.linkClicked.connect(self.handleLinkClicked) - - do_action('WebPageInit', Bunch(locals())) - - def handleFrameUrlChanged(self, url): - qDebug('URL Changed: %s' % url.toString()) - - def handleLinkClicked(self, url): - qDebug('URL Clicked: %s' % url.toString()) - - def javaScriptAlert(self, webframe, msg): - print 'JavaScript alert: %s' % msg - - def javaScriptConsoleMessage(self, message, lineNumber, sourceID): - if sourceID: - print '%s:%d %s' % (sourceID, lineNumber, message) - else: - print message + do_action('CustomPageInit', Bunch(locals())) def shouldInterruptJavaScript(self): QApplication.processEvents(QEventLoop.AllEvents, 42) return False + def javaScriptAlert(self, originatingFrame, msg): + self.parent.emitAlert(msg) + + def javaScriptConsoleMessage(self, message, lineNumber, sourceID): + if sourceID: + message = '%s:%d %s' % (sourceID, lineNumber, message) + self.parent.emitConsoleMessage(message) + def userAgentForUrl(self, url): return self.m_userAgent - def chooseFile(self, webframe, suggestedFile): - if self.m_nextFileTag in self.parent.m_upload_file: - return self.parent.m_upload_file[self.m_nextFileTag] - return '' + do_action('CustomPage', Bunch(locals())) + + +class WebPage(QObject): + def __init__(self, parent=None): + QObject.__init__(self, parent) + + # variable declarations + self.m_paperSize = {} + self.m_clipRect = QRect() + + self.setObjectName('WebPage') + self.m_webPage = CustomPage(self) + self.m_mainFrame = self.m_webPage.mainFrame() + + self.m_webPage.loadFinished.connect(self.finish) + + # Start with transparent background + palette = self.m_webPage.palette() + palette.setBrush(QPalette.Base, Qt.transparent) + self.m_webPage.setPalette(palette) + + # Page size does not need to take scrollbars into account + self.m_webPage.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) + self.m_webPage.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) + + self.m_webPage.settings().setAttribute(QWebSettings.OfflineStorageDatabaseEnabled, True) + self.m_webPage.settings().setOfflineStoragePath(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) + self.m_webPage.settings().setAttribute(QWebSettings.LocalStorageDatabaseEnabled, True) + self.m_webPage.settings().setAttribute(QWebSettings.FrameFlatteningEnabled, True) + self.m_webPage.settings().setAttribute(QWebSettings.LocalStorageEnabled, True) + self.m_webPage.settings().setLocalStoragePath(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) + + # Ensure we have a document.body. + self.m_webPage.mainFrame().setHtml('') + + self.m_webPage.setViewportSize(QSize(400, 300)) + + do_action('WebPageInit', Bunch(locals())) + + def applySettings(self, defaults): + opt = self.m_webPage.settings() + + opt.setAttribute(QWebSettings.AutoLoadImages, defaults['loadImages']) + opt.setAttribute(QWebSettings.PluginsEnabled, defaults['loadPlugins']) + if 'userAgent' in defaults: + self.m_webPage.m_userAgent = defaults['userAgent'] + + javaScriptAlertSent = pyqtSignal(str) + def emitAlert(self, msg): + self.javaScriptAlertSent.emit(msg) + + javaScriptConsoleMessageSent = pyqtSignal(str) + def emitConsoleMessage(self, msg): + self.javaScriptConsoleMessageSent.emit(msg) + + loadStatusChanged = pyqtSignal(str) + def finish(self, ok): + status = 'success' if ok else 'fail' + self.loadStatusChanged.emit(status) + + def mainFrame(self): + return self.m_mainFrame + + def renderImage(self): + frameRect = QRect(QPoint(0, 0), self.m_mainFrame.contentsSize()) + if not self.m_clipRect.isEmpty(): + frameRect = self.m_clipRect + + viewportSize = self.m_webPage.viewportSize() + self.m_webPage.setViewportSize(self.m_mainFrame.contentsSize()) + + image = QImage(frameRect.size(), QImage.Format_ARGB32) + image.fill(qRgba(255, 255, 255, 0)) + + painter = QPainter() + + # We use tiling approach to work-around Qt software rasterizer bug + # when dealing with very large paint device. + # See http://code.google.com/p/phantomjs/issues/detail?id=54. + tileSize = 4096 + htiles = (image.width() + tileSize - 1) / tileSize + vtiles = (image.height() + tileSize - 1) / tileSize + for x in range(htiles): + for y in range(vtiles): + tileBuffer = QImage(tileSize, tileSize, QImage.Format_ARGB32) + tileBuffer.fill(qRgba(255, 255, 255, 0)) + + # Render the web page onto the small tile first + painter.begin(tileBuffer) + painter.setRenderHint(QPainter.Antialiasing, True) + painter.setRenderHint(QPainter.TextAntialiasing, True) + painter.setRenderHint(QPainter.SmoothPixmapTransform, True) + painter.translate(-frameRect.left(), -frameRect.top()) + painter.translate(-x * tileSize, -y * tileSize) + self.m_mainFrame.render(painter, QRegion(frameRect)) + painter.end() + + # Copy the tile to the main buffer + painter.begin(image) + painter.setCompositionMode(QPainter.CompositionMode_Source) + painter.drawImage(x * tileSize, y * tileSize, tileBuffer) + painter.end() + + self.m_webPage.setViewportSize(viewportSize) + return image + + def renderPdf(self, fileName): + p = QPrinter() + p.setOutputFormat(QPrinter.PdfFormat) + p.setOutputFileName(fileName) + p.setResolution(pdf_dpi) + paperSize = self.m_paperSize + + if not len(paperSize): + pageSize = QSize(self.m_webPage.mainFrame().contentsSize()) + paperSize['width'] = str(pageSize.width()) + 'px' + paperSize['height'] = str(pageSize.height()) + 'px' + paperSize['border'] = '0px' + + if paperSize.get('width') and paperSize.get('height'): + sizePt = QSizeF(ceil(self.stringToPointSize(paperSize['width'])), + ceil(self.stringToPointSize(paperSize['height']))) + p.setPaperSize(sizePt, QPrinter.Point) + elif 'format' in paperSize: + orientation = QPrinter.Landscape if paperSize.get('orientation') and paperSize['orientation'].lower() == 'landscape' else QPrinter.Portrait + orientation = QPrinter.Orientation(orientation) + p.setOrientation(orientation) + + formats = { + 'A3': QPrinter.A3, + 'A4': QPrinter.A4, + 'A5': QPrinter.A5, + 'Legal': QPrinter.Legal, + 'Letter': QPrinter.Letter, + 'Tabloid': QPrinter.Tabloid + } + + p.setPaperSize(QPrinter.A4) # fallback + for format, size in formats.items(): + if format.lower() == paperSize['format'].lower(): + p.setPaperSize(size) + break + else: + return False + + border = floor(self.stringToPointSize(paperSize['border'])) if paperSize.get('border') else 0 + p.setPageMargins(border, border, border, border, QPrinter.Point) + + self.m_webPage.mainFrame().print_(p) + return True + + def setNetworkAccessManager(self, networkAccessManager): + self.m_webPage.setNetworkAccessManager(networkAccessManager) + + def stringToPointSize(self, string): + units = ( + ('mm', 72 / 25.4), + ('cm', 72 / 2.54), + ('in', 72.0), + ('px', 72.0 / pdf_dpi / 2.54), + ('', 72.0 / pdf_dpi / 2.54) + ) + + for unit, format in units: + if string.endswith(unit): + value = string.rstrip(unit) + return float(value) * format + return 0 + + def userAgent(self): + return self.m_webPage.m_userAgent + + ## + # Properties and methods exposed to JavaScript + ## + + @pyqtProperty('QVariantMap') + def clipRect(self): + result = { + 'width': self.m_clipRect.width(), + 'height': self.m_clipRect.height(), + 'top': self.m_clipRect.top(), + 'left': self.m_clipRect.left() + } + return result + + @clipRect.setter + def clipRect(self, size): + names = ('width', 'height', 'top', 'left') + for item in names: + try: + globals()[item] = int(size[item]) + if globals()[item] < 0: + if item not in ('top', 'left'): + globals()[item] = 0 + except KeyError: + globals()[item] = getattr(self.m_clipRect, item)() + + self.m_clipRect = QRect(left, top, width, height) + + @pyqtProperty(str) + def content(self): + return self.m_mainFrame.toHtml() + + @content.setter + def content(self, content): + self.m_mainFrame.setHtml(content) + + @pyqtSlot(str, result='QVariant') + def evaluate(self, code): + function = '(%s)()' % code + return self.m_mainFrame.evaluateJavaScript(function) + + @pyqtSlot(str, str, 'QVariantMap') + @pyqtSlot(str, 'QVariantMap', 'QVariantMap') + def openUrl(self, address, op, settings): + operation = op + body = QByteArray() + + self.applySettings(settings) + self.m_webPage.triggerAction(QWebPage.Stop) + + if type(op) is dict: + operation = op.get('operation') + body = QByteArray(op.get('body')) + + if operation == '': + operation = 'get' + + networkOp = QNetworkAccessManager.CustomOperation + operation = operation.lower() + if operation == 'get': + networkOp = QNetworkAccessManager.GetOperation + elif operation == 'head': + networkOp = QNetworkAccessManager.HeadOperation + elif operation == 'put': + networkOp = QNetworkAccessManager.PutOperation + elif operation == 'post': + networkOp = QNetworkAccessManager.PostOperation + elif operation == 'delete': + networkOp = QNetworkAccessManager.DeleteOperation + + if networkOp == QNetworkAccessManager.CustomOperation: + self.m_mainFrame.evaluateJavaScript('console.error("Unknown network operation: %s");' % operation) + return + + self.m_mainFrame.load(QNetworkRequest(QUrl(address)), networkOp, body) + + @pyqtProperty('QVariantMap') + def paperSize(self): + return self.m_paperSize + + @paperSize.setter + def paperSize(self, size): + self.m_paperSize = size + + @pyqtSlot(str, result=bool) + def render(self, fileName): + if self.m_mainFrame.contentsSize() == '': + return False + + fileInfo = QFileInfo(fileName) + path = QDir() + path.mkpath(fileInfo.absolutePath()) + + if fileName.lower().endswith('.pdf'): + return self.renderPdf(fileName) + + image = self.renderImage() + + return image.save(fileName) + + @pyqtProperty('QVariantMap') + def viewportSize(self): + size = self.m_webPage.viewportSize() + result = { + 'width': size.width(), + 'height': size.height() + } + return result + + @viewportSize.setter + def viewportSize(self, size): + names = ('width', 'height') + for item in names: + try: + globals()[item] = int(size[item]) + if globals()[item] < 0: + globals()[item] = 0 + except KeyError: + globals()[item] = getattr(self.m_webPage.viewportSize(), item)() + + self.m_webPage.setViewportSize(QSize(width, height)) do_action('WebPage', Bunch(locals()))