okular/generators/xps/generator_xps.cpp
2020-09-30 16:09:57 +00:00

2164 lines
78 KiB
C++

/*
Copyright (C) 2006, 2009 Brad Hards <bradh@frogmouth.net>
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 2
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, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301, USA.
*/
#include "generator_xps.h"
#include <KAboutData>
#include <KLocalizedString>
#include <QBuffer>
#include <QDateTime>
#include <QFile>
#include <QImageReader>
#include <QList>
#include <QMutex>
#include <QPainter>
#include <QPrinter>
#include <QUrl>
#include <core/area.h>
#include <core/document.h>
#include <core/fileprinter.h>
#include <core/page.h>
OKULAR_EXPORT_PLUGIN(XpsGenerator, "libokularGenerator_xps.json")
Q_DECLARE_METATYPE(QGradient *)
Q_DECLARE_METATYPE(XpsPathFigure *)
Q_DECLARE_METATYPE(XpsPathGeometry *)
// From Qt4
static int hex2int(char hex)
{
QChar hexchar = QLatin1Char(hex);
int v;
if (hexchar.isDigit())
v = hexchar.digitValue();
else if (hexchar >= QLatin1Char('A') && hexchar <= QLatin1Char('F'))
v = hexchar.cell() - 'A' + 10;
else if (hexchar >= QLatin1Char('a') && hexchar <= QLatin1Char('f'))
v = hexchar.cell() - 'a' + 10;
else
v = -1;
return v;
}
// Modified from Qt4
static QColor hexToRgba(const QByteArray &name)
{
const int len = name.length();
if (len == 0 || name[0] != '#')
return QColor();
int r, g, b;
int a = 255;
if (len == 7) {
r = (hex2int(name[1]) << 4) + hex2int(name[2]);
g = (hex2int(name[3]) << 4) + hex2int(name[4]);
b = (hex2int(name[5]) << 4) + hex2int(name[6]);
} else if (len == 9) {
a = (hex2int(name[1]) << 4) + hex2int(name[2]);
r = (hex2int(name[3]) << 4) + hex2int(name[4]);
g = (hex2int(name[5]) << 4) + hex2int(name[6]);
b = (hex2int(name[7]) << 4) + hex2int(name[8]);
} else {
r = g = b = -1;
}
if ((uint)r > 255 || (uint)g > 255 || (uint)b > 255) {
return QColor();
}
return QColor(r, g, b, a);
}
static QRectF stringToRectF(const QString &data)
{
QStringList numbers = data.split(QLatin1Char(','));
QPointF origin(numbers.at(0).toDouble(), numbers.at(1).toDouble());
QSizeF size(numbers.at(2).toDouble(), numbers.at(3).toDouble());
return QRectF(origin, size);
}
static bool parseGUID(const QString &guidString, unsigned short guid[16])
{
if (guidString.length() <= 35) {
return false;
}
// Maps bytes to positions in guidString
const static int indexes[] = {6, 4, 2, 0, 11, 9, 16, 14, 19, 21, 24, 26, 28, 30, 32, 34};
for (int i = 0; i < 16; i++) {
int hex1 = hex2int(guidString[indexes[i]].cell());
int hex2 = hex2int(guidString[indexes[i] + 1].cell());
if ((hex1 < 0) || (hex2 < 0)) {
return false;
}
guid[i] = hex1 * 16 + hex2;
}
return true;
}
// Read next token of abbreviated path data
static bool nextAbbPathToken(AbbPathToken *token)
{
int *curPos = &token->curPos;
QString data = token->data;
while ((*curPos < data.length()) && (data.at(*curPos).isSpace())) {
(*curPos)++;
}
if (*curPos == data.length()) {
token->type = abtEOF;
return true;
}
QChar ch = data.at(*curPos);
if (ch.isNumber() || (ch == QLatin1Char('+')) || (ch == QLatin1Char('-'))) {
int start = *curPos;
while ((*curPos < data.length()) && (!data.at(*curPos).isSpace()) && (data.at(*curPos) != QLatin1Char(',') && (!data.at(*curPos).isLetter() || data.at(*curPos) == QLatin1Char('e')))) {
(*curPos)++;
}
token->number = data.midRef(start, *curPos - start).toDouble();
token->type = abtNumber;
} else if (ch == QLatin1Char(',')) {
token->type = abtComma;
(*curPos)++;
} else if (ch.isLetter()) {
token->type = abtCommand;
token->command = data.at(*curPos).cell();
(*curPos)++;
} else {
(*curPos)++;
return false;
}
return true;
}
/**
Read point (two reals delimited by comma) from abbreviated path data
*/
static QPointF getPointFromString(AbbPathToken *token, bool relative, const QPointF currentPosition)
{
// TODO Check grammar
QPointF result;
result.rx() = token->number;
nextAbbPathToken(token);
nextAbbPathToken(token); // ,
result.ry() = token->number;
nextAbbPathToken(token);
if (relative) {
result += currentPosition;
}
return result;
}
/**
Read point (two reals delimited by comma) from string
*/
static QPointF getPointFromString(const QString &string)
{
const int commaPos = string.indexOf(QLatin1Char(QLatin1Char(',')));
if (commaPos == -1 || string.indexOf(QLatin1Char(QLatin1Char(',')), commaPos + 1) != -1)
return QPointF();
QPointF result;
bool ok = false;
QStringRef ref = string.midRef(0, commaPos);
result.setX(QString::fromRawData(ref.constData(), ref.count()).toDouble(&ok));
if (!ok)
return QPointF();
ref = string.midRef(commaPos + 1);
result.setY(QString::fromRawData(ref.constData(), ref.count()).toDouble(&ok));
if (!ok)
return QPointF();
return result;
}
static Qt::FillRule fillRuleFromString(const QString &data, Qt::FillRule def = Qt::OddEvenFill)
{
if (data == QLatin1String("EvenOdd")) {
return Qt::OddEvenFill;
} else if (data == QLatin1String("NonZero")) {
return Qt::WindingFill;
}
return def;
}
/**
Parse an abbreviated path "Data" description
\param data the string containing the whitespace separated values
\see XPS specification 4.2.3 and Appendix G
*/
static QPainterPath parseAbbreviatedPathData(const QString &data)
{
QPainterPath path;
AbbPathToken token;
token.data = data;
token.curPos = 0;
nextAbbPathToken(&token);
// Used by Smooth cubic curve (command s)
char lastCommand = ' ';
QPointF lastSecondControlPoint;
while (true) {
if (token.type != abtCommand) {
if (token.type != abtEOF) {
qCWarning(OkularXpsDebug).nospace() << "Error in parsing abbreviated path data (" << token.type << "@" << token.curPos << "): " << data;
}
return path;
}
char command = QChar::fromLatin1(token.command).toLower().cell();
bool isRelative = QChar::fromLatin1(token.command).isLower();
QPointF currPos = path.currentPosition();
nextAbbPathToken(&token);
switch (command) {
case 'f':
int rule;
rule = (int)token.number;
if (rule == 0) {
path.setFillRule(Qt::OddEvenFill);
} else if (rule == 1) {
// In xps specs rule 1 means NonZero fill. I think it's equivalent to WindingFill but I'm not sure
path.setFillRule(Qt::WindingFill);
}
nextAbbPathToken(&token);
break;
case 'm': // Move
while (token.type == abtNumber) {
QPointF point = getPointFromString(&token, isRelative, currPos);
path.moveTo(point);
}
break;
case 'l': // Line
while (token.type == abtNumber) {
QPointF point = getPointFromString(&token, isRelative, currPos);
path.lineTo(point);
}
break;
case 'h': // Horizontal line
while (token.type == abtNumber) {
double x = token.number;
if (isRelative)
x += path.currentPosition().x();
path.lineTo(x, path.currentPosition().y());
nextAbbPathToken(&token);
}
break;
case 'v': // Vertical line
while (token.type == abtNumber) {
double y = token.number;
if (isRelative)
y += path.currentPosition().y();
path.lineTo(path.currentPosition().x(), y);
nextAbbPathToken(&token);
}
break;
case 'c': // Cubic bezier curve
while (token.type == abtNumber) {
QPointF firstControl = getPointFromString(&token, isRelative, currPos);
QPointF secondControl = getPointFromString(&token, isRelative, currPos);
QPointF endPoint = getPointFromString(&token, isRelative, currPos);
path.cubicTo(firstControl, secondControl, endPoint);
lastSecondControlPoint = secondControl;
}
break;
case 'q': // Quadratic bezier curve
while (token.type == abtNumber) {
QPointF point1 = getPointFromString(&token, isRelative, currPos);
QPointF point2 = getPointFromString(&token, isRelative, currPos);
path.quadTo(point1, point2);
}
break;
case 's': // Smooth cubic bezier curve
while (token.type == abtNumber) {
QPointF firstControl;
if ((lastCommand == 's') || (lastCommand == 'c')) {
firstControl = lastSecondControlPoint + (lastSecondControlPoint + path.currentPosition());
} else {
firstControl = path.currentPosition();
}
QPointF secondControl = getPointFromString(&token, isRelative, currPos);
QPointF endPoint = getPointFromString(&token, isRelative, currPos);
path.cubicTo(firstControl, secondControl, endPoint);
}
break;
case 'a': // Arc
// TODO Implement Arc drawing
while (token.type == abtNumber) {
/*QPointF rp =*/getPointFromString(&token, isRelative, currPos);
/*double r = token.number;*/
nextAbbPathToken(&token);
/*double fArc = token.number; */
nextAbbPathToken(&token);
/*double fSweep = token.number; */
nextAbbPathToken(&token);
/*QPointF point = */ getPointFromString(&token, isRelative, currPos);
}
break;
case 'z': // Close path
path.closeSubpath();
break;
}
lastCommand = command;
}
return path;
}
/**
Parse a "Matrix" attribute string
\param csv the comma separated list of values
\return the QTransform corresponding to the affine transform
given in the attribute
\see XPS specification 7.4.1
*/
static QTransform attsToMatrix(const QString &csv)
{
QStringList values = csv.split(QLatin1Char(','));
if (values.count() != 6) {
return QTransform(); // that is an identity matrix - no effect
}
return QTransform(values.at(0).toDouble(), values.at(1).toDouble(), values.at(2).toDouble(), values.at(3).toDouble(), values.at(4).toDouble(), values.at(5).toDouble());
}
/**
\return Brush with given color or brush specified by reference to resource
*/
static QBrush parseRscRefColorForBrush(const QString &data)
{
if (data[0] == QLatin1Char('{')) {
// TODO
qCWarning(OkularXpsDebug) << "Reference" << data;
return QBrush();
} else {
return QBrush(hexToRgba(data.toLatin1()));
}
}
/**
\return Pen with given color or Pen specified by reference to resource
*/
static QPen parseRscRefColorForPen(const QString &data)
{
if (data[0] == QLatin1Char('{')) {
// TODO
qCWarning(OkularXpsDebug) << "Reference" << data;
return QPen();
} else {
return QPen(hexToRgba(data.toLatin1()));
}
}
/**
\return Matrix specified by given data or by referenced dictionary
*/
static QTransform parseRscRefMatrix(const QString &data)
{
if (data[0] == QLatin1Char('{')) {
// TODO
qCWarning(OkularXpsDebug) << "Reference" << data;
return QTransform();
} else {
return attsToMatrix(data);
}
}
/**
\return Path specified by given data or by referenced dictionary
*/
static QPainterPath parseRscRefPath(const QString &data)
{
if (data[0] == QLatin1Char('{')) {
// TODO
qCWarning(OkularXpsDebug) << "Reference" << data;
return QPainterPath();
} else {
return parseAbbreviatedPathData(data);
}
}
/**
\return The path of the entry
*/
static QString entryPath(const QString &entry)
{
const QChar slash = QChar::fromLatin1('/');
const int index = entry.lastIndexOf(slash);
QString ret = entry.mid(0, index);
if (index > 0) {
ret.append(slash);
}
if (!ret.startsWith(slash))
ret.prepend(slash);
return ret;
}
/**
\return The path of the entry
*/
static QString entryPath(const KZipFileEntry *entry)
{
return entryPath(entry->path());
}
/**
\return The absolute path of the \p location, according to \p path if it's non-absolute
*/
static QString absolutePath(const QString &path, const QString &location)
{
QString retPath;
if (location.startsWith(QLatin1Char('/'))) {
// already absolute
retPath = location;
} else {
QUrl u = QUrl::fromLocalFile(path);
QUrl u2 = QUrl(location);
retPath = u.resolved(u2).toDisplayString(QUrl::PreferLocalFile);
}
// it seems paths & file names can also be percent-encoded
// (XPS won't ever finish surprising me)
if (retPath.contains(QLatin1Char('%'))) {
retPath = QUrl::fromPercentEncoding(retPath.toUtf8());
}
return retPath;
}
/**
Read the content of an archive entry in both the cases:
a) single file
+ foobar
b) directory
+ foobar/
+ [0].piece
+ [1].piece
+ ...
+ [x].last.piece
\see XPS specification 10.1.2
*/
static QByteArray readFileOrDirectoryParts(const KArchiveEntry *entry, QString *pathOfFile = nullptr)
{
QByteArray data;
if (entry->isDirectory()) {
const KArchiveDirectory *relDir = static_cast<const KArchiveDirectory *>(entry);
QStringList entries = relDir->entries();
std::sort(entries.begin(), entries.end());
for (const QString &entry : qAsConst(entries)) {
const KArchiveEntry *relSubEntry = relDir->entry(entry);
if (!relSubEntry->isFile())
continue;
const KZipFileEntry *relSubFile = static_cast<const KZipFileEntry *>(relSubEntry);
data.append(relSubFile->data());
}
} else {
const KZipFileEntry *relFile = static_cast<const KZipFileEntry *>(entry);
data.append(relFile->data());
if (pathOfFile) {
*pathOfFile = entryPath(relFile);
}
}
return data;
}
/**
Load the resource \p fileName from the specified \p archive using the case sensitivity \p cs
*/
static const KArchiveEntry *loadEntry(KZip *archive, const QString &fileName, Qt::CaseSensitivity cs)
{
// first attempt: loading the entry straight as requested
const KArchiveEntry *entry = archive->directory()->entry(fileName);
// in case sensitive mode, or if we actually found something, return what we found
if (cs == Qt::CaseSensitive || entry) {
return entry;
}
QString path;
QString entryName;
const int index = fileName.lastIndexOf(QChar::fromLatin1('/'));
if (index > 0) {
path = fileName.left(index);
entryName = fileName.mid(index + 1);
} else {
path = QLatin1Char('/');
entryName = fileName;
}
const KArchiveEntry *newEntry = archive->directory()->entry(path);
if (newEntry->isDirectory()) {
const KArchiveDirectory *relDir = static_cast<const KArchiveDirectory *>(newEntry);
QStringList relEntries = relDir->entries();
std::sort(relEntries.begin(), relEntries.end());
for (const QString &relEntry : qAsConst(relEntries)) {
if (relEntry.compare(entryName, Qt::CaseInsensitive) == 0) {
return relDir->entry(relEntry);
}
}
}
return nullptr;
}
static const KZipFileEntry *loadFile(KZip *archive, const QString &fileName, Qt::CaseSensitivity cs)
{
const KArchiveEntry *entry = loadEntry(archive, fileName, cs);
return entry->isFile() ? static_cast<const KZipFileEntry *>(entry) : nullptr;
}
/**
\return The name of a resource from the \p fileName
*/
static QString resourceName(const QString &fileName)
{
QString resource = fileName;
const int slashPos = fileName.lastIndexOf(QLatin1Char('/'));
const int dotPos = fileName.lastIndexOf(QLatin1Char('.'));
if (slashPos > -1) {
if (dotPos > -1 && dotPos > slashPos) {
resource = fileName.mid(slashPos + 1, dotPos - slashPos - 1);
} else {
resource = fileName.mid(slashPos + 1);
}
}
return resource;
}
static QColor interpolatedColor(const QColor &c1, const QColor &c2)
{
QColor res;
res.setAlpha((c1.alpha() + c2.alpha()) / 2);
res.setRed((c1.red() + c2.red()) / 2);
res.setGreen((c1.green() + c2.green()) / 2);
res.setBlue((c1.blue() + c2.blue()) / 2);
return res;
}
static bool xpsGradientLessThan(const XpsGradient &g1, const XpsGradient &g2)
{
return qFuzzyCompare(g1.offset, g2.offset) ? g1.color.name() < g2.color.name() : g1.offset < g2.offset;
}
static int xpsGradientWithOffset(const QList<XpsGradient> &gradients, double offset)
{
int i = 0;
for (const XpsGradient &grad : gradients) {
if (grad.offset == offset) {
return i;
}
++i;
}
return -1;
}
/**
Preprocess a list of gradients.
\see XPS specification 11.3.1.1
*/
static void preprocessXpsGradients(QList<XpsGradient> &gradients)
{
if (gradients.isEmpty())
return;
// sort the gradients (case 1.)
std::stable_sort(gradients.begin(), gradients.end(), xpsGradientLessThan);
// no gradient with stop 0.0 (case 2.)
if (xpsGradientWithOffset(gradients, 0.0) == -1) {
int firstGreaterThanZero = 0;
while (firstGreaterThanZero < gradients.count() && gradients.at(firstGreaterThanZero).offset < 0.0)
++firstGreaterThanZero;
// case 2.a: no gradients with stop less than 0.0
if (firstGreaterThanZero == 0) {
gradients.prepend(XpsGradient(0.0, gradients.first().color));
}
// case 2.b: some gradients with stop more than 0.0
else if (firstGreaterThanZero != gradients.count()) {
QColor col1 = gradients.at(firstGreaterThanZero - 1).color;
QColor col2 = gradients.at(firstGreaterThanZero).color;
for (int i = 0; i < firstGreaterThanZero; ++i) {
gradients.removeFirst();
}
gradients.prepend(XpsGradient(0.0, interpolatedColor(col1, col2)));
}
// case 2.c: no gradients with stop more than 0.0
else {
XpsGradient newGrad(0.0, gradients.last().color);
gradients.clear();
gradients.append(newGrad);
}
}
if (gradients.isEmpty())
return;
// no gradient with stop 1.0 (case 3.)
if (xpsGradientWithOffset(gradients, 1.0) == -1) {
int firstLessThanOne = gradients.count() - 1;
while (firstLessThanOne >= 0 && gradients.at(firstLessThanOne).offset > 1.0)
--firstLessThanOne;
// case 2.a: no gradients with stop greater than 1.0
if (firstLessThanOne == gradients.count() - 1) {
gradients.append(XpsGradient(1.0, gradients.last().color));
}
// case 2.b: some gradients with stop more than 1.0
else if (firstLessThanOne != -1) {
QColor col1 = gradients.at(firstLessThanOne).color;
QColor col2 = gradients.at(firstLessThanOne + 1).color;
for (int i = firstLessThanOne + 1; i < gradients.count(); ++i) {
gradients.removeLast();
}
gradients.append(XpsGradient(1.0, interpolatedColor(col1, col2)));
}
// case 2.c: no gradients with stop less than 1.0
else {
XpsGradient newGrad(1.0, gradients.first().color);
gradients.clear();
gradients.append(newGrad);
}
}
}
static void addXpsGradientsToQGradient(const QList<XpsGradient> &gradients, QGradient *qgrad)
{
for (const XpsGradient &grad : gradients) {
qgrad->setColorAt(grad.offset, grad.color);
}
}
static void applySpreadStyleToQGradient(const QString &style, QGradient *qgrad)
{
if (style.isEmpty())
return;
if (style == QLatin1String("Pad")) {
qgrad->setSpread(QGradient::PadSpread);
} else if (style == QLatin1String("Reflect")) {
qgrad->setSpread(QGradient::ReflectSpread);
} else if (style == QLatin1String("Repeat")) {
qgrad->setSpread(QGradient::RepeatSpread);
}
}
/**
Read an UnicodeString
\param raw the raw value of UnicodeString
\see XPS specification 5.1.4
*/
static QString unicodeString(const QString &raw)
{
QString ret;
if (raw.startsWith(QLatin1String("{}"))) {
ret = raw.mid(2);
} else {
ret = raw;
}
return ret;
}
XpsHandler::XpsHandler(XpsPage *page)
: m_page(page)
{
m_painter = nullptr;
}
XpsHandler::~XpsHandler()
{
}
bool XpsHandler::startDocument()
{
qCWarning(OkularXpsDebug) << "start document" << m_page->m_fileName;
XpsRenderNode node;
node.name = QStringLiteral("document");
m_nodes.push(node);
return true;
}
bool XpsHandler::startElement(const QString &nameSpace, const QString &localName, const QString &qname, const QXmlAttributes &atts)
{
Q_UNUSED(nameSpace)
Q_UNUSED(qname)
XpsRenderNode node;
node.name = localName;
node.attributes = atts;
processStartElement(node);
m_nodes.push(node);
return true;
}
bool XpsHandler::endElement(const QString &nameSpace, const QString &localName, const QString &qname)
{
Q_UNUSED(nameSpace)
Q_UNUSED(qname)
XpsRenderNode node = m_nodes.pop();
if (node.name != localName) {
qCWarning(OkularXpsDebug) << "Name doesn't match";
}
processEndElement(node);
node.children.clear();
m_nodes.top().children.append(node);
return true;
}
void XpsHandler::processGlyph(XpsRenderNode &node)
{
// TODO Currently ignored attributes: CaretStops, DeviceFontName, IsSideways, OpacityMask, Name, FixedPage.NavigateURI, xml:lang, x:key
// TODO Indices is only partially implemented
// TODO Currently ignored child elements: Clip, OpacityMask
// Handled separately: RenderTransform
QString att;
m_painter->save();
// Get font (doesn't work well because qt doesn't allow to load font from file)
// This works despite the fact that font size isn't specified in points as required by qt. It's because I set point size to be equal to drawing unit.
float fontSize = node.attributes.value(QStringLiteral("FontRenderingEmSize")).toFloat();
// qCWarning(OkularXpsDebug) << "Font Rendering EmSize:" << fontSize;
// a value of 0.0 means the text is not visible (see XPS specs, chapter 12, "Glyphs")
if (fontSize < 0.1) {
m_painter->restore();
return;
}
const QString absoluteFileName = absolutePath(entryPath(m_page->fileName()), node.attributes.value(QStringLiteral("FontUri")));
QFont font = m_page->m_file->getFontByName(absoluteFileName, fontSize);
att = node.attributes.value(QStringLiteral("StyleSimulations"));
if (!att.isEmpty()) {
if (att == QLatin1String("ItalicSimulation")) {
font.setItalic(true);
} else if (att == QLatin1String("BoldSimulation")) {
font.setBold(true);
} else if (att == QLatin1String("BoldItalicSimulation")) {
font.setItalic(true);
font.setBold(true);
}
}
m_painter->setFont(font);
// Origin
QPointF origin(node.attributes.value(QStringLiteral("OriginX")).toDouble(), node.attributes.value(QStringLiteral("OriginY")).toDouble());
// Fill
QBrush brush;
att = node.attributes.value(QStringLiteral("Fill"));
if (att.isEmpty()) {
QVariant data = node.getChildData(QStringLiteral("Glyphs.Fill"));
if (data.canConvert<QBrush>()) {
brush = data.value<QBrush>();
} else {
// no "Fill" attribute and no "Glyphs.Fill" child, so show nothing
// (see XPS specs, 5.10)
m_painter->restore();
return;
}
} else {
brush = parseRscRefColorForBrush(att);
if (brush.style() > Qt::NoBrush && brush.style() < Qt::LinearGradientPattern && brush.color().alpha() == 0) {
m_painter->restore();
return;
}
}
m_painter->setBrush(brush);
m_painter->setPen(QPen(brush, 0));
// Opacity
att = node.attributes.value(QStringLiteral("Opacity"));
if (!att.isEmpty()) {
bool ok = true;
double value = att.toDouble(&ok);
if (ok && value >= 0.1) {
m_painter->setOpacity(value);
} else {
m_painter->restore();
return;
}
}
// RenderTransform
att = node.attributes.value(QStringLiteral("RenderTransform"));
if (!att.isEmpty()) {
m_painter->setWorldTransform(parseRscRefMatrix(att), true);
}
// Clip
att = node.attributes.value(QStringLiteral("Clip"));
if (!att.isEmpty()) {
QPainterPath clipPath = parseRscRefPath(att);
if (!clipPath.isEmpty()) {
m_painter->setClipPath(clipPath);
}
}
// BiDiLevel - default Left-to-Right
m_painter->setLayoutDirection(Qt::LeftToRight);
att = node.attributes.value(QStringLiteral("BiDiLevel"));
if (!att.isEmpty()) {
if ((att.toInt() % 2) == 1) {
// odd BiDiLevel, so Right-to-Left
m_painter->setLayoutDirection(Qt::RightToLeft);
}
}
// Indices - partial handling only
att = node.attributes.value(QStringLiteral("Indices"));
QList<qreal> advanceWidths;
if (!att.isEmpty()) {
QStringList indicesElements = att.split(QLatin1Char(';'));
for (int i = 0; i < indicesElements.size(); ++i) {
if (indicesElements.at(i).contains(QStringLiteral(","))) {
QStringList parts = indicesElements.at(i).split(QLatin1Char(','));
if (parts.size() == 2) {
// regular advance case, no offsets
advanceWidths.append(parts.at(1).toDouble() * fontSize / 100.0);
} else if (parts.size() == 3) {
// regular advance case, with uOffset
qreal AdvanceWidth = parts.at(1).toDouble() * fontSize / 100.0;
qreal uOffset = parts.at(2).toDouble() / 100.0;
advanceWidths.append(AdvanceWidth + uOffset);
} else {
// has vertical offset, but don't know how to handle that yet
qCWarning(OkularXpsDebug) << "Unhandled Indices element: " << indicesElements.at(i);
advanceWidths.append(-1.0);
}
} else {
// no special advance case
advanceWidths.append(-1.0);
}
}
}
// UnicodeString
QString stringToDraw(unicodeString(node.attributes.value(QStringLiteral("UnicodeString"))));
QPointF originAdvance(0, 0);
QFontMetrics metrics = m_painter->fontMetrics();
for (int i = 0; i < stringToDraw.size(); ++i) {
QChar thisChar = stringToDraw.at(i);
m_painter->drawText(origin + originAdvance, QString(thisChar));
const qreal advanceWidth = advanceWidths.value(i, qreal(-1.0));
if (advanceWidth > 0.0) {
originAdvance.rx() += advanceWidth;
} else {
originAdvance.rx() += metrics.horizontalAdvance(thisChar);
}
}
// qCWarning(OkularXpsDebug) << "Glyphs: " << atts.value("Fill") << ", " << atts.value("FontUri");
// qCWarning(OkularXpsDebug) << " Origin: " << atts.value("OriginX") << "," << atts.value("OriginY");
// qCWarning(OkularXpsDebug) << " Unicode: " << atts.value("UnicodeString");
m_painter->restore();
}
void XpsHandler::processFill(XpsRenderNode &node)
{
// TODO Ignored child elements: VirtualBrush
if (node.children.size() != 1) {
qCWarning(OkularXpsDebug) << "Fill element should have exactly one child";
} else {
node.data = node.children[0].data;
}
}
void XpsHandler::processStroke(XpsRenderNode &node)
{
// TODO Ignored child elements: VirtualBrush
if (node.children.size() != 1) {
qCWarning(OkularXpsDebug) << "Stroke element should have exactly one child";
} else {
node.data = node.children[0].data;
}
}
void XpsHandler::processImageBrush(XpsRenderNode &node)
{
// TODO Ignored attributes: Opacity, x:key, TileMode, ViewBoxUnits, ViewPortUnits
// TODO Check whether transformation works for non standard situations (viewbox different that whole image, Transform different that simple move & scale, Viewport different than [0, 0, 1, 1]
QString att;
QBrush brush;
QRectF viewport = stringToRectF(node.attributes.value(QStringLiteral("Viewport")));
QRectF viewbox = stringToRectF(node.attributes.value(QStringLiteral("Viewbox")));
QImage image = m_page->loadImageFromFile(node.attributes.value(QStringLiteral("ImageSource")));
// Matrix which can transform [0, 0, 1, 1] rectangle to given viewbox
QTransform viewboxMatrix = QTransform(viewbox.width() * image.physicalDpiX() / 96, 0, 0, viewbox.height() * image.physicalDpiY() / 96, viewbox.x(), viewbox.y());
// Matrix which can transform [0, 0, 1, 1] rectangle to given viewport
// TODO Take ViewPort into account
QTransform viewportMatrix;
att = node.attributes.value(QStringLiteral("Transform"));
if (att.isEmpty()) {
QVariant data = node.getChildData(QStringLiteral("ImageBrush.Transform"));
if (data.canConvert<QTransform>()) {
viewportMatrix = data.value<QTransform>();
} else {
viewportMatrix = QTransform();
}
} else {
viewportMatrix = parseRscRefMatrix(att);
}
viewportMatrix = viewportMatrix * QTransform(viewport.width(), 0, 0, viewport.height(), viewport.x(), viewport.y());
brush = QBrush(image);
brush.setTransform(viewboxMatrix.inverted() * viewportMatrix);
node.data = QVariant::fromValue(brush);
}
void XpsHandler::processPath(XpsRenderNode &node)
{
// TODO Ignored attributes: Clip, OpacityMask, StrokeEndLineCap, StorkeStartLineCap, Name, FixedPage.NavigateURI, xml:lang, x:key, AutomationProperties.Name, AutomationProperties.HelpText, SnapsToDevicePixels
// TODO Ignored child elements: RenderTransform, Clip, OpacityMask
// Handled separately: RenderTransform
m_painter->save();
QString att;
QVariant data;
// Get path
XpsPathGeometry *pathdata = node.getChildData(QStringLiteral("Path.Data")).value<XpsPathGeometry *>();
att = node.attributes.value(QStringLiteral("Data"));
if (!att.isEmpty()) {
QPainterPath path = parseRscRefPath(att);
delete pathdata;
pathdata = new XpsPathGeometry();
pathdata->paths.append(new XpsPathFigure(path, true));
}
if (!pathdata) {
// nothing to draw
m_painter->restore();
return;
}
// Set Fill
att = node.attributes.value(QStringLiteral("Fill"));
QBrush brush;
if (!att.isEmpty()) {
brush = parseRscRefColorForBrush(att);
} else {
data = node.getChildData(QStringLiteral("Path.Fill"));
if (data.canConvert<QBrush>()) {
brush = data.value<QBrush>();
}
}
m_painter->setBrush(brush);
// Stroke (pen)
att = node.attributes.value(QStringLiteral("Stroke"));
QPen pen(Qt::transparent);
if (!att.isEmpty()) {
pen = parseRscRefColorForPen(att);
} else {
data = node.getChildData(QStringLiteral("Path.Stroke"));
if (data.canConvert<QBrush>()) {
pen.setBrush(data.value<QBrush>());
}
}
att = node.attributes.value(QStringLiteral("StrokeThickness"));
if (!att.isEmpty()) {
bool ok = false;
int thickness = att.toInt(&ok);
if (ok)
pen.setWidth(thickness);
}
att = node.attributes.value(QStringLiteral("StrokeDashArray"));
if (!att.isEmpty()) {
const QStringList pieces = att.split(QLatin1Char(' '), QString::SkipEmptyParts);
QVector<qreal> dashPattern(pieces.count());
bool ok = false;
for (int i = 0; i < pieces.count(); ++i) {
qreal value = pieces.at(i).toInt(&ok);
if (ok) {
dashPattern[i] = value;
} else {
break;
}
}
if (ok) {
pen.setDashPattern(dashPattern);
}
}
att = node.attributes.value(QStringLiteral("StrokeDashOffset"));
if (!att.isEmpty()) {
bool ok = false;
int offset = att.toInt(&ok);
if (ok)
pen.setDashOffset(offset);
}
att = node.attributes.value(QStringLiteral("StrokeDashCap"));
if (!att.isEmpty()) {
Qt::PenCapStyle cap = Qt::FlatCap;
if (att == QLatin1String("Flat")) {
cap = Qt::FlatCap;
} else if (att == QLatin1String("Round")) {
cap = Qt::RoundCap;
} else if (att == QLatin1String("Square")) {
cap = Qt::SquareCap;
}
// ### missing "Triangle"
pen.setCapStyle(cap);
}
att = node.attributes.value(QStringLiteral("StrokeLineJoin"));
if (!att.isEmpty()) {
Qt::PenJoinStyle joinStyle = Qt::MiterJoin;
if (att == QLatin1String("Miter")) {
joinStyle = Qt::MiterJoin;
} else if (att == QLatin1String("Bevel")) {
joinStyle = Qt::BevelJoin;
} else if (att == QLatin1String("Round")) {
joinStyle = Qt::RoundJoin;
}
pen.setJoinStyle(joinStyle);
}
att = node.attributes.value(QStringLiteral("StrokeMiterLimit"));
if (!att.isEmpty()) {
bool ok = false;
double limit = att.toDouble(&ok);
if (ok) {
// we have to divide it by two, as XPS consider half of the stroke width,
// while Qt the whole of it
pen.setMiterLimit(limit / 2);
}
}
m_painter->setPen(pen);
// Opacity
att = node.attributes.value(QStringLiteral("Opacity"));
if (!att.isEmpty()) {
m_painter->setOpacity(att.toDouble());
}
// RenderTransform
att = node.attributes.value(QStringLiteral("RenderTransform"));
if (!att.isEmpty()) {
m_painter->setWorldTransform(parseRscRefMatrix(att), true);
}
if (!pathdata->transform.isIdentity()) {
m_painter->setWorldTransform(pathdata->transform, true);
}
for (const XpsPathFigure *figure : qAsConst(pathdata->paths)) {
m_painter->setBrush(figure->isFilled ? brush : QBrush());
m_painter->drawPath(figure->path);
}
delete pathdata;
m_painter->restore();
}
void XpsHandler::processPathData(XpsRenderNode &node)
{
if (node.children.size() != 1) {
qCWarning(OkularXpsDebug) << "Path.Data element should have exactly one child";
} else {
node.data = node.children[0].data;
}
}
void XpsHandler::processPathGeometry(XpsRenderNode &node)
{
XpsPathGeometry *geom = new XpsPathGeometry();
for (const XpsRenderNode &child : qAsConst(node.children)) {
if (child.data.canConvert<XpsPathFigure *>()) {
XpsPathFigure *figure = child.data.value<XpsPathFigure *>();
geom->paths.append(figure);
}
}
QString att;
att = node.attributes.value(QStringLiteral("Figures"));
if (!att.isEmpty()) {
QPainterPath path = parseRscRefPath(att);
qDeleteAll(geom->paths);
geom->paths.clear();
geom->paths.append(new XpsPathFigure(path, true));
}
att = node.attributes.value(QStringLiteral("FillRule"));
if (!att.isEmpty()) {
geom->fillRule = fillRuleFromString(att);
}
// Transform
att = node.attributes.value(QStringLiteral("Transform"));
if (!att.isEmpty()) {
geom->transform = parseRscRefMatrix(att);
}
if (!geom->paths.isEmpty()) {
node.data = QVariant::fromValue(geom);
} else {
delete geom;
}
}
void XpsHandler::processPathFigure(XpsRenderNode &node)
{
// TODO Ignored child elements: ArcSegment
QString att;
QPainterPath path;
att = node.attributes.value(QStringLiteral("StartPoint"));
if (!att.isEmpty()) {
QPointF point = getPointFromString(att);
path.moveTo(point);
} else {
return;
}
for (const XpsRenderNode &child : qAsConst(node.children)) {
bool isStroked = true;
att = node.attributes.value(QStringLiteral("IsStroked"));
if (!att.isEmpty()) {
isStroked = att == QLatin1String("true");
}
if (!isStroked) {
continue;
}
// PolyLineSegment
if (child.name == QLatin1String("PolyLineSegment")) {
att = child.attributes.value(QStringLiteral("Points"));
if (!att.isEmpty()) {
const QStringList points = att.split(QLatin1Char(' '), QString::SkipEmptyParts);
for (const QString &p : points) {
QPointF point = getPointFromString(p);
path.lineTo(point);
}
}
}
// PolyBezierSegment
else if (child.name == QLatin1String("PolyBezierSegment")) {
att = child.attributes.value(QStringLiteral("Points"));
if (!att.isEmpty()) {
const QStringList points = att.split(QLatin1Char(' '), QString::SkipEmptyParts);
if (points.count() % 3 == 0) {
for (int i = 0; i < points.count();) {
QPointF firstControl = getPointFromString(points.at(i++));
QPointF secondControl = getPointFromString(points.at(i++));
QPointF endPoint = getPointFromString(points.at(i++));
path.cubicTo(firstControl, secondControl, endPoint);
}
}
}
}
// PolyQuadraticBezierSegment
else if (child.name == QLatin1String("PolyQuadraticBezierSegment")) {
att = child.attributes.value(QStringLiteral("Points"));
if (!att.isEmpty()) {
const QStringList points = att.split(QLatin1Char(' '), QString::SkipEmptyParts);
if (points.count() % 2 == 0) {
for (int i = 0; i < points.count();) {
QPointF point1 = getPointFromString(points.at(i++));
QPointF point2 = getPointFromString(points.at(i++));
path.quadTo(point1, point2);
}
}
}
}
}
bool closePath = false;
att = node.attributes.value(QStringLiteral("IsClosed"));
if (!att.isEmpty()) {
closePath = att == QLatin1String("true");
}
if (closePath) {
path.closeSubpath();
}
bool isFilled = true;
att = node.attributes.value(QStringLiteral("IsFilled"));
if (!att.isEmpty()) {
isFilled = att == QLatin1String("true");
}
if (!path.isEmpty()) {
node.data = QVariant::fromValue(new XpsPathFigure(path, isFilled));
}
}
void XpsHandler::processStartElement(XpsRenderNode &node)
{
if (node.name == QLatin1String("Canvas")) {
m_painter->save();
QString att = node.attributes.value(QStringLiteral("RenderTransform"));
if (!att.isEmpty()) {
m_painter->setWorldTransform(parseRscRefMatrix(att), true);
}
att = node.attributes.value(QStringLiteral("Opacity"));
if (!att.isEmpty()) {
double value = att.toDouble();
if (value > 0.0 && value <= 1.0) {
m_painter->setOpacity(m_painter->opacity() * value);
} else {
// setting manually to 0 is necessary to "disable"
// all the stuff inside
m_painter->setOpacity(0.0);
}
}
}
}
void XpsHandler::processEndElement(XpsRenderNode &node)
{
if (node.name == QLatin1String("Glyphs")) {
processGlyph(node);
} else if (node.name == QLatin1String("Path")) {
processPath(node);
} else if (node.name == QLatin1String("MatrixTransform")) {
// TODO Ignoring x:key
node.data = QVariant::fromValue(QTransform(attsToMatrix(node.attributes.value(QStringLiteral("Matrix")))));
} else if ((node.name == QLatin1String("Canvas.RenderTransform")) || (node.name == QLatin1String("Glyphs.RenderTransform")) || (node.name == QLatin1String("Path.RenderTransform"))) {
QVariant data = node.getRequiredChildData(QStringLiteral("MatrixTransform"));
if (data.canConvert<QTransform>()) {
m_painter->setWorldTransform(data.value<QTransform>(), true);
}
} else if (node.name == QLatin1String("Canvas")) {
m_painter->restore();
} else if ((node.name == QLatin1String("Path.Fill")) || (node.name == QLatin1String("Glyphs.Fill"))) {
processFill(node);
} else if (node.name == QLatin1String("Path.Stroke")) {
processStroke(node);
} else if (node.name == QLatin1String("SolidColorBrush")) {
// TODO Ignoring opacity, x:key
node.data = QVariant::fromValue(QBrush(QColor(hexToRgba(node.attributes.value(QStringLiteral("Color")).toLatin1()))));
} else if (node.name == QLatin1String("ImageBrush")) {
processImageBrush(node);
} else if (node.name == QLatin1String("ImageBrush.Transform")) {
node.data = node.getRequiredChildData(QStringLiteral("MatrixTransform"));
} else if (node.name == QLatin1String("LinearGradientBrush")) {
const XpsRenderNode *gradients = node.findChild(QStringLiteral("LinearGradientBrush.GradientStops"));
if (gradients && gradients->data.canConvert<QGradient *>()) {
QPointF start = getPointFromString(node.attributes.value(QStringLiteral("StartPoint")));
QPointF end = getPointFromString(node.attributes.value(QStringLiteral("EndPoint")));
QLinearGradient *qgrad = static_cast<QLinearGradient *>(gradients->data.value<QGradient *>());
qgrad->setStart(start);
qgrad->setFinalStop(end);
applySpreadStyleToQGradient(node.attributes.value(QStringLiteral("SpreadMethod")), qgrad);
node.data = QVariant::fromValue(QBrush(*qgrad));
delete qgrad;
}
} else if (node.name == QLatin1String("RadialGradientBrush")) {
const XpsRenderNode *gradients = node.findChild(QStringLiteral("RadialGradientBrush.GradientStops"));
if (gradients && gradients->data.canConvert<QGradient *>()) {
QPointF center = getPointFromString(node.attributes.value(QStringLiteral("Center")));
QPointF origin = getPointFromString(node.attributes.value(QStringLiteral("GradientOrigin")));
double radiusX = node.attributes.value(QStringLiteral("RadiusX")).toDouble();
double radiusY = node.attributes.value(QStringLiteral("RadiusY")).toDouble();
QRadialGradient *qgrad = static_cast<QRadialGradient *>(gradients->data.value<QGradient *>());
qgrad->setCenter(center);
qgrad->setFocalPoint(origin);
// TODO what in case of different radii?
qgrad->setRadius(qMin(radiusX, radiusY));
applySpreadStyleToQGradient(node.attributes.value(QStringLiteral("SpreadMethod")), qgrad);
node.data = QVariant::fromValue(QBrush(*qgrad));
delete qgrad;
}
} else if (node.name == QLatin1String("LinearGradientBrush.GradientStops")) {
QList<XpsGradient> gradients;
for (const XpsRenderNode &child : qAsConst(node.children)) {
double offset = child.attributes.value(QStringLiteral("Offset")).toDouble();
QColor color = hexToRgba(child.attributes.value(QStringLiteral("Color")).toLatin1());
gradients.append(XpsGradient(offset, color));
}
preprocessXpsGradients(gradients);
if (!gradients.isEmpty()) {
QLinearGradient *qgrad = new QLinearGradient();
addXpsGradientsToQGradient(gradients, qgrad);
node.data = QVariant::fromValue<QGradient *>(qgrad);
}
} else if (node.name == QLatin1String("RadialGradientBrush.GradientStops")) {
QList<XpsGradient> gradients;
for (const XpsRenderNode &child : qAsConst(node.children)) {
double offset = child.attributes.value(QStringLiteral("Offset")).toDouble();
QColor color = hexToRgba(child.attributes.value(QStringLiteral("Color")).toLatin1());
gradients.append(XpsGradient(offset, color));
}
preprocessXpsGradients(gradients);
if (!gradients.isEmpty()) {
QRadialGradient *qgrad = new QRadialGradient();
addXpsGradientsToQGradient(gradients, qgrad);
node.data = QVariant::fromValue<QGradient *>(qgrad);
}
} else if (node.name == QLatin1String("PathFigure")) {
processPathFigure(node);
} else if (node.name == QLatin1String("PathGeometry")) {
processPathGeometry(node);
} else if (node.name == QLatin1String("Path.Data")) {
processPathData(node);
} else {
// qCWarning(OkularXpsDebug) << "Unknown element: " << node->name;
}
}
XpsPage::XpsPage(XpsFile *file, const QString &fileName)
: m_file(file)
, m_fileName(fileName)
, m_pageIsRendered(false)
{
m_pageImage = nullptr;
// qCWarning(OkularXpsDebug) << "page file name: " << fileName;
const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(fileName));
QXmlStreamReader xml;
xml.addData(readFileOrDirectoryParts(pageFile));
while (!xml.atEnd()) {
xml.readNext();
if (xml.isStartElement() && (xml.name() == QStringLiteral("FixedPage"))) {
QXmlStreamAttributes attributes = xml.attributes();
m_pageSize.setWidth(attributes.value(QStringLiteral("Width")).toString().toDouble());
m_pageSize.setHeight(attributes.value(QStringLiteral("Height")).toString().toDouble());
break;
}
}
if (xml.error()) {
qCWarning(OkularXpsDebug) << "Could not parse XPS page:" << xml.errorString();
}
}
XpsPage::~XpsPage()
{
delete m_pageImage;
}
bool XpsPage::renderToImage(QImage *p)
{
if ((m_pageImage == nullptr) || (m_pageImage->size() != p->size())) {
delete m_pageImage;
m_pageImage = new QImage(p->size(), QImage::Format_ARGB32);
// Set one point = one drawing unit. Useful for fonts, because xps specifies font size using drawing units, not points as usual
m_pageImage->setDotsPerMeterX(2835);
m_pageImage->setDotsPerMeterY(2835);
m_pageIsRendered = false;
}
if (!m_pageIsRendered) {
m_pageImage->fill(qRgba(255, 255, 255, 255));
QPainter painter(m_pageImage);
renderToPainter(&painter);
m_pageIsRendered = true;
}
*p = *m_pageImage;
return true;
}
bool XpsPage::renderToPainter(QPainter *painter)
{
XpsHandler handler(this);
handler.m_painter = painter;
handler.m_painter->setWorldTransform(QTransform().scale((qreal)painter->device()->width() / size().width(), (qreal)painter->device()->height() / size().height()));
QXmlSimpleReader parser;
parser.setContentHandler(&handler);
parser.setErrorHandler(&handler);
const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(m_fileName));
QByteArray data = readFileOrDirectoryParts(pageFile);
QBuffer buffer(&data);
QXmlInputSource source(&buffer);
bool ok = parser.parse(source);
qCWarning(OkularXpsDebug) << "Parse result: " << ok;
return true;
}
QSizeF XpsPage::size() const
{
return m_pageSize;
}
QFont XpsFile::getFontByName(const QString &absoluteFileName, float size)
{
// qCWarning(OkularXpsDebug) << "trying to get font: " << fileName << ", size: " << size;
int index = m_fontCache.value(absoluteFileName, -1);
if (index == -1) {
index = loadFontByName(absoluteFileName);
m_fontCache[absoluteFileName] = index;
}
if (index == -1) {
qCWarning(OkularXpsDebug) << "Requesting unknown font:" << absoluteFileName;
return QFont();
}
const QStringList fontFamilies = m_fontDatabase.applicationFontFamilies(index);
if (fontFamilies.isEmpty()) {
qCWarning(OkularXpsDebug) << "The unexpected has happened. No font family for a known font:" << absoluteFileName << index;
return QFont();
}
const QString fontFamily = fontFamilies[0];
const QStringList fontStyles = m_fontDatabase.styles(fontFamily);
if (fontStyles.isEmpty()) {
qCWarning(OkularXpsDebug) << "The unexpected has happened. No font style for a known font family:" << absoluteFileName << index << fontFamily;
return QFont();
}
const QString fontStyle = fontStyles[0];
return m_fontDatabase.font(fontFamily, fontStyle, qRound(size));
}
int XpsFile::loadFontByName(const QString &absoluteFileName)
{
// qCWarning(OkularXpsDebug) << "font file name: " << absoluteFileName;
const KArchiveEntry *fontFile = loadEntry(m_xpsArchive, absoluteFileName, Qt::CaseInsensitive);
if (!fontFile) {
return -1;
}
QByteArray fontData = readFileOrDirectoryParts(fontFile); // once per file, according to the docs
int result = m_fontDatabase.addApplicationFontFromData(fontData);
if (-1 == result) {
// Try to deobfuscate font
// TODO Use deobfuscation depending on font content type, don't do it always when standard loading fails
const QString baseName = resourceName(absoluteFileName);
unsigned short guid[16];
if (!parseGUID(baseName, guid)) {
qCWarning(OkularXpsDebug) << "File to load font - file name isn't a GUID";
} else {
if (fontData.length() < 32) {
qCWarning(OkularXpsDebug) << "Font file is too small";
} else {
// Obfuscation - xor bytes in font binary with bytes from guid (font's filename)
const static int mapping[] = {15, 14, 13, 12, 11, 10, 9, 8, 6, 7, 4, 5, 0, 1, 2, 3};
for (int i = 0; i < 16; i++) {
fontData[i] = fontData[i] ^ guid[mapping[i]];
fontData[i + 16] = fontData[i + 16] ^ guid[mapping[i]];
}
result = m_fontDatabase.addApplicationFontFromData(fontData);
}
}
}
// qCWarning(OkularXpsDebug) << "Loaded font: " << m_fontDatabase.applicationFontFamilies( result );
return result; // a font ID
}
KZip *XpsFile::xpsArchive()
{
return m_xpsArchive;
}
QImage XpsPage::loadImageFromFile(const QString &fileName)
{
// qCWarning(OkularXpsDebug) << "image file name: " << fileName;
if (fileName.at(0) == QLatin1Char('{')) {
// for example: '{ColorConvertedBitmap /Resources/bla.wdp /Resources/foobar.icc}'
// TODO: properly read a ColorConvertedBitmap
return QImage();
}
QString absoluteFileName = absolutePath(entryPath(m_fileName), fileName);
const KZipFileEntry *imageFile = loadFile(m_file->xpsArchive(), absoluteFileName, Qt::CaseInsensitive);
if (!imageFile) {
// image not found
return QImage();
}
/* WORKAROUND:
XPS standard requires to use 96dpi for images which doesn't have dpi specified (in file). When Qt loads such an image,
it sets its dpi to qt_defaultDpi and doesn't allow to find out that it happend.
To workaround this I used this procedure: load image, set its dpi to 96, load image again. When dpi isn't set in file,
dpi set by me stays unchanged.
Trolltech task ID: 159527.
*/
QImage image;
QByteArray data = imageFile->data();
QBuffer buffer(&data);
buffer.open(QBuffer::ReadOnly);
QImageReader reader(&buffer);
image = reader.read();
image.setDotsPerMeterX(qRound(96 / 0.0254));
image.setDotsPerMeterY(qRound(96 / 0.0254));
buffer.seek(0);
reader.setDevice(&buffer);
reader.read(&image);
return image;
}
Okular::TextPage *XpsPage::textPage()
{
// qCWarning(OkularXpsDebug) << "Parsing XpsPage, text extraction";
Okular::TextPage *textPage = new Okular::TextPage();
const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(m_fileName));
QXmlStreamReader xml;
xml.addData(readFileOrDirectoryParts(pageFile));
QTransform matrix = QTransform();
QStack<QTransform> matrices;
matrices.push(QTransform());
bool useMatrix = false;
QXmlStreamAttributes glyphsAtts;
while (!xml.atEnd()) {
xml.readNext();
if (xml.isStartElement()) {
if (xml.name() == QStringLiteral("Canvas")) {
matrices.push(matrix);
QString att = xml.attributes().value(QStringLiteral("RenderTransform")).toString();
if (!att.isEmpty()) {
matrix = parseRscRefMatrix(att) * matrix;
}
} else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) {
useMatrix = true;
} else if (xml.name() == QStringLiteral("MatrixTransform")) {
if (useMatrix) {
matrix = attsToMatrix(xml.attributes().value(QStringLiteral("Matrix")).toString()) * matrix;
}
} else if (xml.name() == QStringLiteral("Glyphs")) {
matrices.push(matrix);
glyphsAtts = xml.attributes();
} else if ((xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) ||
(xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) ||
(xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment"))) {
// those are only graphical - no use in text handling
} else if ((xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources"))) {
// not useful for text extraction
} else {
qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction start: " << xml.name().toString();
}
} else if (xml.isEndElement()) {
if (xml.name() == QStringLiteral("Canvas")) {
matrix = matrices.pop();
} else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) {
useMatrix = false;
} else if (xml.name() == QStringLiteral("MatrixTransform")) {
// not clear if we need to do anything here yet.
} else if (xml.name() == QStringLiteral("Glyphs")) {
QString att = glyphsAtts.value(QStringLiteral("RenderTransform")).toString();
if (!att.isEmpty()) {
matrix = parseRscRefMatrix(att) * matrix;
}
QString text = unicodeString(glyphsAtts.value(QStringLiteral("UnicodeString")).toString());
// Get font (doesn't work well because qt doesn't allow to load font from file)
const QString absoluteFileName = absolutePath(entryPath(m_fileName), glyphsAtts.value(QStringLiteral("FontUri")).toString());
QFont font = m_file->getFontByName(absoluteFileName, glyphsAtts.value(QStringLiteral("FontRenderingEmSize")).toString().toFloat() * 72 / 96);
QFontMetrics metrics = QFontMetrics(font);
// Origin
QPointF origin(glyphsAtts.value(QStringLiteral("OriginX")).toString().toDouble(), glyphsAtts.value(QStringLiteral("OriginY")).toString().toDouble());
int lastWidth = 0;
for (int i = 0; i < text.length(); i++) {
const int width = metrics.horizontalAdvance(text, i + 1);
Okular::NormalizedRect *rect =
new Okular::NormalizedRect((origin.x() + lastWidth) / m_pageSize.width(), (origin.y() - metrics.height()) / m_pageSize.height(), (origin.x() + width) / m_pageSize.width(), origin.y() / m_pageSize.height());
rect->transform(matrix);
textPage->append(text.mid(i, 1), rect);
lastWidth = width;
}
matrix = matrices.pop();
} else if ((xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) ||
(xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) ||
(xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment"))) {
// those are only graphical - no use in text handling
} else if ((xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources"))) {
// not useful for text extraction
} else {
qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction end: " << xml.name().toString();
}
}
}
if (xml.error()) {
qCWarning(OkularXpsDebug) << "Error parsing XpsPage text: " << xml.errorString();
}
return textPage;
}
void XpsDocument::parseDocumentStructure(const QString &documentStructureFileName)
{
qCWarning(OkularXpsDebug) << "document structure file name: " << documentStructureFileName;
m_haveDocumentStructure = false;
const KZipFileEntry *documentStructureFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(documentStructureFileName));
QXmlStreamReader xml;
xml.addData(documentStructureFile->data());
while (!xml.atEnd()) {
xml.readNext();
if (xml.isStartElement()) {
if (xml.name() == QStringLiteral("DocumentStructure")) {
// just a container for optional outline and story elements - nothing to do here
} else if (xml.name() == QStringLiteral("DocumentStructure.Outline")) {
qCWarning(OkularXpsDebug) << "found DocumentStructure.Outline";
} else if (xml.name() == QStringLiteral("DocumentOutline")) {
qCWarning(OkularXpsDebug) << "found DocumentOutline";
m_docStructure = new Okular::DocumentSynopsis;
} else if (xml.name() == QStringLiteral("OutlineEntry")) {
m_haveDocumentStructure = true;
QXmlStreamAttributes attributes = xml.attributes();
int outlineLevel = attributes.value(QStringLiteral("OutlineLevel")).toString().toInt();
QString description = attributes.value(QStringLiteral("Description")).toString();
QDomElement synopsisElement = m_docStructure->createElement(description);
synopsisElement.setAttribute(QStringLiteral("OutlineLevel"), outlineLevel);
QString target = attributes.value(QStringLiteral("OutlineTarget")).toString();
int hashPosition = target.lastIndexOf(QLatin1Char('#'));
target = target.mid(hashPosition + 1);
// qCWarning(OkularXpsDebug) << "target: " << target;
Okular::DocumentViewport viewport;
viewport.pageNumber = m_docStructurePageMap.value(target);
synopsisElement.setAttribute(QStringLiteral("Viewport"), viewport.toString());
if (outlineLevel == 1) {
// qCWarning(OkularXpsDebug) << "Description: "
// << outlineEntryElement.attribute( "Description" );
m_docStructure->appendChild(synopsisElement);
} else {
// find the last next highest element (so it this is level 3, we need
// to find the most recent level 2 node)
QDomNode maybeParentNode = m_docStructure->lastChild();
while (!maybeParentNode.isNull()) {
if (maybeParentNode.toElement().attribute(QStringLiteral("OutlineLevel")).toInt() == (outlineLevel - 1)) {
// we have the right parent
maybeParentNode.appendChild(synopsisElement);
break;
}
maybeParentNode = maybeParentNode.lastChild();
}
}
} else if (xml.name() == QStringLiteral("Story")) {
// we need to handle Story here, but I have no idea what to do with it.
} else if (xml.name() == QStringLiteral("StoryFragment")) {
// we need to handle StoryFragment here, but I have no idea what to do with it.
} else if (xml.name() == QStringLiteral("StoryFragmentReference")) {
// we need to handle StoryFragmentReference here, but I have no idea what to do with it.
} else {
qCWarning(OkularXpsDebug) << "Unhandled entry in DocumentStructure: " << xml.name().toString();
}
}
}
}
const Okular::DocumentSynopsis *XpsDocument::documentStructure()
{
return m_docStructure;
}
bool XpsDocument::hasDocumentStructure()
{
return m_haveDocumentStructure;
}
XpsDocument::XpsDocument(XpsFile *file, const QString &fileName)
: m_file(file)
, m_haveDocumentStructure(false)
, m_docStructure(nullptr)
{
qCWarning(OkularXpsDebug) << "document file name: " << fileName;
const KArchiveEntry *documentEntry = file->xpsArchive()->directory()->entry(fileName);
QString documentFilePath = fileName;
const QString documentEntryPath = entryPath(fileName);
QXmlStreamReader docXml;
docXml.addData(readFileOrDirectoryParts(documentEntry, &documentFilePath));
while (!docXml.atEnd()) {
docXml.readNext();
if (docXml.isStartElement()) {
if (docXml.name() == QStringLiteral("PageContent")) {
QString pagePath = docXml.attributes().value(QStringLiteral("Source")).toString();
qCWarning(OkularXpsDebug) << "Page Path: " << pagePath;
XpsPage *page = new XpsPage(file, absolutePath(documentFilePath, pagePath));
m_pages.append(page);
} else if (docXml.name() == QStringLiteral("PageContent.LinkTargets")) {
// do nothing - wait for the real LinkTarget elements
} else if (docXml.name() == QStringLiteral("LinkTarget")) {
QString targetName = docXml.attributes().value(QStringLiteral("Name")).toString();
if (!targetName.isEmpty()) {
m_docStructurePageMap[targetName] = m_pages.count() - 1;
}
} else if (docXml.name() == QStringLiteral("FixedDocument")) {
// we just ignore this - it is just a container
} else {
qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocument: " << docXml.name().toString();
}
}
}
if (docXml.error()) {
qCWarning(OkularXpsDebug) << "Could not parse main XPS document file: " << docXml.errorString();
}
// There might be a relationships entry for this document - typically used to tell us where to find the
// content structure description
// We should be able to find this using a reference from some other part of the document, but I can't see it.
const int slashPosition = fileName.lastIndexOf(QLatin1Char('/'));
const QString documentRelationshipFile = absolutePath(documentEntryPath, QStringLiteral("_rels/") + fileName.mid(slashPosition + 1) + QStringLiteral(".rels"));
const KZipFileEntry *relFile = static_cast<const KZipFileEntry *>(file->xpsArchive()->directory()->entry(documentRelationshipFile));
QString documentStructureFile;
if (relFile) {
QXmlStreamReader xml;
xml.addData(readFileOrDirectoryParts(relFile));
while (!xml.atEnd()) {
xml.readNext();
if (xml.isStartElement() && (xml.name() == QStringLiteral("Relationship"))) {
QXmlStreamAttributes attributes = xml.attributes();
if (attributes.value(QStringLiteral("Type")).toString() == QLatin1String("http://schemas.microsoft.com/xps/2005/06/documentstructure")) {
documentStructureFile = attributes.value(QStringLiteral("Target")).toString();
} else {
qCWarning(OkularXpsDebug) << "Unknown document relationships element: " << attributes.value(QStringLiteral("Type")).toString() << " : " << attributes.value(QStringLiteral("Target")).toString();
}
}
}
if (xml.error()) {
qCWarning(OkularXpsDebug) << "Could not parse XPS page relationships file ( " << documentRelationshipFile << " ) - " << xml.errorString();
}
} else { // the page relationship file didn't exist in the zipfile
// this isn't fatal
qCWarning(OkularXpsDebug) << "Could not open Document relationship file from " << documentRelationshipFile;
}
if (!documentStructureFile.isEmpty()) {
// qCWarning(OkularXpsDebug) << "Document structure filename: " << documentStructureFile;
// make the document path absolute
documentStructureFile = absolutePath(documentEntryPath, documentStructureFile);
// qCWarning(OkularXpsDebug) << "Document structure absolute path: " << documentStructureFile;
parseDocumentStructure(documentStructureFile);
}
}
XpsDocument::~XpsDocument()
{
qDeleteAll(m_pages);
m_pages.clear();
if (m_docStructure)
delete m_docStructure;
}
int XpsDocument::numPages() const
{
return m_pages.size();
}
XpsPage *XpsDocument::page(int pageNum) const
{
return m_pages.at(pageNum);
}
XpsFile::XpsFile()
{
}
XpsFile::~XpsFile()
{
for (int fontId : qAsConst(m_fontCache)) {
m_fontDatabase.removeApplicationFont(fontId);
}
}
bool XpsFile::loadDocument(const QString &filename)
{
m_xpsArchive = new KZip(filename);
if (m_xpsArchive->open(QIODevice::ReadOnly) == true) {
qCWarning(OkularXpsDebug) << "Successful open of " << m_xpsArchive->fileName();
} else {
qCWarning(OkularXpsDebug) << "Could not open XPS archive: " << m_xpsArchive->fileName();
delete m_xpsArchive;
return false;
}
// The only fixed entry in XPS is /_rels/.rels
const KArchiveEntry *relEntry = m_xpsArchive->directory()->entry(QStringLiteral("_rels/.rels"));
if (!relEntry) {
// this might occur if we can't read the zip directory, or it doesn't have the relationships entry
return false;
}
QXmlStreamReader relXml;
relXml.addData(readFileOrDirectoryParts(relEntry));
QString fixedRepresentationFileName;
// We work through the relationships document and pull out each element.
while (!relXml.atEnd()) {
relXml.readNext();
if (relXml.isStartElement()) {
if (relXml.name() == QStringLiteral("Relationship")) {
QXmlStreamAttributes attributes = relXml.attributes();
QString type = attributes.value(QStringLiteral("Type")).toString();
QString target = attributes.value(QStringLiteral("Target")).toString();
if (QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") == type) {
m_thumbnailFileName = target;
} else if (QStringLiteral("http://schemas.microsoft.com/xps/2005/06/fixedrepresentation") == type) {
fixedRepresentationFileName = target;
} else if (QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties") == type) {
m_corePropertiesFileName = target;
} else if (QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin") == type) {
m_signatureOrigin = target;
} else {
qCWarning(OkularXpsDebug) << "Unknown relationships element: " << type << " : " << target;
}
} else if (relXml.name() == QStringLiteral("Relationships")) {
// nothing to do here - this is just the container level
} else {
qCWarning(OkularXpsDebug) << "unexpected element in _rels/.rels: " << relXml.name().toString();
}
}
}
if (relXml.error()) {
qCWarning(OkularXpsDebug) << "Could not parse _rels/.rels: " << relXml.errorString();
return false;
}
if (fixedRepresentationFileName.isEmpty()) {
// FixedRepresentation is a required part of the XPS document
return false;
}
const KArchiveEntry *fixedRepEntry = m_xpsArchive->directory()->entry(fixedRepresentationFileName);
QString fixedRepresentationFilePath = fixedRepresentationFileName;
QXmlStreamReader fixedRepXml;
fixedRepXml.addData(readFileOrDirectoryParts(fixedRepEntry, &fixedRepresentationFileName));
while (!fixedRepXml.atEnd()) {
fixedRepXml.readNext();
if (fixedRepXml.isStartElement()) {
if (fixedRepXml.name() == QStringLiteral("DocumentReference")) {
const QString source = fixedRepXml.attributes().value(QStringLiteral("Source")).toString();
XpsDocument *doc = new XpsDocument(this, absolutePath(fixedRepresentationFilePath, source));
for (int lv = 0; lv < doc->numPages(); ++lv) {
// our own copy of the pages list
m_pages.append(doc->page(lv));
}
m_documents.append(doc);
} else if (fixedRepXml.name() == QStringLiteral("FixedDocumentSequence")) {
// we don't do anything here - this is just a container for one or more DocumentReference elements
} else {
qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocumentSequence: " << fixedRepXml.name().toString();
}
}
}
if (fixedRepXml.error()) {
qCWarning(OkularXpsDebug) << "Could not parse FixedRepresentation file:" << fixedRepXml.errorString();
return false;
}
return true;
}
Okular::DocumentInfo XpsFile::generateDocumentInfo() const
{
Okular::DocumentInfo docInfo;
docInfo.set(Okular::DocumentInfo::MimeType, QStringLiteral("application/oxps"));
if (!m_corePropertiesFileName.isEmpty()) {
const KZipFileEntry *corepropsFile = static_cast<const KZipFileEntry *>(m_xpsArchive->directory()->entry(m_corePropertiesFileName));
QXmlStreamReader xml;
xml.addData(corepropsFile->data());
while (!xml.atEnd()) {
xml.readNext();
if (xml.isEndElement())
break;
if (xml.isStartElement()) {
if (xml.name() == QStringLiteral("title")) {
docInfo.set(Okular::DocumentInfo::Title, xml.readElementText());
} else if (xml.name() == QStringLiteral("subject")) {
docInfo.set(Okular::DocumentInfo::Subject, xml.readElementText());
} else if (xml.name() == QStringLiteral("description")) {
docInfo.set(Okular::DocumentInfo::Description, xml.readElementText());
} else if (xml.name() == QStringLiteral("creator")) {
docInfo.set(Okular::DocumentInfo::Creator, xml.readElementText());
} else if (xml.name() == QStringLiteral("category")) {
docInfo.set(Okular::DocumentInfo::Category, xml.readElementText());
} else if (xml.name() == QStringLiteral("created")) {
QDateTime createdDate = QDateTime::fromString(xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ"));
docInfo.set(Okular::DocumentInfo::CreationDate, QLocale().toString(createdDate, QLocale::LongFormat));
} else if (xml.name() == QStringLiteral("modified")) {
QDateTime modifiedDate = QDateTime::fromString(xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ"));
docInfo.set(Okular::DocumentInfo::ModificationDate, QLocale().toString(modifiedDate, QLocale::LongFormat));
} else if (xml.name() == QStringLiteral("keywords")) {
docInfo.set(Okular::DocumentInfo::Keywords, xml.readElementText());
} else if (xml.name() == QStringLiteral("revision")) {
docInfo.set(QStringLiteral("revision"), xml.readElementText(), i18n("Revision"));
}
}
}
if (xml.error()) {
qCWarning(OkularXpsDebug) << "Could not parse XPS core properties:" << xml.errorString();
}
} else {
qCWarning(OkularXpsDebug) << "No core properties filename";
}
docInfo.set(Okular::DocumentInfo::Pages, QString::number(numPages()));
return docInfo;
}
bool XpsFile::closeDocument()
{
qDeleteAll(m_documents);
m_documents.clear();
delete m_xpsArchive;
return true;
}
int XpsFile::numPages() const
{
return m_pages.size();
}
int XpsFile::numDocuments() const
{
return m_documents.size();
}
XpsDocument *XpsFile::document(int documentNum) const
{
return m_documents.at(documentNum);
}
XpsPage *XpsFile::page(int pageNum) const
{
return m_pages.at(pageNum);
}
XpsGenerator::XpsGenerator(QObject *parent, const QVariantList &args)
: Okular::Generator(parent, args)
, m_xpsFile(nullptr)
{
setFeature(TextExtraction);
setFeature(PrintNative);
setFeature(PrintToFile);
setFeature(Threaded);
userMutex();
}
XpsGenerator::~XpsGenerator()
{
}
bool XpsGenerator::loadDocument(const QString &fileName, QVector<Okular::Page *> &pagesVector)
{
m_xpsFile = new XpsFile();
m_xpsFile->loadDocument(fileName);
pagesVector.resize(m_xpsFile->numPages());
int pagesVectorOffset = 0;
for (int docNum = 0; docNum < m_xpsFile->numDocuments(); ++docNum) {
XpsDocument *doc = m_xpsFile->document(docNum);
for (int pageNum = 0; pageNum < doc->numPages(); ++pageNum) {
QSizeF pageSize = doc->page(pageNum)->size();
pagesVector[pagesVectorOffset] = new Okular::Page(pagesVectorOffset, pageSize.width(), pageSize.height(), Okular::Rotation0);
++pagesVectorOffset;
}
}
return true;
}
bool XpsGenerator::doCloseDocument()
{
m_xpsFile->closeDocument();
delete m_xpsFile;
m_xpsFile = nullptr;
return true;
}
QImage XpsGenerator::image(Okular::PixmapRequest *request)
{
QMutexLocker lock(userMutex());
QSize size((int)request->width(), (int)request->height());
QImage image(size, QImage::Format_RGB32);
XpsPage *pageToRender = m_xpsFile->page(request->page()->number());
pageToRender->renderToImage(&image);
return image;
}
Okular::TextPage *XpsGenerator::textPage(Okular::TextRequest *request)
{
QMutexLocker lock(userMutex());
XpsPage *xpsPage = m_xpsFile->page(request->page()->number());
return xpsPage->textPage();
}
Okular::DocumentInfo XpsGenerator::generateDocumentInfo(const QSet<Okular::DocumentInfo::Key> &keys) const
{
Q_UNUSED(keys);
qCWarning(OkularXpsDebug) << "generating document metadata";
return m_xpsFile->generateDocumentInfo();
}
const Okular::DocumentSynopsis *XpsGenerator::generateDocumentSynopsis()
{
qCWarning(OkularXpsDebug) << "generating document synopsis";
// we only generate the synopsis for the first file.
if (!m_xpsFile || !m_xpsFile->document(0))
return nullptr;
if (m_xpsFile->document(0)->hasDocumentStructure())
return m_xpsFile->document(0)->documentStructure();
return nullptr;
}
Okular::ExportFormat::List XpsGenerator::exportFormats() const
{
static Okular::ExportFormat::List formats;
if (formats.isEmpty()) {
formats.append(Okular::ExportFormat::standardFormat(Okular::ExportFormat::PlainText));
}
return formats;
}
bool XpsGenerator::exportTo(const QString &fileName, const Okular::ExportFormat &format)
{
if (format.mimeType().inherits(QStringLiteral("text/plain"))) {
QFile f(fileName);
if (!f.open(QIODevice::WriteOnly))
return false;
QTextStream ts(&f);
for (int i = 0; i < m_xpsFile->numPages(); ++i) {
Okular::TextPage *textPage = m_xpsFile->page(i)->textPage();
QString text = textPage->text();
ts << text;
ts << QLatin1Char('\n');
delete textPage;
}
f.close();
return true;
}
return false;
}
bool XpsGenerator::print(QPrinter &printer)
{
QList<int> pageList = Okular::FilePrinter::pageList(printer, document()->pages(), document()->currentPage() + 1, document()->bookmarkedPageList());
QPainter painter(&printer);
for (int i = 0; i < pageList.count(); ++i) {
if (i != 0)
printer.newPage();
const int page = pageList.at(i) - 1;
XpsPage *pageToRender = m_xpsFile->page(page);
pageToRender->renderToPainter(&painter);
}
return true;
}
const XpsRenderNode *XpsRenderNode::findChild(const QString &name) const
{
for (const XpsRenderNode &child : children) {
if (child.name == name) {
return &child;
}
}
return nullptr;
}
QVariant XpsRenderNode::getRequiredChildData(const QString &name) const
{
const XpsRenderNode *child = findChild(name);
if (child == nullptr) {
qCWarning(OkularXpsDebug) << "Required element " << name << " is missing in " << this->name;
return QVariant();
}
return child->data;
}
QVariant XpsRenderNode::getChildData(const QString &name) const
{
const XpsRenderNode *child = findChild(name);
if (child == nullptr) {
return QVariant();
} else {
return child->data;
}
}
Q_LOGGING_CATEGORY(OkularXpsDebug, "org.kde.okular.generators.xps", QtWarningMsg)
#include "generator_xps.moc"