1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qpdf_p.h"
5
6#ifndef QT_NO_PDF
7
8#include "qplatformdefs.h"
9
10#include <private/qcmyk_p.h>
11#include <private/qfont_p.h>
12#include <private/qmath_p.h>
13#include <private/qpainter_p.h>
14
15#include <qbuffer.h>
16#include <qcryptographichash.h>
17#include <qdatetime.h>
18#include <qdebug.h>
19#include <qfile.h>
20#include <qimagewriter.h>
21#include <qnumeric.h>
22#include <qtemporaryfile.h>
23#include <qtimezone.h>
24#include <quuid.h>
25#include <qxmlstream.h>
26
27#include <cstdio>
28#include <map>
29
30#ifndef QT_NO_COMPRESS
31#include <zlib.h>
32#endif
33
34#ifdef QT_NO_COMPRESS
35static const bool do_compress = false;
36#else
37static const bool do_compress = true;
38#endif
39
40// might be helpful for smooth transforms of images
41// Can't use it though, as gs generates completely wrong images if this is true.
42static const bool interpolateImages = false;
43
44static void initResources()
45{
46 Q_INIT_RESOURCE(qpdf);
47}
48
49QT_BEGIN_NAMESPACE
50
51using namespace Qt::StringLiterals;
52
53constexpr QPaintEngine::PaintEngineFeatures qt_pdf_decide_features()
54{
55 QPaintEngine::PaintEngineFeatures f = QPaintEngine::AllFeatures;
56 f &= ~(QPaintEngine::PorterDuff
57 | QPaintEngine::PerspectiveTransform
58 | QPaintEngine::ObjectBoundingModeGradients
59 | QPaintEngine::ConicalGradientFill);
60 return f;
61}
62
63extern bool qt_isExtendedRadialGradient(const QBrush &brush);
64
65// helper function to remove transparency from brush in PDF/A-1b mode
66static void removeTransparencyFromBrush(QBrush &brush)
67{
68 if (brush.style() == Qt::SolidPattern) {
69 QColor color = brush.color();
70 if (color.alpha() != 255) {
71 color.setAlpha(255);
72 brush.setColor(color);
73 }
74
75 return;
76 }
77
78 if (qt_isExtendedRadialGradient(brush)) {
79 brush = QBrush(Qt::black); // the safest we can do so far...
80 return;
81 }
82
83 if (brush.style() == Qt::LinearGradientPattern
84 || brush.style() == Qt::RadialGradientPattern
85 || brush.style() == Qt::ConicalGradientPattern) {
86
87 QGradientStops stops = brush.gradient()->stops();
88 for (int i = 0; i < stops.size(); ++i) {
89 if (stops[i].second.alpha() != 255)
90 stops[i].second.setAlpha(255);
91 }
92
93 const_cast<QGradient*>(brush.gradient())->setStops(stops);
94 return;
95 }
96
97 if (brush.style() == Qt::TexturePattern) {
98 // handled inside QPdfEnginePrivate::addImage() already
99 return;
100 }
101}
102
103
104/* also adds a space at the end of the number */
105const char *qt_real_to_string(qreal val, char *buf) {
106 const char *ret = buf;
107
108 if (!qIsFinite(d: val) || std::abs(x: val) > std::numeric_limits<quint32>::max()) {
109 *(buf++) = '0';
110 *(buf++) = ' ';
111 *buf = 0;
112 return ret;
113 }
114
115 if (val < 0) {
116 *(buf++) = '-';
117 val = -val;
118 }
119 qreal frac = std::modf(x: val, iptr: &val);
120 quint32 ival(val);
121
122 int ifrac = (int)(frac * 1000000000);
123 if (ifrac == 1000000000) {
124 ++ival;
125 ifrac = 0;
126 }
127 char output[256];
128 int i = 0;
129 while (ival) {
130 output[i] = '0' + (ival % 10);
131 ++i;
132 ival /= 10;
133 }
134 int fact = 100000000;
135 if (i == 0) {
136 *(buf++) = '0';
137 } else {
138 while (i) {
139 *(buf++) = output[--i];
140 fact /= 10;
141 ifrac /= 10;
142 }
143 }
144
145 if (ifrac) {
146 *(buf++) = '.';
147 while (fact) {
148 *(buf++) = '0' + ((ifrac/fact) % 10);
149 fact /= 10;
150 }
151 }
152 *(buf++) = ' ';
153 *buf = 0;
154 return ret;
155}
156
157const char *qt_int_to_string(int val, char *buf) {
158 const char *ret = buf;
159 if (val < 0) {
160 *(buf++) = '-';
161 val = -val;
162 }
163 char output[256];
164 int i = 0;
165 while (val) {
166 output[i] = '0' + (val % 10);
167 ++i;
168 val /= 10;
169 }
170 if (i == 0) {
171 *(buf++) = '0';
172 } else {
173 while (i)
174 *(buf++) = output[--i];
175 }
176 *(buf++) = ' ';
177 *buf = 0;
178 return ret;
179}
180
181
182namespace QPdf {
183 ByteStream::ByteStream(QByteArray *byteArray, bool fileBacking)
184 : dev(new QBuffer(byteArray)),
185 fileBackingEnabled(fileBacking),
186 fileBackingActive(false),
187 handleDirty(false)
188 {
189 dev->open(mode: QIODevice::ReadWrite | QIODevice::Append);
190 }
191
192 ByteStream::ByteStream(bool fileBacking)
193 : dev(new QBuffer(&ba)),
194 fileBackingEnabled(fileBacking),
195 fileBackingActive(false),
196 handleDirty(false)
197 {
198 dev->open(mode: QIODevice::ReadWrite);
199 }
200
201 ByteStream::~ByteStream()
202 {
203 delete dev;
204 }
205
206 ByteStream &ByteStream::operator <<(char chr)
207 {
208 if (handleDirty) prepareBuffer();
209 dev->write(data: &chr, len: 1);
210 return *this;
211 }
212
213 ByteStream &ByteStream::operator <<(const char *str)
214 {
215 if (handleDirty) prepareBuffer();
216 dev->write(data: str, len: strlen(s: str));
217 return *this;
218 }
219
220 ByteStream &ByteStream::operator <<(const QByteArray &str)
221 {
222 if (handleDirty) prepareBuffer();
223 dev->write(data: str);
224 return *this;
225 }
226
227 ByteStream &ByteStream::operator <<(const ByteStream &src)
228 {
229 Q_ASSERT(!src.dev->isSequential());
230 if (handleDirty) prepareBuffer();
231 // We do play nice here, even though it looks ugly.
232 // We save the position and restore it afterwards.
233 ByteStream &s = const_cast<ByteStream&>(src);
234 qint64 pos = s.dev->pos();
235 s.dev->reset();
236 while (!s.dev->atEnd()) {
237 QByteArray buf = s.dev->read(maxlen: chunkSize());
238 dev->write(data: buf);
239 }
240 s.dev->seek(pos);
241 return *this;
242 }
243
244 ByteStream &ByteStream::operator <<(qreal val) {
245 char buf[256];
246 qt_real_to_string(val, buf);
247 *this << buf;
248 return *this;
249 }
250
251 ByteStream &ByteStream::operator <<(int val) {
252 char buf[256];
253 qt_int_to_string(val, buf);
254 *this << buf;
255 return *this;
256 }
257
258 ByteStream &ByteStream::operator <<(const QPointF &p) {
259 char buf[256];
260 qt_real_to_string(val: p.x(), buf);
261 *this << buf;
262 qt_real_to_string(val: p.y(), buf);
263 *this << buf;
264 return *this;
265 }
266
267 QIODevice *ByteStream::stream()
268 {
269 dev->reset();
270 handleDirty = true;
271 return dev;
272 }
273
274 void ByteStream::clear()
275 {
276 dev->open(mode: QIODevice::ReadWrite | QIODevice::Truncate);
277 }
278
279 void ByteStream::prepareBuffer()
280 {
281 Q_ASSERT(!dev->isSequential());
282 qint64 size = dev->size();
283 if (fileBackingEnabled && !fileBackingActive
284 && size > maxMemorySize()) {
285 // Switch to file backing.
286 QTemporaryFile *newFile = new QTemporaryFile;
287 if (newFile->open()) {
288 dev->reset();
289 while (!dev->atEnd()) {
290 QByteArray buf = dev->read(maxlen: chunkSize());
291 newFile->write(data: buf);
292 }
293 delete dev;
294 dev = newFile;
295 ba.clear();
296 fileBackingActive = true;
297 }
298 }
299 if (dev->pos() != size) {
300 dev->seek(pos: size);
301 handleDirty = false;
302 }
303 }
304}
305
306#define QT_PATH_ELEMENT(elm)
307
308QByteArray QPdf::generatePath(const QPainterPath &path, const QTransform &matrix, PathFlags flags)
309{
310 QByteArray result;
311 if (!path.elementCount())
312 return result;
313
314 ByteStream s(&result);
315
316 int start = -1;
317 for (int i = 0; i < path.elementCount(); ++i) {
318 const QPainterPath::Element &elm = path.elementAt(i);
319 switch (elm.type) {
320 case QPainterPath::MoveToElement:
321 if (start >= 0
322 && path.elementAt(i: start).x == path.elementAt(i: i-1).x
323 && path.elementAt(i: start).y == path.elementAt(i: i-1).y)
324 s << "h\n";
325 s << matrix.map(p: QPointF(elm.x, elm.y)) << "m\n";
326 start = i;
327 break;
328 case QPainterPath::LineToElement:
329 s << matrix.map(p: QPointF(elm.x, elm.y)) << "l\n";
330 break;
331 case QPainterPath::CurveToElement:
332 Q_ASSERT(path.elementAt(i+1).type == QPainterPath::CurveToDataElement);
333 Q_ASSERT(path.elementAt(i+2).type == QPainterPath::CurveToDataElement);
334 s << matrix.map(p: QPointF(elm.x, elm.y))
335 << matrix.map(p: QPointF(path.elementAt(i: i+1).x, path.elementAt(i: i+1).y))
336 << matrix.map(p: QPointF(path.elementAt(i: i+2).x, path.elementAt(i: i+2).y))
337 << "c\n";
338 i += 2;
339 break;
340 default:
341 qFatal(msg: "QPdf::generatePath(), unhandled type: %d", elm.type);
342 }
343 }
344 if (start >= 0
345 && path.elementAt(i: start).x == path.elementAt(i: path.elementCount()-1).x
346 && path.elementAt(i: start).y == path.elementAt(i: path.elementCount()-1).y)
347 s << "h\n";
348
349 Qt::FillRule fillRule = path.fillRule();
350
351 const char *op = "";
352 switch (flags) {
353 case ClipPath:
354 op = (fillRule == Qt::WindingFill) ? "W n\n" : "W* n\n";
355 break;
356 case FillPath:
357 op = (fillRule == Qt::WindingFill) ? "f\n" : "f*\n";
358 break;
359 case StrokePath:
360 op = "S\n";
361 break;
362 case FillAndStrokePath:
363 op = (fillRule == Qt::WindingFill) ? "B\n" : "B*\n";
364 break;
365 }
366 s << op;
367 return result;
368}
369
370QByteArray QPdf::generateMatrix(const QTransform &matrix)
371{
372 QByteArray result;
373 ByteStream s(&result);
374 s << matrix.m11()
375 << matrix.m12()
376 << matrix.m21()
377 << matrix.m22()
378 << matrix.dx()
379 << matrix.dy()
380 << "cm\n";
381 return result;
382}
383
384QByteArray QPdf::generateDashes(const QPen &pen)
385{
386 QByteArray result;
387 ByteStream s(&result);
388 s << '[';
389
390 QList<qreal> dasharray = pen.dashPattern();
391 qreal w = pen.widthF();
392 if (w < 0.001)
393 w = 1;
394 for (int i = 0; i < dasharray.size(); ++i) {
395 qreal dw = dasharray.at(i)*w;
396 if (dw < 0.0001) dw = 0.0001;
397 s << dw;
398 }
399 s << ']';
400 s << pen.dashOffset() * w;
401 s << " d\n";
402 return result;
403}
404
405
406
407static const char* const pattern_for_brush[] = {
408 nullptr, // NoBrush
409 nullptr, // SolidPattern
410 "0 J\n"
411 "6 w\n"
412 "[] 0 d\n"
413 "4 0 m\n"
414 "4 8 l\n"
415 "0 4 m\n"
416 "8 4 l\n"
417 "S\n", // Dense1Pattern
418
419 "0 J\n"
420 "2 w\n"
421 "[6 2] 1 d\n"
422 "0 0 m\n"
423 "0 8 l\n"
424 "8 0 m\n"
425 "8 8 l\n"
426 "S\n"
427 "[] 0 d\n"
428 "2 0 m\n"
429 "2 8 l\n"
430 "6 0 m\n"
431 "6 8 l\n"
432 "S\n"
433 "[6 2] -3 d\n"
434 "4 0 m\n"
435 "4 8 l\n"
436 "S\n", // Dense2Pattern
437
438 "0 J\n"
439 "2 w\n"
440 "[6 2] 1 d\n"
441 "0 0 m\n"
442 "0 8 l\n"
443 "8 0 m\n"
444 "8 8 l\n"
445 "S\n"
446 "[2 2] -1 d\n"
447 "2 0 m\n"
448 "2 8 l\n"
449 "6 0 m\n"
450 "6 8 l\n"
451 "S\n"
452 "[6 2] -3 d\n"
453 "4 0 m\n"
454 "4 8 l\n"
455 "S\n", // Dense3Pattern
456
457 "0 J\n"
458 "2 w\n"
459 "[2 2] 1 d\n"
460 "0 0 m\n"
461 "0 8 l\n"
462 "8 0 m\n"
463 "8 8 l\n"
464 "S\n"
465 "[2 2] -1 d\n"
466 "2 0 m\n"
467 "2 8 l\n"
468 "6 0 m\n"
469 "6 8 l\n"
470 "S\n"
471 "[2 2] 1 d\n"
472 "4 0 m\n"
473 "4 8 l\n"
474 "S\n", // Dense4Pattern
475
476 "0 J\n"
477 "2 w\n"
478 "[2 6] -1 d\n"
479 "0 0 m\n"
480 "0 8 l\n"
481 "8 0 m\n"
482 "8 8 l\n"
483 "S\n"
484 "[2 2] 1 d\n"
485 "2 0 m\n"
486 "2 8 l\n"
487 "6 0 m\n"
488 "6 8 l\n"
489 "S\n"
490 "[2 6] 3 d\n"
491 "4 0 m\n"
492 "4 8 l\n"
493 "S\n", // Dense5Pattern
494
495 "0 J\n"
496 "2 w\n"
497 "[2 6] -1 d\n"
498 "0 0 m\n"
499 "0 8 l\n"
500 "8 0 m\n"
501 "8 8 l\n"
502 "S\n"
503 "[2 6] 3 d\n"
504 "4 0 m\n"
505 "4 8 l\n"
506 "S\n", // Dense6Pattern
507
508 "0 J\n"
509 "2 w\n"
510 "[2 6] -1 d\n"
511 "0 0 m\n"
512 "0 8 l\n"
513 "8 0 m\n"
514 "8 8 l\n"
515 "S\n", // Dense7Pattern
516
517 "1 w\n"
518 "0 4 m\n"
519 "8 4 l\n"
520 "S\n", // HorPattern
521
522 "1 w\n"
523 "4 0 m\n"
524 "4 8 l\n"
525 "S\n", // VerPattern
526
527 "1 w\n"
528 "4 0 m\n"
529 "4 8 l\n"
530 "0 4 m\n"
531 "8 4 l\n"
532 "S\n", // CrossPattern
533
534 "1 w\n"
535 "-1 5 m\n"
536 "5 -1 l\n"
537 "3 9 m\n"
538 "9 3 l\n"
539 "S\n", // BDiagPattern
540
541 "1 w\n"
542 "-1 3 m\n"
543 "5 9 l\n"
544 "3 -1 m\n"
545 "9 5 l\n"
546 "S\n", // FDiagPattern
547
548 "1 w\n"
549 "-1 3 m\n"
550 "5 9 l\n"
551 "3 -1 m\n"
552 "9 5 l\n"
553 "-1 5 m\n"
554 "5 -1 l\n"
555 "3 9 m\n"
556 "9 3 l\n"
557 "S\n", // DiagCrossPattern
558};
559
560QByteArray QPdf::patternForBrush(const QBrush &b)
561{
562 int style = b.style();
563 if (style > Qt::DiagCrossPattern)
564 return QByteArray();
565 return pattern_for_brush[style];
566}
567
568
569static void moveToHook(qfixed x, qfixed y, void *data)
570{
571 QPdf::Stroker *t = (QPdf::Stroker *)data;
572 if (!t->first)
573 *t->stream << "h\n";
574 if (!t->cosmeticPen)
575 t->matrix.map(x, y, tx: &x, ty: &y);
576 *t->stream << x << y << "m\n";
577 t->first = false;
578}
579
580static void lineToHook(qfixed x, qfixed y, void *data)
581{
582 QPdf::Stroker *t = (QPdf::Stroker *)data;
583 if (!t->cosmeticPen)
584 t->matrix.map(x, y, tx: &x, ty: &y);
585 *t->stream << x << y << "l\n";
586}
587
588static void cubicToHook(qfixed c1x, qfixed c1y,
589 qfixed c2x, qfixed c2y,
590 qfixed ex, qfixed ey,
591 void *data)
592{
593 QPdf::Stroker *t = (QPdf::Stroker *)data;
594 if (!t->cosmeticPen) {
595 t->matrix.map(x: c1x, y: c1y, tx: &c1x, ty: &c1y);
596 t->matrix.map(x: c2x, y: c2y, tx: &c2x, ty: &c2y);
597 t->matrix.map(x: ex, y: ey, tx: &ex, ty: &ey);
598 }
599 *t->stream << c1x << c1y
600 << c2x << c2y
601 << ex << ey
602 << "c\n";
603}
604
605QPdf::Stroker::Stroker()
606 : stream(nullptr),
607 first(true),
608 dashStroker(&basicStroker)
609{
610 stroker = &basicStroker;
611 basicStroker.setMoveToHook(moveToHook);
612 basicStroker.setLineToHook(lineToHook);
613 basicStroker.setCubicToHook(cubicToHook);
614 cosmeticPen = true;
615 basicStroker.setStrokeWidth(.1);
616}
617
618void QPdf::Stroker::setPen(const QPen &pen, QPainter::RenderHints)
619{
620 if (pen.style() == Qt::NoPen) {
621 stroker = nullptr;
622 return;
623 }
624 qreal w = pen.widthF();
625 bool zeroWidth = w < 0.0001;
626 cosmeticPen = pen.isCosmetic();
627 if (zeroWidth)
628 w = .1;
629
630 basicStroker.setStrokeWidth(w);
631 basicStroker.setCapStyle(pen.capStyle());
632 basicStroker.setJoinStyle(pen.joinStyle());
633 basicStroker.setMiterLimit(pen.miterLimit());
634
635 QList<qreal> dashpattern = pen.dashPattern();
636 if (zeroWidth) {
637 for (int i = 0; i < dashpattern.size(); ++i)
638 dashpattern[i] *= 10.;
639 }
640 if (!dashpattern.isEmpty()) {
641 dashStroker.setDashPattern(dashpattern);
642 dashStroker.setDashOffset(pen.dashOffset());
643 stroker = &dashStroker;
644 } else {
645 stroker = &basicStroker;
646 }
647}
648
649void QPdf::Stroker::strokePath(const QPainterPath &path)
650{
651 if (!stroker)
652 return;
653 first = true;
654
655 stroker->strokePath(path, data: this, matrix: cosmeticPen ? matrix : QTransform());
656 *stream << "h f\n";
657}
658
659QByteArray QPdf::ascii85Encode(const QByteArray &input)
660{
661 int isize = input.size()/4*4;
662 QByteArray output;
663 output.resize(size: input.size()*5/4+7);
664 char *out = output.data();
665 const uchar *in = (const uchar *)input.constData();
666 for (int i = 0; i < isize; i += 4) {
667 uint val = (((uint)in[i])<<24) + (((uint)in[i+1])<<16) + (((uint)in[i+2])<<8) + (uint)in[i+3];
668 if (val == 0) {
669 *out = 'z';
670 ++out;
671 } else {
672 char base[5];
673 base[4] = val % 85;
674 val /= 85;
675 base[3] = val % 85;
676 val /= 85;
677 base[2] = val % 85;
678 val /= 85;
679 base[1] = val % 85;
680 val /= 85;
681 base[0] = val % 85;
682 *(out++) = base[0] + '!';
683 *(out++) = base[1] + '!';
684 *(out++) = base[2] + '!';
685 *(out++) = base[3] + '!';
686 *(out++) = base[4] + '!';
687 }
688 }
689 //write the last few bytes
690 int remaining = input.size() - isize;
691 if (remaining) {
692 uint val = 0;
693 for (int i = isize; i < input.size(); ++i)
694 val = (val << 8) + in[i];
695 val <<= 8*(4-remaining);
696 char base[5];
697 base[4] = val % 85;
698 val /= 85;
699 base[3] = val % 85;
700 val /= 85;
701 base[2] = val % 85;
702 val /= 85;
703 base[1] = val % 85;
704 val /= 85;
705 base[0] = val % 85;
706 for (int i = 0; i < remaining+1; ++i)
707 *(out++) = base[i] + '!';
708 }
709 *(out++) = '~';
710 *(out++) = '>';
711 output.resize(size: out-output.data());
712 return output;
713}
714
715const char *QPdf::toHex(ushort u, char *buffer)
716{
717 int i = 3;
718 while (i >= 0) {
719 ushort hex = (u & 0x000f);
720 if (hex < 0x0a)
721 buffer[i] = '0'+hex;
722 else
723 buffer[i] = 'A'+(hex-0x0a);
724 u = u >> 4;
725 i--;
726 }
727 buffer[4] = '\0';
728 return buffer;
729}
730
731const char *QPdf::toHex(uchar u, char *buffer)
732{
733 int i = 1;
734 while (i >= 0) {
735 ushort hex = (u & 0x000f);
736 if (hex < 0x0a)
737 buffer[i] = '0'+hex;
738 else
739 buffer[i] = 'A'+(hex-0x0a);
740 u = u >> 4;
741 i--;
742 }
743 buffer[2] = '\0';
744 return buffer;
745}
746
747
748QPdfPage::QPdfPage()
749 : QPdf::ByteStream(true) // Enable file backing
750{
751}
752
753void QPdfPage::streamImage(int w, int h, uint object)
754{
755 *this << w << "0 0 " << -h << "0 " << h << "cm /Im" << object << " Do\n";
756 if (!images.contains(t: object))
757 images.append(t: object);
758}
759
760
761QPdfEngine::QPdfEngine(QPdfEnginePrivate &dd)
762 : QPaintEngine(dd, qt_pdf_decide_features())
763{
764}
765
766QPdfEngine::QPdfEngine()
767 : QPaintEngine(*new QPdfEnginePrivate(), qt_pdf_decide_features())
768{
769}
770
771void QPdfEngine::setOutputFilename(const QString &filename)
772{
773 Q_D(QPdfEngine);
774 d->outputFileName = filename;
775}
776
777
778void QPdfEngine::drawPoints (const QPointF *points, int pointCount)
779{
780 if (!points)
781 return;
782
783 Q_D(QPdfEngine);
784 QPainterPath p;
785 for (int i=0; i!=pointCount;++i) {
786 p.moveTo(p: points[i]);
787 p.lineTo(p: points[i] + QPointF(0, 0.001));
788 }
789
790 bool hadBrush = d->hasBrush;
791 d->hasBrush = false;
792 drawPath(path: p);
793 d->hasBrush = hadBrush;
794}
795
796void QPdfEngine::drawLines (const QLineF *lines, int lineCount)
797{
798 if (!lines)
799 return;
800
801 Q_D(QPdfEngine);
802 QPainterPath p;
803 for (int i=0; i!=lineCount;++i) {
804 p.moveTo(p: lines[i].p1());
805 p.lineTo(p: lines[i].p2());
806 }
807 bool hadBrush = d->hasBrush;
808 d->hasBrush = false;
809 drawPath(path: p);
810 d->hasBrush = hadBrush;
811}
812
813void QPdfEngine::drawRects (const QRectF *rects, int rectCount)
814{
815 if (!rects)
816 return;
817
818 Q_D(QPdfEngine);
819
820 if (d->clipEnabled && d->allClipped)
821 return;
822 if (!d->hasPen && !d->hasBrush)
823 return;
824
825 if ((d->simplePen && !d->needsTransform) || !d->hasPen) {
826 // draw natively in this case for better output
827 if (!d->hasPen && d->needsTransform) // i.e. this is just a fillrect
828 *d->currentPage << "q\n" << QPdf::generateMatrix(matrix: d->stroker.matrix);
829 for (int i = 0; i < rectCount; ++i)
830 *d->currentPage << rects[i].x() << rects[i].y() << rects[i].width() << rects[i].height() << "re\n";
831 *d->currentPage << (d->hasPen ? (d->hasBrush ? "B\n" : "S\n") : "f\n");
832 if (!d->hasPen && d->needsTransform)
833 *d->currentPage << "Q\n";
834 } else {
835 QPainterPath p;
836 for (int i=0; i!=rectCount; ++i)
837 p.addRect(rect: rects[i]);
838 drawPath(path: p);
839 }
840}
841
842void QPdfEngine::drawPolygon(const QPointF *points, int pointCount, PolygonDrawMode mode)
843{
844 Q_D(QPdfEngine);
845
846 if (!points || !pointCount)
847 return;
848
849 bool hb = d->hasBrush;
850 QPainterPath p;
851
852 switch(mode) {
853 case OddEvenMode:
854 p.setFillRule(Qt::OddEvenFill);
855 break;
856 case ConvexMode:
857 case WindingMode:
858 p.setFillRule(Qt::WindingFill);
859 break;
860 case PolylineMode:
861 d->hasBrush = false;
862 break;
863 default:
864 break;
865 }
866
867 p.moveTo(p: points[0]);
868 for (int i = 1; i < pointCount; ++i)
869 p.lineTo(p: points[i]);
870
871 if (mode != PolylineMode)
872 p.closeSubpath();
873 drawPath(path: p);
874
875 d->hasBrush = hb;
876}
877
878void QPdfEngine::drawPath (const QPainterPath &p)
879{
880 Q_D(QPdfEngine);
881
882 if (d->clipEnabled && d->allClipped)
883 return;
884 if (!d->hasPen && !d->hasBrush)
885 return;
886
887 if (d->simplePen) {
888 // draw strokes natively in this case for better output
889 *d->currentPage << QPdf::generatePath(path: p, matrix: d->needsTransform ? d->stroker.matrix : QTransform(),
890 flags: d->hasBrush ? QPdf::FillAndStrokePath : QPdf::StrokePath);
891 } else {
892 if (d->hasBrush)
893 *d->currentPage << QPdf::generatePath(path: p, matrix: d->stroker.matrix, flags: QPdf::FillPath);
894 if (d->hasPen) {
895 *d->currentPage << "q\n";
896 QBrush b = d->brush;
897 d->brush = d->pen.brush();
898 setBrush();
899 d->stroker.strokePath(path: p);
900 *d->currentPage << "Q\n";
901 d->brush = b;
902 }
903 }
904}
905
906void QPdfEngine::drawPixmap (const QRectF &rectangle, const QPixmap &pixmap, const QRectF &sr)
907{
908 if (sr.isEmpty() || rectangle.isEmpty() || pixmap.isNull())
909 return;
910 Q_D(QPdfEngine);
911
912 QBrush b = d->brush;
913
914 QRect sourceRect = sr.toRect();
915 QPixmap pm = sourceRect != pixmap.rect() ? pixmap.copy(rect: sourceRect) : pixmap;
916 QImage image = pm.toImage();
917 bool bitmap = true;
918 const bool lossless = painter()->testRenderHint(hint: QPainter::LosslessImageRendering);
919 const int object = d->addImage(image, bitmap: &bitmap, lossless, serial_no: pm.cacheKey());
920 if (object < 0)
921 return;
922
923 *d->currentPage << "q\n";
924
925 if ((d->pdfVersion != QPdfEngine::Version_A1b) && (d->opacity != 1.0)) {
926 int stateObject = d->addConstantAlphaObject(brushAlpha: qRound(d: 255 * d->opacity), penAlpha: qRound(d: 255 * d->opacity));
927 if (stateObject)
928 *d->currentPage << "/GState" << stateObject << "gs\n";
929 else
930 *d->currentPage << "/GSa gs\n";
931 } else {
932 *d->currentPage << "/GSa gs\n";
933 }
934
935 *d->currentPage
936 << QPdf::generateMatrix(matrix: QTransform(rectangle.width() / sr.width(), 0, 0, rectangle.height() / sr.height(),
937 rectangle.x(), rectangle.y()) * (!d->needsTransform ? QTransform() : d->stroker.matrix));
938 if (bitmap) {
939 // set current pen as d->brush
940 d->brush = d->pen.brush();
941 }
942 setBrush();
943 d->currentPage->streamImage(w: image.width(), h: image.height(), object);
944 *d->currentPage << "Q\n";
945
946 d->brush = b;
947}
948
949void QPdfEngine::drawImage(const QRectF &rectangle, const QImage &image, const QRectF &sr, Qt::ImageConversionFlags)
950{
951 if (sr.isEmpty() || rectangle.isEmpty() || image.isNull())
952 return;
953 Q_D(QPdfEngine);
954
955 QRect sourceRect = sr.toRect();
956 QImage im = sourceRect != image.rect() ? image.copy(rect: sourceRect) : image;
957 bool bitmap = true;
958 const bool lossless = painter()->testRenderHint(hint: QPainter::LosslessImageRendering);
959 const int object = d->addImage(image: im, bitmap: &bitmap, lossless, serial_no: im.cacheKey());
960 if (object < 0)
961 return;
962
963 *d->currentPage << "q\n";
964
965 if ((d->pdfVersion != QPdfEngine::Version_A1b) && (d->opacity != 1.0)) {
966 int stateObject = d->addConstantAlphaObject(brushAlpha: qRound(d: 255 * d->opacity), penAlpha: qRound(d: 255 * d->opacity));
967 if (stateObject)
968 *d->currentPage << "/GState" << stateObject << "gs\n";
969 else
970 *d->currentPage << "/GSa gs\n";
971 } else {
972 *d->currentPage << "/GSa gs\n";
973 }
974
975 *d->currentPage
976 << QPdf::generateMatrix(matrix: QTransform(rectangle.width() / sr.width(), 0, 0, rectangle.height() / sr.height(),
977 rectangle.x(), rectangle.y()) * (!d->needsTransform ? QTransform() : d->stroker.matrix));
978 setBrush();
979 d->currentPage->streamImage(w: im.width(), h: im.height(), object);
980 *d->currentPage << "Q\n";
981}
982
983void QPdfEngine::drawTiledPixmap (const QRectF &rectangle, const QPixmap &pixmap, const QPointF &point)
984{
985 Q_D(QPdfEngine);
986
987 bool bitmap = (pixmap.depth() == 1);
988 QBrush b = d->brush;
989 QPointF bo = d->brushOrigin;
990 bool hp = d->hasPen;
991 d->hasPen = false;
992 bool hb = d->hasBrush;
993 d->hasBrush = true;
994
995 d->brush = QBrush(pixmap);
996 if (bitmap)
997 // #### fix bitmap case where we have a brush pen
998 d->brush.setColor(d->pen.color());
999
1000 d->brushOrigin = -point;
1001 *d->currentPage << "q\n";
1002 setBrush();
1003
1004 drawRects(rects: &rectangle, rectCount: 1);
1005 *d->currentPage << "Q\n";
1006
1007 d->hasPen = hp;
1008 d->hasBrush = hb;
1009 d->brush = b;
1010 d->brushOrigin = bo;
1011}
1012
1013void QPdfEngine::drawTextItem(const QPointF &p, const QTextItem &textItem)
1014{
1015 Q_D(QPdfEngine);
1016
1017 if (!d->hasPen || (d->clipEnabled && d->allClipped))
1018 return;
1019
1020 if (d->stroker.matrix.type() >= QTransform::TxProject) {
1021 QPaintEngine::drawTextItem(p, textItem);
1022 return;
1023 }
1024
1025 *d->currentPage << "q\n";
1026 if (d->needsTransform)
1027 *d->currentPage << QPdf::generateMatrix(matrix: d->stroker.matrix);
1028
1029 bool hp = d->hasPen;
1030 d->hasPen = false;
1031 QBrush b = d->brush;
1032 d->brush = d->pen.brush();
1033 setBrush();
1034
1035 const QTextItemInt &ti = static_cast<const QTextItemInt &>(textItem);
1036 Q_ASSERT(ti.fontEngine->type() != QFontEngine::Multi);
1037 d->drawTextItem(p, ti);
1038 d->hasPen = hp;
1039 d->brush = b;
1040 *d->currentPage << "Q\n";
1041}
1042
1043// Used by QtWebKit
1044void QPdfEngine::drawHyperlink(const QRectF &r, const QUrl &url)
1045{
1046 Q_D(QPdfEngine);
1047
1048 // PDF/X-4 (§ 6.17) does not allow annotations that don't lie
1049 // outside the BleedBox/TrimBox, so don't emit an hyperlink
1050 // annotation at all.
1051 if (d->pdfVersion == QPdfEngine::Version_X4)
1052 return;
1053
1054 const uint annot = d->addXrefEntry(object: -1);
1055 const QByteArray urlascii = url.toEncoded();
1056 int len = urlascii.size();
1057 QVarLengthArray<char> url_esc;
1058 url_esc.reserve(sz: len + 1);
1059 for (int j = 0; j < len; j++) {
1060 if (urlascii[j] == '(' || urlascii[j] == ')' || urlascii[j] == '\\')
1061 url_esc.append(t: '\\');
1062 url_esc.append(t: urlascii[j]);
1063 }
1064 url_esc.append(t: '\0');
1065
1066 char buf[256];
1067 const QRectF rr = d->pageMatrix().mapRect(r);
1068 d->xprintf(fmt: "<<\n/Type /Annot\n/Subtype /Link\n");
1069
1070 if (d->pdfVersion == QPdfEngine::Version_A1b)
1071 d->xprintf(fmt: "/F 4\n"); // enable print flag, disable all other
1072
1073 d->xprintf(fmt: "/Rect [");
1074 d->xprintf(fmt: "%s ", qt_real_to_string(val: rr.left(), buf));
1075 d->xprintf(fmt: "%s ", qt_real_to_string(val: rr.top(), buf));
1076 d->xprintf(fmt: "%s ", qt_real_to_string(val: rr.right(), buf));
1077 d->xprintf(fmt: "%s", qt_real_to_string(val: rr.bottom(), buf));
1078 d->xprintf(fmt: "]\n/Border [0 0 0]\n/A <<\n");
1079 d->xprintf(fmt: "/Type /Action\n/S /URI\n/URI (%s)\n", url_esc.constData());
1080 d->xprintf(fmt: ">>\n>>\n");
1081 d->xprintf(fmt: "endobj\n");
1082 d->currentPage->annotations.append(t: annot);
1083}
1084
1085void QPdfEngine::updateState(const QPaintEngineState &state)
1086{
1087 Q_D(QPdfEngine);
1088
1089 QPaintEngine::DirtyFlags flags = state.state();
1090
1091 if (flags & DirtyHints)
1092 flags |= DirtyBrush;
1093
1094 if (flags & DirtyTransform)
1095 d->stroker.matrix = state.transform();
1096
1097 if (flags & DirtyPen) {
1098 if (d->pdfVersion == QPdfEngine::Version_A1b) {
1099 QPen pen = state.pen();
1100
1101 QColor penColor = pen.color();
1102 if (penColor.alpha() != 255)
1103 penColor.setAlpha(255);
1104 pen.setColor(penColor);
1105
1106 QBrush penBrush = pen.brush();
1107 removeTransparencyFromBrush(brush&: penBrush);
1108 pen.setBrush(penBrush);
1109
1110 d->pen = pen;
1111 } else {
1112 d->pen = state.pen();
1113 }
1114 d->hasPen = d->pen.style() != Qt::NoPen;
1115 bool oldCosmetic = d->stroker.cosmeticPen;
1116 d->stroker.setPen(pen: d->pen, state.renderHints());
1117 QBrush penBrush = d->pen.brush();
1118 bool oldSimple = d->simplePen;
1119 d->simplePen = (d->hasPen && (penBrush.style() == Qt::SolidPattern) && penBrush.isOpaque() && d->opacity == 1.0);
1120 if (oldSimple != d->simplePen || oldCosmetic != d->stroker.cosmeticPen)
1121 flags |= DirtyTransform;
1122 } else if (flags & DirtyHints) {
1123 d->stroker.setPen(pen: d->pen, state.renderHints());
1124 }
1125 if (flags & DirtyBrush) {
1126 if (d->pdfVersion == QPdfEngine::Version_A1b) {
1127 QBrush brush = state.brush();
1128 removeTransparencyFromBrush(brush);
1129 d->brush = brush;
1130 } else {
1131 d->brush = state.brush();
1132 }
1133 if (d->brush.color().alpha() == 0 && d->brush.style() == Qt::SolidPattern)
1134 d->brush.setStyle(Qt::NoBrush);
1135 d->hasBrush = d->brush.style() != Qt::NoBrush;
1136 }
1137 if (flags & DirtyBrushOrigin) {
1138 d->brushOrigin = state.brushOrigin();
1139 flags |= DirtyBrush;
1140 }
1141 if (flags & DirtyOpacity) {
1142 d->opacity = state.opacity();
1143 if (d->simplePen && d->opacity != 1.0) {
1144 d->simplePen = false;
1145 flags |= DirtyTransform;
1146 }
1147 }
1148
1149 bool ce = d->clipEnabled;
1150 if (flags & DirtyClipPath) {
1151 d->clipEnabled = true;
1152 updateClipPath(path: state.clipPath(), op: state.clipOperation());
1153 } else if (flags & DirtyClipRegion) {
1154 d->clipEnabled = true;
1155 QPainterPath path;
1156 for (const QRect &rect : state.clipRegion())
1157 path.addRect(rect);
1158 updateClipPath(path, op: state.clipOperation());
1159 flags |= DirtyClipPath;
1160 } else if (flags & DirtyClipEnabled) {
1161 d->clipEnabled = state.isClipEnabled();
1162 }
1163
1164 if (ce != d->clipEnabled)
1165 flags |= DirtyClipPath;
1166 else if (!d->clipEnabled)
1167 flags &= ~DirtyClipPath;
1168
1169 setupGraphicsState(flags);
1170}
1171
1172void QPdfEngine::setupGraphicsState(QPaintEngine::DirtyFlags flags)
1173{
1174 Q_D(QPdfEngine);
1175 if (flags & DirtyClipPath)
1176 flags |= DirtyTransform|DirtyPen|DirtyBrush;
1177
1178 if (flags & DirtyTransform) {
1179 *d->currentPage << "Q\n";
1180 flags |= DirtyPen|DirtyBrush;
1181 }
1182
1183 if (flags & DirtyClipPath) {
1184 *d->currentPage << "Q q\n";
1185
1186 d->allClipped = false;
1187 if (d->clipEnabled && !d->clips.isEmpty()) {
1188 for (int i = 0; i < d->clips.size(); ++i) {
1189 if (d->clips.at(i).isEmpty()) {
1190 d->allClipped = true;
1191 break;
1192 }
1193 }
1194 if (!d->allClipped) {
1195 for (int i = 0; i < d->clips.size(); ++i) {
1196 *d->currentPage << QPdf::generatePath(path: d->clips.at(i), matrix: QTransform(), flags: QPdf::ClipPath);
1197 }
1198 }
1199 }
1200 }
1201
1202 if (flags & DirtyTransform) {
1203 *d->currentPage << "q\n";
1204 d->needsTransform = false;
1205 if (!d->stroker.matrix.isIdentity()) {
1206 if (d->simplePen && !d->stroker.cosmeticPen)
1207 *d->currentPage << QPdf::generateMatrix(matrix: d->stroker.matrix);
1208 else
1209 d->needsTransform = true; // I.e. page-wide xf not set, local xf needed
1210 }
1211 }
1212 if (flags & DirtyBrush)
1213 setBrush();
1214 if (d->simplePen && (flags & DirtyPen))
1215 setPen();
1216}
1217
1218extern QPainterPath qt_regionToPath(const QRegion &region);
1219
1220void QPdfEngine::updateClipPath(const QPainterPath &p, Qt::ClipOperation op)
1221{
1222 Q_D(QPdfEngine);
1223 QPainterPath path = d->stroker.matrix.map(p);
1224 //qDebug() << "updateClipPath: " << d->stroker.matrix << p.boundingRect() << path.boundingRect() << op;
1225
1226 switch (op) {
1227 case Qt::NoClip:
1228 d->clipEnabled = false;
1229 d->clips.clear();
1230 break;
1231 case Qt::ReplaceClip:
1232 d->clips.clear();
1233 d->clips.append(t: path);
1234 break;
1235 case Qt::IntersectClip:
1236 d->clips.append(t: path);
1237 break;
1238 }
1239}
1240
1241void QPdfEngine::setPen()
1242{
1243 Q_D(QPdfEngine);
1244 if (d->pen.style() == Qt::NoPen)
1245 return;
1246 QBrush b = d->pen.brush();
1247 Q_ASSERT(b.style() == Qt::SolidPattern && b.isOpaque());
1248
1249 d->writeColor(domain: QPdfEnginePrivate::ColorDomain::Stroking, color: b.color());
1250 *d->currentPage << "SCN\n";
1251 *d->currentPage << d->pen.widthF() << "w ";
1252
1253 int pdfCapStyle = 0;
1254 switch(d->pen.capStyle()) {
1255 case Qt::FlatCap:
1256 pdfCapStyle = 0;
1257 break;
1258 case Qt::SquareCap:
1259 pdfCapStyle = 2;
1260 break;
1261 case Qt::RoundCap:
1262 pdfCapStyle = 1;
1263 break;
1264 default:
1265 break;
1266 }
1267 *d->currentPage << pdfCapStyle << "J ";
1268
1269 int pdfJoinStyle = 0;
1270 switch(d->pen.joinStyle()) {
1271 case Qt::MiterJoin:
1272 case Qt::SvgMiterJoin:
1273 *d->currentPage << qMax(a: qreal(1.0), b: d->pen.miterLimit()) << "M ";
1274 pdfJoinStyle = 0;
1275 break;
1276 case Qt::BevelJoin:
1277 pdfJoinStyle = 2;
1278 break;
1279 case Qt::RoundJoin:
1280 pdfJoinStyle = 1;
1281 break;
1282 default:
1283 break;
1284 }
1285 *d->currentPage << pdfJoinStyle << "j ";
1286
1287 *d->currentPage << QPdf::generateDashes(pen: d->pen);
1288}
1289
1290
1291void QPdfEngine::setBrush()
1292{
1293 Q_D(QPdfEngine);
1294 Qt::BrushStyle style = d->brush.style();
1295 if (style == Qt::NoBrush)
1296 return;
1297
1298 bool specifyColor;
1299 int gStateObject = 0;
1300 int patternObject = d->addBrushPattern(matrix: d->stroker.matrix, specifyColor: &specifyColor, gStateObject: &gStateObject);
1301 if (!patternObject && !specifyColor)
1302 return;
1303
1304 const auto domain = patternObject ? QPdfEnginePrivate::ColorDomain::NonStrokingPattern
1305 : QPdfEnginePrivate::ColorDomain::NonStroking;
1306 d->writeColor(domain, color: specifyColor ? d->brush.color() : QColor());
1307 if (patternObject)
1308 *d->currentPage << "/Pat" << patternObject;
1309 *d->currentPage << "scn\n";
1310
1311 if (gStateObject)
1312 *d->currentPage << "/GState" << gStateObject << "gs\n";
1313 else
1314 *d->currentPage << "/GSa gs\n";
1315}
1316
1317
1318bool QPdfEngine::newPage()
1319{
1320 Q_D(QPdfEngine);
1321 if (!isActive())
1322 return false;
1323 d->newPage();
1324
1325 setupGraphicsState(DirtyBrush|DirtyPen|DirtyClipPath);
1326 QFile *outfile = qobject_cast<QFile*> (object: d->outDevice);
1327 if (outfile && outfile->error() != QFile::NoError)
1328 return false;
1329 return true;
1330}
1331
1332QPaintEngine::Type QPdfEngine::type() const
1333{
1334 return QPaintEngine::Pdf;
1335}
1336
1337void QPdfEngine::setResolution(int resolution)
1338{
1339 Q_D(QPdfEngine);
1340 d->resolution = resolution;
1341}
1342
1343int QPdfEngine::resolution() const
1344{
1345 Q_D(const QPdfEngine);
1346 return d->resolution;
1347}
1348
1349void QPdfEngine::setPdfVersion(PdfVersion version)
1350{
1351 Q_D(QPdfEngine);
1352 d->pdfVersion = version;
1353}
1354
1355void QPdfEngine::setDocumentXmpMetadata(const QByteArray &xmpMetadata)
1356{
1357 Q_D(QPdfEngine);
1358 d->xmpDocumentMetadata = xmpMetadata;
1359}
1360
1361QByteArray QPdfEngine::documentXmpMetadata() const
1362{
1363 Q_D(const QPdfEngine);
1364 return d->xmpDocumentMetadata;
1365}
1366
1367void QPdfEngine::setPageLayout(const QPageLayout &pageLayout)
1368{
1369 Q_D(QPdfEngine);
1370 d->m_pageLayout = pageLayout;
1371}
1372
1373void QPdfEngine::setPageSize(const QPageSize &pageSize)
1374{
1375 Q_D(QPdfEngine);
1376 d->m_pageLayout.setPageSize(pageSize);
1377}
1378
1379void QPdfEngine::setPageOrientation(QPageLayout::Orientation orientation)
1380{
1381 Q_D(QPdfEngine);
1382 d->m_pageLayout.setOrientation(orientation);
1383}
1384
1385void QPdfEngine::setPageMargins(const QMarginsF &margins, QPageLayout::Unit units)
1386{
1387 Q_D(QPdfEngine);
1388 d->m_pageLayout.setUnits(units);
1389 d->m_pageLayout.setMargins(margins);
1390}
1391
1392QPageLayout QPdfEngine::pageLayout() const
1393{
1394 Q_D(const QPdfEngine);
1395 return d->m_pageLayout;
1396}
1397
1398// Metrics are in Device Pixels
1399int QPdfEngine::metric(QPaintDevice::PaintDeviceMetric metricType) const
1400{
1401 Q_D(const QPdfEngine);
1402 int val;
1403 switch (metricType) {
1404 case QPaintDevice::PdmWidth:
1405 val = d->m_pageLayout.paintRectPixels(resolution: d->resolution).width();
1406 break;
1407 case QPaintDevice::PdmHeight:
1408 val = d->m_pageLayout.paintRectPixels(resolution: d->resolution).height();
1409 break;
1410 case QPaintDevice::PdmDpiX:
1411 case QPaintDevice::PdmDpiY:
1412 val = d->resolution;
1413 break;
1414 case QPaintDevice::PdmPhysicalDpiX:
1415 case QPaintDevice::PdmPhysicalDpiY:
1416 val = 1200;
1417 break;
1418 case QPaintDevice::PdmWidthMM:
1419 val = qRound(d: d->m_pageLayout.paintRect(units: QPageLayout::Millimeter).width());
1420 break;
1421 case QPaintDevice::PdmHeightMM:
1422 val = qRound(d: d->m_pageLayout.paintRect(units: QPageLayout::Millimeter).height());
1423 break;
1424 case QPaintDevice::PdmNumColors:
1425 val = INT_MAX;
1426 break;
1427 case QPaintDevice::PdmDepth:
1428 val = 32;
1429 break;
1430 case QPaintDevice::PdmDevicePixelRatio:
1431 val = 1;
1432 break;
1433 case QPaintDevice::PdmDevicePixelRatioScaled:
1434 val = 1 * QPaintDevice::devicePixelRatioFScale();
1435 break;
1436 default:
1437 qWarning(msg: "QPdfWriter::metric: Invalid metric command");
1438 return 0;
1439 }
1440 return val;
1441}
1442
1443QPdfEnginePrivate::QPdfEnginePrivate()
1444 : clipEnabled(false), allClipped(false), hasPen(true), hasBrush(false), simplePen(false),
1445 needsTransform(false), pdfVersion(QPdfEngine::Version_1_4),
1446 colorModel(QPdfEngine::ColorModel::Auto),
1447 outDevice(nullptr), ownsDevice(false),
1448 embedFonts(true),
1449 m_pageLayout(QPageSize(QPageSize::A4), QPageLayout::Portrait, QMarginsF(10, 10, 10, 10))
1450{
1451 initResources();
1452 resolution = 1200;
1453 currentObject = 1;
1454 currentPage = nullptr;
1455 stroker.stream = nullptr;
1456
1457 streampos = 0;
1458
1459 stream = new QDataStream;
1460}
1461
1462bool QPdfEngine::begin(QPaintDevice *pdev)
1463{
1464 Q_D(QPdfEngine);
1465 d->pdev = pdev;
1466
1467 if (!d->outDevice) {
1468 if (!d->outputFileName.isEmpty()) {
1469 QFile *file = new QFile(d->outputFileName);
1470 if (!file->open(flags: QFile::WriteOnly|QFile::Truncate)) {
1471 delete file;
1472 return false;
1473 }
1474 d->outDevice = file;
1475 } else {
1476 return false;
1477 }
1478 d->ownsDevice = true;
1479 }
1480
1481 d->currentObject = 1;
1482
1483 d->currentPage = new QPdfPage;
1484 d->stroker.stream = d->currentPage;
1485 d->opacity = 1.0;
1486
1487 d->stream->setDevice(d->outDevice);
1488
1489 d->streampos = 0;
1490 d->hasPen = true;
1491 d->hasBrush = false;
1492 d->clipEnabled = false;
1493 d->allClipped = false;
1494
1495 d->xrefPositions.clear();
1496 d->pageRoot = 0;
1497 d->namesRoot = 0;
1498 d->destsRoot = 0;
1499 d->attachmentsRoot = 0;
1500 d->catalog = 0;
1501 d->info = 0;
1502 d->graphicsState = 0;
1503 d->patternColorSpaceRGB = 0;
1504 d->patternColorSpaceGrayscale = 0;
1505 d->patternColorSpaceCMYK = 0;
1506 d->simplePen = false;
1507 d->needsTransform = false;
1508
1509 d->pages.clear();
1510 d->imageCache.clear();
1511 d->alphaCache.clear();
1512
1513 setActive(true);
1514 d->writeHeader();
1515 newPage();
1516
1517 return true;
1518}
1519
1520bool QPdfEngine::end()
1521{
1522 Q_D(QPdfEngine);
1523 d->writeTail();
1524
1525 d->stream->setDevice(nullptr);
1526
1527 qDeleteAll(c: d->fonts);
1528 d->fonts.clear();
1529 delete d->currentPage;
1530 d->currentPage = nullptr;
1531
1532 if (d->outDevice && d->ownsDevice) {
1533 d->outDevice->close();
1534 delete d->outDevice;
1535 d->outDevice = nullptr;
1536 }
1537
1538 d->destCache.clear();
1539 d->fileCache.clear();
1540
1541 setActive(false);
1542 return true;
1543}
1544
1545void QPdfEngine::addFileAttachment(const QString &fileName, const QByteArray &data, const QString &mimeType)
1546{
1547 Q_D(QPdfEngine);
1548 d->fileCache.push_back(t: {fileName, data, mimeType});
1549}
1550
1551QPdfEnginePrivate::~QPdfEnginePrivate()
1552{
1553 qDeleteAll(c: fonts);
1554 delete currentPage;
1555 delete stream;
1556}
1557
1558void QPdfEnginePrivate::writeHeader()
1559{
1560 addXrefEntry(object: 0,printostr: false);
1561
1562 // Keep in sync with QPdfEngine::PdfVersion!
1563 static const char mapping[][4] = {
1564 "1.4", // Version_1_4
1565 "1.4", // Version_A1b
1566 "1.6", // Version_1_6
1567 "1.6", // Version_X4
1568 };
1569 static const size_t numMappings = sizeof mapping / sizeof *mapping;
1570 const char *verStr = mapping[size_t(pdfVersion) < numMappings ? pdfVersion : 0];
1571
1572 xprintf(fmt: "%%PDF-%s\n", verStr);
1573 xprintf(fmt: "%%\303\242\303\243\n");
1574
1575#if QT_CONFIG(timezone)
1576 const QDateTime now = QDateTime::currentDateTime(zone: QTimeZone::systemTimeZone());
1577#else
1578 const QDateTime now = QDateTime::currentDateTimeUtc();
1579#endif
1580
1581 writeInfo(date: now);
1582
1583 const int metaDataObj = writeXmpDocumentMetaData(date: now);
1584 const int outputIntentObj = [&]() {
1585 switch (pdfVersion) {
1586 case QPdfEngine::Version_1_4:
1587 case QPdfEngine::Version_1_6:
1588 break;
1589 case QPdfEngine::Version_A1b:
1590 case QPdfEngine::Version_X4:
1591 return writeOutputIntent();
1592 }
1593
1594 return -1;
1595 }();
1596
1597 catalog = addXrefEntry(object: -1);
1598 pageRoot = requestObject();
1599 namesRoot = requestObject();
1600
1601 // catalog
1602 {
1603 QByteArray catalog;
1604 QPdf::ByteStream s(&catalog);
1605 s << "<<\n"
1606 << "/Type /Catalog\n"
1607 << "/Pages " << pageRoot << "0 R\n"
1608 << "/Names " << namesRoot << "0 R\n";
1609
1610 s << "/Metadata " << metaDataObj << "0 R\n";
1611
1612 if (outputIntentObj >= 0)
1613 s << "/OutputIntents [" << outputIntentObj << "0 R]\n";
1614
1615 s << ">>\n"
1616 << "endobj\n";
1617
1618 write(data: catalog);
1619 }
1620
1621 // graphics state
1622 graphicsState = addXrefEntry(object: -1);
1623 xprintf(fmt: "<<\n"
1624 "/Type /ExtGState\n"
1625 "/SA true\n"
1626 "/SM 0.02\n"
1627 "/ca 1.0\n"
1628 "/CA 1.0\n"
1629 "/AIS false\n"
1630 "/SMask /None"
1631 ">>\n"
1632 "endobj\n");
1633
1634 // color spaces for pattern
1635 patternColorSpaceRGB = addXrefEntry(object: -1);
1636 xprintf(fmt: "[/Pattern /DeviceRGB]\n"
1637 "endobj\n");
1638 patternColorSpaceGrayscale = addXrefEntry(object: -1);
1639 xprintf(fmt: "[/Pattern /DeviceGray]\n"
1640 "endobj\n");
1641 patternColorSpaceCMYK = addXrefEntry(object: -1);
1642 xprintf(fmt: "[/Pattern /DeviceCMYK]\n"
1643 "endobj\n");
1644}
1645
1646QPdfEngine::ColorModel QPdfEnginePrivate::colorModelForColor(const QColor &color) const
1647{
1648 switch (colorModel) {
1649 case QPdfEngine::ColorModel::RGB:
1650 case QPdfEngine::ColorModel::Grayscale:
1651 case QPdfEngine::ColorModel::CMYK:
1652 return colorModel;
1653 case QPdfEngine::ColorModel::Auto:
1654 switch (color.spec()) {
1655 case QColor::Invalid:
1656 case QColor::Rgb:
1657 case QColor::Hsv:
1658 case QColor::Hsl:
1659 case QColor::ExtendedRgb:
1660 return QPdfEngine::ColorModel::RGB;
1661 case QColor::Cmyk:
1662 return QPdfEngine::ColorModel::CMYK;
1663 }
1664
1665 break;
1666 }
1667
1668 Q_UNREACHABLE_RETURN(QPdfEngine::ColorModel::RGB);
1669}
1670
1671void QPdfEnginePrivate::writeColor(ColorDomain domain, const QColor &color)
1672{
1673 // Switch to the right colorspace.
1674 // For simplicity: do it even if it redundant (= already in that colorspace)
1675 const QPdfEngine::ColorModel actualColorModel = colorModelForColor(color);
1676
1677 switch (actualColorModel) {
1678 case QPdfEngine::ColorModel::RGB:
1679 switch (domain) {
1680 case ColorDomain::Stroking:
1681 *currentPage << "/CSp CS\n"; break;
1682 case ColorDomain::NonStroking:
1683 *currentPage << "/CSp cs\n"; break;
1684 case ColorDomain::NonStrokingPattern:
1685 *currentPage << "/PCSp cs\n"; break;
1686 }
1687 break;
1688 case QPdfEngine::ColorModel::Grayscale:
1689 switch (domain) {
1690 case ColorDomain::Stroking:
1691 *currentPage << "/CSpg CS\n"; break;
1692 case ColorDomain::NonStroking:
1693 *currentPage << "/CSpg cs\n"; break;
1694 case ColorDomain::NonStrokingPattern:
1695 *currentPage << "/PCSpg cs\n"; break;
1696 }
1697 break;
1698 case QPdfEngine::ColorModel::CMYK:
1699 switch (domain) {
1700 case ColorDomain::Stroking:
1701 *currentPage << "/CSpcmyk CS\n"; break;
1702 case ColorDomain::NonStroking:
1703 *currentPage << "/CSpcmyk cs\n"; break;
1704 case ColorDomain::NonStrokingPattern:
1705 *currentPage << "/PCSpcmyk cs\n"; break;
1706 }
1707 break;
1708 case QPdfEngine::ColorModel::Auto:
1709 Q_UNREACHABLE_RETURN();
1710 }
1711
1712 // If we also have a color specified, write it out.
1713 if (!color.isValid())
1714 return;
1715
1716 switch (actualColorModel) {
1717 case QPdfEngine::ColorModel::RGB:
1718 *currentPage << color.redF()
1719 << color.greenF()
1720 << color.blueF();
1721 break;
1722 case QPdfEngine::ColorModel::Grayscale: {
1723 const qreal gray = qGray(rgb: color.rgba()) / 255.;
1724 *currentPage << gray;
1725 break;
1726 }
1727 case QPdfEngine::ColorModel::CMYK:
1728 *currentPage << color.cyanF()
1729 << color.magentaF()
1730 << color.yellowF()
1731 << color.blackF();
1732 break;
1733 case QPdfEngine::ColorModel::Auto:
1734 Q_UNREACHABLE_RETURN();
1735 }
1736}
1737
1738void QPdfEnginePrivate::writeInfo(const QDateTime &date)
1739{
1740 info = addXrefEntry(object: -1);
1741 write(data: "<<\n/Title ");
1742 printString(string: title);
1743 write(data: "\n/Creator ");
1744 printString(string: creator);
1745 write(data: "\n/Producer ");
1746 printString(string: QString::fromLatin1(ba: "Qt " QT_VERSION_STR));
1747
1748 const QTime t = date.time();
1749 const QDate d = date.date();
1750 // (D:YYYYMMDDHHmmSSOHH'mm')
1751 constexpr size_t formattedDateSize = 26;
1752 char formattedDate[formattedDateSize];
1753 const int year = qBound(min: 0, val: d.year(), max: 9999); // ASN.1, max 4 digits
1754 auto printedSize = std::snprintf(s: formattedDate,
1755 maxlen: formattedDateSize,
1756 format: "(D:%04d%02d%02d%02d%02d%02d",
1757 year,
1758 d.month(),
1759 d.day(),
1760 t.hour(),
1761 t.minute(),
1762 t.second());
1763 const int offset = date.offsetFromUtc();
1764 const int hours = (offset / 60) / 60;
1765 const int mins = (offset / 60) % 60;
1766 if (offset < 0) {
1767 std::snprintf(s: formattedDate + printedSize,
1768 maxlen: formattedDateSize - printedSize,
1769 format: "-%02d'%02d')", -hours, -mins);
1770 } else if (offset > 0) {
1771 std::snprintf(s: formattedDate + printedSize,
1772 maxlen: formattedDateSize - printedSize,
1773 format: "+%02d'%02d')", hours, mins);
1774 } else {
1775 std::snprintf(s: formattedDate + printedSize,
1776 maxlen: formattedDateSize - printedSize,
1777 format: "Z)");
1778 }
1779
1780 write(data: "\n/CreationDate ");
1781 write(data: formattedDate);
1782 write(data: "\n/ModDate ");
1783 write(data: formattedDate);
1784
1785 write(data: "\n/Trapped /False\n"
1786 "2\n"
1787 "endobj\n");
1788}
1789
1790int QPdfEnginePrivate::writeXmpDocumentMetaData(const QDateTime &date)
1791{
1792 const int metaDataObj = addXrefEntry(object: -1);
1793 QByteArray metaDataContent;
1794
1795 if (!xmpDocumentMetadata.isEmpty()) {
1796 metaDataContent = xmpDocumentMetadata;
1797 } else {
1798 const QString producer(QString::fromLatin1(ba: "Qt " QT_VERSION_STR));
1799 const QString metaDataDate = date.toString(format: Qt::ISODate);
1800
1801 using namespace Qt::Literals;
1802 constexpr QLatin1String xmlNS = "http://www.w3.org/XML/1998/namespace"_L1;
1803
1804 constexpr QLatin1String adobeNS = "adobe:ns:meta/"_L1;
1805 constexpr QLatin1String rdfNS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"_L1;
1806 constexpr QLatin1String dcNS = "http://purl.org/dc/elements/1.1/"_L1;
1807 constexpr QLatin1String xmpNS = "http://ns.adobe.com/xap/1.0/"_L1;
1808 constexpr QLatin1String xmpMMNS = "http://ns.adobe.com/xap/1.0/mm/"_L1;
1809 constexpr QLatin1String pdfNS = "http://ns.adobe.com/pdf/1.3/"_L1;
1810 constexpr QLatin1String pdfaidNS = "http://www.aiim.org/pdfa/ns/id/"_L1;
1811 constexpr QLatin1String pdfxidNS = "http://www.npes.org/pdfx/ns/id/"_L1;
1812
1813 QBuffer output(&metaDataContent);
1814 output.open(openMode: QIODevice::WriteOnly);
1815 output.write(data: "<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>");
1816
1817 QXmlStreamWriter w(&output);
1818 w.setAutoFormatting(true);
1819 w.writeNamespace(namespaceUri: adobeNS, prefix: "x");
1820 w.writeNamespace(namespaceUri: rdfNS, prefix: "rdf");
1821 w.writeNamespace(namespaceUri: dcNS, prefix: "dc");
1822 w.writeNamespace(namespaceUri: xmpNS, prefix: "xmp");
1823 w.writeNamespace(namespaceUri: xmpMMNS, prefix: "xmpMM");
1824 w.writeNamespace(namespaceUri: pdfNS, prefix: "pdf");
1825 w.writeNamespace(namespaceUri: pdfaidNS, prefix: "pdfaid");
1826 w.writeNamespace(namespaceUri: pdfxidNS, prefix: "pdfxid");
1827
1828 w.writeStartElement(namespaceUri: adobeNS, name: "xmpmeta");
1829 w.writeStartElement(namespaceUri: rdfNS, name: "RDF");
1830
1831 /*
1832 XMP says: "The recommended approach is to have either a
1833 single rdf:Description element containing all XMP
1834 properties or a separate rdf:Description element for each
1835 XMP property namespace."
1836 We do the the latter.
1837 */
1838
1839 // DC
1840 w.writeStartElement(namespaceUri: rdfNS, name: "Description");
1841 w.writeAttribute(namespaceUri: rdfNS, name: "about", value: "");
1842 w.writeStartElement(namespaceUri: dcNS, name: "title");
1843 w.writeStartElement(namespaceUri: rdfNS, name: "Alt");
1844 w.writeStartElement(namespaceUri: rdfNS, name: "li");
1845 w.writeAttribute(namespaceUri: xmlNS, name: "lang", value: "x-default");
1846 w.writeCharacters(text: title);
1847 w.writeEndElement();
1848 w.writeEndElement();
1849 w.writeEndElement();
1850 w.writeEndElement();
1851
1852 // PDF
1853 w.writeStartElement(namespaceUri: rdfNS, name: "Description");
1854 w.writeAttribute(namespaceUri: rdfNS, name: "about", value: "");
1855 w.writeAttribute(namespaceUri: pdfNS, name: "Producer", value: producer);
1856 w.writeAttribute(namespaceUri: pdfNS, name: "Trapped", value: "False");
1857 w.writeEndElement();
1858
1859 // XMP
1860 w.writeStartElement(namespaceUri: rdfNS, name: "Description");
1861 w.writeAttribute(namespaceUri: rdfNS, name: "about", value: "");
1862 w.writeAttribute(namespaceUri: xmpNS, name: "CreatorTool", value: creator);
1863 w.writeAttribute(namespaceUri: xmpNS, name: "CreateDate", value: metaDataDate);
1864 w.writeAttribute(namespaceUri: xmpNS, name: "ModifyDate", value: metaDataDate);
1865 w.writeAttribute(namespaceUri: xmpNS, name: "MetadataDate", value: metaDataDate);
1866 w.writeEndElement();
1867
1868 // XMPMM
1869 w.writeStartElement(namespaceUri: rdfNS, name: "Description");
1870 w.writeAttribute(namespaceUri: rdfNS, name: "about", value: "");
1871 w.writeAttribute(namespaceUri: xmpMMNS, name: "DocumentID", value: "uuid:"_L1 + documentId.toString(mode: QUuid::WithoutBraces));
1872 w.writeAttribute(namespaceUri: xmpMMNS, name: "VersionID", value: "1");
1873 w.writeAttribute(namespaceUri: xmpMMNS, name: "RenditionClass", value: "default");
1874 w.writeEndElement();
1875
1876 // Version-specific
1877 switch (pdfVersion) {
1878 case QPdfEngine::Version_1_4:
1879 break;
1880 case QPdfEngine::Version_A1b:
1881 w.writeStartElement(namespaceUri: rdfNS, name: "Description");
1882 w.writeAttribute(namespaceUri: rdfNS, name: "about", value: "");
1883 w.writeAttribute(namespaceUri: pdfaidNS, name: "part", value: "1");
1884 w.writeAttribute(namespaceUri: pdfaidNS, name: "conformance", value: "B");
1885 w.writeEndElement();
1886 break;
1887 case QPdfEngine::Version_1_6:
1888 break;
1889 case QPdfEngine::Version_X4:
1890 w.writeStartElement(namespaceUri: rdfNS, name: "Description");
1891 w.writeAttribute(namespaceUri: rdfNS, name: "about", value: "");
1892 w.writeAttribute(namespaceUri: pdfxidNS, name: "GTS_PDFXVersion", value: "PDF/X-4");
1893 w.writeEndElement();
1894 break;
1895 }
1896
1897 w.writeEndElement(); // </RDF>
1898 w.writeEndElement(); // </xmpmeta>
1899
1900 w.writeEndDocument();
1901 output.write(data: "<?xpacket end='w'?>");
1902 }
1903
1904 xprintf(fmt: "<<\n"
1905 "/Type /Metadata /Subtype /XML\n"
1906 "/Length %d\n"
1907 ">>\n"
1908 "stream\n", metaDataContent.size());
1909 write(data: metaDataContent);
1910 xprintf(fmt: "\nendstream\n"
1911 "endobj\n");
1912
1913 return metaDataObj;
1914}
1915
1916int QPdfEnginePrivate::writeOutputIntent()
1917{
1918 const int colorProfileEntry = addXrefEntry(object: -1);
1919 {
1920 const QColorSpace profile = outputIntent.outputProfile();
1921 const QByteArray colorProfileData = profile.iccProfile();
1922
1923 QByteArray data;
1924 QPdf::ByteStream s(&data);
1925 int length_object = requestObject();
1926
1927 s << "<<\n";
1928
1929 switch (profile.colorModel()) {
1930 case QColorSpace::ColorModel::Undefined:
1931 qWarning(msg: "QPdfEngine: undefined color model in the output intent profile, assuming RGB");
1932 [[fallthrough]];
1933 case QColorSpace::ColorModel::Rgb:
1934 s << "/N 3\n";
1935 s << "/Alternate /DeviceRGB\n";
1936 break;
1937 case QColorSpace::ColorModel::Gray:
1938 s << "/N 1\n";
1939 s << "/Alternate /DeviceGray\n";
1940 break;
1941 case QColorSpace::ColorModel::Cmyk:
1942 s << "/N 4\n";
1943 s << "/Alternate /DeviceCMYK\n";
1944 break;
1945 }
1946
1947 s << "/Length " << length_object << "0 R\n";
1948 if (do_compress)
1949 s << "/Filter /FlateDecode\n";
1950 s << ">>\n";
1951 s << "stream\n";
1952 write(data);
1953 const int len = writeCompressed(data: colorProfileData);
1954 write(data: "\nendstream\n"
1955 "endobj\n");
1956 addXrefEntry(object: length_object);
1957 xprintf(fmt: "%d\n"
1958 "endobj\n", len);
1959 }
1960
1961 const int outputIntentEntry = addXrefEntry(object: -1);
1962 {
1963 write(data: "<<\n");
1964 write(data: "/Type /OutputIntent\n");
1965
1966 switch (pdfVersion) {
1967 case QPdfEngine::Version_1_4:
1968 case QPdfEngine::Version_1_6:
1969 Q_UNREACHABLE(); // no output intent for these versions
1970 break;
1971 case QPdfEngine::Version_A1b:
1972 write(data: "/S/GTS_PDFA1\n");
1973 break;
1974 case QPdfEngine::Version_X4:
1975 write(data: "/S/GTS_PDFX\n");
1976 break;
1977 }
1978
1979 xprintf(fmt: "/DestOutputProfile %d 0 R\n", colorProfileEntry);
1980 write(data: "/OutputConditionIdentifier ");
1981 printString(string: outputIntent.outputConditionIdentifier());
1982 write(data: "\n");
1983
1984 write(data: "/Info ");
1985 printString(string: outputIntent.outputCondition());
1986 write(data: "\n");
1987
1988 write(data: "/OutputCondition ");
1989 printString(string: outputIntent.outputCondition());
1990 write(data: "\n");
1991
1992 if (const auto registryName = outputIntent.registryName(); !registryName.isEmpty()) {
1993 write(data: "/RegistryName ");
1994 printString(string: registryName.toString());
1995 write(data: "\n");
1996 }
1997
1998 write(data: ">>\n");
1999 write(data: "endobj\n");
2000 }
2001
2002 return outputIntentEntry;
2003}
2004
2005void QPdfEnginePrivate::writePageRoot()
2006{
2007 addXrefEntry(object: pageRoot);
2008
2009 xprintf(fmt: "<<\n"
2010 "/Type /Pages\n"
2011 "/Kids \n"
2012 "[\n");
2013 int size = pages.size();
2014 for (int i = 0; i < size; ++i)
2015 xprintf(fmt: "%d 0 R\n", pages[i]);
2016 xprintf(fmt: "]\n");
2017
2018 //xprintf("/Group <</S /Transparency /I true /K false>>\n");
2019 xprintf(fmt: "/Count %d\n", pages.size());
2020
2021 xprintf(fmt: "/ProcSet [/PDF /Text /ImageB /ImageC]\n"
2022 ">>\n"
2023 "endobj\n");
2024}
2025
2026void QPdfEnginePrivate::writeDestsRoot()
2027{
2028 if (destCache.isEmpty())
2029 return;
2030
2031 std::map<QString, int> destObjects;
2032 QByteArray xs, ys;
2033 for (const DestInfo &destInfo : std::as_const(t&: destCache)) {
2034 int destObj = addXrefEntry(object: -1);
2035 xs.setNum(static_cast<double>(destInfo.coords.x()), format: 'f');
2036 ys.setNum(static_cast<double>(destInfo.coords.y()), format: 'f');
2037 xprintf(fmt: "[%d 0 R /XYZ %s %s 0]\n", destInfo.pageObj, xs.constData(), ys.constData());
2038 xprintf(fmt: "endobj\n");
2039 destObjects.insert_or_assign(k: destInfo.anchor, obj&: destObj);
2040 }
2041
2042 // names
2043 destsRoot = addXrefEntry(object: -1);
2044 xprintf(fmt: "<<\n/Limits [");
2045 printString(string: destObjects.begin()->first);
2046 xprintf(fmt: " ");
2047 printString(string: destObjects.rbegin()->first);
2048 xprintf(fmt: "]\n/Names [\n");
2049 for (const auto &[anchor, destObject] : destObjects) {
2050 printString(string: anchor);
2051 xprintf(fmt: " %d 0 R\n", destObject);
2052 }
2053 xprintf(fmt: "]\n>>\n"
2054 "endobj\n");
2055}
2056
2057void QPdfEnginePrivate::writeAttachmentRoot()
2058{
2059 if (fileCache.isEmpty())
2060 return;
2061
2062 QList<int> attachments;
2063 const int size = fileCache.size();
2064 for (int i = 0; i < size; ++i) {
2065 auto attachment = fileCache.at(i);
2066 const int attachmentID = addXrefEntry(object: -1);
2067 xprintf(fmt: "<<\n");
2068 if (do_compress)
2069 xprintf(fmt: "/Filter /FlateDecode\n");
2070
2071 const int lenobj = requestObject();
2072 xprintf(fmt: "/Length %d 0 R\n", lenobj);
2073 int len = 0;
2074 xprintf(fmt: ">>\nstream\n");
2075 len = writeCompressed(data: attachment.data);
2076 xprintf(fmt: "\nendstream\n"
2077 "endobj\n");
2078 addXrefEntry(object: lenobj);
2079 xprintf(fmt: "%d\n"
2080 "endobj\n", len);
2081
2082 attachments.push_back(t: addXrefEntry(object: -1));
2083 xprintf(fmt: "<<\n"
2084 "/F ");
2085 printString(string: attachment.fileName);
2086
2087 xprintf(fmt: "\n/EF <</F %d 0 R>>\n"
2088 "/Type/Filespec\n"
2089 , attachmentID);
2090 if (!attachment.mimeType.isEmpty())
2091 xprintf(fmt: "/Subtype/%s\n",
2092 attachment.mimeType.replace(before: "/"_L1, after: "#2F"_L1).toLatin1().constData());
2093 xprintf(fmt: ">>\nendobj\n");
2094 }
2095
2096 // names
2097 attachmentsRoot = addXrefEntry(object: -1);
2098 xprintf(fmt: "<</Names[");
2099 for (int i = 0; i < size; ++i) {
2100 auto attachment = fileCache.at(i);
2101 printString(string: attachment.fileName);
2102 xprintf(fmt: "%d 0 R\n", attachments.at(i));
2103 }
2104 xprintf(fmt: "]>>\n"
2105 "endobj\n");
2106}
2107
2108void QPdfEnginePrivate::writeNamesRoot()
2109{
2110 addXrefEntry(object: namesRoot);
2111 xprintf(fmt: "<<\n");
2112
2113 if (attachmentsRoot)
2114 xprintf(fmt: "/EmbeddedFiles %d 0 R\n", attachmentsRoot);
2115
2116 if (destsRoot)
2117 xprintf(fmt: "/Dests %d 0 R\n", destsRoot);
2118
2119 xprintf(fmt: ">>\n");
2120 xprintf(fmt: "endobj\n");
2121}
2122
2123void QPdfEnginePrivate::embedFont(QFontSubset *font)
2124{
2125 //qDebug() << "embedFont" << font->object_id;
2126 int fontObject = font->object_id;
2127 QByteArray fontData = font->toTruetype();
2128#ifdef FONT_DUMP
2129 static int i = 0;
2130 QString fileName("font%1.ttf");
2131 fileName = fileName.arg(i++);
2132 QFile ff(fileName);
2133 ff.open(QFile::WriteOnly);
2134 ff.write(fontData);
2135 ff.close();
2136#endif
2137
2138 int fontDescriptor = requestObject();
2139 int fontstream = requestObject();
2140 int cidfont = requestObject();
2141 int toUnicode = requestObject();
2142 int cidset = requestObject();
2143
2144 QFontEngine::Properties properties = font->fontEngine->properties();
2145 QByteArray postscriptName = properties.postscriptName.replace(before: ' ', after: '_');
2146
2147 {
2148 qreal scale = 1000/properties.emSquare.toReal();
2149 addXrefEntry(object: fontDescriptor);
2150 QByteArray descriptor;
2151 QPdf::ByteStream s(&descriptor);
2152 s << "<< /Type /FontDescriptor\n"
2153 "/FontName /Q";
2154 int tag = fontDescriptor;
2155 for (int i = 0; i < 5; ++i) {
2156 s << (char)('A' + (tag % 26));
2157 tag /= 26;
2158 }
2159 s << '+' << postscriptName << "\n"
2160 "/Flags " << 4 << "\n"
2161 "/FontBBox ["
2162 << properties.boundingBox.x()*scale
2163 << -(properties.boundingBox.y() + properties.boundingBox.height())*scale
2164 << (properties.boundingBox.x() + properties.boundingBox.width())*scale
2165 << -properties.boundingBox.y()*scale << "]\n"
2166 "/ItalicAngle " << properties.italicAngle.toReal() << "\n"
2167 "/Ascent " << properties.ascent.toReal()*scale << "\n"
2168 "/Descent " << -properties.descent.toReal()*scale << "\n"
2169 "/CapHeight " << properties.capHeight.toReal()*scale << "\n"
2170 "/StemV " << properties.lineWidth.toReal()*scale << "\n"
2171 "/FontFile2 " << fontstream << "0 R\n"
2172 "/CIDSet " << cidset << "0 R\n"
2173 ">>\nendobj\n";
2174 write(data: descriptor);
2175 }
2176 {
2177 addXrefEntry(object: fontstream);
2178 QByteArray header;
2179 QPdf::ByteStream s(&header);
2180
2181 int length_object = requestObject();
2182 s << "<<\n"
2183 "/Length1 " << fontData.size() << "\n"
2184 "/Length " << length_object << "0 R\n";
2185 if (do_compress)
2186 s << "/Filter /FlateDecode\n";
2187 s << ">>\n"
2188 "stream\n";
2189 write(data: header);
2190 int len = writeCompressed(data: fontData);
2191 write(data: "\nendstream\n"
2192 "endobj\n");
2193 addXrefEntry(object: length_object);
2194 xprintf(fmt: "%d\n"
2195 "endobj\n", len);
2196 }
2197 {
2198 addXrefEntry(object: cidfont);
2199 QByteArray cid;
2200 QPdf::ByteStream s(&cid);
2201 s << "<< /Type /Font\n"
2202 "/Subtype /CIDFontType2\n"
2203 "/BaseFont /" << postscriptName << "\n"
2204 "/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>\n"
2205 "/FontDescriptor " << fontDescriptor << "0 R\n"
2206 "/CIDToGIDMap /Identity\n"
2207 << font->widthArray() <<
2208 ">>\n"
2209 "endobj\n";
2210 write(data: cid);
2211 }
2212 {
2213 addXrefEntry(object: toUnicode);
2214 QByteArray touc = font->createToUnicodeMap();
2215 xprintf(fmt: "<< /Length %d >>\n"
2216 "stream\n", touc.size());
2217 write(data: touc);
2218 write(data: "\nendstream\n"
2219 "endobj\n");
2220 }
2221 {
2222 addXrefEntry(object: fontObject);
2223 QByteArray font;
2224 QPdf::ByteStream s(&font);
2225 s << "<< /Type /Font\n"
2226 "/Subtype /Type0\n"
2227 "/BaseFont /" << postscriptName << "\n"
2228 "/Encoding /Identity-H\n"
2229 "/DescendantFonts [" << cidfont << "0 R]\n"
2230 "/ToUnicode " << toUnicode << "0 R"
2231 ">>\n"
2232 "endobj\n";
2233 write(data: font);
2234 }
2235 {
2236 QByteArray cidSetStream(font->nGlyphs() / 8 + 1, 0);
2237 int byteCounter = 0;
2238 int bitCounter = 0;
2239 for (qsizetype i = 0; i < font->nGlyphs(); ++i) {
2240 cidSetStream.data()[byteCounter] |= (1 << (7 - bitCounter));
2241
2242 bitCounter++;
2243 if (bitCounter == 8) {
2244 bitCounter = 0;
2245 byteCounter++;
2246 }
2247 }
2248
2249 addXrefEntry(object: cidset);
2250 xprintf(fmt: "<<\n");
2251 xprintf(fmt: "/Length %d\n", cidSetStream.size());
2252 xprintf(fmt: ">>\n");
2253 xprintf(fmt: "stream\n");
2254 write(data: cidSetStream);
2255 xprintf(fmt: "\nendstream\n");
2256 xprintf(fmt: "endobj\n");
2257 }
2258}
2259
2260qreal QPdfEnginePrivate::calcUserUnit() const
2261{
2262 // PDF standards < 1.6 support max 200x200in pages (no UserUnit)
2263 if (pdfVersion < QPdfEngine::Version_1_6)
2264 return 1.0;
2265
2266 const int maxLen = qMax(a: currentPage->pageSize.width(), b: currentPage->pageSize.height());
2267 if (maxLen <= 14400)
2268 return 1.0; // for pages up to 200x200in (14400x14400 units) use default scaling
2269
2270 // for larger pages, rescale units so we can have up to 381x381km
2271 return qMin(a: maxLen / 14400.0, b: 75000.0);
2272}
2273
2274void QPdfEnginePrivate::writeFonts()
2275{
2276 for (QHash<QFontEngine::FaceId, QFontSubset *>::iterator it = fonts.begin(); it != fonts.end(); ++it) {
2277 embedFont(font: *it);
2278 delete *it;
2279 }
2280 fonts.clear();
2281}
2282
2283void QPdfEnginePrivate::writePage()
2284{
2285 if (pages.empty())
2286 return;
2287
2288 *currentPage << "Q Q\n";
2289
2290 uint pageStream = requestObject();
2291 uint pageStreamLength = requestObject();
2292 uint resources = requestObject();
2293 uint annots = requestObject();
2294
2295 qreal userUnit = calcUserUnit();
2296
2297 addXrefEntry(object: pages.constLast());
2298
2299 // make sure we use the pagesize from when we started the page, since the user may have changed it
2300 const QByteArray formattedPageWidth = QByteArray::number(currentPage->pageSize.width() / userUnit, format: 'f');
2301 const QByteArray formattedPageHeight = QByteArray::number(currentPage->pageSize.height() / userUnit, format: 'f');
2302
2303 xprintf(fmt: "<<\n"
2304 "/Type /Page\n"
2305 "/Parent %d 0 R\n"
2306 "/Contents %d 0 R\n"
2307 "/Resources %d 0 R\n"
2308 "/Annots %d 0 R\n"
2309 "/MediaBox [0 0 %s %s]\n"
2310 "/TrimBox [0 0 %s %s]\n",
2311 pageRoot, pageStream, resources, annots,
2312 formattedPageWidth.constData(),
2313 formattedPageHeight.constData(),
2314 formattedPageWidth.constData(),
2315 formattedPageHeight.constData());
2316
2317 if (pdfVersion >= QPdfEngine::Version_1_6)
2318 xprintf(fmt: "/UserUnit %s\n", QByteArray::number(userUnit, format: 'f').constData());
2319
2320 xprintf(fmt: ">>\n"
2321 "endobj\n");
2322
2323 addXrefEntry(object: resources);
2324 xprintf(fmt: "<<\n"
2325 "/ColorSpace <<\n"
2326 "/PCSp %d 0 R\n"
2327 "/PCSpg %d 0 R\n"
2328 "/PCSpcmyk %d 0 R\n"
2329 "/CSp /DeviceRGB\n"
2330 "/CSpg /DeviceGray\n"
2331 "/CSpcmyk /DeviceCMYK\n"
2332 ">>\n"
2333 "/ExtGState <<\n"
2334 "/GSa %d 0 R\n",
2335 patternColorSpaceRGB,
2336 patternColorSpaceGrayscale,
2337 patternColorSpaceCMYK,
2338 graphicsState);
2339
2340 for (int i = 0; i < currentPage->graphicStates.size(); ++i)
2341 xprintf(fmt: "/GState%d %d 0 R\n", currentPage->graphicStates.at(i), currentPage->graphicStates.at(i));
2342 xprintf(fmt: ">>\n");
2343
2344 xprintf(fmt: "/Pattern <<\n");
2345 for (int i = 0; i < currentPage->patterns.size(); ++i)
2346 xprintf(fmt: "/Pat%d %d 0 R\n", currentPage->patterns.at(i), currentPage->patterns.at(i));
2347 xprintf(fmt: ">>\n");
2348
2349 xprintf(fmt: "/Font <<\n");
2350 for (int i = 0; i < currentPage->fonts.size();++i)
2351 xprintf(fmt: "/F%d %d 0 R\n", currentPage->fonts[i], currentPage->fonts[i]);
2352 xprintf(fmt: ">>\n");
2353
2354 xprintf(fmt: "/XObject <<\n");
2355 for (int i = 0; i<currentPage->images.size(); ++i) {
2356 xprintf(fmt: "/Im%d %d 0 R\n", currentPage->images.at(i), currentPage->images.at(i));
2357 }
2358 xprintf(fmt: ">>\n");
2359
2360 xprintf(fmt: ">>\n"
2361 "endobj\n");
2362
2363 addXrefEntry(object: annots);
2364 xprintf(fmt: "[ ");
2365 for (int i = 0; i<currentPage->annotations.size(); ++i) {
2366 xprintf(fmt: "%d 0 R ", currentPage->annotations.at(i));
2367 }
2368 xprintf(fmt: "]\nendobj\n");
2369
2370 addXrefEntry(object: pageStream);
2371 xprintf(fmt: "<<\n"
2372 "/Length %d 0 R\n", pageStreamLength); // object number for stream length object
2373 if (do_compress)
2374 xprintf(fmt: "/Filter /FlateDecode\n");
2375
2376 xprintf(fmt: ">>\n");
2377 xprintf(fmt: "stream\n");
2378 QIODevice *content = currentPage->stream();
2379 int len = writeCompressed(dev: content);
2380 xprintf(fmt: "\nendstream\n"
2381 "endobj\n");
2382
2383 addXrefEntry(object: pageStreamLength);
2384 xprintf(fmt: "%d\nendobj\n",len);
2385}
2386
2387void QPdfEnginePrivate::writeTail()
2388{
2389 writePage();
2390 writeFonts();
2391 writePageRoot();
2392 writeDestsRoot();
2393 writeAttachmentRoot();
2394 writeNamesRoot();
2395
2396 addXrefEntry(object: xrefPositions.size(),printostr: false);
2397 xprintf(fmt: "xref\n"
2398 "0 %d\n"
2399 "%010d 65535 f \n", xrefPositions.size()-1, xrefPositions[0]);
2400
2401 for (int i = 1; i < xrefPositions.size()-1; ++i)
2402 xprintf(fmt: "%010d 00000 n \n", xrefPositions[i]);
2403
2404 {
2405 QByteArray trailer;
2406 QPdf::ByteStream s(&trailer);
2407
2408 s << "trailer\n"
2409 << "<<\n"
2410 << "/Size " << xrefPositions.size() - 1 << "\n"
2411 << "/Info " << info << "0 R\n"
2412 << "/Root " << catalog << "0 R\n";
2413
2414 const QByteArray id = documentId.toString(mode: QUuid::WithoutBraces).toUtf8().toHex();
2415 s << "/ID [ <" << id << "> <" << id << "> ]\n";
2416
2417 s << ">>\n"
2418 << "startxref\n" << xrefPositions.constLast() << "\n"
2419 << "%%EOF\n";
2420
2421 write(data: trailer);
2422 }
2423}
2424
2425int QPdfEnginePrivate::addXrefEntry(int object, bool printostr)
2426{
2427 if (object < 0)
2428 object = requestObject();
2429
2430 if (object>=xrefPositions.size())
2431 xrefPositions.resize(size: object+1);
2432
2433 xrefPositions[object] = streampos;
2434 if (printostr)
2435 xprintf(fmt: "%d 0 obj\n",object);
2436
2437 return object;
2438}
2439
2440void QPdfEnginePrivate::printString(QStringView string)
2441{
2442 if (string.isEmpty()) {
2443 write(data: "()");
2444 return;
2445 }
2446
2447 // The 'text string' type in PDF is encoded either as PDFDocEncoding, or
2448 // Unicode UTF-16 with a Unicode byte order mark as the first character
2449 // (0xfeff), with the high-order byte first.
2450 QByteArray array("(\xfe\xff");
2451 const char16_t *utf16 = string.utf16();
2452
2453 for (qsizetype i = 0; i < string.size(); ++i) {
2454 char part[2] = {char((*(utf16 + i)) >> 8), char((*(utf16 + i)) & 0xff)};
2455 for(int j=0; j < 2; ++j) {
2456 if (part[j] == '(' || part[j] == ')' || part[j] == '\\')
2457 array.append(c: '\\');
2458 array.append(c: part[j]);
2459 }
2460 }
2461 array.append(c: ')');
2462 write(data: array);
2463}
2464
2465
2466void QPdfEnginePrivate::xprintf(const char* fmt, ...)
2467{
2468 if (!stream)
2469 return;
2470
2471 const int msize = 10000;
2472 char buf[msize];
2473
2474 va_list args;
2475 va_start(args, fmt);
2476 int bufsize = std::vsnprintf(s: buf, maxlen: msize, format: fmt, arg: args);
2477 va_end(args);
2478
2479 if (Q_LIKELY(bufsize < msize)) {
2480 stream->writeRawData(buf, len: bufsize);
2481 } else {
2482 // Fallback for abnormal cases
2483 QScopedArrayPointer<char> tmpbuf(new char[bufsize + 1]);
2484 va_start(args, fmt);
2485 bufsize = std::vsnprintf(s: tmpbuf.data(), maxlen: bufsize + 1, format: fmt, arg: args);
2486 va_end(args);
2487 stream->writeRawData(tmpbuf.data(), len: bufsize);
2488 }
2489 streampos += bufsize;
2490}
2491
2492int QPdfEnginePrivate::writeCompressed(QIODevice *dev)
2493{
2494#ifndef QT_NO_COMPRESS
2495 if (do_compress) {
2496 int size = QPdfPage::chunkSize();
2497 int sum = 0;
2498 ::z_stream zStruct;
2499 zStruct.zalloc = Z_NULL;
2500 zStruct.zfree = Z_NULL;
2501 zStruct.opaque = Z_NULL;
2502 if (::deflateInit(&zStruct, Z_DEFAULT_COMPRESSION) != Z_OK) {
2503 qWarning(msg: "QPdfStream::writeCompressed: Error in deflateInit()");
2504 return sum;
2505 }
2506 zStruct.avail_in = 0;
2507 QByteArray in, out;
2508 out.resize(size);
2509 while (!dev->atEnd() || zStruct.avail_in != 0) {
2510 if (zStruct.avail_in == 0) {
2511 in = dev->read(maxlen: size);
2512 zStruct.avail_in = in.size();
2513 zStruct.next_in = reinterpret_cast<unsigned char*>(in.data());
2514 if (in.size() <= 0) {
2515 qWarning(msg: "QPdfStream::writeCompressed: Error in read()");
2516 ::deflateEnd(strm: &zStruct);
2517 return sum;
2518 }
2519 }
2520 zStruct.next_out = reinterpret_cast<unsigned char*>(out.data());
2521 zStruct.avail_out = out.size();
2522 if (::deflate(strm: &zStruct, flush: 0) != Z_OK) {
2523 qWarning(msg: "QPdfStream::writeCompressed: Error in deflate()");
2524 ::deflateEnd(strm: &zStruct);
2525 return sum;
2526 }
2527 int written = out.size() - zStruct.avail_out;
2528 stream->writeRawData(out.constData(), len: written);
2529 streampos += written;
2530 sum += written;
2531 }
2532 int ret;
2533 do {
2534 zStruct.next_out = reinterpret_cast<unsigned char*>(out.data());
2535 zStruct.avail_out = out.size();
2536 ret = ::deflate(strm: &zStruct, Z_FINISH);
2537 if (ret != Z_OK && ret != Z_STREAM_END) {
2538 qWarning(msg: "QPdfStream::writeCompressed: Error in deflate()");
2539 ::deflateEnd(strm: &zStruct);
2540 return sum;
2541 }
2542 int written = out.size() - zStruct.avail_out;
2543 stream->writeRawData(out.constData(), len: written);
2544 streampos += written;
2545 sum += written;
2546 } while (ret == Z_OK);
2547
2548 ::deflateEnd(strm: &zStruct);
2549
2550 return sum;
2551 } else
2552#endif
2553 {
2554 QByteArray arr;
2555 int sum = 0;
2556 while (!dev->atEnd()) {
2557 arr = dev->read(maxlen: QPdfPage::chunkSize());
2558 stream->writeRawData(arr.constData(), len: arr.size());
2559 streampos += arr.size();
2560 sum += arr.size();
2561 }
2562 return sum;
2563 }
2564}
2565
2566int QPdfEnginePrivate::writeCompressed(const char *src, int len)
2567{
2568#ifndef QT_NO_COMPRESS
2569 if (do_compress) {
2570 const QByteArray data = qCompress(data: reinterpret_cast<const uchar *>(src), nbytes: len);
2571 constexpr qsizetype HeaderSize = 4;
2572 if (!data.isNull()) {
2573 stream->writeRawData(data.data() + HeaderSize, len: data.size() - HeaderSize);
2574 len = data.size() - HeaderSize;
2575 } else {
2576 qWarning(msg: "QPdfStream::writeCompressed: Error in compress()");
2577 len = 0;
2578 }
2579 } else
2580#endif
2581 {
2582 stream->writeRawData(src,len);
2583 }
2584 streampos += len;
2585 return len;
2586}
2587
2588int QPdfEnginePrivate::writeImage(const QByteArray &data, int width, int height, WriteImageOption option,
2589 int maskObject, int softMaskObject, bool dct, bool isMono)
2590{
2591 int image = addXrefEntry(object: -1);
2592 xprintf(fmt: "<<\n"
2593 "/Type /XObject\n"
2594 "/Subtype /Image\n"
2595 "/Width %d\n"
2596 "/Height %d\n", width, height);
2597
2598 switch (option) {
2599 case WriteImageOption::Monochrome:
2600 if (!isMono) {
2601 xprintf(fmt: "/ImageMask true\n"
2602 "/Decode [1 0]\n");
2603 } else {
2604 xprintf(fmt: "/BitsPerComponent 1\n"
2605 "/ColorSpace /DeviceGray\n");
2606 }
2607 break;
2608 case WriteImageOption::Grayscale:
2609 xprintf(fmt: "/BitsPerComponent 8\n"
2610 "/ColorSpace /DeviceGray\n");
2611 break;
2612 case WriteImageOption::RGB:
2613 xprintf(fmt: "/BitsPerComponent 8\n"
2614 "/ColorSpace /DeviceRGB\n");
2615 break;
2616 case WriteImageOption::CMYK:
2617 xprintf(fmt: "/BitsPerComponent 8\n"
2618 "/ColorSpace /DeviceCMYK\n");
2619 break;
2620 }
2621
2622 if (maskObject > 0)
2623 xprintf(fmt: "/Mask %d 0 R\n", maskObject);
2624 if (softMaskObject > 0)
2625 xprintf(fmt: "/SMask %d 0 R\n", softMaskObject);
2626
2627 int lenobj = requestObject();
2628 xprintf(fmt: "/Length %d 0 R\n", lenobj);
2629 if (interpolateImages)
2630 xprintf(fmt: "/Interpolate true\n");
2631 int len = 0;
2632 if (dct) {
2633 //qDebug("DCT");
2634 xprintf(fmt: "/Filter /DCTDecode\n>>\nstream\n");
2635 write(data);
2636 len = data.size();
2637 } else {
2638 if (do_compress)
2639 xprintf(fmt: "/Filter /FlateDecode\n>>\nstream\n");
2640 else
2641 xprintf(fmt: ">>\nstream\n");
2642 len = writeCompressed(data);
2643 }
2644 xprintf(fmt: "\nendstream\n"
2645 "endobj\n");
2646 addXrefEntry(object: lenobj);
2647 xprintf(fmt: "%d\n"
2648 "endobj\n", len);
2649 return image;
2650}
2651
2652struct QGradientBound {
2653 qreal start;
2654 qreal stop;
2655 int function;
2656 bool reverse;
2657};
2658Q_DECLARE_TYPEINFO(QGradientBound, Q_PRIMITIVE_TYPE);
2659
2660void QPdfEnginePrivate::ShadingFunctionResult::writeColorSpace(QPdf::ByteStream *stream) const
2661{
2662 *stream << "/ColorSpace ";
2663 switch (colorModel) {
2664 case QPdfEngine::ColorModel::RGB:
2665 *stream << "/DeviceRGB\n"; break;
2666 case QPdfEngine::ColorModel::Grayscale:
2667 *stream << "/DeviceGray\n"; break;
2668 case QPdfEngine::ColorModel::CMYK:
2669 *stream << "/DeviceCMYK\n"; break;
2670 case QPdfEngine::ColorModel::Auto:
2671 Q_UNREACHABLE(); break;
2672 }
2673}
2674
2675QPdfEnginePrivate::ShadingFunctionResult
2676QPdfEnginePrivate::createShadingFunction(const QGradient *gradient, int from, int to, bool reflect, bool alpha)
2677{
2678 QGradientStops stops = gradient->stops();
2679 if (stops.isEmpty()) {
2680 stops << QGradientStop(0, Qt::black);
2681 stops << QGradientStop(1, Qt::white);
2682 }
2683 if (stops.at(i: 0).first > 0)
2684 stops.prepend(t: QGradientStop(0, stops.at(i: 0).second));
2685 if (stops.at(i: stops.size() - 1).first < 1)
2686 stops.append(t: QGradientStop(1, stops.at(i: stops.size() - 1).second));
2687
2688 // Color to use which colorspace to use
2689 const QColor referenceColor = stops.constFirst().second;
2690
2691 switch (colorModel) {
2692 case QPdfEngine::ColorModel::RGB:
2693 case QPdfEngine::ColorModel::Grayscale:
2694 case QPdfEngine::ColorModel::CMYK:
2695 break;
2696 case QPdfEngine::ColorModel::Auto: {
2697 // Make sure that all the stops have the same color spec
2698 // (we don't support anything else)
2699 const QColor::Spec referenceSpec = referenceColor.spec();
2700 bool warned = false;
2701 for (QGradientStop &stop : stops) {
2702 if (stop.second.spec() != referenceSpec) {
2703 if (!warned) {
2704 qWarning(msg: "QPdfEngine: unable to create a gradient between colors of different spec");
2705 warned = true;
2706 }
2707 stop.second = stop.second.convertTo(colorSpec: referenceSpec);
2708 }
2709 }
2710 break;
2711 }
2712 }
2713
2714 ShadingFunctionResult result;
2715 result.colorModel = colorModelForColor(color: referenceColor);
2716
2717 QList<int> functions;
2718 const int numStops = stops.size();
2719 functions.reserve(asize: numStops - 1);
2720 for (int i = 0; i < numStops - 1; ++i) {
2721 int f = addXrefEntry(object: -1);
2722 QByteArray data;
2723 QPdf::ByteStream s(&data);
2724 s << "<<\n"
2725 "/FunctionType 2\n"
2726 "/Domain [0 1]\n"
2727 "/N 1\n";
2728 if (alpha) {
2729 s << "/C0 [" << stops.at(i).second.alphaF() << "]\n"
2730 "/C1 [" << stops.at(i: i + 1).second.alphaF() << "]\n";
2731 } else {
2732 switch (result.colorModel) {
2733 case QPdfEngine::ColorModel::RGB:
2734 s << "/C0 [" << stops.at(i).second.redF() << stops.at(i).second.greenF() << stops.at(i).second.blueF() << "]\n"
2735 "/C1 [" << stops.at(i: i + 1).second.redF() << stops.at(i: i + 1).second.greenF() << stops.at(i: i + 1).second.blueF() << "]\n";
2736 break;
2737 case QPdfEngine::ColorModel::Grayscale: {
2738 constexpr qreal normalisationFactor = 1. / 255.;
2739 s << "/C0 [" << (qGray(rgb: stops.at(i).second.rgba()) * normalisationFactor) << "]\n"
2740 "/C1 [" << (qGray(rgb: stops.at(i: i + 1).second.rgba()) * normalisationFactor) << "]\n";
2741 break;
2742 }
2743 case QPdfEngine::ColorModel::CMYK:
2744 s << "/C0 [" << stops.at(i).second.cyanF()
2745 << stops.at(i).second.magentaF()
2746 << stops.at(i).second.yellowF()
2747 << stops.at(i).second.blackF() << "]\n"
2748 "/C1 [" << stops.at(i: i + 1).second.cyanF()
2749 << stops.at(i: i + 1).second.magentaF()
2750 << stops.at(i: i + 1).second.yellowF()
2751 << stops.at(i: i + 1).second.blackF() << "]\n";
2752 break;
2753
2754 case QPdfEngine::ColorModel::Auto:
2755 Q_UNREACHABLE();
2756 break;
2757 }
2758
2759 }
2760 s << ">>\n"
2761 "endobj\n";
2762 write(data);
2763 functions << f;
2764 }
2765
2766 QList<QGradientBound> gradientBounds;
2767 gradientBounds.reserve(asize: (to - from) * (numStops - 1));
2768
2769 for (int step = from; step < to; ++step) {
2770 if (reflect && step % 2) {
2771 for (int i = numStops - 1; i > 0; --i) {
2772 QGradientBound b;
2773 b.start = step + 1 - qBound(min: qreal(0.), val: stops.at(i).first, max: qreal(1.));
2774 b.stop = step + 1 - qBound(min: qreal(0.), val: stops.at(i: i - 1).first, max: qreal(1.));
2775 b.function = functions.at(i: i - 1);
2776 b.reverse = true;
2777 gradientBounds << b;
2778 }
2779 } else {
2780 for (int i = 0; i < numStops - 1; ++i) {
2781 QGradientBound b;
2782 b.start = step + qBound(min: qreal(0.), val: stops.at(i).first, max: qreal(1.));
2783 b.stop = step + qBound(min: qreal(0.), val: stops.at(i: i + 1).first, max: qreal(1.));
2784 b.function = functions.at(i);
2785 b.reverse = false;
2786 gradientBounds << b;
2787 }
2788 }
2789 }
2790
2791 // normalize bounds to [0..1]
2792 qreal bstart = gradientBounds.at(i: 0).start;
2793 qreal bend = gradientBounds.at(i: gradientBounds.size() - 1).stop;
2794 qreal norm = 1./(bend - bstart);
2795 for (int i = 0; i < gradientBounds.size(); ++i) {
2796 gradientBounds[i].start = (gradientBounds[i].start - bstart)*norm;
2797 gradientBounds[i].stop = (gradientBounds[i].stop - bstart)*norm;
2798 }
2799
2800 int function;
2801 if (gradientBounds.size() > 1) {
2802 function = addXrefEntry(object: -1);
2803 QByteArray data;
2804 QPdf::ByteStream s(&data);
2805 s << "<<\n"
2806 "/FunctionType 3\n"
2807 "/Domain [0 1]\n"
2808 "/Bounds [";
2809 for (int i = 1; i < gradientBounds.size(); ++i)
2810 s << gradientBounds.at(i).start;
2811 s << "]\n"
2812 "/Encode [";
2813 for (int i = 0; i < gradientBounds.size(); ++i)
2814 s << (gradientBounds.at(i).reverse ? "1 0 " : "0 1 ");
2815 s << "]\n"
2816 "/Functions [";
2817 for (int i = 0; i < gradientBounds.size(); ++i)
2818 s << gradientBounds.at(i).function << "0 R ";
2819 s << "]\n"
2820 ">>\n"
2821 "endobj\n";
2822 write(data);
2823 } else {
2824 function = functions.at(i: 0);
2825 }
2826 result.function = function;
2827 return result;
2828}
2829
2830int QPdfEnginePrivate::generateLinearGradientShader(const QLinearGradient *gradient, const QTransform &matrix, bool alpha)
2831{
2832 QPointF start = gradient->start();
2833 QPointF stop = gradient->finalStop();
2834 QPointF offset = stop - start;
2835 Q_ASSERT(gradient->coordinateMode() == QGradient::LogicalMode);
2836
2837 int from = 0;
2838 int to = 1;
2839 bool reflect = false;
2840 switch (gradient->spread()) {
2841 case QGradient::PadSpread:
2842 break;
2843 case QGradient::ReflectSpread:
2844 reflect = true;
2845 Q_FALLTHROUGH();
2846 case QGradient::RepeatSpread: {
2847 // calculate required bounds
2848 QRectF pageRect = m_pageLayout.fullRectPixels(resolution);
2849 QTransform inv = matrix.inverted();
2850 QPointF page_rect[4] = { inv.map(p: pageRect.topLeft()),
2851 inv.map(p: pageRect.topRight()),
2852 inv.map(p: pageRect.bottomLeft()),
2853 inv.map(p: pageRect.bottomRight()) };
2854
2855 qreal length = offset.x()*offset.x() + offset.y()*offset.y();
2856
2857 // find the max and min values in offset and orth direction that are needed to cover
2858 // the whole page
2859 from = INT_MAX;
2860 to = INT_MIN;
2861 for (int i = 0; i < 4; ++i) {
2862 qreal off = ((page_rect[i].x() - start.x()) * offset.x() + (page_rect[i].y() - start.y()) * offset.y())/length;
2863 from = qMin(a: from, b: qFloor(v: off));
2864 to = qMax(a: to, b: qCeil(v: off));
2865 }
2866
2867 stop = start + to * offset;
2868 start = start + from * offset;\
2869 break;
2870 }
2871 }
2872
2873 const auto shadingFunctionResult = createShadingFunction(gradient, from, to, reflect, alpha);
2874
2875 QByteArray shader;
2876 QPdf::ByteStream s(&shader);
2877 s << "<<\n"
2878 "/ShadingType 2\n";
2879
2880 if (alpha)
2881 s << "/ColorSpace /DeviceGray\n";
2882 else
2883 shadingFunctionResult.writeColorSpace(stream: &s);
2884
2885 s << "/AntiAlias true\n"
2886 "/Coords [" << start.x() << start.y() << stop.x() << stop.y() << "]\n"
2887 "/Extend [true true]\n"
2888 "/Function " << shadingFunctionResult.function << "0 R\n"
2889 ">>\n"
2890 "endobj\n";
2891 int shaderObject = addXrefEntry(object: -1);
2892 write(data: shader);
2893 return shaderObject;
2894}
2895
2896int QPdfEnginePrivate::generateRadialGradientShader(const QRadialGradient *gradient, const QTransform &matrix, bool alpha)
2897{
2898 QPointF p1 = gradient->center();
2899 qreal r1 = gradient->centerRadius();
2900 QPointF p0 = gradient->focalPoint();
2901 qreal r0 = gradient->focalRadius();
2902
2903 Q_ASSERT(gradient->coordinateMode() == QGradient::LogicalMode);
2904
2905 int from = 0;
2906 int to = 1;
2907 bool reflect = false;
2908 switch (gradient->spread()) {
2909 case QGradient::PadSpread:
2910 break;
2911 case QGradient::ReflectSpread:
2912 reflect = true;
2913 Q_FALLTHROUGH();
2914 case QGradient::RepeatSpread: {
2915 Q_ASSERT(qFuzzyIsNull(r0)); // QPainter emulates if this is not 0
2916
2917 QRectF pageRect = m_pageLayout.fullRectPixels(resolution);
2918 QTransform inv = matrix.inverted();
2919 QPointF page_rect[4] = { inv.map(p: pageRect.topLeft()),
2920 inv.map(p: pageRect.topRight()),
2921 inv.map(p: pageRect.bottomLeft()),
2922 inv.map(p: pageRect.bottomRight()) };
2923
2924 // increase to until the whole page fits into it
2925 bool done = false;
2926 while (!done) {
2927 QPointF center = QPointF(p0.x() + to*(p1.x() - p0.x()), p0.y() + to*(p1.y() - p0.y()));
2928 double radius = r0 + to*(r1 - r0);
2929 double r2 = radius*radius;
2930 done = true;
2931 for (int i = 0; i < 4; ++i) {
2932 QPointF off = page_rect[i] - center;
2933 if (off.x()*off.x() + off.y()*off.y() > r2) {
2934 ++to;
2935 done = false;
2936 break;
2937 }
2938 }
2939 }
2940 p1 = QPointF(p0.x() + to*(p1.x() - p0.x()), p0.y() + to*(p1.y() - p0.y()));
2941 r1 = r0 + to*(r1 - r0);
2942 break;
2943 }
2944 }
2945
2946 const auto shadingFunctionResult = createShadingFunction(gradient, from, to, reflect, alpha);
2947
2948 QByteArray shader;
2949 QPdf::ByteStream s(&shader);
2950 s << "<<\n"
2951 "/ShadingType 3\n";
2952
2953 if (alpha)
2954 s << "/ColorSpace /DeviceGray\n";
2955 else
2956 shadingFunctionResult.writeColorSpace(stream: &s);
2957
2958 s << "/AntiAlias true\n"
2959 "/Domain [0 1]\n"
2960 "/Coords [" << p0.x() << p0.y() << r0 << p1.x() << p1.y() << r1 << "]\n"
2961 "/Extend [true true]\n"
2962 "/Function " << shadingFunctionResult.function << "0 R\n"
2963 ">>\n"
2964 "endobj\n";
2965 int shaderObject = addXrefEntry(object: -1);
2966 write(data: shader);
2967 return shaderObject;
2968}
2969
2970int QPdfEnginePrivate::generateGradientShader(const QGradient *gradient, const QTransform &matrix, bool alpha)
2971{
2972 switch (gradient->type()) {
2973 case QGradient::LinearGradient:
2974 return generateLinearGradientShader(gradient: static_cast<const QLinearGradient *>(gradient), matrix, alpha);
2975 case QGradient::RadialGradient:
2976 return generateRadialGradientShader(gradient: static_cast<const QRadialGradient *>(gradient), matrix, alpha);
2977 case QGradient::ConicalGradient:
2978 Q_UNIMPLEMENTED(); // ### Implement me!
2979 break;
2980 case QGradient::NoGradient:
2981 break;
2982 }
2983 return 0;
2984}
2985
2986int QPdfEnginePrivate::gradientBrush(const QBrush &b, const QTransform &matrix, int *gStateObject)
2987{
2988 const QGradient *gradient = b.gradient();
2989
2990 if (!gradient || gradient->coordinateMode() != QGradient::LogicalMode)
2991 return 0;
2992
2993 QRectF pageRect = m_pageLayout.fullRectPixels(resolution);
2994
2995 QTransform m = b.transform() * matrix;
2996 int shaderObject = generateGradientShader(gradient, matrix: m);
2997
2998 QByteArray str;
2999 QPdf::ByteStream s(&str);
3000 s << "<<\n"
3001 "/Type /Pattern\n"
3002 "/PatternType 2\n"
3003 "/Shading " << shaderObject << "0 R\n"
3004 "/Matrix ["
3005 << m.m11()
3006 << m.m12()
3007 << m.m21()
3008 << m.m22()
3009 << m.dx()
3010 << m.dy() << "]\n";
3011 s << ">>\n"
3012 "endobj\n";
3013
3014 int patternObj = addXrefEntry(object: -1);
3015 write(data: str);
3016 currentPage->patterns.append(t: patternObj);
3017
3018 if (!b.isOpaque()) {
3019 bool ca = true;
3020 QGradientStops stops = gradient->stops();
3021 int a = stops.at(i: 0).second.alpha();
3022 for (int i = 1; i < stops.size(); ++i) {
3023 if (stops.at(i).second.alpha() != a) {
3024 ca = false;
3025 break;
3026 }
3027 }
3028 if (ca) {
3029 *gStateObject = addConstantAlphaObject(brushAlpha: stops.at(i: 0).second.alpha());
3030 } else {
3031 int alphaShaderObject = generateGradientShader(gradient, matrix: m, alpha: true);
3032
3033 QByteArray content;
3034 QPdf::ByteStream c(&content);
3035 c << "/Shader" << alphaShaderObject << "sh\n";
3036
3037 QByteArray form;
3038 QPdf::ByteStream f(&form);
3039 f << "<<\n"
3040 "/Type /XObject\n"
3041 "/Subtype /Form\n"
3042 "/BBox [0 0 " << pageRect.width() << pageRect.height() << "]\n"
3043 "/Group <</S /Transparency >>\n"
3044 "/Resources <<\n"
3045 "/Shading << /Shader" << alphaShaderObject << alphaShaderObject << "0 R >>\n"
3046 ">>\n";
3047
3048 f << "/Length " << content.size() << "\n"
3049 ">>\n"
3050 "stream\n"
3051 << content
3052 << "\nendstream\n"
3053 "endobj\n";
3054
3055 int softMaskFormObject = addXrefEntry(object: -1);
3056 write(data: form);
3057 *gStateObject = addXrefEntry(object: -1);
3058 xprintf(fmt: "<< /SMask << /S /Alpha /G %d 0 R >> >>\n"
3059 "endobj\n", softMaskFormObject);
3060 currentPage->graphicStates.append(t: *gStateObject);
3061 }
3062 }
3063
3064 return patternObj;
3065}
3066
3067int QPdfEnginePrivate::addConstantAlphaObject(int brushAlpha, int penAlpha)
3068{
3069 if (brushAlpha == 255 && penAlpha == 255)
3070 return 0;
3071 uint object = alphaCache.value(key: QPair<uint, uint>(brushAlpha, penAlpha), defaultValue: 0);
3072 if (!object) {
3073 object = addXrefEntry(object: -1);
3074 QByteArray alphaDef;
3075 QPdf::ByteStream s(&alphaDef);
3076 s << "<<\n/ca " << (brushAlpha/qreal(255.)) << '\n';
3077 s << "/CA " << (penAlpha/qreal(255.)) << "\n>>";
3078 xprintf(fmt: "%s\nendobj\n", alphaDef.constData());
3079 alphaCache.insert(key: QPair<uint, uint>(brushAlpha, penAlpha), value: object);
3080 }
3081 if (currentPage->graphicStates.indexOf(t: object) < 0)
3082 currentPage->graphicStates.append(t: object);
3083
3084 return object;
3085}
3086
3087
3088int QPdfEnginePrivate::addBrushPattern(const QTransform &m, bool *specifyColor, int *gStateObject)
3089{
3090 Q_Q(QPdfEngine);
3091
3092 int paintType = 2; // Uncolored tiling
3093 int w = 8;
3094 int h = 8;
3095
3096 *specifyColor = true;
3097 *gStateObject = 0;
3098
3099 const Qt::BrushStyle style = brush.style();
3100 const bool isCosmetic = style >= Qt::Dense1Pattern && style <= Qt::DiagCrossPattern
3101 && !q->painter()->testRenderHint(hint: QPainter::NonCosmeticBrushPatterns);
3102 QTransform matrix;
3103 if (!isCosmetic)
3104 matrix = m;
3105 matrix.translate(dx: brushOrigin.x(), dy: brushOrigin.y());
3106 matrix = matrix * pageMatrix();
3107
3108 if (style == Qt::LinearGradientPattern || style == Qt::RadialGradientPattern) {// && style <= Qt::ConicalGradientPattern) {
3109 *specifyColor = false;
3110 return gradientBrush(b: brush, matrix, gStateObject);
3111 }
3112
3113 if (!isCosmetic)
3114 matrix = brush.transform() * matrix;
3115
3116 if ((!brush.isOpaque() && brush.style() < Qt::LinearGradientPattern) || opacity != 1.0)
3117 *gStateObject = addConstantAlphaObject(brushAlpha: qRound(d: brush.color().alpha() * opacity),
3118 penAlpha: qRound(d: pen.color().alpha() * opacity));
3119
3120 int imageObject = -1;
3121 QByteArray pattern = QPdf::patternForBrush(b: brush);
3122 if (pattern.isEmpty()) {
3123 if (brush.style() != Qt::TexturePattern)
3124 return 0;
3125 QImage image = brush.textureImage();
3126 bool bitmap = true;
3127 const bool lossless = q->painter()->testRenderHint(hint: QPainter::LosslessImageRendering);
3128 imageObject = addImage(image, bitmap: &bitmap, lossless, serial_no: image.cacheKey());
3129 if (imageObject != -1) {
3130 QImage::Format f = image.format();
3131 if (f != QImage::Format_MonoLSB && f != QImage::Format_Mono) {
3132 paintType = 1; // Colored tiling
3133 *specifyColor = false;
3134 }
3135 w = image.width();
3136 h = image.height();
3137 QTransform m(w, 0, 0, -h, 0, h);
3138 QPdf::ByteStream s(&pattern);
3139 s << QPdf::generateMatrix(matrix: m);
3140 s << "/Im" << imageObject << " Do\n";
3141 }
3142 }
3143
3144 QByteArray str;
3145 QPdf::ByteStream s(&str);
3146 s << "<<\n"
3147 "/Type /Pattern\n"
3148 "/PatternType 1\n"
3149 "/PaintType " << paintType << "\n"
3150 "/TilingType 1\n"
3151 "/BBox [0 0 " << w << h << "]\n"
3152 "/XStep " << w << "\n"
3153 "/YStep " << h << "\n"
3154 "/Matrix ["
3155 << matrix.m11()
3156 << matrix.m12()
3157 << matrix.m21()
3158 << matrix.m22()
3159 << matrix.dx()
3160 << matrix.dy() << "]\n"
3161 "/Resources \n<< "; // open resource tree
3162 if (imageObject > 0) {
3163 s << "/XObject << /Im" << imageObject << ' ' << imageObject << "0 R >> ";
3164 }
3165 s << ">>\n"
3166 "/Length " << pattern.size() << "\n"
3167 ">>\n"
3168 "stream\n"
3169 << pattern
3170 << "\nendstream\n"
3171 "endobj\n";
3172
3173 int patternObj = addXrefEntry(object: -1);
3174 write(data: str);
3175 currentPage->patterns.append(t: patternObj);
3176 return patternObj;
3177}
3178
3179static inline bool is_monochrome(const QList<QRgb> &colorTable)
3180{
3181 return colorTable.size() == 2
3182 && colorTable.at(i: 0) == QColor(Qt::black).rgba()
3183 && colorTable.at(i: 1) == QColor(Qt::white).rgba()
3184 ;
3185}
3186
3187/*!
3188 * Adds an image to the pdf and return the pdf-object id. Returns -1 if adding the image failed.
3189 */
3190int QPdfEnginePrivate::addImage(const QImage &img, bool *bitmap, bool lossless, qint64 serial_no)
3191{
3192 if (img.isNull())
3193 return -1;
3194
3195 int object = imageCache.value(key: serial_no);
3196 if (object)
3197 return object;
3198
3199 QImage image = img;
3200 QImage::Format format = image.format();
3201 const bool grayscale = (colorModel == QPdfEngine::ColorModel::Grayscale);
3202
3203 if (pdfVersion == QPdfEngine::Version_A1b) {
3204 if (image.hasAlphaChannel()) {
3205 // transparent images are not allowed in PDF/A-1b, so we convert it to
3206 // a format without alpha channel first
3207
3208 QImage alphaLessImage(image.width(), image.height(), QImage::Format_RGB32);
3209 alphaLessImage.fill(color: Qt::white);
3210
3211 QPainter p(&alphaLessImage);
3212 p.drawImage(x: 0, y: 0, image);
3213
3214 image = alphaLessImage;
3215 format = image.format();
3216 }
3217 }
3218
3219 if (image.depth() == 1 && *bitmap && is_monochrome(colorTable: img.colorTable())) {
3220 if (format == QImage::Format_MonoLSB)
3221 image = image.convertToFormat(f: QImage::Format_Mono);
3222 format = QImage::Format_Mono;
3223 } else {
3224 *bitmap = false;
3225 if (format != QImage::Format_RGB32 && format != QImage::Format_ARGB32 && format != QImage::Format_CMYK8888) {
3226 image = image.convertToFormat(f: QImage::Format_ARGB32);
3227 format = QImage::Format_ARGB32;
3228 }
3229 }
3230
3231 int w = image.width();
3232 int h = image.height();
3233
3234 if (format == QImage::Format_Mono) {
3235 int bytesPerLine = (w + 7) >> 3;
3236 QByteArray data;
3237 data.resize(size: bytesPerLine * h);
3238 char *rawdata = data.data();
3239 for (int y = 0; y < h; ++y) {
3240 memcpy(dest: rawdata, src: image.constScanLine(y), n: bytesPerLine);
3241 rawdata += bytesPerLine;
3242 }
3243 object = writeImage(data, width: w, height: h, option: WriteImageOption::Monochrome, maskObject: 0, softMaskObject: 0, dct: false, isMono: is_monochrome(colorTable: img.colorTable()));
3244 } else {
3245 QByteArray softMaskData;
3246 bool dct = false;
3247 QByteArray imageData;
3248 bool hasAlpha = false;
3249 bool hasMask = false;
3250
3251 if (QImageWriter::supportedImageFormats().contains(t: "jpeg") && !grayscale && !lossless) {
3252 QBuffer buffer(&imageData);
3253 QImageWriter writer(&buffer, "jpeg");
3254 writer.setQuality(94);
3255 if (format == QImage::Format_CMYK8888) {
3256 // PDFs require CMYK colors not to be inverted in the JPEG encoding
3257 writer.setSubType("CMYK");
3258 }
3259 writer.write(image);
3260 dct = true;
3261
3262 if (format != QImage::Format_RGB32 && format != QImage::Format_CMYK8888) {
3263 softMaskData.resize(size: w * h);
3264 uchar *sdata = (uchar *)softMaskData.data();
3265 for (int y = 0; y < h; ++y) {
3266 const QRgb *rgb = (const QRgb *)image.constScanLine(y);
3267 for (int x = 0; x < w; ++x) {
3268 uchar alpha = qAlpha(rgb: *rgb);
3269 *sdata++ = alpha;
3270 hasMask |= (alpha < 255);
3271 hasAlpha |= (alpha != 0 && alpha != 255);
3272 ++rgb;
3273 }
3274 }
3275 }
3276 } else {
3277 if (format == QImage::Format_CMYK8888) {
3278 imageData.resize(size: grayscale ? w * h : w * h * 4);
3279 uchar *data = (uchar *)imageData.data();
3280 const qsizetype bytesPerLine = image.bytesPerLine();
3281 if (grayscale) {
3282 for (int y = 0; y < h; ++y) {
3283 const uint *cmyk = (const uint *)image.constScanLine(y);
3284 for (int x = 0; x < w; ++x)
3285 *data++ = qGray(rgb: QCmyk32::fromCmyk32(cmyk: *cmyk++).toColor().rgba());
3286 }
3287 } else {
3288 for (int y = 0; y < h; ++y) {
3289 uchar *start = data + y * w * 4;
3290 memcpy(dest: start, src: image.constScanLine(y), n: bytesPerLine);
3291 }
3292 }
3293 } else {
3294 imageData.resize(size: grayscale ? w * h : 3 * w * h);
3295 uchar *data = (uchar *)imageData.data();
3296 softMaskData.resize(size: w * h);
3297 uchar *sdata = (uchar *)softMaskData.data();
3298 for (int y = 0; y < h; ++y) {
3299 const QRgb *rgb = (const QRgb *)image.constScanLine(y);
3300 if (grayscale) {
3301 for (int x = 0; x < w; ++x) {
3302 *(data++) = qGray(rgb: *rgb);
3303 uchar alpha = qAlpha(rgb: *rgb);
3304 *sdata++ = alpha;
3305 hasMask |= (alpha < 255);
3306 hasAlpha |= (alpha != 0 && alpha != 255);
3307 ++rgb;
3308 }
3309 } else {
3310 for (int x = 0; x < w; ++x) {
3311 *(data++) = qRed(rgb: *rgb);
3312 *(data++) = qGreen(rgb: *rgb);
3313 *(data++) = qBlue(rgb: *rgb);
3314 uchar alpha = qAlpha(rgb: *rgb);
3315 *sdata++ = alpha;
3316 hasMask |= (alpha < 255);
3317 hasAlpha |= (alpha != 0 && alpha != 255);
3318 ++rgb;
3319 }
3320 }
3321 }
3322 }
3323 if (format == QImage::Format_RGB32 || format == QImage::Format_CMYK8888)
3324 hasAlpha = hasMask = false;
3325 }
3326 int maskObject = 0;
3327 int softMaskObject = 0;
3328 if (hasAlpha) {
3329 softMaskObject = writeImage(data: softMaskData, width: w, height: h, option: WriteImageOption::Grayscale, maskObject: 0, softMaskObject: 0);
3330 } else if (hasMask) {
3331 // dither the soft mask to 1bit and add it. This also helps PDF viewers
3332 // without transparency support
3333 int bytesPerLine = (w + 7) >> 3;
3334 QByteArray mask(bytesPerLine * h, 0);
3335 uchar *mdata = (uchar *)mask.data();
3336 const uchar *sdata = (const uchar *)softMaskData.constData();
3337 for (int y = 0; y < h; ++y) {
3338 for (int x = 0; x < w; ++x) {
3339 if (*sdata)
3340 mdata[x>>3] |= (0x80 >> (x&7));
3341 ++sdata;
3342 }
3343 mdata += bytesPerLine;
3344 }
3345 maskObject = writeImage(data: mask, width: w, height: h, option: WriteImageOption::Monochrome, maskObject: 0, softMaskObject: 0);
3346 }
3347
3348 const WriteImageOption option = [&]() {
3349 if (grayscale)
3350 return WriteImageOption::Grayscale;
3351 if (format == QImage::Format_CMYK8888)
3352 return WriteImageOption::CMYK;
3353 return WriteImageOption::RGB;
3354 }();
3355
3356 object = writeImage(data: imageData, width: w, height: h, option,
3357 maskObject, softMaskObject, dct);
3358 }
3359 imageCache.insert(key: serial_no, value: object);
3360 return object;
3361}
3362
3363void QPdfEnginePrivate::drawTextItem(const QPointF &p, const QTextItemInt &ti)
3364{
3365 Q_Q(QPdfEngine);
3366
3367 const bool isLink = ti.charFormat.hasProperty(propertyId: QTextFormat::AnchorHref);
3368 const bool isAnchor = ti.charFormat.hasProperty(propertyId: QTextFormat::AnchorName);
3369 // PDF/X-4 (§ 6.17) does not allow annotations that don't lie
3370 // outside the BleedBox/TrimBox, so don't emit an hyperlink
3371 // annotation at all.
3372 const bool isX4 = pdfVersion == QPdfEngine::Version_X4;
3373 if ((isLink && !isX4) || isAnchor) {
3374 qreal size = ti.fontEngine->fontDef.pixelSize;
3375 int synthesized = ti.fontEngine->synthesized();
3376 qreal stretch = synthesized & QFontEngine::SynthesizedStretch ? ti.fontEngine->fontDef.stretch/100. : 1.;
3377 Q_ASSERT(stretch > qreal(0));
3378
3379 QTransform trans;
3380 // Build text rendering matrix (Trm). We need it to map the text area to user
3381 // space units on the PDF page.
3382 trans = QTransform(size*stretch, 0, 0, size, 0, 0);
3383 // Apply text matrix (Tm).
3384 trans *= QTransform(1,0,0,-1,p.x(),p.y());
3385 // Apply page displacement (Identity for first page).
3386 trans *= stroker.matrix;
3387 // Apply Current Transformation Matrix (CTM)
3388 trans *= pageMatrix();
3389 qreal x1, y1, x2, y2;
3390 trans.map(x: 0, y: 0, tx: &x1, ty: &y1);
3391 trans.map(x: ti.width.toReal()/size, y: (ti.ascent.toReal()-ti.descent.toReal())/size, tx: &x2, ty: &y2);
3392
3393 if (isLink) {
3394 uint annot = addXrefEntry(object: -1);
3395 QByteArray x1s, y1s, x2s, y2s;
3396 x1s.setNum(static_cast<double>(x1), format: 'f');
3397 y1s.setNum(static_cast<double>(y1), format: 'f');
3398 x2s.setNum(static_cast<double>(x2), format: 'f');
3399 y2s.setNum(static_cast<double>(y2), format: 'f');
3400 QByteArray rectData = x1s + ' ' + y1s + ' ' + x2s + ' ' + y2s;
3401 xprintf(fmt: "<<\n/Type /Annot\n/Subtype /Link\n");
3402
3403 if (pdfVersion == QPdfEngine::Version_A1b)
3404 xprintf(fmt: "/F 4\n"); // enable print flag, disable all other
3405
3406 xprintf(fmt: "/Rect [");
3407 xprintf(fmt: rectData.constData());
3408#ifdef Q_DEBUG_PDF_LINKS
3409 xprintf("]\n/Border [16 16 1]\n");
3410#else
3411 xprintf(fmt: "]\n/Border [0 0 0]\n");
3412#endif
3413 const QString link = ti.charFormat.anchorHref();
3414 const bool isInternal = link.startsWith(c: QLatin1Char('#'));
3415 if (!isInternal) {
3416 xprintf(fmt: "/A <<\n");
3417 xprintf(fmt: "/Type /Action\n/S /URI\n/URI (%s)\n", link.toLatin1().constData());
3418 xprintf(fmt: ">>\n");
3419 } else {
3420 xprintf(fmt: "/Dest ");
3421 printString(string: link.sliced(pos: 1));
3422 xprintf(fmt: "\n");
3423 }
3424 xprintf(fmt: ">>\n");
3425 xprintf(fmt: "endobj\n");
3426
3427 if (!currentPage->annotations.contains(t: annot)) {
3428 currentPage->annotations.append(t: annot);
3429 }
3430 } else {
3431 const QString anchor = ti.charFormat.anchorNames().constFirst();
3432 const uint curPage = pages.last();
3433 destCache.append(t: DestInfo({ .anchor: anchor, .pageObj: curPage, .coords: QPointF(x1, y2) }));
3434 }
3435 }
3436
3437 QFontEngine *fe = ti.fontEngine;
3438
3439 QFontEngine::FaceId face_id = fe->faceId();
3440 bool noEmbed = false;
3441 if (!embedFonts
3442 || face_id.filename.isEmpty()
3443 || fe->fsType & 0x200 /* bitmap embedding only */
3444 || fe->fsType == 2 /* no embedding allowed */) {
3445 *currentPage << "Q\n";
3446 q->QPaintEngine::drawTextItem(p, textItem: ti);
3447 *currentPage << "q\n";
3448 if (face_id.filename.isEmpty())
3449 return;
3450 noEmbed = true;
3451 }
3452
3453 QFontSubset *font = fonts.value(key: face_id, defaultValue: nullptr);
3454 if (!font) {
3455 font = new QFontSubset(fe, requestObject());
3456 font->noEmbed = noEmbed;
3457 }
3458 fonts.insert(key: face_id, value: font);
3459
3460 if (!currentPage->fonts.contains(t: font->object_id))
3461 currentPage->fonts.append(t: font->object_id);
3462
3463 qreal size = ti.fontEngine->fontDef.pixelSize;
3464
3465 QVarLengthArray<glyph_t> glyphs;
3466 QVarLengthArray<QFixedPoint> positions;
3467 QTransform m = QTransform::fromTranslate(dx: p.x(), dy: p.y());
3468 ti.fontEngine->getGlyphPositions(glyphs: ti.glyphs, matrix: m, flags: ti.flags,
3469 glyphs_out&: glyphs, positions);
3470 if (glyphs.size() == 0)
3471 return;
3472 int synthesized = ti.fontEngine->synthesized();
3473 qreal stretch = synthesized & QFontEngine::SynthesizedStretch ? ti.fontEngine->fontDef.stretch/100. : 1.;
3474 Q_ASSERT(stretch > qreal(0));
3475
3476 *currentPage << "BT\n"
3477 << "/F" << font->object_id << size << "Tf "
3478 << stretch << (synthesized & QFontEngine::SynthesizedItalic
3479 ? "0 .3 -1 0 0 Tm\n"
3480 : "0 0 -1 0 0 Tm\n");
3481
3482
3483#if 0
3484 // #### implement actual text for complex languages
3485 const unsigned short *logClusters = ti.logClusters;
3486 int pos = 0;
3487 do {
3488 int end = pos + 1;
3489 while (end < ti.num_chars && logClusters[end] == logClusters[pos])
3490 ++end;
3491 *currentPage << "/Span << /ActualText <FEFF";
3492 for (int i = pos; i < end; ++i) {
3493 s << toHex((ushort)ti.chars[i].unicode(), buf);
3494 }
3495 *currentPage << "> >>\n"
3496 "BDC\n"
3497 "<";
3498 int ge = end == ti.num_chars ? ti.num_glyphs : logClusters[end];
3499 for (int gs = logClusters[pos]; gs < ge; ++gs)
3500 *currentPage << toHex((ushort)ti.glyphs[gs].glyph, buf);
3501 *currentPage << "> Tj\n"
3502 "EMC\n";
3503 pos = end;
3504 } while (pos < ti.num_chars);
3505#else
3506 qreal last_x = 0.;
3507 qreal last_y = 0.;
3508 for (int i = 0; i < glyphs.size(); ++i) {
3509 qreal x = positions[i].x.toReal();
3510 qreal y = positions[i].y.toReal();
3511 if (synthesized & QFontEngine::SynthesizedItalic)
3512 x += .3*y;
3513 x /= stretch;
3514 char buf[5];
3515 qsizetype g = font->addGlyph(index: glyphs[i]);
3516 *currentPage << x - last_x << last_y - y << "Td <"
3517 << QPdf::toHex(u: (ushort)g, buffer: buf) << "> Tj\n";
3518 last_x = x;
3519 last_y = y;
3520 }
3521 if (synthesized & QFontEngine::SynthesizedBold) {
3522 *currentPage << stretch << (synthesized & QFontEngine::SynthesizedItalic
3523 ? "0 .3 -1 0 0 Tm\n"
3524 : "0 0 -1 0 0 Tm\n");
3525 *currentPage << "/Span << /ActualText <> >> BDC\n";
3526 last_x = 0.5*fe->lineThickness().toReal();
3527 last_y = 0.;
3528 for (int i = 0; i < glyphs.size(); ++i) {
3529 qreal x = positions[i].x.toReal();
3530 qreal y = positions[i].y.toReal();
3531 if (synthesized & QFontEngine::SynthesizedItalic)
3532 x += .3*y;
3533 x /= stretch;
3534 char buf[5];
3535 qsizetype g = font->addGlyph(index: glyphs[i]);
3536 *currentPage << x - last_x << last_y - y << "Td <"
3537 << QPdf::toHex(u: (ushort)g, buffer: buf) << "> Tj\n";
3538 last_x = x;
3539 last_y = y;
3540 }
3541 *currentPage << "EMC\n";
3542 }
3543#endif
3544
3545 *currentPage << "ET\n";
3546}
3547
3548QTransform QPdfEnginePrivate::pageMatrix() const
3549{
3550 qreal userUnit = calcUserUnit();
3551 qreal scale = 72. / userUnit / resolution;
3552 QTransform tmp(scale, 0.0, 0.0, -scale, 0.0, m_pageLayout.fullRectPoints().height() / userUnit);
3553 if (m_pageLayout.mode() != QPageLayout::FullPageMode) {
3554 QRect r = m_pageLayout.paintRectPixels(resolution);
3555 tmp.translate(dx: r.left(), dy: r.top());
3556 }
3557 return tmp;
3558}
3559
3560void QPdfEnginePrivate::newPage()
3561{
3562 if (currentPage && currentPage->pageSize.isEmpty())
3563 currentPage->pageSize = m_pageLayout.fullRectPoints().size();
3564 writePage();
3565
3566 delete currentPage;
3567 currentPage = new QPdfPage;
3568 currentPage->pageSize = m_pageLayout.fullRectPoints().size();
3569 stroker.stream = currentPage;
3570 pages.append(t: requestObject());
3571
3572 *currentPage << "/GSa gs /CSp cs /CSp CS\n"
3573 << QPdf::generateMatrix(matrix: pageMatrix())
3574 << "q q\n";
3575}
3576
3577QT_END_NAMESPACE
3578
3579#endif // QT_NO_PDF
3580

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qtbase/src/gui/painting/qpdf.cpp