PyPhantomJS version 1.2.0

Refactored most of functionalities to WebPage object
WebPage object can have settings
Support different HTTP operations for loading a page
Simplify buffer handling in render()
Refactor render(), split the buffer rendering to its own function
Render the web page using the tiling approach
WebPage callback for it's console message
Make exit() synchronous
WebPage callback for its JS alert
Updated all examples
1.2
IceArmy 2011-05-31 16:23:06 -07:00
parent a7a8a6749a
commit a313d1befd
11 changed files with 554 additions and 340 deletions

40
python/bootstrap.js Normal file
View File

@ -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;
}

View File

@ -17,16 +17,21 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
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 ''

View File

@ -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)

View File

@ -17,82 +17,67 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
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('<html><body></body></html>')
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()))

View File

@ -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.

View File

@ -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())

View File

@ -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():

View File

@ -1,5 +1,6 @@
<RCC>
<qresource prefix="/">
<file>bootstrap.js</file>
<file>resources/pyphantomjs-icon.png</file>
<file>resources/coffee-script.js</file>
</qresource>

View File

@ -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,

View File

@ -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 <http://www.gnu.org/licenses/>.
''' % 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):

View File

@ -17,51 +17,340 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
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('<html><body></body></html>')
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()))