/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtGui module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qpdf_p.h" #ifndef QT_NO_PDF #include "qplatformdefs.h" #include #include #include #include #include #include #include #include #include #include #include #include #ifndef QT_NO_COMPRESS #include #endif #ifdef QT_NO_COMPRESS static const bool do_compress = false; #else static const bool do_compress = true; #endif // might be helpful for smooth transforms of images // Can't use it though, as gs generates completely wrong images if this is true. static const bool interpolateImages = false; static void initResources() { Q_INIT_RESOURCE(qpdf); } QT_BEGIN_NAMESPACE inline QPaintEngine::PaintEngineFeatures qt_pdf_decide_features() { QPaintEngine::PaintEngineFeatures f = QPaintEngine::AllFeatures; f &= ~(QPaintEngine::PorterDuff | QPaintEngine::PerspectiveTransform | QPaintEngine::ObjectBoundingModeGradients | QPaintEngine::ConicalGradientFill); return f; } extern bool qt_isExtendedRadialGradient(const QBrush &brush); // helper function to remove transparency from brush in PDF/A-1b mode static void removeTransparencyFromBrush(QBrush &brush) { if (brush.style() == Qt::SolidPattern) { QColor color = brush.color(); if (color.alpha() != 255) { color.setAlpha(255); brush.setColor(color); } return; } if (qt_isExtendedRadialGradient(brush)) { brush = QBrush(Qt::black); // the safest we can do so far... return; } if (brush.style() == Qt::LinearGradientPattern || brush.style() == Qt::RadialGradientPattern || brush.style() == Qt::ConicalGradientPattern) { QGradientStops stops = brush.gradient()->stops(); for (int i = 0; i < stops.size(); ++i) { if (stops[i].second.alpha() != 255) stops[i].second.setAlpha(255); } const_cast(brush.gradient())->setStops(stops); return; } if (brush.style() == Qt::TexturePattern) { // handled inside QPdfEnginePrivate::addImage() already return; } } /* also adds a space at the end of the number */ const char *qt_real_to_string(qreal val, char *buf) { const char *ret = buf; if (qIsNaN(val)) { *(buf++) = '0'; *(buf++) = ' '; *buf = 0; return ret; } if (val < 0) { *(buf++) = '-'; val = -val; } unsigned int ival = (unsigned int) val; qreal frac = val - (qreal)ival; int ifrac = (int)(frac * 1000000000); if (ifrac == 1000000000) { ++ival; ifrac = 0; } char output[256]; int i = 0; while (ival) { output[i] = '0' + (ival % 10); ++i; ival /= 10; } int fact = 100000000; if (i == 0) { *(buf++) = '0'; } else { while (i) { *(buf++) = output[--i]; fact /= 10; ifrac /= 10; } } if (ifrac) { *(buf++) = '.'; while (fact) { *(buf++) = '0' + ((ifrac/fact) % 10); fact /= 10; } } *(buf++) = ' '; *buf = 0; return ret; } const char *qt_int_to_string(int val, char *buf) { const char *ret = buf; if (val < 0) { *(buf++) = '-'; val = -val; } char output[256]; int i = 0; while (val) { output[i] = '0' + (val % 10); ++i; val /= 10; } if (i == 0) { *(buf++) = '0'; } else { while (i) *(buf++) = output[--i]; } *(buf++) = ' '; *buf = 0; return ret; } namespace QPdf { ByteStream::ByteStream(QByteArray *byteArray, bool fileBacking) : dev(new QBuffer(byteArray)), fileBackingEnabled(fileBacking), fileBackingActive(false), handleDirty(false) { dev->open(QIODevice::ReadWrite | QIODevice::Append); } ByteStream::ByteStream(bool fileBacking) : dev(new QBuffer(&ba)), fileBackingEnabled(fileBacking), fileBackingActive(false), handleDirty(false) { dev->open(QIODevice::ReadWrite); } ByteStream::~ByteStream() { delete dev; } ByteStream &ByteStream::operator <<(char chr) { if (handleDirty) prepareBuffer(); dev->write(&chr, 1); return *this; } ByteStream &ByteStream::operator <<(const char *str) { if (handleDirty) prepareBuffer(); dev->write(str, strlen(str)); return *this; } ByteStream &ByteStream::operator <<(const QByteArray &str) { if (handleDirty) prepareBuffer(); dev->write(str); return *this; } ByteStream &ByteStream::operator <<(const ByteStream &src) { Q_ASSERT(!src.dev->isSequential()); if (handleDirty) prepareBuffer(); // We do play nice here, even though it looks ugly. // We save the position and restore it afterwards. ByteStream &s = const_cast(src); qint64 pos = s.dev->pos(); s.dev->reset(); while (!s.dev->atEnd()) { QByteArray buf = s.dev->read(chunkSize()); dev->write(buf); } s.dev->seek(pos); return *this; } ByteStream &ByteStream::operator <<(qreal val) { char buf[256]; qt_real_to_string(val, buf); *this << buf; return *this; } ByteStream &ByteStream::operator <<(int val) { char buf[256]; qt_int_to_string(val, buf); *this << buf; return *this; } ByteStream &ByteStream::operator <<(const QPointF &p) { char buf[256]; qt_real_to_string(p.x(), buf); *this << buf; qt_real_to_string(p.y(), buf); *this << buf; return *this; } QIODevice *ByteStream::stream() { dev->reset(); handleDirty = true; return dev; } void ByteStream::clear() { dev->open(QIODevice::ReadWrite | QIODevice::Truncate); } void ByteStream::constructor_helper(QByteArray *ba) { delete dev; dev = new QBuffer(ba); dev->open(QIODevice::ReadWrite); } void ByteStream::prepareBuffer() { Q_ASSERT(!dev->isSequential()); qint64 size = dev->size(); if (fileBackingEnabled && !fileBackingActive && size > maxMemorySize()) { // Switch to file backing. QTemporaryFile *newFile = new QTemporaryFile; newFile->open(); dev->reset(); while (!dev->atEnd()) { QByteArray buf = dev->read(chunkSize()); newFile->write(buf); } delete dev; dev = newFile; ba.clear(); fileBackingActive = true; } if (dev->pos() != size) { dev->seek(size); handleDirty = false; } } } #define QT_PATH_ELEMENT(elm) QByteArray QPdf::generatePath(const QPainterPath &path, const QTransform &matrix, PathFlags flags) { QByteArray result; if (!path.elementCount()) return result; ByteStream s(&result); int start = -1; for (int i = 0; i < path.elementCount(); ++i) { const QPainterPath::Element &elm = path.elementAt(i); switch (elm.type) { case QPainterPath::MoveToElement: if (start >= 0 && path.elementAt(start).x == path.elementAt(i-1).x && path.elementAt(start).y == path.elementAt(i-1).y) s << "h\n"; s << matrix.map(QPointF(elm.x, elm.y)) << "m\n"; start = i; break; case QPainterPath::LineToElement: s << matrix.map(QPointF(elm.x, elm.y)) << "l\n"; break; case QPainterPath::CurveToElement: Q_ASSERT(path.elementAt(i+1).type == QPainterPath::CurveToDataElement); Q_ASSERT(path.elementAt(i+2).type == QPainterPath::CurveToDataElement); s << matrix.map(QPointF(elm.x, elm.y)) << matrix.map(QPointF(path.elementAt(i+1).x, path.elementAt(i+1).y)) << matrix.map(QPointF(path.elementAt(i+2).x, path.elementAt(i+2).y)) << "c\n"; i += 2; break; default: qFatal("QPdf::generatePath(), unhandled type: %d", elm.type); } } if (start >= 0 && path.elementAt(start).x == path.elementAt(path.elementCount()-1).x && path.elementAt(start).y == path.elementAt(path.elementCount()-1).y) s << "h\n"; Qt::FillRule fillRule = path.fillRule(); const char *op = ""; switch (flags) { case ClipPath: op = (fillRule == Qt::WindingFill) ? "W n\n" : "W* n\n"; break; case FillPath: op = (fillRule == Qt::WindingFill) ? "f\n" : "f*\n"; break; case StrokePath: op = "S\n"; break; case FillAndStrokePath: op = (fillRule == Qt::WindingFill) ? "B\n" : "B*\n"; break; } s << op; return result; } QByteArray QPdf::generateMatrix(const QTransform &matrix) { QByteArray result; ByteStream s(&result); s << matrix.m11() << matrix.m12() << matrix.m21() << matrix.m22() << matrix.dx() << matrix.dy() << "cm\n"; return result; } QByteArray QPdf::generateDashes(const QPen &pen) { QByteArray result; ByteStream s(&result); s << '['; QList dasharray = pen.dashPattern(); qreal w = pen.widthF(); if (w < 0.001) w = 1; for (int i = 0; i < dasharray.size(); ++i) { qreal dw = dasharray.at(i)*w; if (dw < 0.0001) dw = 0.0001; s << dw; } s << ']'; s << pen.dashOffset() * w; s << " d\n"; return result; } static const char* const pattern_for_brush[] = { nullptr, // NoBrush nullptr, // SolidPattern "0 J\n" "6 w\n" "[] 0 d\n" "4 0 m\n" "4 8 l\n" "0 4 m\n" "8 4 l\n" "S\n", // Dense1Pattern "0 J\n" "2 w\n" "[6 2] 1 d\n" "0 0 m\n" "0 8 l\n" "8 0 m\n" "8 8 l\n" "S\n" "[] 0 d\n" "2 0 m\n" "2 8 l\n" "6 0 m\n" "6 8 l\n" "S\n" "[6 2] -3 d\n" "4 0 m\n" "4 8 l\n" "S\n", // Dense2Pattern "0 J\n" "2 w\n" "[6 2] 1 d\n" "0 0 m\n" "0 8 l\n" "8 0 m\n" "8 8 l\n" "S\n" "[2 2] -1 d\n" "2 0 m\n" "2 8 l\n" "6 0 m\n" "6 8 l\n" "S\n" "[6 2] -3 d\n" "4 0 m\n" "4 8 l\n" "S\n", // Dense3Pattern "0 J\n" "2 w\n" "[2 2] 1 d\n" "0 0 m\n" "0 8 l\n" "8 0 m\n" "8 8 l\n" "S\n" "[2 2] -1 d\n" "2 0 m\n" "2 8 l\n" "6 0 m\n" "6 8 l\n" "S\n" "[2 2] 1 d\n" "4 0 m\n" "4 8 l\n" "S\n", // Dense4Pattern "0 J\n" "2 w\n" "[2 6] -1 d\n" "0 0 m\n" "0 8 l\n" "8 0 m\n" "8 8 l\n" "S\n" "[2 2] 1 d\n" "2 0 m\n" "2 8 l\n" "6 0 m\n" "6 8 l\n" "S\n" "[2 6] 3 d\n" "4 0 m\n" "4 8 l\n" "S\n", // Dense5Pattern "0 J\n" "2 w\n" "[2 6] -1 d\n" "0 0 m\n" "0 8 l\n" "8 0 m\n" "8 8 l\n" "S\n" "[2 6] 3 d\n" "4 0 m\n" "4 8 l\n" "S\n", // Dense6Pattern "0 J\n" "2 w\n" "[2 6] -1 d\n" "0 0 m\n" "0 8 l\n" "8 0 m\n" "8 8 l\n" "S\n", // Dense7Pattern "1 w\n" "0 4 m\n" "8 4 l\n" "S\n", // HorPattern "1 w\n" "4 0 m\n" "4 8 l\n" "S\n", // VerPattern "1 w\n" "4 0 m\n" "4 8 l\n" "0 4 m\n" "8 4 l\n" "S\n", // CrossPattern "1 w\n" "-1 5 m\n" "5 -1 l\n" "3 9 m\n" "9 3 l\n" "S\n", // BDiagPattern "1 w\n" "-1 3 m\n" "5 9 l\n" "3 -1 m\n" "9 5 l\n" "S\n", // FDiagPattern "1 w\n" "-1 3 m\n" "5 9 l\n" "3 -1 m\n" "9 5 l\n" "-1 5 m\n" "5 -1 l\n" "3 9 m\n" "9 3 l\n" "S\n", // DiagCrossPattern }; QByteArray QPdf::patternForBrush(const QBrush &b) { int style = b.style(); if (style > Qt::DiagCrossPattern) return QByteArray(); return pattern_for_brush[style]; } static void moveToHook(qfixed x, qfixed y, void *data) { QPdf::Stroker *t = (QPdf::Stroker *)data; if (!t->first) *t->stream << "h\n"; if (!t->cosmeticPen) t->matrix.map(x, y, &x, &y); *t->stream << x << y << "m\n"; t->first = false; } static void lineToHook(qfixed x, qfixed y, void *data) { QPdf::Stroker *t = (QPdf::Stroker *)data; if (!t->cosmeticPen) t->matrix.map(x, y, &x, &y); *t->stream << x << y << "l\n"; } static void cubicToHook(qfixed c1x, qfixed c1y, qfixed c2x, qfixed c2y, qfixed ex, qfixed ey, void *data) { QPdf::Stroker *t = (QPdf::Stroker *)data; if (!t->cosmeticPen) { t->matrix.map(c1x, c1y, &c1x, &c1y); t->matrix.map(c2x, c2y, &c2x, &c2y); t->matrix.map(ex, ey, &ex, &ey); } *t->stream << c1x << c1y << c2x << c2y << ex << ey << "c\n"; } QPdf::Stroker::Stroker() : stream(nullptr), first(true), dashStroker(&basicStroker) { stroker = &basicStroker; basicStroker.setMoveToHook(moveToHook); basicStroker.setLineToHook(lineToHook); basicStroker.setCubicToHook(cubicToHook); cosmeticPen = true; basicStroker.setStrokeWidth(.1); } void QPdf::Stroker::setPen(const QPen &pen, QPainter::RenderHints hints) { if (pen.style() == Qt::NoPen) { stroker = nullptr; return; } qreal w = pen.widthF(); bool zeroWidth = w < 0.0001; cosmeticPen = qt_pen_is_cosmetic(pen, hints); if (zeroWidth) w = .1; basicStroker.setStrokeWidth(w); basicStroker.setCapStyle(pen.capStyle()); basicStroker.setJoinStyle(pen.joinStyle()); basicStroker.setMiterLimit(pen.miterLimit()); QList dashpattern = pen.dashPattern(); if (zeroWidth) { for (int i = 0; i < dashpattern.size(); ++i) dashpattern[i] *= 10.; } if (!dashpattern.isEmpty()) { dashStroker.setDashPattern(dashpattern); dashStroker.setDashOffset(pen.dashOffset()); stroker = &dashStroker; } else { stroker = &basicStroker; } } void QPdf::Stroker::strokePath(const QPainterPath &path) { if (!stroker) return; first = true; stroker->strokePath(path, this, cosmeticPen ? matrix : QTransform()); *stream << "h f\n"; } QByteArray QPdf::ascii85Encode(const QByteArray &input) { int isize = input.size()/4*4; QByteArray output; output.resize(input.size()*5/4+7); char *out = output.data(); const uchar *in = (const uchar *)input.constData(); for (int i = 0; i < isize; i += 4) { uint val = (((uint)in[i])<<24) + (((uint)in[i+1])<<16) + (((uint)in[i+2])<<8) + (uint)in[i+3]; if (val == 0) { *out = 'z'; ++out; } else { char base[5]; base[4] = val % 85; val /= 85; base[3] = val % 85; val /= 85; base[2] = val % 85; val /= 85; base[1] = val % 85; val /= 85; base[0] = val % 85; *(out++) = base[0] + '!'; *(out++) = base[1] + '!'; *(out++) = base[2] + '!'; *(out++) = base[3] + '!'; *(out++) = base[4] + '!'; } } //write the last few bytes int remaining = input.size() - isize; if (remaining) { uint val = 0; for (int i = isize; i < input.size(); ++i) val = (val << 8) + in[i]; val <<= 8*(4-remaining); char base[5]; base[4] = val % 85; val /= 85; base[3] = val % 85; val /= 85; base[2] = val % 85; val /= 85; base[1] = val % 85; val /= 85; base[0] = val % 85; for (int i = 0; i < remaining+1; ++i) *(out++) = base[i] + '!'; } *(out++) = '~'; *(out++) = '>'; output.resize(out-output.data()); return output; } const char *QPdf::toHex(ushort u, char *buffer) { int i = 3; while (i >= 0) { ushort hex = (u & 0x000f); if (hex < 0x0a) buffer[i] = '0'+hex; else buffer[i] = 'A'+(hex-0x0a); u = u >> 4; i--; } buffer[4] = '\0'; return buffer; } const char *QPdf::toHex(uchar u, char *buffer) { int i = 1; while (i >= 0) { ushort hex = (u & 0x000f); if (hex < 0x0a) buffer[i] = '0'+hex; else buffer[i] = 'A'+(hex-0x0a); u = u >> 4; i--; } buffer[2] = '\0'; return buffer; } QPdfPage::QPdfPage() : QPdf::ByteStream(true) // Enable file backing { } void QPdfPage::streamImage(int w, int h, int object) { *this << w << "0 0 " << -h << "0 " << h << "cm /Im" << object << " Do\n"; if (!images.contains(object)) images.append(object); } QPdfEngine::QPdfEngine(QPdfEnginePrivate &dd) : QPaintEngine(dd, qt_pdf_decide_features()) { } QPdfEngine::QPdfEngine() : QPaintEngine(*new QPdfEnginePrivate(), qt_pdf_decide_features()) { } void QPdfEngine::setOutputFilename(const QString &filename) { Q_D(QPdfEngine); d->outputFileName = filename; } void QPdfEngine::drawPoints (const QPointF *points, int pointCount) { if (!points) return; Q_D(QPdfEngine); QPainterPath p; for (int i=0; i!=pointCount;++i) { p.moveTo(points[i]); p.lineTo(points[i] + QPointF(0, 0.001)); } bool hadBrush = d->hasBrush; d->hasBrush = false; drawPath(p); d->hasBrush = hadBrush; } void QPdfEngine::drawLines (const QLineF *lines, int lineCount) { if (!lines) return; Q_D(QPdfEngine); QPainterPath p; for (int i=0; i!=lineCount;++i) { p.moveTo(lines[i].p1()); p.lineTo(lines[i].p2()); } bool hadBrush = d->hasBrush; d->hasBrush = false; drawPath(p); d->hasBrush = hadBrush; } void QPdfEngine::drawRects (const QRectF *rects, int rectCount) { if (!rects) return; Q_D(QPdfEngine); if (d->clipEnabled && d->allClipped) return; if (!d->hasPen && !d->hasBrush) return; if (d->simplePen || !d->hasPen) { // draw strokes natively in this case for better output if(!d->simplePen && !d->stroker.matrix.isIdentity()) *d->currentPage << "q\n" << QPdf::generateMatrix(d->stroker.matrix); for (int i = 0; i < rectCount; ++i) *d->currentPage << rects[i].x() << rects[i].y() << rects[i].width() << rects[i].height() << "re\n"; *d->currentPage << (d->hasPen ? (d->hasBrush ? "B\n" : "S\n") : "f\n"); if(!d->simplePen && !d->stroker.matrix.isIdentity()) *d->currentPage << "Q\n"; } else { QPainterPath p; for (int i=0; i!=rectCount; ++i) p.addRect(rects[i]); drawPath(p); } } void QPdfEngine::drawPolygon(const QPointF *points, int pointCount, PolygonDrawMode mode) { Q_D(QPdfEngine); if (!points || !pointCount) return; bool hb = d->hasBrush; QPainterPath p; switch(mode) { case OddEvenMode: p.setFillRule(Qt::OddEvenFill); break; case ConvexMode: case WindingMode: p.setFillRule(Qt::WindingFill); break; case PolylineMode: d->hasBrush = false; break; default: break; } p.moveTo(points[0]); for (int i = 1; i < pointCount; ++i) p.lineTo(points[i]); if (mode != PolylineMode) p.closeSubpath(); drawPath(p); d->hasBrush = hb; } void QPdfEngine::drawPath (const QPainterPath &p) { Q_D(QPdfEngine); if (d->clipEnabled && d->allClipped) return; if (!d->hasPen && !d->hasBrush) return; if (d->simplePen) { // draw strokes natively in this case for better output *d->currentPage << QPdf::generatePath(p, QTransform(), d->hasBrush ? QPdf::FillAndStrokePath : QPdf::StrokePath); } else { if (d->hasBrush) *d->currentPage << QPdf::generatePath(p, d->stroker.matrix, QPdf::FillPath); if (d->hasPen) { *d->currentPage << "q\n"; QBrush b = d->brush; d->brush = d->pen.brush(); setBrush(); d->stroker.strokePath(p); *d->currentPage << "Q\n"; d->brush = b; } } } void QPdfEngine::drawPixmap (const QRectF &rectangle, const QPixmap &pixmap, const QRectF &sr) { if (sr.isEmpty() || rectangle.isEmpty() || pixmap.isNull()) return; Q_D(QPdfEngine); QBrush b = d->brush; QRect sourceRect = sr.toRect(); QPixmap pm = sourceRect != pixmap.rect() ? pixmap.copy(sourceRect) : pixmap; QImage image = pm.toImage(); bool bitmap = true; const bool lossless = painter()->testRenderHint(QPainter::LosslessImageRendering); const int object = d->addImage(image, &bitmap, lossless, pm.cacheKey()); if (object < 0) return; *d->currentPage << "q\n"; if ((d->pdfVersion != QPdfEngine::Version_A1b) && (d->opacity != 1.0)) { int stateObject = d->addConstantAlphaObject(qRound(255 * d->opacity), qRound(255 * d->opacity)); if (stateObject) *d->currentPage << "/GState" << stateObject << "gs\n"; else *d->currentPage << "/GSa gs\n"; } else { *d->currentPage << "/GSa gs\n"; } *d->currentPage << QPdf::generateMatrix(QTransform(rectangle.width() / sr.width(), 0, 0, rectangle.height() / sr.height(), rectangle.x(), rectangle.y()) * (d->simplePen ? QTransform() : d->stroker.matrix)); if (bitmap) { // set current pen as d->brush d->brush = d->pen.brush(); } setBrush(); d->currentPage->streamImage(image.width(), image.height(), object); *d->currentPage << "Q\n"; d->brush = b; } void QPdfEngine::drawImage(const QRectF &rectangle, const QImage &image, const QRectF &sr, Qt::ImageConversionFlags) { if (sr.isEmpty() || rectangle.isEmpty() || image.isNull()) return; Q_D(QPdfEngine); QRect sourceRect = sr.toRect(); QImage im = sourceRect != image.rect() ? image.copy(sourceRect) : image; bool bitmap = true; const bool lossless = painter()->testRenderHint(QPainter::LosslessImageRendering); const int object = d->addImage(im, &bitmap, lossless, im.cacheKey()); if (object < 0) return; *d->currentPage << "q\n"; if ((d->pdfVersion != QPdfEngine::Version_A1b) && (d->opacity != 1.0)) { int stateObject = d->addConstantAlphaObject(qRound(255 * d->opacity), qRound(255 * d->opacity)); if (stateObject) *d->currentPage << "/GState" << stateObject << "gs\n"; else *d->currentPage << "/GSa gs\n"; } else { *d->currentPage << "/GSa gs\n"; } *d->currentPage << QPdf::generateMatrix(QTransform(rectangle.width() / sr.width(), 0, 0, rectangle.height() / sr.height(), rectangle.x(), rectangle.y()) * (d->simplePen ? QTransform() : d->stroker.matrix)); setBrush(); d->currentPage->streamImage(im.width(), im.height(), object); *d->currentPage << "Q\n"; } void QPdfEngine::drawTiledPixmap (const QRectF &rectangle, const QPixmap &pixmap, const QPointF &point) { Q_D(QPdfEngine); bool bitmap = (pixmap.depth() == 1); QBrush b = d->brush; QPointF bo = d->brushOrigin; bool hp = d->hasPen; d->hasPen = false; bool hb = d->hasBrush; d->hasBrush = true; d->brush = QBrush(pixmap); if (bitmap) // #### fix bitmap case where we have a brush pen d->brush.setColor(d->pen.color()); d->brushOrigin = -point; *d->currentPage << "q\n"; setBrush(); drawRects(&rectangle, 1); *d->currentPage << "Q\n"; d->hasPen = hp; d->hasBrush = hb; d->brush = b; d->brushOrigin = bo; } void QPdfEngine::drawTextItem(const QPointF &p, const QTextItem &textItem) { Q_D(QPdfEngine); if (!d->hasPen || (d->clipEnabled && d->allClipped)) return; if (d->stroker.matrix.type() >= QTransform::TxProject) { QPaintEngine::drawTextItem(p, textItem); return; } *d->currentPage << "q\n"; if(!d->simplePen) *d->currentPage << QPdf::generateMatrix(d->stroker.matrix); bool hp = d->hasPen; d->hasPen = false; QBrush b = d->brush; d->brush = d->pen.brush(); setBrush(); const QTextItemInt &ti = static_cast(textItem); Q_ASSERT(ti.fontEngine->type() != QFontEngine::Multi); d->drawTextItem(p, ti); d->hasPen = hp; d->brush = b; *d->currentPage << "Q\n"; } void QPdfEngine::drawHyperlink(const QRectF &r, const QUrl &url) { Q_D(QPdfEngine); const uint annot = d->addXrefEntry(-1); const QByteArray urlascii = url.toEncoded(); int len = urlascii.size(); QVarLengthArray url_esc; url_esc.reserve(len + 1); for (int j = 0; j < len; j++) { if (urlascii[j] == '(' || urlascii[j] == ')' || urlascii[j] == '\\') url_esc.append('\\'); url_esc.append(urlascii[j]); } url_esc.append('\0'); char buf[256]; const QRectF rr = d->pageMatrix().mapRect(r); d->xprintf("<<\n/Type /Annot\n/Subtype /Link\n"); if (d->pdfVersion == QPdfEngine::Version_A1b) d->xprintf("/F 4\n"); // enable print flag, disable all other d->xprintf("/Rect ["); d->xprintf("%s ", qt_real_to_string(rr.left(), buf)); d->xprintf("%s ", qt_real_to_string(rr.top(), buf)); d->xprintf("%s ", qt_real_to_string(rr.right(), buf)); d->xprintf("%s", qt_real_to_string(rr.bottom(), buf)); d->xprintf("]\n/Border [0 0 0]\n/A <<\n"); d->xprintf("/Type /Action\n/S /URI\n/URI (%s)\n", url_esc.constData()); d->xprintf(">>\n>>\n"); d->xprintf("endobj\n"); d->currentPage->annotations.append(annot); } void QPdfEngine::updateState(const QPaintEngineState &state) { Q_D(QPdfEngine); QPaintEngine::DirtyFlags flags = state.state(); if (flags & DirtyTransform) d->stroker.matrix = state.transform(); if (flags & DirtyPen) { if (d->pdfVersion == QPdfEngine::Version_A1b) { QPen pen = state.pen(); QColor penColor = pen.color(); if (penColor.alpha() != 255) penColor.setAlpha(255); pen.setColor(penColor); QBrush penBrush = pen.brush(); removeTransparencyFromBrush(penBrush); pen.setBrush(penBrush); d->pen = pen; } else { d->pen = state.pen(); } d->hasPen = d->pen.style() != Qt::NoPen; d->stroker.setPen(d->pen, state.renderHints()); QBrush penBrush = d->pen.brush(); bool cosmeticPen = qt_pen_is_cosmetic(d->pen, state.renderHints()); bool oldSimple = d->simplePen; d->simplePen = (d->hasPen && !cosmeticPen && (penBrush.style() == Qt::SolidPattern) && penBrush.isOpaque() && d->opacity == 1.0); if (oldSimple != d->simplePen) flags |= DirtyTransform; } else if (flags & DirtyHints) { d->stroker.setPen(d->pen, state.renderHints()); } if (flags & DirtyBrush) { if (d->pdfVersion == QPdfEngine::Version_A1b) { QBrush brush = state.brush(); removeTransparencyFromBrush(brush); d->brush = brush; } else { d->brush = state.brush(); } if (d->brush.color().alpha() == 0 && d->brush.style() == Qt::SolidPattern) d->brush.setStyle(Qt::NoBrush); d->hasBrush = d->brush.style() != Qt::NoBrush; } if (flags & DirtyBrushOrigin) { d->brushOrigin = state.brushOrigin(); flags |= DirtyBrush; } if (flags & DirtyOpacity) { d->opacity = state.opacity(); if (d->simplePen && d->opacity != 1.0) { d->simplePen = false; flags |= DirtyTransform; } } bool ce = d->clipEnabled; if (flags & DirtyClipPath) { d->clipEnabled = true; updateClipPath(state.clipPath(), state.clipOperation()); } else if (flags & DirtyClipRegion) { d->clipEnabled = true; QPainterPath path; for (const QRect &rect : state.clipRegion()) path.addRect(rect); updateClipPath(path, state.clipOperation()); flags |= DirtyClipPath; } else if (flags & DirtyClipEnabled) { d->clipEnabled = state.isClipEnabled(); } if (ce != d->clipEnabled) flags |= DirtyClipPath; else if (!d->clipEnabled) flags &= ~DirtyClipPath; setupGraphicsState(flags); } void QPdfEngine::setupGraphicsState(QPaintEngine::DirtyFlags flags) { Q_D(QPdfEngine); if (flags & DirtyClipPath) flags |= DirtyTransform|DirtyPen|DirtyBrush; if (flags & DirtyTransform) { *d->currentPage << "Q\n"; flags |= DirtyPen|DirtyBrush; } if (flags & DirtyClipPath) { *d->currentPage << "Q q\n"; d->allClipped = false; if (d->clipEnabled && !d->clips.isEmpty()) { for (int i = 0; i < d->clips.size(); ++i) { if (d->clips.at(i).isEmpty()) { d->allClipped = true; break; } } if (!d->allClipped) { for (int i = 0; i < d->clips.size(); ++i) { *d->currentPage << QPdf::generatePath(d->clips.at(i), QTransform(), QPdf::ClipPath); } } } } if (flags & DirtyTransform) { *d->currentPage << "q\n"; if (d->simplePen && !d->stroker.matrix.isIdentity()) *d->currentPage << QPdf::generateMatrix(d->stroker.matrix); } if (flags & DirtyBrush) setBrush(); if (d->simplePen && (flags & DirtyPen)) setPen(); } extern QPainterPath qt_regionToPath(const QRegion ®ion); void QPdfEngine::updateClipPath(const QPainterPath &p, Qt::ClipOperation op) { Q_D(QPdfEngine); QPainterPath path = d->stroker.matrix.map(p); //qDebug() << "updateClipPath: " << d->stroker.matrix << p.boundingRect() << path.boundingRect() << op; if (op == Qt::NoClip) { d->clipEnabled = false; d->clips.clear(); } else if (op == Qt::ReplaceClip) { d->clips.clear(); d->clips.append(path); } else if (op == Qt::IntersectClip) { d->clips.append(path); } else { // UniteClip // ask the painter for the current clipping path. that's the easiest solution path = painter()->clipPath(); path = d->stroker.matrix.map(path); d->clips.clear(); d->clips.append(path); } } void QPdfEngine::setPen() { Q_D(QPdfEngine); if (d->pen.style() == Qt::NoPen) return; QBrush b = d->pen.brush(); Q_ASSERT(b.style() == Qt::SolidPattern && b.isOpaque()); QColor rgba = b.color(); if (d->grayscale) { qreal gray = qGray(rgba.rgba())/255.; *d->currentPage << gray << gray << gray; } else { *d->currentPage << rgba.redF() << rgba.greenF() << rgba.blueF(); } *d->currentPage << "SCN\n"; *d->currentPage << d->pen.widthF() << "w "; int pdfCapStyle = 0; switch(d->pen.capStyle()) { case Qt::FlatCap: pdfCapStyle = 0; break; case Qt::SquareCap: pdfCapStyle = 2; break; case Qt::RoundCap: pdfCapStyle = 1; break; default: break; } *d->currentPage << pdfCapStyle << "J "; int pdfJoinStyle = 0; switch(d->pen.joinStyle()) { case Qt::MiterJoin: case Qt::SvgMiterJoin: *d->currentPage << qMax(qreal(1.0), d->pen.miterLimit()) << "M "; pdfJoinStyle = 0; break; case Qt::BevelJoin: pdfJoinStyle = 2; break; case Qt::RoundJoin: pdfJoinStyle = 1; break; default: break; } *d->currentPage << pdfJoinStyle << "j "; *d->currentPage << QPdf::generateDashes(d->pen); } void QPdfEngine::setBrush() { Q_D(QPdfEngine); Qt::BrushStyle style = d->brush.style(); if (style == Qt::NoBrush) return; bool specifyColor; int gStateObject = 0; int patternObject = d->addBrushPattern(d->stroker.matrix, &specifyColor, &gStateObject); if (!patternObject && !specifyColor) return; *d->currentPage << (patternObject ? "/PCSp cs " : "/CSp cs "); if (specifyColor) { QColor rgba = d->brush.color(); if (d->grayscale) { qreal gray = qGray(rgba.rgba())/255.; *d->currentPage << gray << gray << gray; } else { *d->currentPage << rgba.redF() << rgba.greenF() << rgba.blueF(); } } if (patternObject) *d->currentPage << "/Pat" << patternObject; *d->currentPage << "scn\n"; if (gStateObject) *d->currentPage << "/GState" << gStateObject << "gs\n"; else *d->currentPage << "/GSa gs\n"; } bool QPdfEngine::newPage() { Q_D(QPdfEngine); if (!isActive()) return false; d->newPage(); setupGraphicsState(DirtyBrush|DirtyPen|DirtyClipPath); QFile *outfile = qobject_cast (d->outDevice); if (outfile && outfile->error() != QFile::NoError) return false; return true; } QPaintEngine::Type QPdfEngine::type() const { return QPaintEngine::Pdf; } void QPdfEngine::setResolution(int resolution) { Q_D(QPdfEngine); d->resolution = resolution; } int QPdfEngine::resolution() const { Q_D(const QPdfEngine); return d->resolution; } void QPdfEngine::setPdfVersion(PdfVersion version) { Q_D(QPdfEngine); d->pdfVersion = version; } void QPdfEngine::setDocumentXmpMetadata(const QByteArray &xmpMetadata) { Q_D(QPdfEngine); d->xmpDocumentMetadata = xmpMetadata; } QByteArray QPdfEngine::documentXmpMetadata() const { Q_D(const QPdfEngine); return d->xmpDocumentMetadata; } void QPdfEngine::setPageLayout(const QPageLayout &pageLayout) { Q_D(QPdfEngine); d->m_pageLayout = pageLayout; } void QPdfEngine::setPageSize(const QPageSize &pageSize) { Q_D(QPdfEngine); d->m_pageLayout.setPageSize(pageSize); } void QPdfEngine::setPageOrientation(QPageLayout::Orientation orientation) { Q_D(QPdfEngine); d->m_pageLayout.setOrientation(orientation); } void QPdfEngine::setPageMargins(const QMarginsF &margins, QPageLayout::Unit units) { Q_D(QPdfEngine); d->m_pageLayout.setUnits(units); d->m_pageLayout.setMargins(margins); } QPageLayout QPdfEngine::pageLayout() const { Q_D(const QPdfEngine); return d->m_pageLayout; } // Metrics are in Device Pixels int QPdfEngine::metric(QPaintDevice::PaintDeviceMetric metricType) const { Q_D(const QPdfEngine); int val; switch (metricType) { case QPaintDevice::PdmWidth: val = d->m_pageLayout.paintRectPixels(d->resolution).width(); break; case QPaintDevice::PdmHeight: val = d->m_pageLayout.paintRectPixels(d->resolution).height(); break; case QPaintDevice::PdmDpiX: case QPaintDevice::PdmDpiY: val = d->resolution; break; case QPaintDevice::PdmPhysicalDpiX: case QPaintDevice::PdmPhysicalDpiY: val = 1200; break; case QPaintDevice::PdmWidthMM: val = qRound(d->m_pageLayout.paintRect(QPageLayout::Millimeter).width()); break; case QPaintDevice::PdmHeightMM: val = qRound(d->m_pageLayout.paintRect(QPageLayout::Millimeter).height()); break; case QPaintDevice::PdmNumColors: val = INT_MAX; break; case QPaintDevice::PdmDepth: val = 32; break; case QPaintDevice::PdmDevicePixelRatio: val = 1; break; case QPaintDevice::PdmDevicePixelRatioScaled: val = 1 * QPaintDevice::devicePixelRatioFScale(); break; default: qWarning("QPdfWriter::metric: Invalid metric command"); return 0; } return val; } QPdfEnginePrivate::QPdfEnginePrivate() : clipEnabled(false), allClipped(false), hasPen(true), hasBrush(false), simplePen(false), pdfVersion(QPdfEngine::Version_1_4), outDevice(nullptr), ownsDevice(false), embedFonts(true), grayscale(false), m_pageLayout(QPageSize(QPageSize::A4), QPageLayout::Portrait, QMarginsF(10, 10, 10, 10)) { initResources(); resolution = 1200; currentObject = 1; currentPage = nullptr; stroker.stream = nullptr; streampos = 0; stream = new QDataStream; } bool QPdfEngine::begin(QPaintDevice *pdev) { Q_D(QPdfEngine); d->pdev = pdev; if (!d->outDevice) { if (!d->outputFileName.isEmpty()) { QFile *file = new QFile(d->outputFileName); if (!file->open(QFile::WriteOnly|QFile::Truncate)) { delete file; return false; } d->outDevice = file; } else { return false; } d->ownsDevice = true; } d->currentObject = 1; d->currentPage = new QPdfPage; d->stroker.stream = d->currentPage; d->opacity = 1.0; d->stream->setDevice(d->outDevice); d->streampos = 0; d->hasPen = true; d->hasBrush = false; d->clipEnabled = false; d->allClipped = false; d->xrefPositions.clear(); d->pageRoot = 0; d->embeddedfilesRoot = 0; d->namesRoot = 0; d->catalog = 0; d->info = 0; d->graphicsState = 0; d->patternColorSpace = 0; d->simplePen = false; d->pages.clear(); d->imageCache.clear(); d->alphaCache.clear(); setActive(true); d->writeHeader(); newPage(); return true; } bool QPdfEngine::end() { Q_D(QPdfEngine); d->writeTail(); d->stream->setDevice(nullptr); qDeleteAll(d->fonts); d->fonts.clear(); delete d->currentPage; d->currentPage = nullptr; if (d->outDevice && d->ownsDevice) { d->outDevice->close(); delete d->outDevice; d->outDevice = nullptr; } d->fileCache.clear(); setActive(false); return true; } void QPdfEngine::addFileAttachment(const QString &fileName, const QByteArray &data, const QString &mimeType) { Q_D(QPdfEngine); d->fileCache.push_back({fileName, data, mimeType}); } QPdfEnginePrivate::~QPdfEnginePrivate() { qDeleteAll(fonts); delete currentPage; delete stream; } void QPdfEnginePrivate::writeHeader() { addXrefEntry(0,false); // Keep in sync with QPdfEngine::PdfVersion! static const char mapping[][4] = { "1.4", // Version_1_4 "1.4", // Version_A1b "1.6", // Version_1_6 }; static const size_t numMappings = sizeof mapping / sizeof *mapping; const char *verStr = mapping[size_t(pdfVersion) < numMappings ? pdfVersion : 0]; xprintf("%%PDF-%s\n", verStr); xprintf("%%\303\242\303\243\n"); writeInfo(); int metaDataObj = -1; int outputIntentObj = -1; if (pdfVersion == QPdfEngine::Version_A1b || !xmpDocumentMetadata.isEmpty()) { metaDataObj = writeXmpDcumentMetaData(); } if (pdfVersion == QPdfEngine::Version_A1b) { outputIntentObj = writeOutputIntent(); } catalog = addXrefEntry(-1); pageRoot = requestObject(); if (!fileCache.isEmpty()) { namesRoot = requestObject(); embeddedfilesRoot = requestObject(); } // catalog { QByteArray catalog; QPdf::ByteStream s(&catalog); s << "<<\n" << "/Type /Catalog\n" << "/Pages " << pageRoot << "0 R\n"; // Embedded files, if any if (!fileCache.isEmpty()) s << "/Names " << embeddedfilesRoot << "0 R\n"; if (pdfVersion == QPdfEngine::Version_A1b || !xmpDocumentMetadata.isEmpty()) s << "/Metadata " << metaDataObj << "0 R\n"; if (pdfVersion == QPdfEngine::Version_A1b) s << "/OutputIntents [" << outputIntentObj << "0 R]\n"; s << ">>\n" << "endobj\n"; write(catalog); } if (!fileCache.isEmpty()) { addXrefEntry(embeddedfilesRoot); xprintf("<>\n" "endobj\n", namesRoot); } // graphics state graphicsState = addXrefEntry(-1); xprintf("<<\n" "/Type /ExtGState\n" "/SA true\n" "/SM 0.02\n" "/ca 1.0\n" "/CA 1.0\n" "/AIS false\n" "/SMask /None" ">>\n" "endobj\n"); // color space for pattern patternColorSpace = addXrefEntry(-1); xprintf("[/Pattern /DeviceRGB]\n" "endobj\n"); } void QPdfEnginePrivate::writeInfo() { info = addXrefEntry(-1); xprintf("<<\n/Title "); printString(title); xprintf("\n/Creator "); printString(creator); xprintf("\n/Producer "); printString(QString::fromLatin1("Qt " QT_VERSION_STR)); QDateTime now = QDateTime::currentDateTime(); QTime t = now.time(); QDate d = now.date(); xprintf("\n/CreationDate (D:%d%02d%02d%02d%02d%02d", d.year(), d.month(), d.day(), t.hour(), t.minute(), t.second()); int offset = now.offsetFromUtc(); int hours = (offset / 60) / 60; int mins = (offset / 60) % 60; if (offset < 0) xprintf("-%02d'%02d')\n", -hours, -mins); else if (offset > 0) xprintf("+%02d'%02d')\n", hours , mins); else xprintf("Z)\n"); xprintf(">>\n" "endobj\n"); } int QPdfEnginePrivate::writeXmpDcumentMetaData() { const int metaDataObj = addXrefEntry(-1); QByteArray metaDataContent; if (xmpDocumentMetadata.isEmpty()) { const QString producer(QString::fromLatin1("Qt " QT_VERSION_STR)); const QDateTime now = QDateTime::currentDateTime(); const QDate date = now.date(); const QTime time = now.time(); const QString timeStr = QString::asprintf("%d-%02d-%02dT%02d:%02d:%02d", date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second()); const int offset = now.offsetFromUtc(); const int hours = (offset / 60) / 60; const int mins = (offset / 60) % 60; QString tzStr; if (offset < 0) tzStr = QString::asprintf("-%02d:%02d", -hours, -mins); else if (offset > 0) tzStr = QString::asprintf("+%02d:%02d", hours , mins); else tzStr = QLatin1String("Z"); const QString metaDataDate = timeStr + tzStr; QFile metaDataFile(QLatin1String(":/qpdf/qpdfa_metadata.xml")); metaDataFile.open(QIODevice::ReadOnly); metaDataContent = QString::fromUtf8(metaDataFile.readAll()).arg(producer.toHtmlEscaped(), title.toHtmlEscaped(), creator.toHtmlEscaped(), metaDataDate).toUtf8(); } else metaDataContent = xmpDocumentMetadata; xprintf("<<\n" "/Type /Metadata /Subtype /XML\n" "/Length %d\n" ">>\n" "stream\n", metaDataContent.size()); write(metaDataContent); xprintf("\nendstream\n" "endobj\n"); return metaDataObj; } int QPdfEnginePrivate::writeOutputIntent() { const int colorProfile = addXrefEntry(-1); { QFile colorProfileFile(QLatin1String(":/qpdf/sRGB2014.icc")); colorProfileFile.open(QIODevice::ReadOnly); const QByteArray colorProfileData = colorProfileFile.readAll(); QByteArray data; QPdf::ByteStream s(&data); int length_object = requestObject(); s << "<<\n"; s << "/N 3\n"; s << "/Alternate /DeviceRGB\n"; s << "/Length " << length_object << "0 R\n"; s << "/Filter /FlateDecode\n"; s << ">>\n"; s << "stream\n"; write(data); const int len = writeCompressed(colorProfileData); write("\nendstream\n" "endobj\n"); addXrefEntry(length_object); xprintf("%d\n" "endobj\n", len); } const int outputIntent = addXrefEntry(-1); { xprintf("<<\n"); xprintf("/Type /OutputIntent\n"); xprintf("/S/GTS_PDFA1\n"); xprintf("/OutputConditionIdentifier (sRGB_IEC61966-2-1_black_scaled)\n"); xprintf("/DestOutputProfile %d 0 R\n", colorProfile); xprintf("/Info(sRGB IEC61966 v2.1 with black scaling)\n"); xprintf("/RegistryName(http://www.color.org)\n"); xprintf(">>\n"); xprintf("endobj\n"); } return outputIntent; } void QPdfEnginePrivate::writePageRoot() { addXrefEntry(pageRoot); xprintf("<<\n" "/Type /Pages\n" "/Kids \n" "[\n"); int size = pages.size(); for (int i = 0; i < size; ++i) xprintf("%d 0 R\n", pages[i]); xprintf("]\n"); //xprintf("/Group <>\n"); xprintf("/Count %d\n", pages.size()); xprintf("/ProcSet [/PDF /Text /ImageB /ImageC]\n" ">>\n" "endobj\n"); } void QPdfEnginePrivate::writeAttachmentRoot() { if (fileCache.isEmpty()) return; QList attachments; const int size = fileCache.size(); for (int i = 0; i < size; ++i) { auto attachment = fileCache.at(i); const int attachmentID = addXrefEntry(-1); xprintf("<<\n"); if (do_compress) xprintf("/Filter /FlateDecode\n"); const int lenobj = requestObject(); xprintf("/Length %d 0 R\n", lenobj); int len = 0; xprintf(">>\nstream\n"); len = writeCompressed(attachment.data); xprintf("\nendstream\n" "endobj\n"); addXrefEntry(lenobj); xprintf("%d\n" "endobj\n", len); attachments.push_back(addXrefEntry(-1)); xprintf("<<\n" "/F (%s)", attachment.fileName.toLatin1().constData()); xprintf("\n/EF <>\n" "/Type/Filespec\n" , attachmentID); if (!attachment.mimeType.isEmpty()) xprintf("/Subtype/%s\n", attachment.mimeType.replace(QLatin1String("/"), QLatin1String("#2F")).toLatin1().constData()); xprintf(">>\nendobj\n"); } // names addXrefEntry(namesRoot); xprintf("<>\n" "endobj\n"); } void QPdfEnginePrivate::embedFont(QFontSubset *font) { //qDebug() << "embedFont" << font->object_id; int fontObject = font->object_id; QByteArray fontData = font->toTruetype(); #ifdef FONT_DUMP static int i = 0; QString fileName("font%1.ttf"); fileName = fileName.arg(i++); QFile ff(fileName); ff.open(QFile::WriteOnly); ff.write(fontData); ff.close(); #endif int fontDescriptor = requestObject(); int fontstream = requestObject(); int cidfont = requestObject(); int toUnicode = requestObject(); int cidset = requestObject(); QFontEngine::Properties properties = font->fontEngine->properties(); QByteArray postscriptName = properties.postscriptName.replace(' ', '_'); { qreal scale = 1000/properties.emSquare.toReal(); addXrefEntry(fontDescriptor); QByteArray descriptor; QPdf::ByteStream s(&descriptor); s << "<< /Type /FontDescriptor\n" "/FontName /Q"; int tag = fontDescriptor; for (int i = 0; i < 5; ++i) { s << (char)('A' + (tag % 26)); tag /= 26; } s << '+' << postscriptName << "\n" "/Flags " << 4 << "\n" "/FontBBox [" << properties.boundingBox.x()*scale << -(properties.boundingBox.y() + properties.boundingBox.height())*scale << (properties.boundingBox.x() + properties.boundingBox.width())*scale << -properties.boundingBox.y()*scale << "]\n" "/ItalicAngle " << properties.italicAngle.toReal() << "\n" "/Ascent " << properties.ascent.toReal()*scale << "\n" "/Descent " << -properties.descent.toReal()*scale << "\n" "/CapHeight " << properties.capHeight.toReal()*scale << "\n" "/StemV " << properties.lineWidth.toReal()*scale << "\n" "/FontFile2 " << fontstream << "0 R\n" "/CIDSet " << cidset << "0 R\n" ">>\nendobj\n"; write(descriptor); } { addXrefEntry(fontstream); QByteArray header; QPdf::ByteStream s(&header); int length_object = requestObject(); s << "<<\n" "/Length1 " << fontData.size() << "\n" "/Length " << length_object << "0 R\n"; if (do_compress) s << "/Filter /FlateDecode\n"; s << ">>\n" "stream\n"; write(header); int len = writeCompressed(fontData); write("\nendstream\n" "endobj\n"); addXrefEntry(length_object); xprintf("%d\n" "endobj\n", len); } { addXrefEntry(cidfont); QByteArray cid; QPdf::ByteStream s(&cid); s << "<< /Type /Font\n" "/Subtype /CIDFontType2\n" "/BaseFont /" << postscriptName << "\n" "/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>\n" "/FontDescriptor " << fontDescriptor << "0 R\n" "/CIDToGIDMap /Identity\n" << font->widthArray() << ">>\n" "endobj\n"; write(cid); } { addXrefEntry(toUnicode); QByteArray touc = font->createToUnicodeMap(); xprintf("<< /Length %d >>\n" "stream\n", touc.length()); write(touc); write("\nendstream\n" "endobj\n"); } { addXrefEntry(fontObject); QByteArray font; QPdf::ByteStream s(&font); s << "<< /Type /Font\n" "/Subtype /Type0\n" "/BaseFont /" << postscriptName << "\n" "/Encoding /Identity-H\n" "/DescendantFonts [" << cidfont << "0 R]\n" "/ToUnicode " << toUnicode << "0 R" ">>\n" "endobj\n"; write(font); } { QByteArray cidSetStream(font->nGlyphs() / 8 + 1, 0); int byteCounter = 0; int bitCounter = 0; for (int i = 0; i < font->nGlyphs(); ++i) { cidSetStream.data()[byteCounter] |= (1 << (7 - bitCounter)); bitCounter++; if (bitCounter == 8) { bitCounter = 0; byteCounter++; } } addXrefEntry(cidset); xprintf("<<\n"); xprintf("/Length %d\n", cidSetStream.size()); xprintf(">>\n"); xprintf("stream\n"); write(cidSetStream); xprintf("\nendstream\n"); xprintf("endobj\n"); } } qreal QPdfEnginePrivate::calcUserUnit() const { // PDF standards < 1.6 support max 200x200in pages (no UserUnit) if (pdfVersion < QPdfEngine::Version_1_6) return 1.0; const int maxLen = qMax(currentPage->pageSize.width(), currentPage->pageSize.height()); if (maxLen <= 14400) return 1.0; // for pages up to 200x200in (14400x14400 units) use default scaling // for larger pages, rescale units so we can have up to 381x381km return qMin(maxLen / 14400.0, 75000.0); } void QPdfEnginePrivate::writeFonts() { for (QHash::iterator it = fonts.begin(); it != fonts.end(); ++it) { embedFont(*it); delete *it; } fonts.clear(); } void QPdfEnginePrivate::writePage() { if (pages.empty()) return; *currentPage << "Q Q\n"; uint pageStream = requestObject(); uint pageStreamLength = requestObject(); uint resources = requestObject(); uint annots = requestObject(); qreal userUnit = calcUserUnit(); addXrefEntry(pages.constLast()); xprintf("<<\n" "/Type /Page\n" "/Parent %d 0 R\n" "/Contents %d 0 R\n" "/Resources %d 0 R\n" "/Annots %d 0 R\n" "/MediaBox [0 0 %s %s]\n", pageRoot, pageStream, resources, annots, // make sure we use the pagesize from when we started the page, since the user may have changed it QByteArray::number(currentPage->pageSize.width() / userUnit, 'f').constData(), QByteArray::number(currentPage->pageSize.height() / userUnit, 'f').constData()); if (pdfVersion >= QPdfEngine::Version_1_6) xprintf("/UserUnit %s\n", QByteArray::number(userUnit, 'f').constData()); xprintf(">>\n" "endobj\n"); addXrefEntry(resources); xprintf("<<\n" "/ColorSpace <<\n" "/PCSp %d 0 R\n" "/CSp /DeviceRGB\n" "/CSpg /DeviceGray\n" ">>\n" "/ExtGState <<\n" "/GSa %d 0 R\n", patternColorSpace, graphicsState); for (int i = 0; i < currentPage->graphicStates.size(); ++i) xprintf("/GState%d %d 0 R\n", currentPage->graphicStates.at(i), currentPage->graphicStates.at(i)); xprintf(">>\n"); xprintf("/Pattern <<\n"); for (int i = 0; i < currentPage->patterns.size(); ++i) xprintf("/Pat%d %d 0 R\n", currentPage->patterns.at(i), currentPage->patterns.at(i)); xprintf(">>\n"); xprintf("/Font <<\n"); for (int i = 0; i < currentPage->fonts.size();++i) xprintf("/F%d %d 0 R\n", currentPage->fonts[i], currentPage->fonts[i]); xprintf(">>\n"); xprintf("/XObject <<\n"); for (int i = 0; iimages.size(); ++i) { xprintf("/Im%d %d 0 R\n", currentPage->images.at(i), currentPage->images.at(i)); } xprintf(">>\n"); xprintf(">>\n" "endobj\n"); addXrefEntry(annots); xprintf("[ "); for (int i = 0; iannotations.size(); ++i) { xprintf("%d 0 R ", currentPage->annotations.at(i)); } xprintf("]\nendobj\n"); addXrefEntry(pageStream); xprintf("<<\n" "/Length %d 0 R\n", pageStreamLength); // object number for stream length object if (do_compress) xprintf("/Filter /FlateDecode\n"); xprintf(">>\n"); xprintf("stream\n"); QIODevice *content = currentPage->stream(); int len = writeCompressed(content); xprintf("\nendstream\n" "endobj\n"); addXrefEntry(pageStreamLength); xprintf("%d\nendobj\n",len); } void QPdfEnginePrivate::writeTail() { writePage(); writeFonts(); writePageRoot(); writeAttachmentRoot(); addXrefEntry(xrefPositions.size(),false); xprintf("xref\n" "0 %d\n" "%010d 65535 f \n", xrefPositions.size()-1, xrefPositions[0]); for (int i = 1; i < xrefPositions.size()-1; ++i) xprintf("%010d 00000 n \n", xrefPositions[i]); { QByteArray trailer; QPdf::ByteStream s(&trailer); s << "trailer\n" << "<<\n" << "/Size " << xrefPositions.size() - 1 << "\n" << "/Info " << info << "0 R\n" << "/Root " << catalog << "0 R\n"; if (pdfVersion == QPdfEngine::Version_A1b) { const QString uniqueId = QUuid::createUuid().toString(); const QByteArray fileIdentifier = QCryptographicHash::hash(uniqueId.toLatin1(), QCryptographicHash::Md5).toHex(); s << "/ID [ <" << fileIdentifier << "> <" << fileIdentifier << "> ]\n"; } s << ">>\n" << "startxref\n" << xrefPositions.constLast() << "\n" << "%%EOF\n"; write(trailer); } } int QPdfEnginePrivate::addXrefEntry(int object, bool printostr) { if (object < 0) object = requestObject(); if (object>=xrefPositions.size()) xrefPositions.resize(object+1); xrefPositions[object] = streampos; if (printostr) xprintf("%d 0 obj\n",object); return object; } void QPdfEnginePrivate::printString(const QString &string) { if (string.isEmpty()) { write("()"); return; } // The 'text string' type in PDF is encoded either as PDFDocEncoding, or // Unicode UTF-16 with a Unicode byte order mark as the first character // (0xfeff), with the high-order byte first. QByteArray array("(\xfe\xff"); const ushort *utf16 = string.utf16(); for (int i=0; i < string.size(); ++i) { char part[2] = {char((*(utf16 + i)) >> 8), char((*(utf16 + i)) & 0xff)}; for(int j=0; j < 2; ++j) { if (part[j] == '(' || part[j] == ')' || part[j] == '\\') array.append('\\'); array.append(part[j]); } } array.append(')'); write(array); } void QPdfEnginePrivate::xprintf(const char* fmt, ...) { if (!stream) return; const int msize = 10000; char buf[msize]; va_list args; va_start(args, fmt); int bufsize = qvsnprintf(buf, msize, fmt, args); va_end(args); if (Q_LIKELY(bufsize < msize)) { stream->writeRawData(buf, bufsize); } else { // Fallback for abnormal cases QScopedArrayPointer tmpbuf(new char[bufsize + 1]); va_start(args, fmt); bufsize = qvsnprintf(tmpbuf.data(), bufsize + 1, fmt, args); va_end(args); stream->writeRawData(tmpbuf.data(), bufsize); } streampos += bufsize; } int QPdfEnginePrivate::writeCompressed(QIODevice *dev) { #ifndef QT_NO_COMPRESS if (do_compress) { int size = QPdfPage::chunkSize(); int sum = 0; ::z_stream zStruct; zStruct.zalloc = Z_NULL; zStruct.zfree = Z_NULL; zStruct.opaque = Z_NULL; if (::deflateInit(&zStruct, Z_DEFAULT_COMPRESSION) != Z_OK) { qWarning("QPdfStream::writeCompressed: Error in deflateInit()"); return sum; } zStruct.avail_in = 0; QByteArray in, out; out.resize(size); while (!dev->atEnd() || zStruct.avail_in != 0) { if (zStruct.avail_in == 0) { in = dev->read(size); zStruct.avail_in = in.size(); zStruct.next_in = reinterpret_cast(in.data()); if (in.size() <= 0) { qWarning("QPdfStream::writeCompressed: Error in read()"); ::deflateEnd(&zStruct); return sum; } } zStruct.next_out = reinterpret_cast(out.data()); zStruct.avail_out = out.size(); if (::deflate(&zStruct, 0) != Z_OK) { qWarning("QPdfStream::writeCompressed: Error in deflate()"); ::deflateEnd(&zStruct); return sum; } int written = out.size() - zStruct.avail_out; stream->writeRawData(out.constData(), written); streampos += written; sum += written; } int ret; do { zStruct.next_out = reinterpret_cast(out.data()); zStruct.avail_out = out.size(); ret = ::deflate(&zStruct, Z_FINISH); if (ret != Z_OK && ret != Z_STREAM_END) { qWarning("QPdfStream::writeCompressed: Error in deflate()"); ::deflateEnd(&zStruct); return sum; } int written = out.size() - zStruct.avail_out; stream->writeRawData(out.constData(), written); streampos += written; sum += written; } while (ret == Z_OK); ::deflateEnd(&zStruct); return sum; } else #endif { QByteArray arr; int sum = 0; while (!dev->atEnd()) { arr = dev->read(QPdfPage::chunkSize()); stream->writeRawData(arr.constData(), arr.size()); streampos += arr.size(); sum += arr.size(); } return sum; } } int QPdfEnginePrivate::writeCompressed(const char *src, int len) { #ifndef QT_NO_COMPRESS if(do_compress) { uLongf destLen = len + len/100 + 13; // zlib requirement Bytef* dest = new Bytef[destLen]; if (Z_OK == ::compress(dest, &destLen, (const Bytef*) src, (uLongf)len)) { stream->writeRawData((const char*)dest, destLen); } else { qWarning("QPdfStream::writeCompressed: Error in compress()"); destLen = 0; } delete [] dest; len = destLen; } else #endif { stream->writeRawData(src,len); } streampos += len; return len; } int QPdfEnginePrivate::writeImage(const QByteArray &data, int width, int height, int depth, int maskObject, int softMaskObject, bool dct, bool isMono) { int image = addXrefEntry(-1); xprintf("<<\n" "/Type /XObject\n" "/Subtype /Image\n" "/Width %d\n" "/Height %d\n", width, height); if (depth == 1) { if (!isMono) { xprintf("/ImageMask true\n" "/Decode [1 0]\n"); } else { xprintf("/BitsPerComponent 1\n" "/ColorSpace /DeviceGray\n"); } } else { xprintf("/BitsPerComponent 8\n" "/ColorSpace %s\n", (depth == 32) ? "/DeviceRGB" : "/DeviceGray"); } if (maskObject > 0) xprintf("/Mask %d 0 R\n", maskObject); if (softMaskObject > 0) xprintf("/SMask %d 0 R\n", softMaskObject); int lenobj = requestObject(); xprintf("/Length %d 0 R\n", lenobj); if (interpolateImages) xprintf("/Interpolate true\n"); int len = 0; if (dct) { //qDebug("DCT"); xprintf("/Filter /DCTDecode\n>>\nstream\n"); write(data); len = data.length(); } else { if (do_compress) xprintf("/Filter /FlateDecode\n>>\nstream\n"); else xprintf(">>\nstream\n"); len = writeCompressed(data); } xprintf("\nendstream\n" "endobj\n"); addXrefEntry(lenobj); xprintf("%d\n" "endobj\n", len); return image; } struct QGradientBound { qreal start; qreal stop; int function; bool reverse; }; Q_DECLARE_TYPEINFO(QGradientBound, Q_PRIMITIVE_TYPE); int QPdfEnginePrivate::createShadingFunction(const QGradient *gradient, int from, int to, bool reflect, bool alpha) { QGradientStops stops = gradient->stops(); if (stops.isEmpty()) { stops << QGradientStop(0, Qt::black); stops << QGradientStop(1, Qt::white); } if (stops.at(0).first > 0) stops.prepend(QGradientStop(0, stops.at(0).second)); if (stops.at(stops.size() - 1).first < 1) stops.append(QGradientStop(1, stops.at(stops.size() - 1).second)); QList functions; const int numStops = stops.size(); functions.reserve(numStops - 1); for (int i = 0; i < numStops - 1; ++i) { int f = addXrefEntry(-1); QByteArray data; QPdf::ByteStream s(&data); s << "<<\n" "/FunctionType 2\n" "/Domain [0 1]\n" "/N 1\n"; if (alpha) { s << "/C0 [" << stops.at(i).second.alphaF() << "]\n" "/C1 [" << stops.at(i + 1).second.alphaF() << "]\n"; } else { s << "/C0 [" << stops.at(i).second.redF() << stops.at(i).second.greenF() << stops.at(i).second.blueF() << "]\n" "/C1 [" << stops.at(i + 1).second.redF() << stops.at(i + 1).second.greenF() << stops.at(i + 1).second.blueF() << "]\n"; } s << ">>\n" "endobj\n"; write(data); functions << f; } QList gradientBounds; gradientBounds.reserve((to - from) * (numStops - 1)); for (int step = from; step < to; ++step) { if (reflect && step % 2) { for (int i = numStops - 1; i > 0; --i) { QGradientBound b; b.start = step + 1 - qBound(qreal(0.), stops.at(i).first, qreal(1.)); b.stop = step + 1 - qBound(qreal(0.), stops.at(i - 1).first, qreal(1.)); b.function = functions.at(i - 1); b.reverse = true; gradientBounds << b; } } else { for (int i = 0; i < numStops - 1; ++i) { QGradientBound b; b.start = step + qBound(qreal(0.), stops.at(i).first, qreal(1.)); b.stop = step + qBound(qreal(0.), stops.at(i + 1).first, qreal(1.)); b.function = functions.at(i); b.reverse = false; gradientBounds << b; } } } // normalize bounds to [0..1] qreal bstart = gradientBounds.at(0).start; qreal bend = gradientBounds.at(gradientBounds.size() - 1).stop; qreal norm = 1./(bend - bstart); for (int i = 0; i < gradientBounds.size(); ++i) { gradientBounds[i].start = (gradientBounds[i].start - bstart)*norm; gradientBounds[i].stop = (gradientBounds[i].stop - bstart)*norm; } int function; if (gradientBounds.size() > 1) { function = addXrefEntry(-1); QByteArray data; QPdf::ByteStream s(&data); s << "<<\n" "/FunctionType 3\n" "/Domain [0 1]\n" "/Bounds ["; for (int i = 1; i < gradientBounds.size(); ++i) s << gradientBounds.at(i).start; s << "]\n" "/Encode ["; for (int i = 0; i < gradientBounds.size(); ++i) s << (gradientBounds.at(i).reverse ? "1 0 " : "0 1 "); s << "]\n" "/Functions ["; for (int i = 0; i < gradientBounds.size(); ++i) s << gradientBounds.at(i).function << "0 R "; s << "]\n" ">>\n" "endobj\n"; write(data); } else { function = functions.at(0); } return function; } int QPdfEnginePrivate::generateLinearGradientShader(const QLinearGradient *gradient, const QTransform &matrix, bool alpha) { QPointF start = gradient->start(); QPointF stop = gradient->finalStop(); QPointF offset = stop - start; Q_ASSERT(gradient->coordinateMode() == QGradient::LogicalMode); int from = 0; int to = 1; bool reflect = false; switch (gradient->spread()) { case QGradient::PadSpread: break; case QGradient::ReflectSpread: reflect = true; Q_FALLTHROUGH(); case QGradient::RepeatSpread: { // calculate required bounds QRectF pageRect = m_pageLayout.fullRectPixels(resolution); QTransform inv = matrix.inverted(); QPointF page_rect[4] = { inv.map(pageRect.topLeft()), inv.map(pageRect.topRight()), inv.map(pageRect.bottomLeft()), inv.map(pageRect.bottomRight()) }; qreal length = offset.x()*offset.x() + offset.y()*offset.y(); // find the max and min values in offset and orth direction that are needed to cover // the whole page from = INT_MAX; to = INT_MIN; for (int i = 0; i < 4; ++i) { qreal off = ((page_rect[i].x() - start.x()) * offset.x() + (page_rect[i].y() - start.y()) * offset.y())/length; from = qMin(from, qFloor(off)); to = qMax(to, qCeil(off)); } stop = start + to * offset; start = start + from * offset;\ break; } } int function = createShadingFunction(gradient, from, to, reflect, alpha); QByteArray shader; QPdf::ByteStream s(&shader); s << "<<\n" "/ShadingType 2\n" "/ColorSpace " << (alpha ? "/DeviceGray\n" : "/DeviceRGB\n") << "/AntiAlias true\n" "/Coords [" << start.x() << start.y() << stop.x() << stop.y() << "]\n" "/Extend [true true]\n" "/Function " << function << "0 R\n" ">>\n" "endobj\n"; int shaderObject = addXrefEntry(-1); write(shader); return shaderObject; } int QPdfEnginePrivate::generateRadialGradientShader(const QRadialGradient *gradient, const QTransform &matrix, bool alpha) { QPointF p1 = gradient->center(); qreal r1 = gradient->centerRadius(); QPointF p0 = gradient->focalPoint(); qreal r0 = gradient->focalRadius(); Q_ASSERT(gradient->coordinateMode() == QGradient::LogicalMode); int from = 0; int to = 1; bool reflect = false; switch (gradient->spread()) { case QGradient::PadSpread: break; case QGradient::ReflectSpread: reflect = true; Q_FALLTHROUGH(); case QGradient::RepeatSpread: { Q_ASSERT(qFuzzyIsNull(r0)); // QPainter emulates if this is not 0 QRectF pageRect = m_pageLayout.fullRectPixels(resolution); QTransform inv = matrix.inverted(); QPointF page_rect[4] = { inv.map(pageRect.topLeft()), inv.map(pageRect.topRight()), inv.map(pageRect.bottomLeft()), inv.map(pageRect.bottomRight()) }; // increase to until the whole page fits into it bool done = false; while (!done) { QPointF center = QPointF(p0.x() + to*(p1.x() - p0.x()), p0.y() + to*(p1.y() - p0.y())); double radius = r0 + to*(r1 - r0); double r2 = radius*radius; done = true; for (int i = 0; i < 4; ++i) { QPointF off = page_rect[i] - center; if (off.x()*off.x() + off.y()*off.y() > r2) { ++to; done = false; break; } } } p1 = QPointF(p0.x() + to*(p1.x() - p0.x()), p0.y() + to*(p1.y() - p0.y())); r1 = r0 + to*(r1 - r0); break; } } int function = createShadingFunction(gradient, from, to, reflect, alpha); QByteArray shader; QPdf::ByteStream s(&shader); s << "<<\n" "/ShadingType 3\n" "/ColorSpace " << (alpha ? "/DeviceGray\n" : "/DeviceRGB\n") << "/AntiAlias true\n" "/Domain [0 1]\n" "/Coords [" << p0.x() << p0.y() << r0 << p1.x() << p1.y() << r1 << "]\n" "/Extend [true true]\n" "/Function " << function << "0 R\n" ">>\n" "endobj\n"; int shaderObject = addXrefEntry(-1); write(shader); return shaderObject; } int QPdfEnginePrivate::generateGradientShader(const QGradient *gradient, const QTransform &matrix, bool alpha) { switch (gradient->type()) { case QGradient::LinearGradient: return generateLinearGradientShader(static_cast(gradient), matrix, alpha); case QGradient::RadialGradient: return generateRadialGradientShader(static_cast(gradient), matrix, alpha); case QGradient::ConicalGradient: Q_UNIMPLEMENTED(); // ### Implement me! break; case QGradient::NoGradient: break; } return 0; } int QPdfEnginePrivate::gradientBrush(const QBrush &b, const QTransform &matrix, int *gStateObject) { const QGradient *gradient = b.gradient(); if (!gradient || gradient->coordinateMode() != QGradient::LogicalMode) return 0; QRectF pageRect = m_pageLayout.fullRectPixels(resolution); QTransform m = b.transform() * matrix; int shaderObject = generateGradientShader(gradient, m); QByteArray str; QPdf::ByteStream s(&str); s << "<<\n" "/Type /Pattern\n" "/PatternType 2\n" "/Shading " << shaderObject << "0 R\n" "/Matrix [" << m.m11() << m.m12() << m.m21() << m.m22() << m.dx() << m.dy() << "]\n"; s << ">>\n" "endobj\n"; int patternObj = addXrefEntry(-1); write(str); currentPage->patterns.append(patternObj); if (!b.isOpaque()) { bool ca = true; QGradientStops stops = gradient->stops(); int a = stops.at(0).second.alpha(); for (int i = 1; i < stops.size(); ++i) { if (stops.at(i).second.alpha() != a) { ca = false; break; } } if (ca) { *gStateObject = addConstantAlphaObject(stops.at(0).second.alpha()); } else { int alphaShaderObject = generateGradientShader(gradient, m, true); QByteArray content; QPdf::ByteStream c(&content); c << "/Shader" << alphaShaderObject << "sh\n"; QByteArray form; QPdf::ByteStream f(&form); f << "<<\n" "/Type /XObject\n" "/Subtype /Form\n" "/BBox [0 0 " << pageRect.width() << pageRect.height() << "]\n" "/Group <>\n" "/Resources <<\n" "/Shading << /Shader" << alphaShaderObject << alphaShaderObject << "0 R >>\n" ">>\n"; f << "/Length " << content.length() << "\n" ">>\n" "stream\n" << content << "\nendstream\n" "endobj\n"; int softMaskFormObject = addXrefEntry(-1); write(form); *gStateObject = addXrefEntry(-1); xprintf("<< /SMask << /S /Alpha /G %d 0 R >> >>\n" "endobj\n", softMaskFormObject); currentPage->graphicStates.append(*gStateObject); } } return patternObj; } int QPdfEnginePrivate::addConstantAlphaObject(int brushAlpha, int penAlpha) { if (brushAlpha == 255 && penAlpha == 255) return 0; int object = alphaCache.value(QPair(brushAlpha, penAlpha), 0); if (!object) { object = addXrefEntry(-1); QByteArray alphaDef; QPdf::ByteStream s(&alphaDef); s << "<<\n/ca " << (brushAlpha/qreal(255.)) << '\n'; s << "/CA " << (penAlpha/qreal(255.)) << "\n>>"; xprintf("%s\nendobj\n", alphaDef.constData()); alphaCache.insert(QPair(brushAlpha, penAlpha), object); } if (currentPage->graphicStates.indexOf(object) < 0) currentPage->graphicStates.append(object); return object; } int QPdfEnginePrivate::addBrushPattern(const QTransform &m, bool *specifyColor, int *gStateObject) { Q_Q(QPdfEngine); int paintType = 2; // Uncolored tiling int w = 8; int h = 8; *specifyColor = true; *gStateObject = 0; QTransform matrix = m; matrix.translate(brushOrigin.x(), brushOrigin.y()); matrix = matrix * pageMatrix(); //qDebug() << brushOrigin << matrix; Qt::BrushStyle style = brush.style(); if (style == Qt::LinearGradientPattern || style == Qt::RadialGradientPattern) {// && style <= Qt::ConicalGradientPattern) { *specifyColor = false; return gradientBrush(brush, matrix, gStateObject); } if ((!brush.isOpaque() && brush.style() < Qt::LinearGradientPattern) || opacity != 1.0) *gStateObject = addConstantAlphaObject(qRound(brush.color().alpha() * opacity), qRound(pen.color().alpha() * opacity)); int imageObject = -1; QByteArray pattern = QPdf::patternForBrush(brush); if (pattern.isEmpty()) { if (brush.style() != Qt::TexturePattern) return 0; QImage image = brush.textureImage(); bool bitmap = true; const bool lossless = q->painter()->testRenderHint(QPainter::LosslessImageRendering); imageObject = addImage(image, &bitmap, lossless, image.cacheKey()); if (imageObject != -1) { QImage::Format f = image.format(); if (f != QImage::Format_MonoLSB && f != QImage::Format_Mono) { paintType = 1; // Colored tiling *specifyColor = false; } w = image.width(); h = image.height(); QTransform m(w, 0, 0, -h, 0, h); QPdf::ByteStream s(&pattern); s << QPdf::generateMatrix(m); s << "/Im" << imageObject << " Do\n"; } } QByteArray str; QPdf::ByteStream s(&str); s << "<<\n" "/Type /Pattern\n" "/PatternType 1\n" "/PaintType " << paintType << "\n" "/TilingType 1\n" "/BBox [0 0 " << w << h << "]\n" "/XStep " << w << "\n" "/YStep " << h << "\n" "/Matrix [" << matrix.m11() << matrix.m12() << matrix.m21() << matrix.m22() << matrix.dx() << matrix.dy() << "]\n" "/Resources \n<< "; // open resource tree if (imageObject > 0) { s << "/XObject << /Im" << imageObject << ' ' << imageObject << "0 R >> "; } s << ">>\n" "/Length " << pattern.length() << "\n" ">>\n" "stream\n" << pattern << "\nendstream\n" "endobj\n"; int patternObj = addXrefEntry(-1); write(str); currentPage->patterns.append(patternObj); return patternObj; } static inline bool is_monochrome(const QList &colorTable) { return colorTable.size() == 2 && colorTable.at(0) == QColor(Qt::black).rgba() && colorTable.at(1) == QColor(Qt::white).rgba() ; } /*! * Adds an image to the pdf and return the pdf-object id. Returns -1 if adding the image failed. */ int QPdfEnginePrivate::addImage(const QImage &img, bool *bitmap, bool lossless, qint64 serial_no) { if (img.isNull()) return -1; int object = imageCache.value(serial_no); if(object) return object; QImage image = img; QImage::Format format = image.format(); if (pdfVersion == QPdfEngine::Version_A1b) { if (image.hasAlphaChannel()) { // transparent images are not allowed in PDF/A-1b, so we convert it to // a format without alpha channel first QImage alphaLessImage(image.width(), image.height(), QImage::Format_RGB32); alphaLessImage.fill(Qt::white); QPainter p(&alphaLessImage); p.drawImage(0, 0, image); image = alphaLessImage; format = image.format(); } } if (image.depth() == 1 && *bitmap && is_monochrome(img.colorTable())) { if (format == QImage::Format_MonoLSB) image = image.convertToFormat(QImage::Format_Mono); format = QImage::Format_Mono; } else { *bitmap = false; if (format != QImage::Format_RGB32 && format != QImage::Format_ARGB32) { image = image.convertToFormat(QImage::Format_ARGB32); format = QImage::Format_ARGB32; } } int w = image.width(); int h = image.height(); int d = image.depth(); if (format == QImage::Format_Mono) { int bytesPerLine = (w + 7) >> 3; QByteArray data; data.resize(bytesPerLine * h); char *rawdata = data.data(); for (int y = 0; y < h; ++y) { memcpy(rawdata, image.constScanLine(y), bytesPerLine); rawdata += bytesPerLine; } object = writeImage(data, w, h, d, 0, 0, false, is_monochrome(img.colorTable())); } else { QByteArray softMaskData; bool dct = false; QByteArray imageData; bool hasAlpha = false; bool hasMask = false; if (QImageWriter::supportedImageFormats().contains("jpeg") && !grayscale && !lossless) { QBuffer buffer(&imageData); QImageWriter writer(&buffer, "jpeg"); writer.setQuality(94); writer.write(image); dct = true; if (format != QImage::Format_RGB32) { softMaskData.resize(w * h); uchar *sdata = (uchar *)softMaskData.data(); for (int y = 0; y < h; ++y) { const QRgb *rgb = (const QRgb *)image.constScanLine(y); for (int x = 0; x < w; ++x) { uchar alpha = qAlpha(*rgb); *sdata++ = alpha; hasMask |= (alpha < 255); hasAlpha |= (alpha != 0 && alpha != 255); ++rgb; } } } } else { imageData.resize(grayscale ? w * h : 3 * w * h); uchar *data = (uchar *)imageData.data(); softMaskData.resize(w * h); uchar *sdata = (uchar *)softMaskData.data(); for (int y = 0; y < h; ++y) { const QRgb *rgb = (const QRgb *)image.constScanLine(y); if (grayscale) { for (int x = 0; x < w; ++x) { *(data++) = qGray(*rgb); uchar alpha = qAlpha(*rgb); *sdata++ = alpha; hasMask |= (alpha < 255); hasAlpha |= (alpha != 0 && alpha != 255); ++rgb; } } else { for (int x = 0; x < w; ++x) { *(data++) = qRed(*rgb); *(data++) = qGreen(*rgb); *(data++) = qBlue(*rgb); uchar alpha = qAlpha(*rgb); *sdata++ = alpha; hasMask |= (alpha < 255); hasAlpha |= (alpha != 0 && alpha != 255); ++rgb; } } } if (format == QImage::Format_RGB32) hasAlpha = hasMask = false; } int maskObject = 0; int softMaskObject = 0; if (hasAlpha) { softMaskObject = writeImage(softMaskData, w, h, 8, 0, 0); } else if (hasMask) { // dither the soft mask to 1bit and add it. This also helps PDF viewers // without transparency support int bytesPerLine = (w + 7) >> 3; QByteArray mask(bytesPerLine * h, 0); uchar *mdata = (uchar *)mask.data(); const uchar *sdata = (const uchar *)softMaskData.constData(); for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { if (*sdata) mdata[x>>3] |= (0x80 >> (x&7)); ++sdata; } mdata += bytesPerLine; } maskObject = writeImage(mask, w, h, 1, 0, 0); } object = writeImage(imageData, w, h, grayscale ? 8 : 32, maskObject, softMaskObject, dct); } imageCache.insert(serial_no, object); return object; } void QPdfEnginePrivate::drawTextItem(const QPointF &p, const QTextItemInt &ti) { Q_Q(QPdfEngine); if (ti.charFormat.isAnchor()) { qreal size = ti.fontEngine->fontDef.pixelSize; int synthesized = ti.fontEngine->synthesized(); qreal stretch = synthesized & QFontEngine::SynthesizedStretch ? ti.fontEngine->fontDef.stretch/100. : 1.; Q_ASSERT(stretch > qreal(0)); QTransform trans; // Build text rendering matrix (Trm). We need it to map the text area to user // space units on the PDF page. trans = QTransform(size*stretch, 0, 0, size, 0, 0); // Apply text matrix (Tm). trans *= QTransform(1,0,0,-1,p.x(),p.y()); // Apply page displacement (Identity for first page). trans *= stroker.matrix; // Apply Current Transformation Matrix (CTM) trans *= pageMatrix(); qreal x1, y1, x2, y2; trans.map(0, 0, &x1, &y1); trans.map(ti.width.toReal()/size, (ti.ascent.toReal()-ti.descent.toReal())/size, &x2, &y2); uint annot = addXrefEntry(-1); QByteArray x1s, y1s, x2s, y2s; x1s.setNum(static_cast(x1), 'f'); y1s.setNum(static_cast(y1), 'f'); x2s.setNum(static_cast(x2), 'f'); y2s.setNum(static_cast(y2), 'f'); QByteArray rectData = x1s + ' ' + y1s + ' ' + x2s + ' ' + y2s; xprintf("<<\n/Type /Annot\n/Subtype /Link\n"); if (pdfVersion == QPdfEngine::Version_A1b) xprintf("/F 4\n"); // enable print flag, disable all other xprintf("/Rect ["); xprintf(rectData.constData()); #ifdef Q_DEBUG_PDF_LINKS xprintf("]\n/Border [16 16 1]\n/A <<\n"); #else xprintf("]\n/Border [0 0 0]\n/A <<\n"); #endif xprintf("/Type /Action\n/S /URI\n/URI (%s)\n", ti.charFormat.anchorHref().toLatin1().constData()); xprintf(">>\n>>\n"); xprintf("endobj\n"); if (!currentPage->annotations.contains(annot)) { currentPage->annotations.append(annot); } } QFontEngine *fe = ti.fontEngine; QFontEngine::FaceId face_id = fe->faceId(); bool noEmbed = false; if (!embedFonts || face_id.filename.isEmpty() || fe->fsType & 0x200 /* bitmap embedding only */ || fe->fsType == 2 /* no embedding allowed */) { *currentPage << "Q\n"; q->QPaintEngine::drawTextItem(p, ti); *currentPage << "q\n"; if (face_id.filename.isEmpty()) return; noEmbed = true; } QFontSubset *font = fonts.value(face_id, nullptr); if (!font) { font = new QFontSubset(fe, requestObject()); font->noEmbed = noEmbed; } fonts.insert(face_id, font); if (!currentPage->fonts.contains(font->object_id)) currentPage->fonts.append(font->object_id); qreal size = ti.fontEngine->fontDef.pixelSize; QVarLengthArray glyphs; QVarLengthArray positions; QTransform m = QTransform::fromTranslate(p.x(), p.y()); ti.fontEngine->getGlyphPositions(ti.glyphs, m, ti.flags, glyphs, positions); if (glyphs.size() == 0) return; int synthesized = ti.fontEngine->synthesized(); qreal stretch = synthesized & QFontEngine::SynthesizedStretch ? ti.fontEngine->fontDef.stretch/100. : 1.; Q_ASSERT(stretch > qreal(0)); *currentPage << "BT\n" << "/F" << font->object_id << size << "Tf " << stretch << (synthesized & QFontEngine::SynthesizedItalic ? "0 .3 -1 0 0 Tm\n" : "0 0 -1 0 0 Tm\n"); #if 0 // #### implement actual text for complex languages const unsigned short *logClusters = ti.logClusters; int pos = 0; do { int end = pos + 1; while (end < ti.num_chars && logClusters[end] == logClusters[pos]) ++end; *currentPage << "/Span << /ActualText >>\n" "BDC\n" "<"; int ge = end == ti.num_chars ? ti.num_glyphs : logClusters[end]; for (int gs = logClusters[pos]; gs < ge; ++gs) *currentPage << toHex((ushort)ti.glyphs[gs].glyph, buf); *currentPage << "> Tj\n" "EMC\n"; pos = end; } while (pos < ti.num_chars); #else qreal last_x = 0.; qreal last_y = 0.; for (int i = 0; i < glyphs.size(); ++i) { qreal x = positions[i].x.toReal(); qreal y = positions[i].y.toReal(); if (synthesized & QFontEngine::SynthesizedItalic) x += .3*y; x /= stretch; char buf[5]; int g = font->addGlyph(glyphs[i]); *currentPage << x - last_x << last_y - y << "Td <" << QPdf::toHex((ushort)g, buf) << "> Tj\n"; last_x = x; last_y = y; } if (synthesized & QFontEngine::SynthesizedBold) { *currentPage << stretch << (synthesized & QFontEngine::SynthesizedItalic ? "0 .3 -1 0 0 Tm\n" : "0 0 -1 0 0 Tm\n"); *currentPage << "/Span << /ActualText <> >> BDC\n"; last_x = 0.5*fe->lineThickness().toReal(); last_y = 0.; for (int i = 0; i < glyphs.size(); ++i) { qreal x = positions[i].x.toReal(); qreal y = positions[i].y.toReal(); if (synthesized & QFontEngine::SynthesizedItalic) x += .3*y; x /= stretch; char buf[5]; int g = font->addGlyph(glyphs[i]); *currentPage << x - last_x << last_y - y << "Td <" << QPdf::toHex((ushort)g, buf) << "> Tj\n"; last_x = x; last_y = y; } *currentPage << "EMC\n"; } #endif *currentPage << "ET\n"; } QTransform QPdfEnginePrivate::pageMatrix() const { qreal userUnit = calcUserUnit(); qreal scale = 72. / userUnit / resolution; QTransform tmp(scale, 0.0, 0.0, -scale, 0.0, m_pageLayout.fullRectPoints().height() / userUnit); if (m_pageLayout.mode() != QPageLayout::FullPageMode) { QRect r = m_pageLayout.paintRectPixels(resolution); tmp.translate(r.left(), r.top()); } return tmp; } void QPdfEnginePrivate::newPage() { if (currentPage && currentPage->pageSize.isEmpty()) currentPage->pageSize = m_pageLayout.fullRectPoints().size(); writePage(); delete currentPage; currentPage = new QPdfPage; currentPage->pageSize = m_pageLayout.fullRectPoints().size(); stroker.stream = currentPage; pages.append(requestObject()); *currentPage << "/GSa gs /CSp cs /CSp CS\n" << QPdf::generateMatrix(pageMatrix()) << "q q\n"; } QT_END_NAMESPACE #endif // QT_NO_PDF