Make it possible to print pages with user-defined headers/footers.

Usage:

page.paperSize = {
  margin: "1cm",
  header: {
    height: "1cm",
    contents: phantom.callback(function(pageNum, numPages) {
      return "<h1>" + pageNum + " / " + numPages + "</h1>";
    })
  },
  footer: {
    height: "0.5cm",
    contents: phantom.callback(function(pageNum, numPages) {
      return "<h2>" + pageNum + " / " + numPages + "</h1>";
    })
  }
};

Note: The contents can return arbitrary HTML but since we cannot
re-layout the whole website for every page, the header/footers
must have the static height defined in the height property.

Note: The new example printheaderfooter.js shows the usage. It
also shows how one could delegate the above to a JavaScript
function on the loaded website, which allows one to print pages
and let the actually printed page decide how the header/footer
should look like.

Note: The page-counter can be reset by adding the class "phantomjs_reset_pagination"
to HTML block-elements that should reset the counter.

ISSUE: 410 (http://code.google.com/p/phantomjs/issues/detail?id=410)
1.6
Milian Wolff 2012-04-11 15:23:25 +02:00 committed by Ariya Hidayat
parent 67d2e8c2f7
commit 24a9665c4d
17 changed files with 495 additions and 6 deletions

View File

@ -0,0 +1,70 @@
var page = require('webpage').create(),
system = require('system');
function someCallback(pageNum, numPages) {
return "<h1> somCallback: " + pageNum + " / " + numPages + "</h1>";
}
if (system.args.length < 3) {
console.log('Usage: printheaderfooter.js URL filename');
phantom.exit();
} else {
var address = system.args[1];
var output = system.args[2];
page.viewportSize = { width: 600, height: 600 };
page.paperSize = {
format: 'A4',
margin: "1cm",
/* default header/footer for pages that don't have custom overwrites (see below) */
header: {
height: "1cm",
contents: phantom.callback(function(pageNum, numPages) {
if (pageNum == 1) {
return "";
}
return "<h1>Header <span style='float:right'>" + pageNum + " / " + numPages + "</span></h1>";
})
},
footer: {
height: "1cm",
contents: phantom.callback(function(pageNum, numPages) {
if (pageNum == numPages) {
return "";
}
return "<h1>Footer <span style='float:right'>" + pageNum + " / " + numPages + "</span></h1>";
})
}
};
page.open(address, function (status) {
if (status !== 'success') {
console.log('Unable to load the address!');
} else {
/* check whether the loaded page overwrites the header/footer setting,
i.e. whether a PhantomJSPriting object exists. Use that then instead
of our defaults above. */
if (page.evaluate(function(){return typeof PhantomJSPrinting == "object";})) {
paperSize = page.paperSize;
paperSize.header.height = page.evaluate(function() {
console.log("woot?", PhantomJSPrinting.header.height);
return PhantomJSPrinting.header.height;
});
paperSize.header.contents = phantom.callback(function(pageNum, numPages) {
return page.evaluate(function(pageNum, numPages){return PhantomJSPrinting.header.contents(pageNum, numPages);}, pageNum, numPages);
});
paperSize.footer.height = page.evaluate(function() {
return PhantomJSPrinting.footer.height;
});
paperSize.footer.contents = phantom.callback(function(pageNum, numPages) {
return page.evaluate(function(pageNum, numPages){return PhantomJSPrinting.footer.contents(pageNum, numPages);}, pageNum, numPages);
});
page.paperSize = paperSize;
console.log(page.paperSize.header.height);
console.log(page.paperSize.footer.height);
}
window.setTimeout(function () {
page.render(output);
phantom.exit();
}, 200);
}
});
}

View File

@ -82,6 +82,15 @@ phantom.defaultErrorHandler = function(error, backtrace) {
})
}
phantom.callback = function(callback) {
var ret = phantom.createCallback();
ret.called.connect(function(args) {
var retVal = callback.apply(this, args);
ret.returnValue = retVal;
});
return ret;
}
phantom.onError = phantom.defaultErrorHandler;
// Legacy way to use WebPage

52
src/callback.cpp Normal file
View File

