1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | |
30 | #include <QtTest/QtTest> |
31 | |
32 | |
33 | #include <QDebug> |
34 | #include <QFile> |
35 | #include <QImage> |
36 | #include <QImageReader> |
37 | #include <QImageWriter> |
38 | #include <QPainter> |
39 | #include <QSet> |
40 | #include <QTemporaryDir> |
41 | |
42 | #ifdef Q_OS_UNIX // for geteuid() |
43 | # include <sys/types.h> |
44 | # include <unistd.h> |
45 | #endif |
46 | |
47 | #include <algorithm> |
48 | |
49 | typedef QMap<QString, QString> QStringMap; |
50 | typedef QVector<int> QIntList; |
51 | Q_DECLARE_METATYPE(QImageWriter::ImageWriterError) |
52 | Q_DECLARE_METATYPE(QImage::Format) |
53 | |
54 | class tst_QImageWriter : public QObject |
55 | { |
56 | Q_OBJECT |
57 | |
58 | public slots: |
59 | void initTestCase(); |
60 | |
61 | private slots: |
62 | void getSetCheck(); |
63 | void writeImage_data(); |
64 | void writeImage(); |
65 | void writeImage2_data(); |
66 | void writeImage2(); |
67 | void supportedFormats(); |
68 | void supportedMimeTypes(); |
69 | |
70 | void writeToInvalidDevice(); |
71 | void testCanWrite(); |
72 | |
73 | void supportsOption_data(); |
74 | void supportsOption(); |
75 | |
76 | void saveWithNoFormat_data(); |
77 | void saveWithNoFormat(); |
78 | |
79 | void saveToTemporaryFile(); |
80 | |
81 | void writeEmpty(); |
82 | |
83 | private: |
84 | QTemporaryDir m_temporaryDir; |
85 | QString prefix; |
86 | QString writePrefix; |
87 | }; |
88 | |
89 | // helper to skip an autotest when the given image format is not supported |
90 | #define SKIP_IF_UNSUPPORTED(format) do { \ |
91 | if (!QByteArray(format).isEmpty() && !QImageReader::supportedImageFormats().contains(format)) \ |
92 | QSKIP('"' + QByteArray(format) + "\" images are not supported"); \ |
93 | } while (0) |
94 | |
95 | static void initializePadding(QImage *image) |
96 | { |
97 | int effectiveBytesPerLine = (image->width() * image->depth() + 7) / 8; |
98 | int paddingBytes = image->bytesPerLine() - effectiveBytesPerLine; |
99 | if (paddingBytes == 0) |
100 | return; |
101 | for (int y = 0; y < image->height(); ++y) { |
102 | memset(s: image->scanLine(y) + effectiveBytesPerLine, c: 0, n: paddingBytes); |
103 | } |
104 | } |
105 | |
106 | void tst_QImageWriter::initTestCase() |
107 | { |
108 | QVERIFY(m_temporaryDir.isValid()); |
109 | prefix = QFINDTESTDATA("images/" ); |
110 | if (prefix.isEmpty()) |
111 | QFAIL("Can't find images directory!" ); |
112 | writePrefix = m_temporaryDir.path() + QLatin1Char('/'); |
113 | } |
114 | |
115 | // Testing get/set functions |
116 | void tst_QImageWriter::getSetCheck() |
117 | { |
118 | QImageWriter obj1; |
119 | // QIODevice * QImageWriter::device() |
120 | // void QImageWriter::setDevice(QIODevice *) |
121 | QFile *var1 = new QFile; |
122 | obj1.setDevice(var1); |
123 | |
124 | QCOMPARE((QIODevice *) var1, obj1.device()); |
125 | // The class should possibly handle a 0-pointer as a device, since |
126 | // there is a default contructor, so it's "handling" a 0 device by default. |
127 | // For example: QMovie::setDevice(0) works just fine |
128 | obj1.setDevice((QIODevice *)0); |
129 | QCOMPARE((QIODevice *) 0, obj1.device()); |
130 | delete var1; |
131 | |
132 | // int QImageWriter::quality() |
133 | // void QImageWriter::setQuality(int) |
134 | obj1.setQuality(0); |
135 | QCOMPARE(0, obj1.quality()); |
136 | obj1.setQuality(INT_MIN); |
137 | QCOMPARE(INT_MIN, obj1.quality()); |
138 | obj1.setQuality(INT_MAX); |
139 | QCOMPARE(INT_MAX, obj1.quality()); |
140 | |
141 | // int QImageWriter::compression() |
142 | // void QImageWriter::setCompression(int) |
143 | obj1.setCompression(0); |
144 | QCOMPARE(0, obj1.compression()); |
145 | obj1.setCompression(INT_MIN); |
146 | QCOMPARE(INT_MIN, obj1.compression()); |
147 | obj1.setCompression(INT_MAX); |
148 | QCOMPARE(INT_MAX, obj1.compression()); |
149 | |
150 | // float QImageWriter::gamma() |
151 | // void QImageWriter::setGamma(float) |
152 | obj1.setGamma(0.0f); |
153 | QCOMPARE(0.0f, obj1.gamma()); |
154 | obj1.setGamma(1.1f); |
155 | QCOMPARE(1.1f, obj1.gamma()); |
156 | } |
157 | |
158 | void tst_QImageWriter::writeImage_data() |
159 | { |
160 | QTest::addColumn<QString>(name: "fileName" ); |
161 | QTest::addColumn<bool>(name: "lossy" ); |
162 | QTest::addColumn<QByteArray>(name: "format" ); |
163 | |
164 | QTest::newRow(dataTag: "BMP: colorful" ) << QString("colorful.bmp" ) << false << QByteArray("bmp" ); |
165 | QTest::newRow(dataTag: "BMP: font" ) << QString("font.bmp" ) << false << QByteArray("bmp" ); |
166 | QTest::newRow(dataTag: "XPM: marble" ) << QString("marble.xpm" ) << false << QByteArray("xpm" ); |
167 | QTest::newRow(dataTag: "PNG: kollada" ) << QString("kollada.png" ) << false << QByteArray("png" ); |
168 | QTest::newRow(dataTag: "PPM: teapot" ) << QString("teapot.ppm" ) << false << QByteArray("ppm" ); |
169 | QTest::newRow(dataTag: "PBM: ship63" ) << QString("ship63.pbm" ) << true << QByteArray("pbm" ); |
170 | QTest::newRow(dataTag: "XBM: gnus" ) << QString("gnus.xbm" ) << false << QByteArray("xbm" ); |
171 | QTest::newRow(dataTag: "JPEG: beavis" ) << QString("beavis.jpg" ) << true << QByteArray("jpeg" ); |
172 | QTest::newRow(dataTag: "ICO: App" ) << QString("App.ico" ) << true << QByteArray("ico" ); |
173 | } |
174 | |
175 | void tst_QImageWriter::writeImage() |
176 | { |
177 | QFETCH(QString, fileName); |
178 | QFETCH(bool, lossy); |
179 | QFETCH(QByteArray, format); |
180 | |
181 | SKIP_IF_UNSUPPORTED(format); |
182 | |
183 | QImage image; |
184 | { |
185 | QImageReader reader(prefix + fileName); |
186 | image = reader.read(); |
187 | QVERIFY2(!image.isNull(), qPrintable(reader.errorString())); |
188 | } |
189 | { |
190 | QImageWriter writer(writePrefix + "gen-" + fileName, format); |
191 | QVERIFY(writer.write(image)); |
192 | } |
193 | |
194 | { |
195 | bool skip = false; |
196 | #if defined(Q_OS_UNIX) |
197 | if (::geteuid() == 0) |
198 | skip = true; |
199 | #endif |
200 | if (!skip) { |
201 | // Shouldn't be able to write to read-only file |
202 | QFile sourceFile(writePrefix + "gen-" + fileName); |
203 | QFile::Permissions permissions = sourceFile.permissions(); |
204 | QVERIFY(sourceFile.setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::ReadGroup | QFile::ReadOther)); |
205 | |
206 | QImageWriter writer(writePrefix + "gen-" + fileName, format); |
207 | QVERIFY(!writer.write(image)); |
208 | |
209 | QVERIFY(sourceFile.setPermissions(permissions)); |
210 | } |
211 | } |
212 | |
213 | QImage image2; |
214 | { |
215 | QImageReader reader(writePrefix + "gen-" + fileName); |
216 | image2 = reader.read(); |
217 | QVERIFY(!image2.isNull()); |
218 | } |
219 | if (!lossy) { |
220 | QCOMPARE(image, image2); |
221 | } else { |
222 | QCOMPARE(image.format(), image2.format()); |
223 | QCOMPARE(image.depth(), image2.depth()); |
224 | } |
225 | } |
226 | |
227 | void tst_QImageWriter::writeImage2_data() |
228 | { |
229 | QTest::addColumn<QString>(name: "fileName" ); |
230 | QTest::addColumn<QByteArray>(name: "format" ); |
231 | QTest::addColumn<QImage>(name: "image" ); |
232 | |
233 | static const QLatin1String formats[] = { |
234 | QLatin1String("bmp" ), |
235 | QLatin1String("xpm" ), |
236 | QLatin1String("png" ), |
237 | QLatin1String("ppm" ), |
238 | QLatin1String("ico" ), |
239 | // QLatin1String("jpeg"), |
240 | }; |
241 | |
242 | QImage image0(70, 70, QImage::Format_RGB32); |
243 | image0.fill(pixel: QColor(Qt::red).rgb()); |
244 | |
245 | QImage::Format imgFormat = QImage::Format_Mono; |
246 | while (imgFormat != QImage::NImageFormats) { |
247 | QImage image = image0.convertToFormat(f: imgFormat); |
248 | initializePadding(image: &image); |
249 | for (QLatin1String format : formats) { |
250 | const QString fileName = QLatin1String("solidcolor_" ) |
251 | + QString::number(imgFormat) + QLatin1Char('.') + format; |
252 | QTest::newRow(dataTag: fileName.toLatin1()) << writePrefix + fileName |
253 | << QByteArray(format.data(), format.size()) |
254 | << image; |
255 | } |
256 | imgFormat = QImage::Format(int(imgFormat) + 1); |
257 | } |
258 | } |
259 | |
260 | /* |
261 | Workaround for the equality operator for indexed formats |
262 | (which fails if the colortables are different). |
263 | |
264 | Images must have the same format and size. |
265 | */ |
266 | static bool equalImageContents(const QImage &image1, const QImage &image2) |
267 | { |
268 | switch (image1.format()) { |
269 | case QImage::Format_Mono: |
270 | case QImage::Format_Indexed8: |
271 | for (int y = 0; y < image1.height(); ++y) |
272 | for (int x = 0; x < image1.width(); ++x) |
273 | if (image1.pixel(x, y) != image2.pixel(x, y)) |
274 | return false; |
275 | return true; |
276 | default: |
277 | return (image1 == image2); |
278 | } |
279 | } |
280 | |
281 | void tst_QImageWriter::writeImage2() |
282 | { |
283 | QFETCH(QString, fileName); |
284 | QFETCH(QByteArray, format); |
285 | QFETCH(QImage, image); |
286 | |
287 | //we reduce the scope of writer so that it closes the associated file |
288 | // and QFile::remove can actually work |
289 | { |
290 | QImageWriter writer(fileName, format); |
291 | QVERIFY(writer.write(image)); |
292 | } |
293 | |
294 | QImage written; |
295 | |
296 | //we reduce the scope of reader so that it closes the associated file |
297 | // and QFile::remove can actually work |
298 | { |
299 | QImageReader reader(fileName, format); |
300 | QVERIFY(reader.read(&written)); |
301 | } |
302 | |
303 | written = written.convertToFormat(f: image.format()); |
304 | if (!equalImageContents(image1: written, image2: image)) { |
305 | qDebug() << "image" << image.format() << image.width() |
306 | << image.height() << image.depth() |
307 | << image.pixelColor(x: 0, y: 0); |
308 | qDebug() << "written" << written.format() << written.width() |
309 | << written.height() << written.depth() |
310 | << written.pixelColor(x: 0, y: 0); |
311 | } |
312 | QVERIFY(equalImageContents(written, image)); |
313 | |
314 | QVERIFY(QFile::remove(fileName)); |
315 | } |
316 | |
317 | namespace { |
318 | // C++98-library version of C++11 std::is_sorted |
319 | template <typename C> |
320 | bool is_sorted(const C &c) |
321 | { |
322 | return std::adjacent_find(c.begin(), c.end(), std::greater_equal<typename C::value_type>()) == c.end(); |
323 | } |
324 | } |
325 | |
326 | void tst_QImageWriter::supportedFormats() |
327 | { |
328 | QList<QByteArray> formats = QImageWriter::supportedImageFormats(); |
329 | |
330 | // check that the list is sorted |
331 | QVERIFY(is_sorted(formats)); |
332 | |
333 | // check that the list does not contain duplicates |
334 | QVERIFY(std::unique(formats.begin(), formats.end()) == formats.end()); |
335 | } |
336 | |
337 | void tst_QImageWriter::supportedMimeTypes() |
338 | { |
339 | QList<QByteArray> mimeTypes = QImageWriter::supportedMimeTypes(); |
340 | |
341 | // check that the list is sorted |
342 | QVERIFY(is_sorted(mimeTypes)); |
343 | |
344 | // check the list as a minimum contains image/bmp |
345 | QVERIFY(mimeTypes.contains("image/bmp" )); |
346 | |
347 | // check that the list does not contain duplicates |
348 | QVERIFY(std::unique(mimeTypes.begin(), mimeTypes.end()) == mimeTypes.end()); |
349 | } |
350 | |
351 | void tst_QImageWriter::writeToInvalidDevice() |
352 | { |
353 | QLatin1String fileName("/these/directories/do/not/exist/001.png" ); |
354 | { |
355 | QImageWriter writer(fileName); |
356 | QVERIFY(!writer.canWrite()); |
357 | QCOMPARE(writer.error(), QImageWriter::DeviceError); |
358 | } |
359 | { |
360 | QImageWriter writer(fileName); |
361 | writer.setFormat("png" ); |
362 | QVERIFY(!writer.canWrite()); |
363 | QCOMPARE(writer.error(), QImageWriter::DeviceError); |
364 | } |
365 | { |
366 | QImageWriter writer(fileName); |
367 | QImage im(10, 10, QImage::Format_ARGB32); |
368 | QVERIFY(!writer.write(im)); |
369 | QCOMPARE(writer.error(), QImageWriter::DeviceError); |
370 | } |
371 | { |
372 | QImageWriter writer(fileName); |
373 | writer.setFormat("png" ); |
374 | QImage im(10, 10, QImage::Format_ARGB32); |
375 | QVERIFY(!writer.write(im)); |
376 | QCOMPARE(writer.error(), QImageWriter::DeviceError); |
377 | } |
378 | } |
379 | |
380 | void tst_QImageWriter::testCanWrite() |
381 | { |
382 | { |
383 | // device is not set |
384 | QImageWriter writer; |
385 | QVERIFY(!writer.canWrite()); |
386 | QCOMPARE(writer.error(), QImageWriter::DeviceError); |
387 | } |
388 | |
389 | { |
390 | // check if canWrite won't leave an empty file |
391 | QTemporaryDir dir; |
392 | QVERIFY2(dir.isValid(), qPrintable(dir.errorString())); |
393 | QString fileName(dir.path() + QLatin1String("/001.garble" )); |
394 | QVERIFY(!QFileInfo(fileName).exists()); |
395 | QImageWriter writer(fileName); |
396 | QVERIFY(!writer.canWrite()); |
397 | QCOMPARE(writer.error(), QImageWriter::UnsupportedFormatError); |
398 | QVERIFY(!QFileInfo(fileName).exists()); |
399 | } |
400 | } |
401 | |
402 | void tst_QImageWriter::supportsOption_data() |
403 | { |
404 | QTest::addColumn<QString>(name: "fileName" ); |
405 | QTest::addColumn<QIntList>(name: "options" ); |
406 | |
407 | QTest::newRow(dataTag: "png" ) << QString("gen-black.png" ) |
408 | << (QIntList() << QImageIOHandler::Gamma |
409 | << QImageIOHandler::Description |
410 | << QImageIOHandler::Quality |
411 | << QImageIOHandler::CompressionRatio |
412 | << QImageIOHandler::Size |
413 | << QImageIOHandler::ScaledSize); |
414 | } |
415 | |
416 | void tst_QImageWriter::supportsOption() |
417 | { |
418 | SKIP_IF_UNSUPPORTED(QTest::currentDataTag()); |
419 | |
420 | QFETCH(QString, fileName); |
421 | QFETCH(QIntList, options); |
422 | |
423 | static Q_CONSTEXPR QImageIOHandler::ImageOption allOptions[] = { |
424 | QImageIOHandler::Size, |
425 | QImageIOHandler::ClipRect, |
426 | QImageIOHandler::Description, |
427 | QImageIOHandler::ScaledClipRect, |
428 | QImageIOHandler::ScaledSize, |
429 | QImageIOHandler::CompressionRatio, |
430 | QImageIOHandler::Gamma, |
431 | QImageIOHandler::Quality, |
432 | QImageIOHandler::Name, |
433 | QImageIOHandler::SubType, |
434 | QImageIOHandler::IncrementalReading, |
435 | QImageIOHandler::Endianness, |
436 | QImageIOHandler::Animation, |
437 | QImageIOHandler::BackgroundColor, |
438 | }; |
439 | |
440 | QImageWriter writer(writePrefix + fileName); |
441 | for (auto option : allOptions) { |
442 | QCOMPARE(writer.supportsOption(option), options.contains(option)); |
443 | } |
444 | } |
445 | |
446 | void tst_QImageWriter::saveWithNoFormat_data() |
447 | { |
448 | QTest::addColumn<QString>(name: "fileName" ); |
449 | QTest::addColumn<QByteArray>(name: "format" ); |
450 | QTest::addColumn<QImageWriter::ImageWriterError>(name: "error" ); |
451 | |
452 | QTest::newRow(dataTag: "garble" ) << writePrefix + QString("gen-out.garble" ) << QByteArray("jpeg" ) << QImageWriter::UnsupportedFormatError; |
453 | QTest::newRow(dataTag: "bmp" ) << writePrefix + QString("gen-out.bmp" ) << QByteArray("bmp" ) << QImageWriter::ImageWriterError(0); |
454 | QTest::newRow(dataTag: "xbm" ) << writePrefix + QString("gen-out.xbm" ) << QByteArray("xbm" ) << QImageWriter::ImageWriterError(0); |
455 | QTest::newRow(dataTag: "xpm" ) << writePrefix + QString("gen-out.xpm" ) << QByteArray("xpm" ) << QImageWriter::ImageWriterError(0); |
456 | QTest::newRow(dataTag: "png" ) << writePrefix + QString("gen-out.png" ) << QByteArray("png" ) << QImageWriter::ImageWriterError(0); |
457 | QTest::newRow(dataTag: "ppm" ) << writePrefix + QString("gen-out.ppm" ) << QByteArray("ppm" ) << QImageWriter::ImageWriterError(0); |
458 | QTest::newRow(dataTag: "pbm" ) << writePrefix + QString("gen-out.pbm" ) << QByteArray("pbm" ) << QImageWriter::ImageWriterError(0); |
459 | } |
460 | |
461 | void tst_QImageWriter::saveWithNoFormat() |
462 | { |
463 | QFETCH(QString, fileName); |
464 | QFETCH(QByteArray, format); |
465 | QFETCH(QImageWriter::ImageWriterError, error); |
466 | |
467 | SKIP_IF_UNSUPPORTED(format); |
468 | |
469 | QImage niceImage(64, 64, QImage::Format_ARGB32); |
470 | memset(s: niceImage.bits(), c: 0, n: niceImage.sizeInBytes()); |
471 | |
472 | QImageWriter writer(fileName /* , 0 - no format! */); |
473 | if (error != 0) { |
474 | QVERIFY(!writer.write(niceImage)); |
475 | QCOMPARE(writer.error(), error); |
476 | return; |
477 | } |
478 | |
479 | QVERIFY2(writer.write(niceImage), qPrintable(writer.errorString())); |
480 | |
481 | QImageReader reader(fileName); |
482 | QCOMPARE(reader.format(), format); |
483 | |
484 | QVERIFY(reader.canRead()); |
485 | |
486 | QImage outImage = reader.read(); |
487 | QVERIFY2(!outImage.isNull(), qPrintable(reader.errorString())); |
488 | } |
489 | |
490 | void tst_QImageWriter::saveToTemporaryFile() |
491 | { |
492 | QImage image(prefix + "kollada.png" ); |
493 | QVERIFY(!image.isNull()); |
494 | |
495 | { |
496 | // 1) Via QImageWriter's API, with a standard temp file name |
497 | QTemporaryFile file; |
498 | QVERIFY2(file.open(), qPrintable(file.errorString())); |
499 | QImageWriter writer(&file, "PNG" ); |
500 | if (writer.canWrite()) |
501 | QVERIFY(writer.write(image)); |
502 | else |
503 | qWarning() << file.errorString(); |
504 | QCOMPARE(QImage(writer.fileName()), image); |
505 | } |
506 | { |
507 | // 2) Via QImage's API, with a standard temp file name |
508 | QTemporaryFile file; |
509 | QVERIFY2(file.open(), qPrintable(file.errorString())); |
510 | QVERIFY(image.save(&file, "PNG" )); |
511 | file.reset(); |
512 | QImage tmp; |
513 | QVERIFY(tmp.load(&file, "PNG" )); |
514 | QCOMPARE(tmp, image); |
515 | } |
516 | { |
517 | // 3) Via QImageWriter's API, with a named temp file |
518 | QTemporaryFile file(writePrefix + QLatin1String("tempXXXXXX" )); |
519 | QVERIFY2(file.open(), qPrintable(file.errorString())); |
520 | QImageWriter writer(&file, "PNG" ); |
521 | QVERIFY(writer.write(image)); |
522 | QCOMPARE(QImage(writer.fileName()), image); |
523 | } |
524 | { |
525 | // 4) Via QImage's API, with a named temp file |
526 | QTemporaryFile file(writePrefix + QLatin1String("tempXXXXXX" )); |
527 | QVERIFY2(file.open(), qPrintable(file.errorString())); |
528 | QVERIFY(image.save(&file, "PNG" )); |
529 | file.reset(); |
530 | QImage tmp; |
531 | QVERIFY(tmp.load(&file, "PNG" )); |
532 | QCOMPARE(tmp, image); |
533 | } |
534 | } |
535 | |
536 | void tst_QImageWriter::writeEmpty() |
537 | { |
538 | // check writing a null QImage errors gracefully |
539 | QTemporaryDir dir; |
540 | QVERIFY2(dir.isValid(), qPrintable(dir.errorString())); |
541 | QString fileName(dir.path() + QLatin1String("/testimage.bmp" )); |
542 | QVERIFY(!QFileInfo(fileName).exists()); |
543 | QImageWriter writer(fileName); |
544 | QVERIFY(!writer.write(QImage())); |
545 | QCOMPARE(writer.error(), QImageWriter::InvalidImageError); |
546 | QVERIFY(!QFileInfo(fileName).exists()); |
547 | } |
548 | |
549 | QTEST_MAIN(tst_QImageWriter) |
550 | #include "tst_qimagewriter.moc" |
551 | |