Files
phantomjs/python/pyphantomjs/webpage.py
T

504 lines
18 KiB
Python
Raw Permalink Normal View History

2011-04-12 01:32:07 -07:00
'''
This file is part of the PyPhantomJS project.
Copyright (C) 2011 James Roe <roejames12@hotmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
2011-04-12 01:37:47 -07:00
'''
2011-04-12 01:32:07 -07:00
2011-05-31 16:23:06 -07:00
from math import ceil, floor
2011-08-23 12:18:25 -07:00
import sip
2011-09-15 17:18:08 -07:00
from PyQt4.QtCore import (pyqtProperty, pyqtSignal, pyqtSlot, QByteArray,
QDir, QEvent, QEventLoop, QFileInfo, QObject,
QPoint, QRect, QSize, QSizeF, Qt, QUrl)
from PyQt4.QtGui import (QApplication, QDesktopServices, QImage,
QMouseEvent, QPainter, QPalette, QPrinter,
QRegion, qRgba)
2011-05-31 16:23:06 -07:00
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
2011-09-15 17:18:08 -07:00
from PyQt4.QtWebKit import QWebPage, QWebSettings
2011-04-12 01:32:07 -07:00
from networkaccessmanager import NetworkAccessManager
2011-09-15 17:18:08 -07:00
from plugincontroller import do_action
from utils import injectJsInFrame
2011-06-03 21:15:59 -07:00
2011-05-31 16:23:06 -07:00
class CustomPage(QWebPage):
def __init__(self, parent):
super(CustomPage, self).__init__(parent)
2011-04-12 01:32:07 -07:00
self.m_userAgent = QWebPage.userAgentForUrl(self, QUrl())
2011-06-05 13:53:16 -07:00
self.m_uploadFile = ''
2011-07-04 02:37:18 -07:00
do_action('CustomPageInit')
2011-04-12 01:32:07 -07:00
2011-06-05 13:53:16 -07:00
def chooseFile(self, originatingFrame, oldFile):
return self.m_uploadFile
2011-04-12 01:32:07 -07:00
def shouldInterruptJavaScript(self):
QApplication.processEvents(QEventLoop.AllEvents, 42)
return False
2011-05-31 16:23:06 -07:00
def javaScriptAlert(self, originatingFrame, msg):
self.parent().javaScriptAlertSent.emit(msg)
2011-05-31 16:23:06 -07:00
def javaScriptConsoleMessage(self, message, lineNumber, sourceID):
self.parent().javaScriptConsoleMessageSent.emit(message, lineNumber, sourceID)
2011-05-31 16:23:06 -07:00
2011-04-12 01:32:07 -07:00
def userAgentForUrl(self, url):
return self.m_userAgent
2011-04-12 01:32:07 -07:00
2011-07-04 02:37:18 -07:00
do_action('CustomPage')
2011-05-31 16:23:06 -07:00
class WebPage(QObject):
2011-08-20 18:50:56 -07:00
initialized = pyqtSignal()
2011-06-03 13:51:37 -07:00
javaScriptAlertSent = pyqtSignal(str)
javaScriptConsoleMessageSent = pyqtSignal(str, int, str)
loadStarted = pyqtSignal()
loadFinished = pyqtSignal(str)
resourceReceived = pyqtSignal('QVariantMap')
resourceRequested = pyqtSignal('QVariantMap')
2011-06-03 13:51:37 -07:00
2011-09-16 07:12:57 -07:00
blankHtml = '<html><head></head><body></body></html>'
2011-09-15 16:19:35 -07:00
def __init__(self, parent, args):
super(WebPage, self).__init__(parent)
2011-05-31 16:23:06 -07:00
# variable declarations
self.m_paperSize = {}
self.m_clipRect = QRect()
2011-06-17 20:17:58 -07:00
self.m_libraryPath = ''
2011-09-18 04:06:00 -07:00
self.m_scrollPosition = QPoint()
2011-05-31 16:23:06 -07:00
self.setObjectName('WebPage')
self.m_webPage = CustomPage(self)
self.m_mainFrame = self.m_webPage.mainFrame()
2011-08-20 18:50:56 -07:00
self.m_mainFrame.javaScriptWindowObjectCleared.connect(self.initialized)
self.m_webPage.loadStarted.connect(self.loadStarted)
2011-05-31 16:23:06 -07:00
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)
2011-06-24 18:33:21 -07:00
self.m_webPage.settings().setAttribute(QWebSettings.OfflineWebApplicationCacheEnabled, True)
self.m_webPage.settings().setOfflineWebApplicationCachePath(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
2011-05-31 16:23:06 -07:00
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.
2011-09-15 16:19:35 -07:00
self.m_webPage.mainFrame().setHtml(self.blankHtml)
2011-05-31 16:23:06 -07:00
# Custom network access manager to allow traffic monitoring
2011-09-18 04:22:49 -07:00
self.m_networkAccessManager = NetworkAccessManager(self.parent(), args)
self.m_webPage.setNetworkAccessManager(self.m_networkAccessManager)
self.m_networkAccessManager.resourceRequested.connect(self.resourceRequested)
self.m_networkAccessManager.resourceReceived.connect(self.resourceReceived)
2011-05-31 16:23:06 -07:00
self.m_webPage.setViewportSize(QSize(400, 300))
2011-07-04 02:37:18 -07:00
do_action('WebPageInit')
2011-05-31 16:23:06 -07:00
def applySettings(self, defaults):
opt = self.m_webPage.settings()
opt.setAttribute(QWebSettings.AutoLoadImages, defaults['loadImages'])
opt.setAttribute(QWebSettings.PluginsEnabled, defaults['loadPlugins'])
opt.setAttribute(QWebSettings.JavascriptEnabled, defaults['javascriptEnabled'])
opt.setAttribute(QWebSettings.XSSAuditingEnabled, defaults['XSSAuditingEnabled'])
opt.setAttribute(QWebSettings.LocalContentCanAccessRemoteUrls, defaults['localToRemoteUrlAccessEnabled'])
2011-09-18 04:22:49 -07:00
2011-05-31 16:23:06 -07:00
if 'userAgent' in defaults:
self.m_webPage.m_userAgent = defaults['userAgent']
2011-09-18 04:22:49 -07:00
if 'userName' in defaults:
self.m_networkAccessManager.m_userName = defaults['userName']
2011-09-18 04:22:49 -07:00
if 'password' in defaults:
self.m_networkAccessManager.m_password = defaults['password']
2011-09-18 04:22:49 -07:00
2011-05-31 16:23:06 -07:00
def finish(self, ok):
status = 'success' if ok else 'fail'
self.loadFinished.emit(status)
2011-05-31 16:23:06 -07:00
def mainFrame(self):
return self.m_mainFrame
def renderImage(self):
contentsSize = self.m_mainFrame.contentsSize()
contentsSize -= QSize(self.m_scrollPosition.x(), self.m_scrollPosition.y())
frameRect = QRect(QPoint(0, 0), contentsSize)
2011-05-31 16:23:06 -07:00
if not self.m_clipRect.isEmpty():
frameRect = self.m_clipRect
viewportSize = self.m_webPage.viewportSize()
self.m_webPage.setViewportSize(contentsSize)
2011-05-31 16:23:06 -07:00
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
2011-08-19 16:09:40 -07:00
# Different defaults.
# OSX: 72, X11: 75(?), Windows: 96
pdf_dpi = 72
2011-05-31 16:23:06 -07:00
def renderPdf(self, fileName):
p = QPrinter()
p.setOutputFormat(QPrinter.PdfFormat)
p.setOutputFileName(fileName)
2011-08-19 16:09:40 -07:00
p.setResolution(self.pdf_dpi)
2011-05-31 16:23:06 -07:00
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 = {
2011-06-07 10:57:08 -07:00
'A0': QPrinter.A0,
'A1': QPrinter.A1,
'A2': QPrinter.A2,
2011-05-31 16:23:06 -07:00
'A3': QPrinter.A3,
'A4': QPrinter.A4,
'A5': QPrinter.A5,
2011-06-07 10:57:08 -07:00
'A6': QPrinter.A6,
'A7': QPrinter.A7,
'A8': QPrinter.A8,
'A9': QPrinter.A9,
'B0': QPrinter.B0,
'B1': QPrinter.B1,
'B2': QPrinter.B2,
'B3': QPrinter.B3,
'B4': QPrinter.B4,
'B5': QPrinter.B5,
'B6': QPrinter.B6,
'B7': QPrinter.B7,
'B8': QPrinter.B8,
'B9': QPrinter.B9,
'B10': QPrinter.B10,
2011-06-07 11:26:41 -07:00
'C5E': QPrinter.C5E,
2011-06-07 10:57:08 -07:00
'Comm10E': QPrinter.Comm10E,
'DLE': QPrinter.DLE,
'Executive': QPrinter.Executive,
'Folio': QPrinter.Folio,
'Ledger': QPrinter.Ledger,
2011-05-31 16:23:06 -07:00
'Legal': QPrinter.Legal,
'Letter': QPrinter.Letter,
'Tabloid': QPrinter.Tabloid
}
p.setPaperSize(QPrinter.A4) # fallback
2011-08-24 14:23:51 -07:00
for format_, size in formats.items():
if format_.lower() == paperSize['format'].lower():
2011-05-31 16:23:06 -07:00
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 stringToPointSize(self, string):
units = (
('mm', 72 / 25.4),
('cm', 72 / 2.54),
('in', 72.0),
2011-08-19 16:09:40 -07:00
('px', 72.0 / self.pdf_dpi / 2.54),
('', 72.0 / self.pdf_dpi / 2.54)
2011-05-31 16:23:06 -07:00
)
2011-08-24 14:23:51 -07:00
for unit, format_ in units:
2011-05-31 16:23:06 -07:00
if string.endswith(unit):
value = string.rstrip(unit)
2011-08-24 14:23:51 -07:00
return float(value) * format_
2011-05-31 16:23:06 -07:00
return 0
def userAgent(self):
return self.m_webPage.m_userAgent
##
# Properties and methods exposed to JavaScript
##
@pyqtSlot(str)
def _appendScriptElement(self, scriptUrl):
self.m_mainFrame.evaluateJavaScript('''
var el = document.createElement('script');
el.onload = function() { alert('%(scriptUrl)s'); };
el.src = '%(scriptUrl)s';
document.body.appendChild(el);
''' % {'scriptUrl': scriptUrl})
2011-05-31 16:23:06 -07:00
@pyqtProperty('QVariantMap')
def clipRect(self):
2011-07-29 02:11:55 -07:00
clipRect = self.m_clipRect
2011-05-31 16:23:06 -07:00
result = {
2011-07-29 02:11:55 -07:00
'width': clipRect.width(),
'height': clipRect.height(),
'top': clipRect.top(),
'left': clipRect.left()
2011-05-31 16:23:06 -07:00
}
return result
@clipRect.setter
def clipRect(self, size):
sizes = {'width': 0, 'height': 0, 'top': 0, 'left': 0}
for item in sizes:
2011-05-31 16:23:06 -07:00
try:
sizes[item] = int(size[item])
if sizes[item] < 0:
2011-05-31 16:23:06 -07:00
if item not in ('top', 'left'):
sizes[item] = 0
except (KeyError, ValueError):
sizes[item] = self.clipRect[item]
2011-05-31 16:23:06 -07:00
self.m_clipRect = QRect(sizes['left'], sizes['top'], sizes['width'], sizes['height'])
2011-05-31 16:23:06 -07:00
@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
2011-05-31 16:23:06 -07:00
return self.m_mainFrame.evaluateJavaScript(function)
@pyqtSlot(str, result=bool)
def injectJs(self, filePath):
return injectJsInFrame(filePath, self.parent().m_scriptEncoding.encoding, self.m_libraryPath, self.m_mainFrame)
2011-05-31 16:23:06 -07:00
@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')
2011-06-01 00:52:25 -07:00
body = QByteArray(op.get('data', ''))
2011-05-31 16:23:06 -07:00
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
2011-09-15 16:19:35 -07:00
if address.lower() == 'about:blank':
self.m_mainFrame.setHtml(self.blankHtml)
else:
self.m_mainFrame.load(QNetworkRequest(QUrl(address)), networkOp, body)
2011-05-31 16:23:06 -07:00
@pyqtProperty('QVariantMap')
def paperSize(self):
return self.m_paperSize
@paperSize.setter
def paperSize(self, size):
self.m_paperSize = size
2011-08-23 12:18:25 -07:00
@pyqtSlot()
def release(self):
self.parent().m_pages.remove(self)
2011-08-23 12:18:25 -07:00
sip.delete(self)
2011-05-31 16:23:06 -07:00
@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(str)
2011-06-17 20:17:58 -07:00
def libraryPath(self):
return self.m_libraryPath
2011-06-17 20:17:58 -07:00
@libraryPath.setter
def libraryPath(self, dirPath):
self.m_libraryPath = dirPath
2011-09-18 04:06:00 -07:00
@pyqtSlot(str, 'QVariant', 'QVariant')
def sendEvent(self, type_, arg1, arg2):
type_ = type_.lower()
if type_ in ('mousedown', 'mouseup', 'mousemove'):
eventType = QMouseEvent.Type(QEvent.None)
button = Qt.MouseButton(Qt.LeftButton)
buttons = Qt.MouseButtons(Qt.LeftButton)
if type_ == 'mousedown':
eventType = QEvent.MouseButtonPress
elif type_ == 'mouseup':
eventType = QEvent.MouseButtonRelease
elif type_ == 'mousemove':
eventType = QEvent.MouseMove
button = buttons = Qt.NoButton
assert eventType != QEvent.None
event = QMouseEvent(eventType, QPoint(arg1, arg2), button, buttons, Qt.NoModifier)
QApplication.postEvent(self.m_webPage, event)
QApplication.processEvents()
return
if type_ == 'click':
self.sendEvent('mousedown', arg1, arg2)
self.sendEvent('mouseup', arg1, arg2)
2011-07-29 01:52:53 -07:00
@pyqtProperty('QVariantMap')
def scrollPosition(self):
scroll = self.m_scrollPosition
2011-07-29 01:52:53 -07:00
result = {
'left': scroll.x(),
'top': scroll.y()
}
return result
@scrollPosition.setter
def scrollPosition(self, size):
positions = {'left': 0, 'top': 0}
for item in positions:
try:
positions[item] = int(size[item])
if positions[item] < 0:
positions[item] = 0
except (KeyError, ValueError):
positions[item] = self.scrollPosition[item]
self.m_scrollPosition = QPoint(positions['left'], positions['top'])
self.m_mainFrame.setScrollPosition(self.m_scrollPosition)
2011-07-29 01:52:53 -07:00
2011-06-05 13:53:16 -07:00
@pyqtSlot(str, str)
def uploadFile(self, selector, fileName):
el = self.m_mainFrame.findFirstElement(selector)
if el.isNull():
return
self.m_webPage.m_uploadFile = fileName
el.evaluateJavaScript('''
(function (el) {
var ev = document.createEvent('MouseEvents');
ev.initEvent('click', true, true);
el.dispatchEvent(ev);
})(this)
''')
2011-05-31 16:23:06 -07:00
@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):
sizes = {'width': 0, 'height': 0}
for item in sizes:
2011-05-31 16:23:06 -07:00
try:
sizes[item] = int(size[item])
if sizes[item] < 0:
sizes[item] = 0
except (KeyError, ValueError):
sizes[item] = self.viewportSize[item]
2011-05-31 16:23:06 -07:00
self.m_webPage.setViewportSize(QSize(sizes['width'], sizes['height']))
2011-07-04 02:37:18 -07:00
do_action('WebPage')