@ -0,0 +1,52 @@
/*
This file is part of the PhantomJS project from Ofi Labs.
Copyright (C) 2012 Milian Wolff, KDAB <milian.wolff@kdab.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "callback.h"
Callback::Callback(QObject* parent)
: QObject(parent)
{
}
QVariant Callback::call(const QVariantList& arguments)
{
emit called(arguments);
return m_returnValue;
}
QVariant Callback::returnValue() const
{
return m_returnValue;
}
void Callback::setReturnValue(const QVariant& returnValue)
{
m_returnValue = returnValue;
}

57
src/callback.h Normal file
View File

@ -0,0 +1,57 @@
/*
This file is part of the PhantomJS project from Ofi Labs.
Copyright (C) 2012 Milian Wolff, KDAB <milian.wolff@kdab.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef CALLBACK_H
#define CALLBACK_H
#include <QObject>
#include <QVariant>
class Callback : public QObject
{
Q_OBJECT
Q_PROPERTY(QVariant returnValue READ returnValue WRITE setReturnValue)
public:
Callback(QObject *parent);
QVariant call(const QVariantList& arguments);
QVariant returnValue() const;
void setReturnValue(const QVariant& returnValue);
signals:
void called(const QVariantList& arguments);
private:
QVariant m_returnValue;
};
#endif // CALLBACK_H

View File

@ -43,6 +43,7 @@
#include "webserver.h"
#include "repl.h"
#include "system.h"
#include "callback.h"
// public:
@ -257,6 +258,11 @@ QObject *Phantom::createSystem()
return m_system;
}
QObject* Phantom::createCallback()
{
return new Callback(this);
}
QString Phantom::loadModuleSource(const QString &name)
{
QString moduleSource;

View File

@ -82,6 +82,7 @@ public slots:
QObject *createWebServer();
QObject *createFilesystem();
QObject *createSystem();
QObject *createCallback();
QString loadModuleSource(const QString &name);
bool injectJs(const QString &jsFilePath);

View File

@ -12,6 +12,7 @@ RESOURCES = phantomjs.qrc
HEADERS += csconverter.h \
phantom.h \
callback.h \
webpage.h \
webserver.h \
consts.h \
@ -28,6 +29,7 @@ HEADERS += csconverter.h \
replcompletable.h
SOURCES += phantom.cpp \
callback.cpp \
webpage.cpp \
webserver.cpp \
main.cpp \

View File

@ -544,6 +544,8 @@ Color Frame::getDocumentBackgroundColor() const
void Frame::setPrinting(bool printing, const FloatSize& pageSize, float maximumShrinkRatio, AdjustViewSizeOrNot shouldAdjustViewSize)
{
m_pageResets.clear();
m_doc->setPrinting(printing);
view()->adjustMediaTypeForPrinting(printing);
@ -561,6 +563,38 @@ void Frame::setPrinting(bool printing, const FloatSize& pageSize, float maximumS
child->setPrinting(printing, IntSize(), 0, shouldAdjustViewSize);
}
void Frame::addResetPage(int page)
{
m_pageResets.append(page);
}
void Frame::getPagination(int page, int pages, int& logicalPage, int& logicalPages) const
{
logicalPage = page;
logicalPages = pages;
int last_j = 0;
int j = 0;
for(size_t i = 0; i < m_pageResets.size(); ++i) {
j = m_pageResets.at(i);
if (j >= page) {
break;
}
last_j = j;
}
if (page > last_j) {
logicalPage = page - last_j;
}
if (last_j) {
if (j > last_j) {
logicalPages = j - last_j;
} else {
logicalPages = pages - last_j;
}
} else if (j >= page && j < pages) {
logicalPages = j;
}
}
void Frame::injectUserScripts(UserScriptInjectionTime injectionTime)
{
if (!m_page)

View File

@ -144,6 +144,8 @@ namespace WebCore {
enum AdjustViewSizeOrNot { DoNotAdjustViewSize, AdjustViewSize };
void setPrinting(bool printing, const FloatSize& pageSize, float maximumShrinkRatio, AdjustViewSizeOrNot);
void addResetPage(int page);
void getPagination(int page, int pages, int &logicalPage, int &logicalPages) const;
bool inViewSourceMode() const;
void setInViewSourceMode(bool = true);
@ -251,6 +253,8 @@ namespace WebCore {
bool m_isDisconnected;
bool m_excludeFromTextSearch;
Vector<int> m_pageResets;
#if ENABLE(TILED_BACKING_STORE)
// FIXME: The tiled backing store belongs in FrameView, not Frame.

View File

@ -82,9 +82,16 @@ void PrintContext::computePageRects(const FloatRect& printRect, float headerHeig
float pageWidth;
float pageHeight;
if (isHorizontal) {
float ratio = printRect.height() / printRect.width();
pageWidth = view->docWidth();
pageHeight = floorf(pageWidth * ratio);
///NOTE: if we do not reuse the previously set logical page height,
/// we can end up with off-by-one erros in the page height,
/// leading to rendering issues (e.g. rows overlap pagebreaks)
if (view->pageLogicalHeight() == 0) {
float ratio = printRect.height() / printRect.width();
pageHeight = floorf(pageWidth * ratio);
} else {
pageHeight = view->pageLogicalHeight();
}
} else {
float ratio = printRect.width() / printRect.height();
pageHeight = view->docHeight();

View File

@ -1348,6 +1348,15 @@ void RenderBlock::layoutBlock(bool relayoutChildren, int pageLogicalHeight)
}
}
setNeedsLayout(false);
if (document()->printing()) {
// PHANTOMJS CUSTOM: reset pagination counter for printing
StyledElement* elem = dynamic_cast<StyledElement*>(generatingNode());
if (elem && elem->hasClass() && elem->classNames().contains("phantomjs_reset_pagination")) {
frame()->addResetPage(y() / view()->layoutState()->m_pageLogicalHeight);
}
}
}
void RenderBlock::addOverflowFromChildren()

View File

@ -110,6 +110,8 @@
#include <qregion.h>
#include <qnetworkrequest.h>
#include "qwebframe_printingaddons_p.h"
using namespace WebCore;
// from text/qfont.cpp
@ -1431,12 +1433,19 @@ bool QWebFrame::event(QEvent *e)
\sa render()
*/
void QWebFrame::print(QPrinter *printer) const
void QWebFrame::print(QPrinter* printer) const
{
print(printer, 0);
}
void QWebFrame::print(QPrinter *printer, PrintCallback *callback) const
{
QPainter painter;
if (!painter.begin(printer))
return;
HeaderFooter headerFooter(this, printer, callback);
const qreal zoomFactorX = (qreal)printer->logicalDpiX() / qt_defaultDpi();
const qreal zoomFactorY = (qreal)printer->logicalDpiY() / qt_defaultDpi();
@ -1449,7 +1458,7 @@ void QWebFrame::print(QPrinter *printer) const
int(qprinterRect.width() / zoomFactorX),
int(qprinterRect.height() / zoomFactorY));
printContext.begin(pageRect.width());
printContext.begin(pageRect.width(), pageRect.height());
printContext.computePageRects(pageRect, /* headerHeight */ 0, /* footerHeight */ 0, /* userScaleFactor */ 1.0, pageHeight);
@ -1499,6 +1508,13 @@ void QWebFrame::print(QPrinter *printer) const
printContext.end();
return;
}
if (headerFooter.isValid()) {
// print header/footer
int logicalPage, logicalPages;
d->frame->getPagination(page, printContext.pageCount(), logicalPage, logicalPages);
headerFooter.paintHeader(ctx, pageRect, logicalPage, logicalPages);
headerFooter.paintFooter(ctx, pageRect, logicalPage, logicalPages);
}
printContext.spoolPage(ctx, page - 1, pageRect.width());
if (j < pageCopies - 1)
printer->newPage();

