// -*- mode: c++; tab-width: 4; indent-tabs-mode: t; eval: (progn (c-set-style "stroustrup") (c-set-offset 'innamespace 0)); -*-
// vi:set ts=4 sts=4 sw=4 noet :
//
// Copyright 2010, 2011 wkhtmltopdf authors
//
// This file is part of wkhtmltopdf.
//
// wkhtmltopdf is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// wkhtmltopdf 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 Lesser General Public License
// along with wkhtmltopdf. If not, see .
#include "pdfconverter_p.hh"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#ifdef Q_OS_WIN32
#include
#include
#endif
#include "dllbegin.inc"
using namespace wkhtmltopdf;
using namespace wkhtmltopdf::settings;
#define STRINGIZE_(x) #x
#define STRINGIZE(x) STRINGIZE_(x)
const qreal PdfConverter::millimeterToPointMultiplier = 2.83464567;
DLL_LOCAL QMap PageObject::webPageToObject;
struct DLL_LOCAL StreamDumper {
QFile out;
QTextStream stream;
StreamDumper(const QString & path): out(path), stream(&out) {
out.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text);
stream.setCodec("UTF-8");
}
};
/*!
\file pageconverter.hh
\brief Defines the PdfConverter class
*/
/*!
\file pageconverter_p.hh
\brief Defines the PdfConverterPrivate class
*/
bool DLL_LOCAL looksLikeHtmlAndNotAUrl(QString str) {
QString s = str.split("?")[0];
return s.count('<') > 0 || str.startsWith("data:", Qt::CaseInsensitive);
}
PdfConverterPrivate::PdfConverterPrivate(PdfGlobal & s, PdfConverter & o) :
settings(s), pageLoader(s.load, true),
out(o), printer(0), painter(0)
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
, webPrinter(0), measuringHFLoader(s.load), hfLoader(s.load), tocLoader1(s.load), tocLoader2(s.load)
, tocLoader(&tocLoader1), tocLoaderOld(&tocLoader2)
, outline(0), currentHeader(0), currentFooter(0)
#endif
{
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
phaseDescriptions.push_back("Loading pages");
phaseDescriptions.push_back("Counting pages");
phaseDescriptions.push_back("Loading TOC");
phaseDescriptions.push_back("Resolving links");
phaseDescriptions.push_back("Loading headers and footers");
#else
phaseDescriptions.push_back("Loading page");
#endif
phaseDescriptions.push_back("Printing pages");
phaseDescriptions.push_back("Done");
connect(&pageLoader, SIGNAL(loadProgress(int)), this, SLOT(loadProgress(int)));
connect(&pageLoader, SIGNAL(loadFinished(bool)), this, SLOT(pagesLoaded(bool)));
connect(&pageLoader, SIGNAL(error(QString)), this, SLOT(forwardError(QString)));
connect(&pageLoader, SIGNAL(warning(QString)), this, SLOT(forwardWarning(QString)));
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
connect(&measuringHFLoader, SIGNAL(loadProgress(int)), this, SLOT(loadProgress(int)));
connect(&measuringHFLoader, SIGNAL(loadFinished(bool)), this, SLOT(measuringHeadersLoaded(bool)));
connect(&measuringHFLoader, SIGNAL(error(QString)), this, SLOT(forwardError(QString)));
connect(&measuringHFLoader, SIGNAL(warning(QString)), this, SLOT(forwardWarning(QString)));
connect(&hfLoader, SIGNAL(loadProgress(int)), this, SLOT(loadProgress(int)));
connect(&hfLoader, SIGNAL(loadFinished(bool)), this, SLOT(headersLoaded(bool)));
connect(&hfLoader, SIGNAL(error(QString)), this, SLOT(forwardError(QString)));
connect(&hfLoader, SIGNAL(warning(QString)), this, SLOT(forwardWarning(QString)));
connect(&tocLoader1, SIGNAL(loadProgress(int)), this, SLOT(loadProgress(int)));
connect(&tocLoader1, SIGNAL(loadFinished(bool)), this, SLOT(tocLoaded(bool)));
connect(&tocLoader1, SIGNAL(error(QString)), this, SLOT(forwardError(QString)));
connect(&tocLoader1, SIGNAL(warning(QString)), this, SLOT(forwardWarning(QString)));
connect(&tocLoader2, SIGNAL(loadProgress(int)), this, SLOT(loadProgress(int)));
connect(&tocLoader2, SIGNAL(loadFinished(bool)), this, SLOT(tocLoaded(bool)));
connect(&tocLoader2, SIGNAL(error(QString)), this, SLOT(forwardError(QString)));
connect(&tocLoader2, SIGNAL(warning(QString)), this, SLOT(forwardWarning(QString)));
#endif
if ( ! settings.viewportSize.isEmpty())
{
QStringList viewportSizeList = settings.viewportSize.split("x");
int width = viewportSizeList.first().toInt();
int height = viewportSizeList.last().toInt();
viewportSize = QSize(width,height);
}
}
PdfConverterPrivate::~PdfConverterPrivate() {
clearResources();
}
void PdfConverterPrivate::beginConvert() {
error=false;
progressString = "0%";
currentPhase=0;
errorCode=0;
#ifndef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
if (objects.size() > 1) {
emit out.error("This version of wkhtmltopdf is build against an unpatched version of QT, and does not support more then one input document.");
fail();
return;
}
#else
bool headerHeightsCalcNeeded = false;
#endif
for (QList::iterator i=objects.begin(); i != objects.end(); ++i) {
PageObject & o=*i;
settings::PdfObject & s = o.settings;
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
if (!s.header.htmlUrl.isEmpty() ) {
if (looksLikeHtmlAndNotAUrl(s.header.htmlUrl)) {
emit out.error("--header-html should be a URL and not a string containing HTML code.");
fail();
return;
}
// we should auto calculate header if top margin is not specified
if (settings.margin.top.first == -1) {
headerHeightsCalcNeeded = true;
o.measuringHeader = &measuringHFLoader.addResource(
MultiPageLoader::guessUrlFromString(s.header.htmlUrl), s.load)->page;
} else {
// or just set static values
// add spacing to prevent moving header out of page
o.headerReserveHeight = settings.margin.top.first + s.header.spacing;
}
}
if (!s.footer.htmlUrl.isEmpty()) {
if (looksLikeHtmlAndNotAUrl(s.footer.htmlUrl)) {
emit out.error("--footer-html should be a URL and not a string containing HTML code.");
fail();
return;
}
if (settings.margin.bottom.first == -1) {
// we should auto calculate footer if top margin is not specified
headerHeightsCalcNeeded = true;
o.measuringFooter = &measuringHFLoader.addResource(
MultiPageLoader::guessUrlFromString(s.footer.htmlUrl), s.load)->page;
} else {
// or just set static values
// add spacing to prevent moving footer out of page
o.footerReserveHeight = settings.margin.bottom.first + s.footer.spacing;
}
}
#endif
if (!s.isTableOfContent) {
o.loaderObject = pageLoader.addResource(s.page, s.load, &o.data);
o.page = &o.loaderObject->page;
PageObject::webPageToObject[o.page] = &o;
updateWebSettings(o.page->settings(), s.web);
}
}
emit out.phaseChanged();
loadProgress(0);
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
if (headerHeightsCalcNeeded) {
// preload header/footer to check their heights
measuringHFLoader.load();
} else {
// set defaults if top or bottom mergin is not specified
if (settings.margin.top.first == -1) {
settings.margin.top.first = 10;
}
if (settings.margin.bottom.first == -1) {
settings.margin.bottom.first = 10;
}
for (QList::iterator i=objects.begin(); i != objects.end(); ++i) {
PageObject & o=*i;
o.headerReserveHeight = settings.margin.top.first;
o.footerReserveHeight = settings.margin.bottom.first;
}
pageLoader.load();
}
#else
pageLoader.load();
#endif
}
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
// calculates header/footer height
// returns millimeters
qreal PdfConverterPrivate::calculateHeaderHeight(PageObject & object, QWebPage & header) {
Q_UNUSED(object);
TempFile tempObj;
QString tempFile = tempObj.create(".pdf");
QPainter * testPainter = new QPainter();
QPrinter * testPrinter = createPrinter(tempFile);
if (!testPainter->begin(testPrinter)) {
emit out.error("Unable to write to temp location");
return 0.0;
}
QWebPrinter wp(header.mainFrame(), testPrinter, *testPainter);
qreal height = wp.elementLocation(header.mainFrame()->findFirstElement("body")).second.height();
delete testPainter;
delete testPrinter;
return (height / PdfConverter::millimeterToPointMultiplier);
}
#endif
QPrinter * PdfConverterPrivate::createPrinter(const QString & tempFile) {
QPrinter * printer = new QPrinter(settings.resolution);
if (settings.dpi != -1) printer->setResolution(settings.dpi);
//Tell the printer object to print the file
printer->setOutputFileName(tempFile);
printer->setOutputFormat(QPrinter::PdfFormat);
if ((settings.size.height.first != -1) && (settings.size.width.first != -1)) {
printer->setPaperSize(QSizeF(settings.size.width.first,settings.size.height.first + 100), settings.size.height.second);
} else {
printer->setPaperSize(settings.size.pageSize);
}
printer->setOrientation(settings.orientation);
printer->setColorMode(settings.colorMode);
printer->setCreator("wkhtmltopdf " STRINGIZE(FULL_VERSION));
return printer;
}
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
void PdfConverterPrivate::preprocessPage(PageObject & obj) {
currentObject++;
if (obj.settings.isTableOfContent) {
obj.pageCount = 1;
pageCount += 1;
outline->addEmptyWebPage();
return;
}
if (!obj.loaderObject || obj.loaderObject->skip) return;
int tot = objects.size();
progressString = QString("Object ")+QString::number(currentObject)+QString(" of ")+QString::number(tot);
emit out.progressChanged((currentObject)*100 / tot);
painter->save();
if (viewportSize.isValid() && ! viewportSize.isEmpty()) {
obj.page->setViewportSize(viewportSize);
obj.page->mainFrame()->setScrollBarPolicy(Qt::Vertical,Qt::ScrollBarAlwaysOff);
obj.page->mainFrame()->setScrollBarPolicy(Qt::Horizontal,Qt::ScrollBarAlwaysOff);
}
QWebPrinter wp(obj.page->mainFrame(), printer, *painter);
obj.pageCount = obj.settings.pagesCount? wp.pageCount(): 0;
pageCount += obj.pageCount;
if (obj.settings.includeInOutline)
outline->addWebPage(obj.page->mainFrame()->title(), wp, obj.page->mainFrame(),
obj.settings, obj.localLinks, obj.anchors);
else
outline->addEmptyWebPage();
painter->restore();
}
#endif
/*!
* Prepares printing out the document to the pdf file
*/
void PdfConverterPrivate::pagesLoaded(bool ok) {
if (errorCode == 0) errorCode = pageLoader.httpErrorCode();
if (!ok) {
fail();
return;
}
lout = settings.out;
if (settings.out == "-") {
#ifndef Q_OS_WIN32
if (QFile::exists("/dev/stdout"))
lout = "/dev/stdout";
else
#endif
lout = tempOut.create(".pdf");
}
if (settings.out.isEmpty())
lout = tempOut.create(".pdf");
printer = new QPrinter(settings.resolution);
if (settings.dpi != -1) printer->setResolution(settings.dpi);
//Tell the printer object to print the file
printer->setOutputFileName(lout);
printer->setOutputFormat(QPrinter::PdfFormat);
//We currently only support margins with the same unit
if (settings.margin.left.second != settings.margin.right.second ||
settings.margin.left.second != settings.margin.top.second ||
settings.margin.left.second != settings.margin.bottom.second) {
emit out.error("Currently all margin units must be the same!");
fail();
return;
}
//Setup margins and papersize
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
double maxHeaderHeight = objects[0].headerReserveHeight;
double maxFooterHeight = objects[0].footerReserveHeight;
for (QList::iterator i=objects.begin(); i != objects.end(); ++i) {
PageObject & o=*i;
maxHeaderHeight = std::max(maxHeaderHeight, o.headerReserveHeight);
maxFooterHeight = std::max(maxFooterHeight, o.footerReserveHeight);
}
printer->setPageMargins(settings.margin.left.first, maxHeaderHeight,
settings.margin.right.first, maxFooterHeight,
settings.margin.left.second);
#else
printer->setPageMargins(settings.margin.left.first, settings.margin.top.first,
settings.margin.right.first, settings.margin.bottom.first,
settings.margin.left.second);
#endif
if ((settings.size.height.first != -1) && (settings.size.width.first != -1)) {
printer->setPaperSize(QSizeF(settings.size.width.first,settings.size.height.first), settings.size.height.second);
} else {
printer->setPaperSize(settings.size.pageSize);
}
printer->setOrientation(settings.orientation);
printer->setColorMode(settings.colorMode);
printer->setCreator("wkhtmltopdf " STRINGIZE(FULL_VERSION));
if (!printer->isValid()) {
emit out.error("Unable to write to destination");
fail();
return;
}
#ifndef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
//If you do not have the hacks you get this crappy solution
printer->setCollateCopies(settings.copies);
printer->setCollateCopies(settings.collate);
printDocument();
#else
printer->printEngine()->setProperty(QPrintEngine::PPK_UseCompression, settings.useCompression);
printer->printEngine()->setProperty(QPrintEngine::PPK_ImageQuality, settings.imageQuality);
printer->printEngine()->setProperty(QPrintEngine::PPK_ImageDPI, settings.imageDPI);
painter = new QPainter();
title = settings.documentTitle;
for (int d=0; d < objects.size(); ++d) {
if (title != "") break;
if (!objects[d].loaderObject || objects[d].loaderObject->skip ||
objects[d].settings.isTableOfContent) continue;
title = objects[d].page->mainFrame()->title();
}
printer->setDocName(title);
if (!painter->begin(printer)) {
emit out.error("Unable to write to destination");
fail();
return;
}
currentPhase = 1;
emit out.phaseChanged();
outline = new Outline(settings);
//This is the first render face, it is done to calculate:
// * The number of pages of each document
// * A visual ordering of the header element
// * The location and page number of each header
pageCount = 0;
currentObject = 0;
for (int d=0; d < objects.size(); ++d)
preprocessPage(objects[d]);
actualPages = pageCount * settings.copies;
loadTocs();
#endif
}
void PdfConverterPrivate::loadHeaders() {
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
currentPhase = 4;
emit out.phaseChanged();
bool hf=false;
int pageNumber=1;
for (int d=0; d < objects.size(); ++d) {
PageObject & obj = objects[d];
if (!obj.loaderObject || obj.loaderObject->skip) continue;
settings::PdfObject & ps = obj.settings;
for (int op=0; op < obj.pageCount; ++op) {
if (!ps.header.htmlUrl.isEmpty() || !ps.footer.htmlUrl.isEmpty()) {
QHash parms;
fillParms(parms, pageNumber, obj);
parms["sitepage"] = QString::number(op+1);
parms["sitepages"] = QString::number(obj.pageCount);
hf = true;
if (!ps.header.htmlUrl.isEmpty())
obj.headers.push_back(loadHeaderFooter(ps.header.htmlUrl, parms, ps) );
if (!ps.footer.htmlUrl.isEmpty()) {
obj.footers.push_back(loadHeaderFooter(ps.footer.htmlUrl, parms, ps) );
}
}
if (ps.pagesCount) ++pageNumber;
}
}
if (hf)
hfLoader.load();
else
printDocument();
#endif
}
void PdfConverterPrivate::loadTocs() {
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
std::swap(tocLoaderOld, tocLoader);
tocLoader->clearResources();
bool toc = false;
for (int d=0; d < objects.size(); ++d) {
PageObject & obj = objects[d];
settings::PdfObject & ps = obj.settings;
if (!ps.isTableOfContent) continue;
obj.clear();
QString style = ps.tocXsl;
if (style.isEmpty()) {
style = obj.tocFile.create(".xsl");
StreamDumper styleDump(style);
dumpDefaultTOCStyleSheet(styleDump.stream, ps.toc);
}
QString path = obj.tocFile.create(".xml");
StreamDumper sd(path);
outline->dump(sd.stream);
QFile styleFile(style);
if (!styleFile.open(QIODevice::ReadOnly)) {
emit out.error("Could not read the TOC XSL");
fail();
}
QFile xmlFile(path);
if (!xmlFile.open(QIODevice::ReadOnly)) {
emit out.error("Could not read the TOC XML");
fail();
}
QString htmlPath = obj.tocFile.create(".html");
QFile htmlFile(htmlPath);
if (!htmlFile.open(QIODevice::WriteOnly)) {
emit out.error("Could not open the TOC for writing");
fail();
}
QXmlQuery query(QXmlQuery::XSLT20);
query.setFocus(&xmlFile);
query.setQuery(&styleFile);
query.evaluateTo(&htmlFile);
obj.loaderObject = tocLoader->addResource(htmlPath, ps.load);
obj.page = &obj.loaderObject->page;
PageObject::webPageToObject[obj.page] = &obj;
updateWebSettings(obj.page->settings(), ps.web);
toc= true;
}
if (toc) {
if (currentPhase != 2) {
currentPhase = 2;
emit out.phaseChanged();
}
tocLoader->load();
} else
tocLoaded(true);
#endif
}
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
void PdfConverterPrivate::findLinks(QWebFrame * frame, QVector > & local, QVector > & external, QHash & anchors) {
bool ulocal=true, uexternal=true;
if (PageObject::webPageToObject.contains(frame->page())) {
ulocal = PageObject::webPageToObject[frame->page()]->settings.useLocalLinks;
uexternal = PageObject::webPageToObject[frame->page()]->settings.useExternalLinks;
}
if (!ulocal && !uexternal) return;
foreach (const QWebElement & elm, frame->findAllElements("a")) {
QString n=elm.attribute("name");
if (n.isEmpty()) n=elm.attribute("ns0:name");
if (n.startsWith("__WKANCHOR_")) anchors[n] = elm;
QString h=elm.attribute("href");
if (h.isEmpty()) h=elm.attribute("ns0:href");
if (h.startsWith("__WKANCHOR_")) {
local.push_back( qMakePair(elm, h) );
} else {
QUrl href(h);
if (href.isEmpty()) continue;
href=frame->baseUrl().resolved(href);
QString key = QUrl::fromPercentEncoding(href.toString(QUrl::RemoveFragment).toLocal8Bit());
if (urlToPageObj.contains(key)) {
if (ulocal) {
PageObject * p = urlToPageObj[key];
QWebElement e;
if (!href.hasFragment())
e = p->page->mainFrame()->findFirstElement("body");
else {
e = p->page->mainFrame()->findFirstElement("a[name=\""+href.fragment()+"\"]");
if (e.isNull())
e = p->page->mainFrame()->findFirstElement("*[id=\""+href.fragment()+"\"]");
if (e.isNull())
e = p->page->mainFrame()->findFirstElement("*[name=\""+href.fragment()+"\"]");
}
if (!e.isNull()) {
p->anchors[href.toString()] = e;
local.push_back( qMakePair(elm, href.toString()) );
}
}
} else if (uexternal) {
external.push_back( qMakePair(elm, settings.resolveRelativeLinks ? href.toString() : h) );
}
}
}
}
void PdfConverterPrivate::fillParms(QHash & parms, int page, const PageObject & object) {
outline->fillHeaderFooterParms(page, parms, object.settings);
parms["doctitle"] = title;
parms["title"] = object.page?object.page->mainFrame()->title():"";
QDateTime t(QDateTime::currentDateTime());
parms["time"] = t.time().toString(Qt::SystemLocaleShortDate);
parms["date"] = t.date().toString(Qt::SystemLocaleShortDate);
parms["isodate"] = t.date().toString(Qt::ISODate);
}
void PdfConverterPrivate::endPage(PageObject & object, bool hasHeaderFooter, int objectPage, int pageNumber) {
typedef QPair p_t;
settings::PdfObject & s = object.settings;
// save margin values
qreal leftMargin, topMargin, rightMargin, bottomMargin;
printer->getPageMargins(&leftMargin, &topMargin, &rightMargin, &bottomMargin, settings.margin.left.second);
if (hasHeaderFooter) {
QHash parms;
fillParms(parms, pageNumber, object);
parms["sitepage"] = QString::number(objectPage+1);
parms["sitepages"] = QString::number(object.pageCount);
//Webkit used all kinds of crazy coordinate transformation, and font setup
//We save it here and restore some sane defaults
painter->save();
painter->resetTransform();
int h=printer->height();
int w=printer->width();
double spacing = s.header.spacing * printer->height() / printer->heightMM();
//If needed draw the header line
if (s.header.line) painter->drawLine(0, -spacing, w, -spacing);
//Guess the height of the header text
painter->setFont(QFont(s.header.fontName, s.header.fontSize));
int dy = painter->boundingRect(0, 0, w, h, Qt::AlignTop, "M").height();
//Draw the header text
QRect r=QRect(0, 0-dy-spacing, w, h);
painter->drawText(r, Qt::AlignTop | Qt::AlignLeft, hfreplace(s.header.left, parms));
painter->drawText(r, Qt::AlignTop | Qt::AlignHCenter, hfreplace(s.header.center, parms));
painter->drawText(r, Qt::AlignTop | Qt::AlignRight, hfreplace(s.header.right, parms));
spacing = s.footer.spacing * printer->height() / printer->heightMM();
//IF needed draw the footer line
if (s.footer.line) painter->drawLine(0, h + spacing, w, h + spacing);
//Guess the height of the footer text
painter->setFont(QFont(s.footer.fontName, s.footer.fontSize));
dy = painter->boundingRect(0, 0, w, h, Qt::AlignTop, "M").height();
//Draw the footer text
r=QRect(0,0,w,h+dy+ spacing);
painter->drawText(r, Qt::AlignBottom | Qt::AlignLeft, hfreplace(s.footer.left, parms));
painter->drawText(r, Qt::AlignBottom | Qt::AlignHCenter, hfreplace(s.footer.center, parms));
painter->drawText(r, Qt::AlignBottom | Qt::AlignRight, hfreplace(s.footer.right, parms));
//Restore Webkit's crazy scaling and font settings
painter->restore();
}
//if (!object.headers.empty()) {
//object.headers[objectPage];
if (currentHeader) {
QWebPage * header = currentHeader;
updateWebSettings(header->settings(), object.settings.web);
painter->save();
painter->resetTransform();
double spacing = s.header.spacing * printer->height() / printer->heightMM();
// clear vertical margins for proper header rendering
printer->setPageMargins(leftMargin, 0, rightMargin, 0, settings.margin.left.second);
painter->translate(0, -spacing);
QWebPrinter wp(header->mainFrame(), printer, *painter);
painter->translate(0,-wp.elementLocation(header->mainFrame()->findFirstElement("body")).second.height());
QVector local;
QVector external;
QHash anchors;
findLinks(header->mainFrame(), local, external, anchors);
foreach (const p_t & p, local) {
QRectF r = wp.elementLocation(p.first).second;
painter->addLink(r, p.second);
}
foreach (const p_t & p, external) {
QRectF r = wp.elementLocation(p.first).second;
painter->addHyperlink(r, QUrl(p.second));
}
wp.spoolPage(1);
// restore margins
printer->setPageMargins(leftMargin, topMargin, rightMargin, bottomMargin, settings.margin.left.second);
painter->restore();
}
if (currentFooter) {
QWebPage * footer=currentFooter;
updateWebSettings(footer->settings(), object.settings.web);
painter->save();
painter->resetTransform();
double spacing = s.footer.spacing * printer->height() / printer->heightMM();
painter->translate(0, printer->height()+ spacing);
// clear vertical margins for proper header rendering
printer->setPageMargins(leftMargin, 0, rightMargin, 0, settings.margin.left.second);
QWebPrinter wp(footer->mainFrame(), printer, *painter);
QVector local;
QVector external;
QHash anchors;
findLinks(footer->mainFrame(), local, external, anchors);
foreach (const p_t & p, local) {
QRectF r = wp.elementLocation(p.first).second;
painter->addLink(r, p.second);
}
foreach (const p_t & p, external) {
QRectF r = wp.elementLocation(p.first).second;
painter->addHyperlink(r, QUrl(p.second));
}
wp.spoolPage(1);
// restore margins
printer->setPageMargins(leftMargin, topMargin, rightMargin, bottomMargin, settings.margin.left.second);
painter->restore();
}
}
void PdfConverterPrivate::handleTocPage(PageObject & obj) {
painter->save();
QWebPrinter wp(obj.page->mainFrame(), printer, *painter);
int pc = obj.settings.pagesCount? wp.pageCount(): 0;
if (pc != obj.pageCount) {
obj.pageCount = pc;
tocChanged=true;
}
pageCount += obj.pageCount;
tocChanged = outline->replaceWebPage(obj.number, obj.settings.toc.captionText, wp, obj.page->mainFrame(), obj.settings, obj.localLinks, obj.anchors) || tocChanged;
painter->restore();
}
#endif
void PdfConverterPrivate::tocLoaded(bool ok) {
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
if (errorCode == 0) errorCode = tocLoader->httpErrorCode();
#endif
if (!ok) {
fail();
return;
}
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
tocChanged = false;
pageCount = 0;
currentObject = 0;
for (int d=0; d < objects.size(); ++d) {
++currentObject;
if (!objects[d].loaderObject || objects[d].loaderObject->skip) continue;
if (!objects[d].settings.isTableOfContent) {
pageCount += objects[d].pageCount;
continue;
}
handleTocPage(objects[d]);
}
actualPages = pageCount * settings.copies;
if (tocChanged)
loadTocs();
else {
//Find and resolve all local links
currentPhase = 3;
emit out.phaseChanged();
QHash urlToDoc;
for (int d=0; d < objects.size(); ++d) {
if (!objects[d].loaderObject || objects[d].loaderObject->skip) continue;
if (objects[d].settings.isTableOfContent) continue;
urlToPageObj[ QUrl::fromPercentEncoding(objects[d].page->mainFrame()->url().toString(QUrl::RemoveFragment).toLocal8Bit()) ] = &objects[d];
}
for (int d=0; d < objects.size(); ++d) {
if (!objects[d].loaderObject || objects[d].loaderObject->skip) continue;
progressString = QString("Object ")+QString::number(d+1)+QString(" of ")+QString::number(objects.size());
emit out.progressChanged((d+1)*100 / objects.size());
findLinks(objects[d].page->mainFrame(), objects[d].localLinks, objects[d].externalLinks, objects[d].anchors );
}
loadHeaders();
}
#endif
}
void PdfConverterPrivate::measuringHeadersLoaded(bool ok) {
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
if (errorCode == 0) errorCode = measuringHFLoader.httpErrorCode();
#endif
if (!ok) {
fail();
return;
}
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
for (int d=0; d < objects.size(); ++d) {
PageObject & obj = objects[d];
if (obj.measuringHeader) {
// add spacing to prevent moving header out of page
obj.headerReserveHeight = calculateHeaderHeight(obj, *obj.measuringHeader) + obj.settings.header.spacing;
}
if (obj.measuringFooter) {
// add spacing to prevent moving footer out of page
obj.footerReserveHeight = calculateHeaderHeight(obj, *obj.measuringFooter) + obj.settings.footer.spacing;
}
}
#endif
pageLoader.load();
}
void PdfConverterPrivate::headersLoaded(bool ok) {
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
if (errorCode == 0) errorCode = hfLoader.httpErrorCode();
#endif
if (!ok) {
fail();
return;
}
printDocument();
}
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
void PdfConverterPrivate::spoolPage(int page) {
progressString = QString("Page ") + QString::number(actualPage) + QString(" of ") + QString::number(actualPages);
emit out.progressChanged(actualPage * 100 / actualPages);
if (actualPage != 1)
printer->newPage();
webPrinter->spoolPage(page+1);
foreach (QWebElement elm, pageFormElements[page+1]) {
QString type = elm.attribute("type");
QString tn = elm.tagName();
QString name = elm.attribute("name");
if (tn == "TEXTAREA" || type == "text" || type == "password") {
painter->addTextField(
webPrinter->elementLocation(elm).second,
tn == "TEXTAREA"?elm.toPlainText():elm.attribute("value"),
name,
tn == "TEXTAREA",
type == "password",
elm.evaluateJavaScript("this.readonly;").toBool(),
elm.hasAttribute("maxlength")?elm.attribute("maxlength").toInt():-1
);
} else if (type == "checkbox") {
painter->addCheckBox(
webPrinter->elementLocation(elm).second,
elm.evaluateJavaScript("this.checked;").toBool(),
name,
elm.evaluateJavaScript("this.readonly;").toBool());
}
}
for (QHash::iterator i=pageAnchors[page+1].begin();
i != pageAnchors[page+1].end(); ++i) {
QRectF r = webPrinter->elementLocation(i.value()).second;
painter->addAnchor(r, i.key());
}
for (QVector< QPair >::iterator i=pageLocalLinks[page+1].begin();
i != pageLocalLinks[page+1].end(); ++i) {
QRectF r = webPrinter->elementLocation(i->first).second;
painter->addLink(r, i->second);
}
for (QVector< QPair >::iterator i=pageExternalLinks[page+1].begin();
i != pageExternalLinks[page+1].end(); ++i) {
QRectF r = webPrinter->elementLocation(i->first).second;
painter->addHyperlink(r, QUrl(i->second));
}
endPage(objects[currentObject], pageHasHeaderFooter, page, pageNumber);
actualPage++;
}
void PdfConverterPrivate::spoolTo(int page) {
int pc=settings.collate?1:settings.copies;
const settings::PdfObject & ps = objects[currentObject].settings;
while (objectPage < page) {
for (int pc_=0; pc_ < pc; ++pc_)
spoolPage(objectPage);
if (ps.pagesCount) ++pageNumber;
++objectPage;
//TODO free header and footer
currentHeader=NULL;
currentFooter=NULL;
}
}
void PdfConverterPrivate::beginPrintObject(PageObject & obj) {
if (obj.number != 0)
endPrintObject(objects[obj.number-1]);
currentObject = obj.number;
if (!obj.loaderObject || obj.loaderObject->skip) return;
QPalette pal = obj.loaderObject->page.palette();
pal.setBrush(QPalette::Base, Qt::transparent);
obj.loaderObject->page.setPalette(pal);
const settings::PdfObject & ps = obj.settings;
pageHasHeaderFooter = ps.header.line || ps.footer.line ||
!ps.header.left.isEmpty() || !ps.footer.left.isEmpty() ||
!ps.header.center.isEmpty() || !ps.footer.center.isEmpty() ||
!ps.header.right.isEmpty() || !ps.footer.right.isEmpty();
painter->save();
if (ps.produceForms) {
foreach (QWebElement elm, obj.page->mainFrame()->findAllElements("input"))
elm.setStyleProperty("color","white");
foreach (QWebElement elm, obj.page->mainFrame()->findAllElements("textarea"))
elm.setStyleProperty("color","white");
}
//output
webPrinter = new QWebPrinter(obj.page->mainFrame(), printer, *painter);
QString l1=obj.page->mainFrame()->url().path().split("/").back()+"#";
QString l2=obj.page->mainFrame()->url().toString() + "#";
outline->fillAnchors(obj.number, obj.anchors);
//Sort anchors and links by page
for (QHash::iterator i=obj.anchors.begin();
i != obj.anchors.end(); ++i)
pageAnchors[webPrinter->elementLocation(i.value()).first][i.key()] = i.value();
for (QVector< QPair >::iterator i=obj.localLinks.begin();
i != obj.localLinks.end(); ++i)
pageLocalLinks[webPrinter->elementLocation(i->first).first].push_back(*i);
for (QVector< QPair >::iterator i=obj.externalLinks.begin();
i != obj.externalLinks.end(); ++i)
pageExternalLinks[webPrinter->elementLocation(i->first).first].push_back(*i);
if (ps.produceForms) {
foreach (const QWebElement & elm, obj.page->mainFrame()->findAllElements("input"))
pageFormElements[webPrinter->elementLocation(elm).first].push_back(elm);
foreach (const QWebElement & elm, obj.page->mainFrame()->findAllElements("textarea"))
pageFormElements[webPrinter->elementLocation(elm).first].push_back(elm);
}
emit out.producingForms(obj.settings.produceForms);
out.emitCheckboxSvgs(obj.settings.load);
objectPage = 0;
}
void PdfConverterPrivate::handleHeader(QWebPage * frame, int page) {
spoolTo(page);
currentHeader = frame;
}
void PdfConverterPrivate::handleFooter(QWebPage * frame, int page) {
spoolTo(page);
currentFooter = frame;
}
void PdfConverterPrivate::endPrintObject(PageObject & obj) {
Q_UNUSED(obj);
// If this page was skipped, we might not have
// anything to spool to printer..
if (webPrinter != 0) spoolTo(webPrinter->pageCount());
pageAnchors.clear();
pageLocalLinks.clear();
pageExternalLinks.clear();
pageFormElements.clear();
if (webPrinter != 0) {
QWebPrinter *tmp = webPrinter;
webPrinter = 0;
delete tmp;
painter->restore();
}
}
#endif
void PdfConverterPrivate::printDocument() {
#ifndef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
currentPhase = 1;
emit out.phaseChanged();
objects[0].page->mainFrame()->print(printer);
progressString = "";
emit out.progressChanged(-1);
#else
actualPage=1;
int cc=settings.collate?settings.copies:1;
currentPhase = 5;
emit out.phaseChanged();
progressString = "Preparing";
emit out.progressChanged(0);
for (int cc_=0; cc_ < cc; ++cc_) {
pageNumber=1;
for (int d=0; d < objects.size(); ++d) {
beginPrintObject(objects[d]);
// XXX: In some cases nothing gets loaded at all,
// so we would get no webPrinter instance.
int pageCount = webPrinter != 0 ? webPrinter->pageCount() : 0;
//const settings::PdfObject & ps = objects[d].settings;
for(int i=0; i < pageCount; ++i) {
if (!objects[d].headers.empty())
handleHeader(objects[d].headers[i], i);
if (!objects[d].footers.empty())
handleFooter(objects[d].footers[i], i);
}
}
endPrintObject(objects[objects.size()-1]);
}
outline->printOutline(printer);
if (!settings.dumpOutline.isEmpty()) {
StreamDumper sd(settings.dumpOutline);
outline->dump(sd.stream);
}
painter->end();
#endif
if (settings.out == "-" && lout != "/dev/stdout") {
QFile i(lout);
QFile o;
#ifdef Q_OS_WIN32
_setmode(_fileno(stdout), _O_BINARY);
#endif
if ( !i.open(QIODevice::ReadOnly) ||
!o.open(stdout,QIODevice::WriteOnly) ||
!MultiPageLoader::copyFile(i,o) ) {
emit out.error("Count not write to stdout");
tempOut.removeAll();
fail();
return;
}
tempOut.removeAll();
}
if (settings.out.isEmpty()) {
QFile i(lout);
if (!i.open(QIODevice::ReadOnly)) {
emit out.error("Reading output failed");
tempOut.removeAll();
fail();
}
outputData = i.readAll();
tempOut.removeAll();
}
clearResources();
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
currentPhase = 6;
#else
currentPhase = 2;
#endif
emit out.phaseChanged();
convertionDone = true;
emit out.finished(true);
qApp->exit(0); // quit qt's event handling
}
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
QWebPage * PdfConverterPrivate::loadHeaderFooter(QString url, const QHash & parms, const settings::PdfObject & ps) {
QUrl u = MultiPageLoader::guessUrlFromString(url);
for (QHash::const_iterator i=parms.begin(); i != parms.end(); ++i)
u.addQueryItem(i.key(), i.value());
return &hfLoader.addResource(u, ps.load)->page;
}
/*!
* Replace some variables in a string used in a header or footer
* \param q the string to substitute in
*/
QString PdfConverterPrivate::hfreplace(const QString & q, const QHash & parms) {
QString r=q;
for (QHash::const_iterator i=parms.begin(); i != parms.end(); ++i)
r=r.replace("["+i.key()+"]", i.value(), Qt::CaseInsensitive);
return r;
}
#endif
void PdfConverterPrivate::clearResources() {
objects.clear();
pageLoader.clearResources();
#ifdef __EXTENSIVE_WKHTMLTOPDF_QT_HACK__
hfLoader.clearResources();
tocLoader1.clearResources();
tocLoader2.clearResources();
if (outline) {
Outline * tmp = outline;
outline = 0;
delete tmp;
}
#endif
if (printer) {
QPrinter * tmp = printer;
printer = 0;
delete tmp;
}
if (painter) {
QPainter * tmp = painter;
painter = 0;
delete tmp;
}
}
Converter & PdfConverterPrivate::outer() {
return out;
}
/*!
\class PdfConverter
\brief Class responsible for converting html pages to pdf
\todo explain something about the conversion process here, and mention stages
*/
/*!
\brief Create a page converter object based on the supplied settings
\param settings Settings for the conversion
*/
PdfConverter::PdfConverter(settings::PdfGlobal & settings):
d(new PdfConverterPrivate(settings, *this)) {
}
/*!
\brief The destructor for the page converter object
*/
PdfConverter::~PdfConverter() {
PdfConverterPrivate *tmp = d;
d = 0;
tmp->deleteLater();;
}
/*!
\brief add a resource we want to convert
\param url The url of the object we want to convert
*/
void PdfConverter::addResource(const settings::PdfObject & page, const QString * data) {
d->objects.push_back( PageObject(page, data) );
d->objects.back().number = d->objects.size()-1;
}
const QByteArray & PdfConverter::output() {
return d->outputData;
}
/*!
\brief Returns the settings object associated with the page converter
*/
const settings::PdfGlobal & PdfConverter::globalSettings() const {
return d->settings;
}
/*!
\fn PdfConverter::warning(const QString & message)
\brief Signal emitted when some non fatal warning occurs during conversion
\param message The warning message
*/
/*!
\fn PdfConverter::error(const QString & message)
\brief Signal emitted when a fatal error has occurred during conversion
\param message A message describing the fatal error
*/
/*!
\fn PdfConverter::phaseChanged()
\brief Signal emitted when the converter has reached a new phase
*/
/*!
\fn PdfConverter::progressChanged()
\brief Signal emitted when some progress has been done in the conversion phase
*/
/*!
\fn PdfConverter::finised()
\brief Signal emitted when conversion has finished.
*/
ConverterPrivate & PdfConverter::priv() {
return *d;
}