View File

@ -199,10 +199,24 @@ public:
QWebSecurityOrigin securityOrigin() const;
#ifndef QT_NO_PRINTER
struct PrintCallback {
/// height of header in points
virtual qreal headerHeight() const = 0;
/// height of footer in points
virtual qreal footerHeight() const = 0;
/// header contents (in HTML) on page @p page
virtual QString header(int page, int numPages) = 0;
/// footer contents (in HTML) on page @p page
virtual QString footer(int page, int numPages) = 0;
};
#endif
public Q_SLOTS:
QVariant evaluateJavaScript(const QString& scriptSource, const QString& file = QString());
#ifndef QT_NO_PRINTER
void print(QPrinter *printer) const;
void print(QPrinter *printer, PrintCallback *callback) const;
#endif
Q_SIGNALS:

View File

@ -108,6 +108,8 @@ public:
void emitUrlChanged();
void _q_orientationChanged();
static WebCore::Frame* webcoreFrame(QWebFrame* frame) { return frame->d->frame; };
QWebFrame *q;
Qt::ScrollBarPolicy horizontalScrollBarPolicy;
Qt::ScrollBarPolicy verticalScrollBarPolicy;

View File

@ -0,0 +1,146 @@
/*
Copyright (C) 2012 Milian Wolff, KDAB (milian.wolff@kdab.com)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library 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
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#ifndef QWEBFRAME_PRINTINGADDONS_P_H
#define QWEBFRAME_PRINTINGADDONS_P_H
#include "qwebframe.h"
#include "qwebframe_p.h"
#include <qprinter.h>
#include <qstring.h>
#include "GraphicsContext.h"
#include "PrintContext.h"
// for custom header or footers in printing
class HeaderFooter
{
public:
HeaderFooter(const QWebFrame* frame, QPrinter* printer, QWebFrame::PrintCallback* callback);
~HeaderFooter();
void setPageRect(const WebCore::IntRect& rect);
void paintHeader(WebCore::GraphicsContext& ctx, const WebCore::IntRect& pageRect, int pageNum, int totalPages);
void paintFooter(WebCore::GraphicsContext& ctx, const WebCore::IntRect& pageRect, int pageNum, int totalPages);
bool isValid()
{
return callback && (headerHeightPixel > 0 || footerHeightPixel > 0);
}
private:
QWebPage page;
QWebFrame::PrintCallback* callback;
int headerHeightPixel;
int footerHeightPixel;
WebCore::PrintContext* printCtx;
void paint(WebCore::GraphicsContext& ctx, const WebCore::IntRect& pageRect, const QString& contents, int height);
};
HeaderFooter::HeaderFooter(const QWebFrame* frame, QPrinter* printer, QWebFrame::PrintCallback* callback_)
: printCtx(0)
, callback(callback_)
, headerHeightPixel(0)
, footerHeightPixel(0)
{
if (callback) {
qreal headerHeight = qMax(qreal(0), callback->headerHeight());
qreal footerHeight = qMax(qreal(0), callback->footerHeight());
if (headerHeight || footerHeight) {
// figure out the header/footer height in *DevicePixel*
// based on the height given in *Points*
qreal marginLeft, marginRight, marginTop, marginBottom;
// find existing margins
printer->getPageMargins(&marginLeft, &marginTop, &marginRight, &marginBottom, QPrinter::DevicePixel);
const qreal oldMarginTop = marginTop;
const qreal oldMarginBottom = marginTop;
printer->getPageMargins(&marginLeft, &marginTop, &marginRight, &marginBottom, QPrinter::Point);
// increase margins to hold header+footer
marginTop += headerHeight;
marginBottom += footerHeight;
printer->setPageMargins(marginLeft, marginTop, marginRight, marginBottom, QPrinter::Point);
// calculate actual heights
printer->getPageMargins(&marginLeft, &marginTop, &marginRight, &marginBottom, QPrinter::DevicePixel);
headerHeightPixel = marginTop - oldMarginTop;
footerHeightPixel = marginBottom - oldMarginBottom;
printCtx = new WebCore::PrintContext(QWebFramePrivate::webcoreFrame(page.mainFrame()));
}
}
}
HeaderFooter::~HeaderFooter()
{
delete printCtx;
printCtx = 0;
}
void HeaderFooter::paintHeader(WebCore::GraphicsContext& ctx, const WebCore::IntRect& pageRect, int pageNum, int totalPages)
{
if (!headerHeightPixel) {
return;
}
const QString c = callback->header(pageNum, totalPages);
if (c.isEmpty()) {
return;
}
ctx.translate(0, -headerHeightPixel);
paint(ctx, pageRect, c, headerHeightPixel);
ctx.translate(0, +headerHeightPixel);
}
void HeaderFooter::paintFooter(WebCore::GraphicsContext& ctx, const WebCore::IntRect& pageRect, int pageNum, int totalPages)
{
if (!footerHeightPixel) {
return;
}
const QString c = callback->footer(pageNum, totalPages);
if (c.isEmpty()) {
return;
}
const int offset = pageRect.height();
ctx.translate(0, +offset);
paint(ctx, pageRect, c, footerHeightPixel);
ctx.translate(0, -offset);
}
void HeaderFooter::paint(WebCore::GraphicsContext& ctx, const WebCore::IntRect& pageRect, const QString& contents, int height)
{
page.mainFrame()->setHtml(contents);
printCtx->begin(pageRect.width(), height);
float tempHeight;
printCtx->computePageRects(pageRect, /* headerHeight */ 0, /* footerHeight */ 0, /* userScaleFactor */ 1.0, tempHeight);
printCtx->spoolPage(ctx, 0, pageRect.width());
printCtx->end();
}
#endif // QWEBFRAME_PRINTINGADDONS_P_H

View File

@ -53,6 +53,7 @@
#include <gifwriter.h>
#include "consts.h"
#include "callback.h"
// Ensure we have at least head and body.
#define BLANK_HTML "<html><head></head><body></body></html>"
@ -613,10 +614,63 @@ bool WebPage::renderPdf(const QString &fileName)
printer.setPageMargins(marginLeft, marginTop, marginRight, marginBottom, QPrinter::Point);
m_mainFrame->print(&printer);
m_mainFrame->print(&printer, this);
return true;
}
qreal getHeight(const QVariantMap &map, const QString &key)
{
QVariant footer = map.value(key);
if (!footer.canConvert(QVariant::Map)) {
return 0;
}
QVariant height = footer.toMap().value("height");
if (!height.canConvert(QVariant::String)) {
return 0;
}
return stringToPointSize(height.toString());
}
qreal WebPage::footerHeight() const
{
return getHeight(m_paperSize, "footer");
}
qreal WebPage::headerHeight() const
{
return getHeight(m_paperSize, "header");
}
QString getHeaderFooter(const QVariantMap &map, const QString &key, QWebFrame *frame, int page, int numPages)
{
QVariant header = map.value(key);
if (!header.canConvert(QVariant::Map)) {
return QString();
}
QVariant callback = header.toMap().value("contents");
if (callback.canConvert<QObject*>()) {
Callback* caller = qobject_cast<Callback*>(callback.value<QObject*>());
if (caller) {
QVariant ret = caller->call(QVariantList() << page << numPages);
if (ret.canConvert(QVariant::String)) {
return ret.toString();
}
}
}
frame->evaluateJavaScript("console.error('Bad header callback given, use phantom.callback);", QString());
return QString();
}
QString WebPage::header(int page, int numPages)
{
return getHeaderFooter(m_paperSize, "header", m_mainFrame, page, numPages);
}
QString WebPage::footer(int page, int numPages)
{
return getHeaderFooter(m_paperSize, "footer", m_mainFrame, page, numPages);
}
void WebPage::uploadFile(const QString &selector, const QString &fileName)
{
QWebElement el = m_mainFrame->findFirstElement(selector);

View File

@ -34,6 +34,7 @@
#include <QMap>
#include <QVariantMap>
#include <QWebPage>
#include <QWebFrame>
#include "replcompletable.h"
@ -43,7 +44,7 @@ class NetworkAccessManager;
class QWebInspector;
class Phantom;
class WebPage: public REPLCompletable
class WebPage: public REPLCompletable, public QWebFrame::PrintCallback
{
Q_OBJECT
Q_PROPERTY(QString content READ content WRITE setContent)
@ -85,6 +86,11 @@ public:
void showInspector(const int remotePort = -1);
QString footer(int page, int numPages);
qreal footerHeight() const;
QString header(int page, int numPages);
qreal headerHeight() const;
public slots:
void openUrl(const QString &address, const QVariant &op, const QVariantMap &settings);
void release();