Compare commits
74 Commits
v5.75.0-rc
...
v5.86.0
Author | SHA1 | Date | |
---|---|---|---|
e89d21f12d | |||
1d2b51ddf1 | |||
1080976abe | |||
abd550c60c | |||
aade392da3 | |||
7642633551 | |||
9f2c5061c8 | |||
28099eed71 | |||
f5d574b3ad | |||
a8f92e5525 | |||
fbeef559b7 | |||
7f56d835f0 | |||
8f87ce4cb2 | |||
5aa03c12ad | |||
3266a9c466 | |||
1b2bf6e931 | |||
894391b000 | |||
ef6be2c077 | |||
96b40da089 | |||
bf3f94da76 | |||
318dacda75 | |||
e358bb0feb | |||
dca6e87c89 | |||
62f9af9a35 | |||
ff53d3d7e9 | |||
780f342825 | |||
297ed9a2fe | |||
55b4077f2c | |||
2429c95336 | |||
224f892b09 | |||
64fa129ed6 | |||
3cb4021afc | |||
95a19a15c3 | |||
1ba23a1e8e | |||
bc3c04c7ce | |||
2755f74fbb | |||
3b0c767f82 | |||
e80fcd7c30 | |||
a73e3d44dd | |||
1169859b07 | |||
04e276dcb3 | |||
e3ab850712 | |||
503b3eee2b | |||
511a22f0b4 | |||
c532227d43 | |||
1462c3abd6 | |||
ca52d4ddf5 | |||
7ba4d6adda | |||
0aaab103b1 | |||
8b9125c913 | |||
8845dd9818 | |||
a4b8295625 | |||
134c96fa61 | |||
3673874a63 | |||
8ad43638ad | |||
c72c9f577b | |||
bf3f99abf5 | |||
b79d1f222d | |||
bf1de9f8f0 | |||
82d5e0f8a4 | |||
bbf945137a | |||
54ed1dda27 | |||
34a9ec1b06 | |||
a6ec69d276 | |||
02cbf3889f | |||
6cf05cf305 | |||
938b8126b5 | |||
d36c191351 | |||
1acb5a6177 | |||
f2ccbf1724 | |||
5825c83235 | |||
b742cb7cc7 | |||
2e6eeebdfc | |||
db0b5d571a |
3
.git-blame-ignore-revs
Normal file
@ -0,0 +1,3 @@
|
||||
#clang-format/tidy
|
||||
1169859b07f25c865ee0bfc2a7dc97a431651776
|
||||
eaca33bec8b19498c3621fc3a20a8c55a2812462
|
6
.gitignore
vendored
@ -20,3 +20,9 @@ random_seed
|
||||
CMakeLists.txt.user*
|
||||
*.unc-backup*
|
||||
.cmake/
|
||||
/.clang-format
|
||||
/compile_commands.json
|
||||
.clangd
|
||||
.idea
|
||||
/cmake-build*
|
||||
.cache
|
||||
|
@ -1,11 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(KImageFormats)
|
||||
|
||||
set (CMAKE_CXX_STANDARD 14)
|
||||
|
||||
include(FeatureSummary)
|
||||
find_package(ECM 5.75.0 NO_MODULE)
|
||||
find_package(ECM 5.86.0 NO_MODULE)
|
||||
set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules")
|
||||
feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||
|
||||
@ -15,11 +13,12 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||
include(KDEInstallDirs)
|
||||
include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE)
|
||||
include(KDECMakeSettings)
|
||||
include(KDEGitCommitHooks)
|
||||
|
||||
|
||||
include(CheckIncludeFiles)
|
||||
|
||||
set(REQUIRED_QT_VERSION 5.12.0)
|
||||
set(REQUIRED_QT_VERSION 5.15.2)
|
||||
find_package(Qt5Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
|
||||
|
||||
find_package(KF5Archive)
|
||||
@ -42,16 +41,32 @@ if (UNIX)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
find_package(OpenEXR 3.0 CONFIG QUIET)
|
||||
if(NOT OpenEXR_FOUND)
|
||||
find_package(OpenEXR)
|
||||
endif()
|
||||
set_package_properties(OpenEXR PROPERTIES
|
||||
TYPE OPTIONAL
|
||||
PURPOSE "Required for the QImage plugin for OpenEXR images"
|
||||
)
|
||||
add_definitions(-DQT_NO_FOREACH)
|
||||
|
||||
find_package(libavif 0.8.2 CONFIG)
|
||||
set_package_properties(libavif PROPERTIES
|
||||
TYPE OPTIONAL
|
||||
PURPOSE "Required for the QImage plugin for AVIF images"
|
||||
)
|
||||
|
||||
option(KIMAGEFORMATS_HEIF "Enable plugin for HEIF format" OFF)
|
||||
if(KIMAGEFORMATS_HEIF)
|
||||
include(FindPkgConfig)
|
||||
pkg_check_modules(LibHeif IMPORTED_TARGET libheif>=1.10.0)
|
||||
endif()
|
||||
add_feature_info(LibHeif LibHeif_FOUND "required for the QImage plugin for HEIF/HEIC images")
|
||||
|
||||
# 050d00 (5.13) triggers a BIC in qimageiohandler.h, in Qt 5.13, so do not enable that until we can require 5.14
|
||||
# https://codereview.qt-project.org/c/qt/qtbase/+/279215
|
||||
add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050c00)
|
||||
add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x054900)
|
||||
add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f02)
|
||||
add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x055100)
|
||||
add_subdirectory(src)
|
||||
if (BUILD_TESTING)
|
||||
add_subdirectory(autotests)
|
||||
@ -59,3 +74,5 @@ if (BUILD_TESTING)
|
||||
endif()
|
||||
|
||||
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||
|
||||
kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT)
|
||||
|
@ -13,6 +13,7 @@ image formats.
|
||||
|
||||
The following image formats have read-only support:
|
||||
|
||||
- Animated Windows cursors (ani)
|
||||
- Gimp (xcf)
|
||||
- OpenEXR (exr)
|
||||
- Photoshop documents (psd)
|
||||
@ -20,6 +21,7 @@ The following image formats have read-only support:
|
||||
|
||||
The following image formats have read and write support:
|
||||
|
||||
- AV1 Image File Format (AVIF)
|
||||
- Encapsulated PostScript (eps)
|
||||
- Personal Computer Exchange (pcx)
|
||||
- SGI images (rgb, rgba, sgi, bw)
|
||||
|
@ -70,6 +70,18 @@ if (KF5Archive_FOUND)
|
||||
)
|
||||
endif()
|
||||
|
||||
if (TARGET avif)
|
||||
kimageformats_read_tests(
|
||||
avif
|
||||
)
|
||||
endif()
|
||||
|
||||
if (LibHeif_FOUND)
|
||||
kimageformats_read_tests(
|
||||
heif
|
||||
)
|
||||
endif()
|
||||
|
||||
# Allow some fuzziness when reading this formats, to allow for
|
||||
# rounding errors (eg: in alpha blending).
|
||||
kimageformats_read_tests(FUZZ 1
|
||||
@ -110,3 +122,8 @@ add_executable(pictest pictest.cpp)
|
||||
target_link_libraries(pictest Qt5::Gui Qt5::Test)
|
||||
ecm_mark_as_test(pictest)
|
||||
add_test(NAME kimageformats-pic COMMAND pictest)
|
||||
|
||||
add_executable(anitest anitest.cpp)
|
||||
target_link_libraries(anitest Qt5::Gui Qt5::Test)
|
||||
ecm_mark_as_test(anitest)
|
||||
add_test(NAME kimageformats-ani COMMAND anitest)
|
||||
|
BIN
autotests/ani/test.ani
Normal file
BIN
autotests/ani/test_1.png
Normal file
After Width: | Height: | Size: 813 B |
BIN
autotests/ani/test_2.png
Normal file
After Width: | Height: | Size: 697 B |
BIN
autotests/ani/test_3.png
Normal file
After Width: | Height: | Size: 810 B |
119
autotests/anitest.cpp
Normal file
@ -0,0 +1,119 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <kde@broulik.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
*/
|
||||
|
||||
#include <QImage>
|
||||
#include <QImageReader>
|
||||
#include <QTest>
|
||||
|
||||
static bool imgEquals(const QImage &im1, const QImage &im2)
|
||||
{
|
||||
const int height = im1.height();
|
||||
const int width = im1.width();
|
||||
for (int i = 0; i < height; ++i) {
|
||||
const auto *line1 = reinterpret_cast<const quint8 *>(im1.scanLine(i));
|
||||
const auto *line2 = reinterpret_cast<const quint8 *>(im2.scanLine(i));
|
||||
for (int j = 0; j < width; ++j) {
|
||||
if (line1[j] - line2[j] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class AniTests : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase()
|
||||
{
|
||||
QCoreApplication::addLibraryPath(QStringLiteral(PLUGIN_DIR));
|
||||
}
|
||||
|
||||
void testReadMetadata()
|
||||
{
|
||||
QImageReader reader(QFINDTESTDATA("ani/test.ani"));
|
||||
|
||||
QVERIFY(reader.canRead());
|
||||
|
||||
QCOMPARE(reader.imageCount(), 4);
|
||||
|
||||
QCOMPARE(reader.size(), QSize(32, 32));
|
||||
|
||||
QCOMPARE(reader.text(QStringLiteral("Title")), QStringLiteral("ANI Test"));
|
||||
QCOMPARE(reader.text(QStringLiteral("Author")), QStringLiteral("KDE Community"));
|
||||
}
|
||||
|
||||
void textRead()
|
||||
{
|
||||
QImageReader reader(QFINDTESTDATA("ani/test.ani"));
|
||||
QVERIFY(reader.canRead());
|
||||
QCOMPARE(reader.currentImageNumber(), 0);
|
||||
|
||||
QImage aniFrame;
|
||||
QVERIFY(reader.read(&aniFrame));
|
||||
|
||||
QImage img1(QFINDTESTDATA("ani/test_1.png"));
|
||||
img1.convertTo(aniFrame.format());
|
||||
|
||||
QVERIFY(imgEquals(aniFrame, img1));
|
||||
|
||||
QCOMPARE(reader.nextImageDelay(), 166); // 10 "jiffies"
|
||||
|
||||
QVERIFY(reader.canRead());
|
||||
// that read() above should have advanced us to the next frame
|
||||
QCOMPARE(reader.currentImageNumber(), 1);
|
||||
|
||||
QVERIFY(reader.read(&aniFrame));
|
||||
QImage img2(QFINDTESTDATA("ani/test_2.png"));
|
||||
img2.convertTo(aniFrame.format());
|
||||
|
||||
QVERIFY(imgEquals(aniFrame, img2));
|
||||
|
||||
// The "middle" frame has a longer delay than the others
|
||||
QCOMPARE(reader.nextImageDelay(), 333); // 20 "jiffies"
|
||||
|
||||
QVERIFY(reader.canRead());
|
||||
QCOMPARE(reader.currentImageNumber(), 2);
|
||||
|
||||
QVERIFY(reader.read(&aniFrame));
|
||||
QImage img3(QFINDTESTDATA("ani/test_3.png"));
|
||||
img3.convertTo(aniFrame.format());
|
||||
|
||||
QVERIFY(imgEquals(aniFrame, img3));
|
||||
|
||||
QCOMPARE(reader.nextImageDelay(), 166);
|
||||
|
||||
QVERIFY(reader.canRead());
|
||||
QCOMPARE(reader.currentImageNumber(), 3);
|
||||
|
||||
QVERIFY(reader.read(&aniFrame));
|
||||
// custom sequence in the ANI file should get us back to img2
|
||||
QVERIFY(imgEquals(aniFrame, img2));
|
||||
|
||||
QCOMPARE(reader.nextImageDelay(), 166);
|
||||
|
||||
// We should have reached the end now
|
||||
QVERIFY(!reader.canRead());
|
||||
QVERIFY(!reader.read(&aniFrame));
|
||||
|
||||
// Jump back to the start
|
||||
QVERIFY(reader.jumpToImage(0));
|
||||
|
||||
QVERIFY(reader.canRead());
|
||||
QCOMPARE(reader.currentImageNumber(), 0);
|
||||
|
||||
QCOMPARE(reader.nextImageDelay(), 166);
|
||||
|
||||
QVERIFY(reader.read(&aniFrame));
|
||||
QVERIFY(imgEquals(aniFrame, img1));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(AniTests)
|
||||
|
||||
#include "anitest.moc"
|
@ -35,61 +35,27 @@ private:
|
||||
QTest::addColumn<QImage::Format>("pngformat");
|
||||
QTest::addColumn<bool>("compress");
|
||||
|
||||
QTest::newRow("4x4 no alpha RLE")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QString()
|
||||
<< false
|
||||
<< QImage::Format_RGB32
|
||||
<< true;
|
||||
QTest::newRow("4x4 no alpha RLE") << QFINDTESTDATA("pic/4x4-simple-color.pic") << QFINDTESTDATA("pic/4x4-simple-color.png") << QString() << false
|
||||
<< QImage::Format_RGB32 << true;
|
||||
|
||||
QTest::newRow("4x4 no alpha raw")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color-uncompressed.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QString()
|
||||
<< false
|
||||
<< QImage::Format_RGB32
|
||||
<< false;
|
||||
QTest::newRow("4x4 no alpha raw") << QFINDTESTDATA("pic/4x4-simple-color-uncompressed.pic") << QFINDTESTDATA("pic/4x4-simple-color.png") << QString()
|
||||
<< false << QImage::Format_RGB32 << false;
|
||||
|
||||
QTest::newRow("Short comment")
|
||||
<< QFINDTESTDATA("pic/short-comment.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QStringLiteral("Test comment value")
|
||||
<< false
|
||||
<< QImage::Format_RGB32
|
||||
<< true;
|
||||
QTest::newRow("Short comment") << QFINDTESTDATA("pic/short-comment.pic") << QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QStringLiteral("Test comment value") << false << QImage::Format_RGB32 << true;
|
||||
|
||||
QTest::newRow("Long comment")
|
||||
<< QFINDTESTDATA("pic/long-comment.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QStringLiteral("Test comment value that goes right up to the end of the comment field and has no")
|
||||
<< false
|
||||
<< QImage::Format_RGB32
|
||||
<< true;
|
||||
QTest::newRow("Long comment") << QFINDTESTDATA("pic/long-comment.pic") << QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QStringLiteral("Test comment value that goes right up to the end of the comment field and has no") << false
|
||||
<< QImage::Format_RGB32 << true;
|
||||
|
||||
QTest::newRow("Long run-lengths")
|
||||
<< QFINDTESTDATA("pic/long-runs.pic")
|
||||
<< QFINDTESTDATA("pic/long-runs.png")
|
||||
<< QString()
|
||||
<< false
|
||||
<< QImage::Format_RGB32
|
||||
<< true;
|
||||
QTest::newRow("Long run-lengths") << QFINDTESTDATA("pic/long-runs.pic") << QFINDTESTDATA("pic/long-runs.png") << QString() << false
|
||||
<< QImage::Format_RGB32 << true;
|
||||
|
||||
QTest::newRow("4x4 with alpha RLE")
|
||||
<< QFINDTESTDATA("pic/4x4-alpha.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-alpha.png")
|
||||
<< QString()
|
||||
<< true
|
||||
<< QImage::Format_ARGB32
|
||||
<< true;
|
||||
QTest::newRow("4x4 with alpha RLE") << QFINDTESTDATA("pic/4x4-alpha.pic") << QFINDTESTDATA("pic/4x4-alpha.png") << QString() << true
|
||||
<< QImage::Format_ARGB32 << true;
|
||||
|
||||
QTest::newRow("4x4 with alpha raw")
|
||||
<< QFINDTESTDATA("pic/4x4-alpha-uncompressed.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-alpha.png")
|
||||
<< QString()
|
||||
<< true
|
||||
<< QImage::Format_ARGB32
|
||||
<< false;
|
||||
QTest::newRow("4x4 with alpha raw") << QFINDTESTDATA("pic/4x4-alpha-uncompressed.pic") << QFINDTESTDATA("pic/4x4-alpha.png") << QString() << true
|
||||
<< QImage::Format_ARGB32 << false;
|
||||
}
|
||||
|
||||
private Q_SLOTS:
|
||||
@ -106,13 +72,8 @@ private Q_SLOTS:
|
||||
// so there is no actual data loss in converting to RGB16.
|
||||
// This just tests that the pic plugin can deal with different
|
||||
// input formats.
|
||||
QTest::newRow("altered format")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QString()
|
||||
<< false
|
||||
<< QImage::Format_RGB16
|
||||
<< true;
|
||||
QTest::newRow("altered format") << QFINDTESTDATA("pic/4x4-simple-color.pic") << QFINDTESTDATA("pic/4x4-simple-color.png") << QString() << false
|
||||
<< QImage::Format_RGB16 << true;
|
||||
}
|
||||
|
||||
void testRead_data()
|
||||
@ -148,17 +109,12 @@ private Q_SLOTS:
|
||||
imgWriter.write(pngImage);
|
||||
|
||||
if (expData != picData) {
|
||||
QString fileNameBase = QUuid::createUuid().toString()
|
||||
.remove(QLatin1Char('{'))
|
||||
.remove(QLatin1Char('}'));
|
||||
QString fileNameBase = QUuid::createUuid().toString().remove(QLatin1Char('{')).remove(QLatin1Char('}'));
|
||||
QFile dumpFile(fileNameBase + QStringLiteral(".pic"));
|
||||
QVERIFY2(dumpFile.open(QIODevice::WriteOnly), qPrintable(dumpFile.errorString()));
|
||||
dumpFile.write(picData);
|
||||
QString msg = QStringLiteral("Written data (")
|
||||
+ dumpFile.fileName()
|
||||
+ QStringLiteral(") differed from expected data (")
|
||||
+ picfile
|
||||
+ QLatin1Char(')');
|
||||
QString msg =
|
||||
QStringLiteral("Written data (") + dumpFile.fileName() + QStringLiteral(") differed from expected data (") + picfile + QLatin1Char(')');
|
||||
QFAIL(qPrintable(msg));
|
||||
}
|
||||
}
|
||||
@ -182,28 +138,19 @@ private Q_SLOTS:
|
||||
QCOMPARE(inputImage.width(), expImage.width());
|
||||
QCOMPARE(inputImage.height(), expImage.height());
|
||||
QCOMPARE(inputImage.hasAlphaChannel(), alpha);
|
||||
QCOMPARE(inputImage.format(), alpha ? QImage::Format_ARGB32
|
||||
: QImage::Format_RGB32);
|
||||
QCOMPARE(inputImage.format(), alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||
|
||||
expImage = expImage.convertToFormat(pngformat);
|
||||
expImage = expImage.convertToFormat(alpha ? QImage::Format_ARGB32
|
||||
: QImage::Format_RGB32);
|
||||
expImage = expImage.convertToFormat(alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||
if (inputImage != expImage) {
|
||||
QString fileNameBase = QUuid::createUuid().toString()
|
||||
.remove(QLatin1Char('{'))
|
||||
.remove(QLatin1Char('}'));
|
||||
QString fileNameBase = QUuid::createUuid().toString().remove(QLatin1Char('{')).remove(QLatin1Char('}'));
|
||||
QFile picDumpFile(fileNameBase + QStringLiteral("-expected.data"));
|
||||
QVERIFY2(picDumpFile.open(QIODevice::WriteOnly), qPrintable(picDumpFile.errorString()));
|
||||
picDumpFile.write(reinterpret_cast<const char *>(inputImage.bits()),
|
||||
inputImage.sizeInBytes());
|
||||
picDumpFile.write(reinterpret_cast<const char *>(inputImage.bits()), inputImage.sizeInBytes());
|
||||
QFile pngDumpFile(fileNameBase + QStringLiteral("-actual.data"));
|
||||
QVERIFY2(pngDumpFile.open(QIODevice::WriteOnly), qPrintable(pngDumpFile.errorString()));
|
||||
pngDumpFile.write(reinterpret_cast<const char *>(expImage.bits()),
|
||||
expImage.sizeInBytes());
|
||||
QString msg = QStringLiteral("Read image (")
|
||||
+ picDumpFile.fileName()
|
||||
+ QStringLiteral(") differed from expected image (")
|
||||
+ pngDumpFile.fileName()
|
||||
pngDumpFile.write(reinterpret_cast<const char *>(expImage.bits()), expImage.sizeInBytes());
|
||||
QString msg = QStringLiteral("Read image (") + picDumpFile.fileName() + QStringLiteral(") differed from expected image (") + pngDumpFile.fileName()
|
||||
+ QLatin1Char(')');
|
||||
QFAIL(qPrintable(msg));
|
||||
}
|
||||
@ -252,8 +199,7 @@ private Q_SLOTS:
|
||||
|
||||
QImageReader inputReader(picfile, "pic");
|
||||
|
||||
QCOMPARE(inputReader.imageFormat(),
|
||||
alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||
QCOMPARE(inputReader.imageFormat(), alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||
}
|
||||
};
|
||||
|
||||
|
BIN
autotests/read/avif/bw.avif
Normal file
After Width: | Height: | Size: 629 B |
BIN
autotests/read/avif/bw.png
Normal file
After Width: | Height: | Size: 743 B |
BIN
autotests/read/avif/bwa.avif
Normal file
After Width: | Height: | Size: 823 B |
BIN
autotests/read/avif/bwa.png
Normal file
After Width: | Height: | Size: 574 B |
BIN
autotests/read/avif/rgb.avif
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
autotests/read/avif/rgb.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
autotests/read/avif/rgba.avif
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
autotests/read/avif/rgba.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
autotests/read/hdr/rgb-landscape.hdr
Executable file
BIN
autotests/read/hdr/rgb-landscape.png
Executable file
After Width: | Height: | Size: 1.2 KiB |
BIN
autotests/read/hdr/rgb-portrait.hdr
Executable file
BIN
autotests/read/hdr/rgb-portrait.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
autotests/read/heif/rgb.heif
Normal file
BIN
autotests/read/heif/rgb.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
autotests/read/heif/rgba.heif
Normal file
BIN
autotests/read/heif/rgba.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
autotests/read/psd/16bit-rle.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
autotests/read/psd/16bit-rle.psd
Normal file
BIN
autotests/read/psd/argb16-raw-affinityphoto-1.8.5.png
Normal file
After Width: | Height: | Size: 983 B |
BIN
autotests/read/psd/argb16-raw-affinityphoto-1.8.5.psd
Normal file
BIN
autotests/read/psd/rgb16-raw-affinityphoto-1.8.5.png
Normal file
After Width: | Height: | Size: 827 B |
BIN
autotests/read/psd/rgb16-raw-affinityphoto-1.8.5.psd
Normal file
@ -22,40 +22,63 @@ static void writeImageData(const char *name, const QString &filename, const QIma
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
qint64 written = file.write(reinterpret_cast<const char *>(image.bits()), image.sizeInBytes());
|
||||
if (written == image.sizeInBytes()) {
|
||||
QTextStream(stdout) << " " << name
|
||||
<< " written to " << filename << "\n";
|
||||
QTextStream(stdout) << " " << name << " written to " << filename << "\n";
|
||||
} else {
|
||||
QTextStream(stdout) << " could not write " << name
|
||||
<< " to " << filename << ":"
|
||||
<< file.errorString() << "\n";
|
||||
QTextStream(stdout) << " could not write " << name << " to " << filename << ":" << file.errorString() << "\n";
|
||||
}
|
||||
} else {
|
||||
QTextStream(stdout) << " could not open "
|
||||
<< filename << ":"
|
||||
<< file.errorString() << "\n";
|
||||
QTextStream(stdout) << " could not open " << filename << ":" << file.errorString() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
template<class Trait>
|
||||
static bool fuzzyeq(const QImage &im1, const QImage &im2, uchar fuzziness)
|
||||
{
|
||||
Q_ASSERT(im1.format() == im2.format());
|
||||
Q_ASSERT(im1.depth() == 24 || im1.depth() == 32 || im1.depth() == 64);
|
||||
|
||||
const int height = im1.height();
|
||||
const int width = im1.width();
|
||||
for (int i = 0; i < height; ++i) {
|
||||
const Trait *line1 = reinterpret_cast<const Trait *>(im1.scanLine(i));
|
||||
const Trait *line2 = reinterpret_cast<const Trait *>(im2.scanLine(i));
|
||||
for (int j = 0; j < width; ++j) {
|
||||
if (line1[j] > line2[j]) {
|
||||
if (line1[j] - line2[j] > fuzziness) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (line2[j] - line1[j] > fuzziness) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// allow each byte to be different by up to 1, to allow for rounding errors
|
||||
static bool fuzzyeq(const QImage &im1, const QImage &im2, uchar fuzziness)
|
||||
{
|
||||
const int height = im1.height();
|
||||
const int width = im1.width();
|
||||
for (int i = 0; i < height; ++i) {
|
||||
const uchar *line1 = im1.scanLine(i);
|
||||
const uchar *line2 = im2.scanLine(i);
|
||||
for (int j = 0; j < width; ++j) {
|
||||
if (line1[j] > line2[j]) {
|
||||
if (line1[j] - line2[j] > fuzziness)
|
||||
return false;
|
||||
} else {
|
||||
if (line2[j] - line1[j] > fuzziness)
|
||||
return false;
|
||||
return (im1.depth() == 64) ? fuzzyeq<quint16>(im1, im2, fuzziness) : fuzzyeq<quint8>(im1, im2, fuzziness);
|
||||
}
|
||||
|
||||
// Returns the original format if we support, or returns
|
||||
// format which we preferred to use for `fuzzyeq()`.
|
||||
// We do only support formats with 8-bits/16-bits pre pixel.
|
||||
// If that changed, don't forget to update `fuzzyeq()` too
|
||||
static QImage::Format preferredFormat(QImage::Format fmt)
|
||||
{
|
||||
switch (fmt) {
|
||||
case QImage::Format_RGB32:
|
||||
case QImage::Format_ARGB32:
|
||||
case QImage::Format_RGBX64:
|
||||
case QImage::Format_RGBA64:
|
||||
return fmt;
|
||||
default:
|
||||
return QImage::Format_ARGB32;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
@ -70,8 +93,7 @@ int main(int argc, char ** argv)
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
parser.addPositionalArgument(QStringLiteral("format"), QStringLiteral("format to test"));
|
||||
QCommandLineOption fuzz(
|
||||
QStringList() << QStringLiteral("f") << QStringLiteral("fuzz"),
|
||||
QCommandLineOption fuzz(QStringList() << QStringLiteral("f") << QStringLiteral("fuzz"),
|
||||
QStringLiteral("Allow for some deviation in ARGB data."),
|
||||
QStringLiteral("max"));
|
||||
parser.addOption(fuzz);
|
||||
@ -109,13 +131,14 @@ int main(int argc, char ** argv)
|
||||
int failed = 0;
|
||||
|
||||
QTextStream(stdout) << "********* "
|
||||
<< "Starting basic read tests for "
|
||||
<< suffix << " images *********\n";
|
||||
<< "Starting basic read tests for " << suffix << " images *********\n";
|
||||
|
||||
const QList<QByteArray> formats = QImageReader::supportedImageFormats();
|
||||
QStringList formatStrings;
|
||||
formatStrings.reserve(formats.size());
|
||||
std::transform(formats.begin(), formats.end(), std::back_inserter(formatStrings), [](const QByteArray &format) { return QString(format); });
|
||||
std::transform(formats.begin(), formats.end(), std::back_inserter(formatStrings), [](const QByteArray &format) {
|
||||
return QString(format);
|
||||
});
|
||||
QTextStream(stdout) << "QImageReader::supportedImageFormats: " << formatStrings.join(", ") << "\n";
|
||||
|
||||
const QFileInfoList lstImgDir = imgdir.entryInfoList();
|
||||
@ -132,79 +155,58 @@ int main(int argc, char ** argv)
|
||||
QImage expImage;
|
||||
|
||||
if (!expReader.read(&expImage)) {
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName()
|
||||
<< ": could not load " << expfilename
|
||||
<< ": " << expReader.errorString()
|
||||
<< "\n";
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not load " << expfilename << ": " << expReader.errorString() << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
if (!inputReader.canRead()) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": failed can read: "
|
||||
<< inputReader.errorString()
|
||||
<< "\n";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed can read: " << inputReader.errorString() << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
if (!inputReader.read(&inputImage)) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": failed to load: "
|
||||
<< inputReader.errorString()
|
||||
<< "\n";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed to load: " << inputReader.errorString() << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
if (expImage.width() != inputImage.width()) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": width was " << inputImage.width()
|
||||
<< " but " << expfilename << " width was "
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": width was " << inputImage.width() << " but " << expfilename << " width was "
|
||||
<< expImage.width() << "\n";
|
||||
++failed;
|
||||
} else if (expImage.height() != inputImage.height()) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": height was " << inputImage.height()
|
||||
<< " but " << expfilename << " height was "
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": height was " << inputImage.height() << " but " << expfilename << " height was "
|
||||
<< expImage.height() << "\n";
|
||||
++failed;
|
||||
} else {
|
||||
if (inputImage.format() != QImage::Format_ARGB32) {
|
||||
QTextStream(stdout) << "INFO : " << fi.fileName()
|
||||
<< ": converting " << fi.fileName()
|
||||
<< " from " << formatToString(inputImage.format())
|
||||
<< " to ARGB32\n";
|
||||
inputImage = inputImage.convertToFormat(QImage::Format_ARGB32);
|
||||
QImage::Format inputFormat = preferredFormat(inputImage.format());
|
||||
QImage::Format expFormat = preferredFormat(expImage.format());
|
||||
QImage::Format cmpFormat = inputFormat == expFormat ? inputFormat : QImage::Format_ARGB32;
|
||||
|
||||
if (inputImage.format() != cmpFormat) {
|
||||
QTextStream(stdout) << "INFO : " << fi.fileName() << ": converting " << fi.fileName() << " from " << formatToString(inputImage.format())
|
||||
<< " to " << formatToString(cmpFormat) << '\n';
|
||||
inputImage = inputImage.convertToFormat(cmpFormat);
|
||||
}
|
||||
if (expImage.format() != QImage::Format_ARGB32) {
|
||||
QTextStream(stdout) << "INFO : " << fi.fileName()
|
||||
<< ": converting " << expfilename
|
||||
<< " from " << formatToString(expImage.format())
|
||||
<< " to ARGB32\n";
|
||||
expImage = expImage.convertToFormat(QImage::Format_ARGB32);
|
||||
if (expImage.format() != cmpFormat) {
|
||||
QTextStream(stdout) << "INFO : " << fi.fileName() << ": converting " << expfilename << " from " << formatToString(expImage.format()) << " to "
|
||||
<< formatToString(cmpFormat) << '\n';
|
||||
expImage = expImage.convertToFormat(cmpFormat);
|
||||
}
|
||||
if (fuzzyeq(inputImage, expImage, fuzziness)) {
|
||||
QTextStream(stdout) << "PASS : " << fi.fileName() << "\n";
|
||||
++passed;
|
||||
} else {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": differs from " << expfilename << "\n";
|
||||
writeImageData("expected data",
|
||||
fi.fileName() + QLatin1String("-expected.data"),
|
||||
expImage);
|
||||
writeImageData("actual data",
|
||||
fi.fileName() + QLatin1String("-actual.data"),
|
||||
inputImage);
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": differs from " << expfilename << "\n";
|
||||
writeImageData("expected data", fi.fileName() + QLatin1String("-expected.data"), expImage);
|
||||
writeImageData("actual data", fi.fileName() + QLatin1String("-actual.data"), inputImage);
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QTextStream(stdout) << "Totals: "
|
||||
<< passed << " passed, "
|
||||
<< failed << " failed\n";
|
||||
QTextStream(stdout) << "Totals: " << passed << " passed, " << failed << " failed\n";
|
||||
QTextStream(stdout) << "********* "
|
||||
<< "Finished basic read tests for "
|
||||
<< suffix << " images *********\n";
|
||||
<< "Finished basic read tests for " << suffix << " images *********\n";
|
||||
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
|
@ -29,8 +29,7 @@ int main(int argc, char ** argv)
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
parser.addPositionalArgument(QStringLiteral("format"), QStringLiteral("format to test."));
|
||||
QCommandLineOption lossless(
|
||||
QStringList() << QStringLiteral("l") << QStringLiteral("lossless"),
|
||||
QCommandLineOption lossless(QStringList() << QStringLiteral("l") << QStringLiteral("lossless"),
|
||||
QStringLiteral("Check that reading back the data gives the same image."));
|
||||
parser.addOption(lossless);
|
||||
|
||||
@ -56,8 +55,7 @@ int main(int argc, char ** argv)
|
||||
int failed = 0;
|
||||
|
||||
QTextStream(stdout) << "********* "
|
||||
<< "Starting basic write tests for "
|
||||
<< suffix << " images *********\n";
|
||||
<< "Starting basic write tests for " << suffix << " images *********\n";
|
||||
const QFileInfoList lstImgDir = imgdir.entryInfoList();
|
||||
for (const QFileInfo &fi : lstImgDir) {
|
||||
int suffixPos = fi.filePath().count() - suffix.count();
|
||||
@ -67,20 +65,14 @@ int main(int argc, char ** argv)
|
||||
QImageReader pngReader(pngfile, "png");
|
||||
QImage pngImage;
|
||||
if (!pngReader.read(&pngImage)) {
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName()
|
||||
<< ": could not load " << pngfilename
|
||||
<< ": " << pngReader.errorString()
|
||||
<< "\n";
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not load " << pngfilename << ": " << pngReader.errorString() << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
|
||||
QFile expFile(fi.filePath());
|
||||
if (!expFile.open(QIODevice::ReadOnly)) {
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName()
|
||||
<< ": could not open " << fi.fileName()
|
||||
<< ": " << expFile.errorString()
|
||||
<< "\n";
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not open " << fi.fileName() << ": " << expFile.errorString() << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
@ -91,10 +83,7 @@ int main(int argc, char ** argv)
|
||||
char buf[1];
|
||||
qint64 result = expFile.read(buf, 1);
|
||||
if (result < 0) {
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName()
|
||||
<< ": could not load " << fi.fileName()
|
||||
<< ": " << expFile.errorString()
|
||||
<< "\n";
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not load " << fi.fileName() << ": " << expFile.errorString() << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
@ -105,16 +94,14 @@ int main(int argc, char ** argv)
|
||||
QBuffer buffer(&writtenData);
|
||||
QImageWriter imgWriter(&buffer, format.constData());
|
||||
if (!imgWriter.write(pngImage)) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": failed to write image data\n";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed to write image data\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (expData != writtenData) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": written data differs from " << fi.fileName() << "\n";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": written data differs from " << fi.fileName() << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
@ -124,8 +111,7 @@ int main(int argc, char ** argv)
|
||||
QBuffer buffer(&writtenData);
|
||||
QImageReader imgReader(&buffer, format.constData());
|
||||
if (!imgReader.read(&reReadImage)) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": could not read back the written data\n";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": could not read back the written data\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
@ -134,8 +120,7 @@ int main(int argc, char ** argv)
|
||||
|
||||
if (parser.isSet(lossless)) {
|
||||
if (pngImage != reReadImage) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": re-reading the data resulted in a different image\n";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": re-reading the data resulted in a different image\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
@ -145,12 +130,9 @@ int main(int argc, char ** argv)
|
||||
++passed;
|
||||
}
|
||||
|
||||
QTextStream(stdout) << "Totals: "
|
||||
<< passed << " passed, "
|
||||
<< failed << " failed\n";
|
||||
QTextStream(stdout) << "Totals: " << passed << " passed, " << failed << " failed\n";
|
||||
QTextStream(stdout) << "********* "
|
||||
<< "Finished basic write tests for "
|
||||
<< suffix << " images *********\n";
|
||||
<< "Finished basic write tests for " << suffix << " images *********\n";
|
||||
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ type: functional
|
||||
platforms:
|
||||
- name: Linux
|
||||
- name: FreeBSD
|
||||
- name: MacOSX
|
||||
- name: macOS
|
||||
- name: Windows
|
||||
note: No EPS support on Windows
|
||||
- name: Android
|
||||
|
@ -4,19 +4,13 @@
|
||||
|
||||
function(kimageformats_add_plugin plugin)
|
||||
set(options)
|
||||
set(oneValueArgs JSON)
|
||||
set(multiValueArgs SOURCES)
|
||||
cmake_parse_arguments(KIF_ADD_PLUGIN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
||||
if(NOT KIF_ADD_PLUGIN_SOURCES)
|
||||
message(FATAL_ERROR "kimageformats_add_plugin called without SOURCES parameter")
|
||||
endif()
|
||||
get_filename_component(json "${KIF_ADD_PLUGIN_JSON}" REALPATH)
|
||||
if(NOT KIF_ADD_PLUGIN_JSON OR NOT EXISTS ${json})
|
||||
message(FATAL_ERROR "JSON file doesn't exist: ${json}")
|
||||
endif()
|
||||
|
||||
add_library(${plugin} MODULE ${KIF_ADD_PLUGIN_SOURCES})
|
||||
set_property(TARGET ${plugin} APPEND PROPERTY AUTOGEN_TARGET_DEPENDS ${json})
|
||||
set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/imageformats")
|
||||
target_link_libraries(${plugin} Qt5::Gui)
|
||||
install(TARGETS ${plugin} DESTINATION ${KDE_INSTALL_QTPLUGINDIR}/imageformats)
|
||||
@ -24,13 +18,26 @@ endfunction()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_ani SOURCES ani.cpp)
|
||||
install(FILES ani.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
if (TARGET avif)
|
||||
kimageformats_add_plugin(kimg_avif SOURCES "avif.cpp")
|
||||
target_link_libraries(kimg_avif "avif")
|
||||
install(FILES avif.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
endif()
|
||||
|
||||
##################################
|
||||
|
||||
install(FILES dds-qt.desktop RENAME dds.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
if (BUILD_EPS_PLUGIN)
|
||||
if (Qt5PrintSupport_FOUND)
|
||||
kimageformats_add_plugin(kimg_eps JSON "eps.json" SOURCES eps.cpp)
|
||||
kimageformats_add_plugin(kimg_eps SOURCES eps.cpp)
|
||||
target_link_libraries(kimg_eps Qt5::PrintSupport)
|
||||
install(FILES eps.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
endif()
|
||||
@ -44,8 +51,17 @@ install(FILES jp2.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugi
|
||||
##################################
|
||||
|
||||
if(OpenEXR_FOUND)
|
||||
kimageformats_add_plugin(kimg_exr JSON "exr.json" SOURCES exr.cpp)
|
||||
kimageformats_add_plugin(kimg_exr SOURCES exr.cpp)
|
||||
if(TARGET OpenEXR::OpenEXR)
|
||||
target_link_libraries(kimg_exr OpenEXR::OpenEXR)
|
||||
else()
|
||||
if(OpenEXR_VERSION_STRING VERSION_LESS 2.3.0)
|
||||
# Older OpenEXR versions use dynamic exception specifications, so
|
||||
# cannot use C++17 with them
|
||||
set_target_properties(kimg_exr PROPERTIES CXX_STANDARD 14)
|
||||
endif()
|
||||
target_link_libraries(kimg_exr OpenEXR::IlmImf)
|
||||
endif()
|
||||
kde_target_enable_exceptions(kimg_exr PRIVATE)
|
||||
|
||||
install(FILES exr.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
@ -53,53 +69,62 @@ endif()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_hdr JSON "hdr.json" SOURCES hdr.cpp)
|
||||
kimageformats_add_plugin(kimg_hdr SOURCES hdr.cpp)
|
||||
install(FILES hdr.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_pcx JSON "pcx.json" SOURCES pcx.cpp)
|
||||
if (LibHeif_FOUND)
|
||||
kimageformats_add_plugin(kimg_heif SOURCES heif.cpp)
|
||||
target_link_libraries(kimg_heif PkgConfig::LibHeif)
|
||||
kde_target_enable_exceptions(kimg_heif PRIVATE)
|
||||
install(FILES heif.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
endif()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_pcx SOURCES pcx.cpp)
|
||||
install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_pic JSON "pic.json" SOURCES pic.cpp)
|
||||
kimageformats_add_plugin(kimg_pic SOURCES pic.cpp)
|
||||
install(FILES pic.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_psd JSON "psd.json" SOURCES psd.cpp)
|
||||
kimageformats_add_plugin(kimg_psd SOURCES psd.cpp)
|
||||
install(FILES psd.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_ras JSON "ras.json" SOURCES ras.cpp)
|
||||
kimageformats_add_plugin(kimg_ras SOURCES ras.cpp)
|
||||
install(FILES ras.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_rgb JSON "rgb.json" SOURCES rgb.cpp)
|
||||
kimageformats_add_plugin(kimg_rgb SOURCES rgb.cpp)
|
||||
install(FILES rgb.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_tga JSON "tga.json" SOURCES tga.cpp)
|
||||
kimageformats_add_plugin(kimg_tga SOURCES tga.cpp)
|
||||
install(FILES tga.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_xcf JSON "xcf.json" SOURCES xcf.cpp)
|
||||
kimageformats_add_plugin(kimg_xcf SOURCES xcf.cpp)
|
||||
install(FILES xcf.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
if (KF5Archive_FOUND)
|
||||
|
||||
kimageformats_add_plugin(kimg_kra JSON "kra.json" SOURCES kra.cpp)
|
||||
kimageformats_add_plugin(kimg_kra SOURCES kra.cpp)
|
||||
target_link_libraries(kimg_kra KF5::Archive)
|
||||
install(FILES kra.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
kimageformats_add_plugin(kimg_ora JSON "ora.json" SOURCES ora.cpp)
|
||||
kimageformats_add_plugin(kimg_ora SOURCES ora.cpp)
|
||||
target_link_libraries(kimg_ora KF5::Archive)
|
||||
install(FILES ora.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
|
||||
|
567
src/imageformats/ani.cpp
Normal file
@ -0,0 +1,567 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <kde@broulik.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "ani_p.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QImage>
|
||||
#include <QScopeGuard>
|
||||
#include <QVariant>
|
||||
#include <QtEndian>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct ChunkHeader {
|
||||
char magic[4];
|
||||
quint32_le size;
|
||||
};
|
||||
|
||||
struct AniHeader {
|
||||
quint32_le cbSize;
|
||||
quint32_le nFrames; // number of actual frames in the file
|
||||
quint32_le nSteps; // number of logical images
|
||||
quint32_le iWidth;
|
||||
quint32_le iHeight;
|
||||
quint32_le iBitCount;
|
||||
quint32_le nPlanes;
|
||||
quint32_le iDispRate;
|
||||
quint32_le bfAttributes; // attributes (0 = bitmap images, 1 = ico/cur, 3 = "seq" block available)
|
||||
};
|
||||
|
||||
struct CurHeader {
|
||||
quint16_le wReserved; // always 0
|
||||
quint16_le wResID; // always 2
|
||||
quint16_le wNumImages;
|
||||
};
|
||||
|
||||
struct CursorDirEntry {
|
||||
quint8 bWidth;
|
||||
quint8 bHeight;
|
||||
quint8 bColorCount;
|
||||
quint8 bReserved; // always 0
|
||||
quint16_le wHotspotX;
|
||||
quint16_le wHotspotY;
|
||||
quint32_le dwBytesInImage;
|
||||
quint32_le dwImageOffset;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ANIHandler::ANIHandler() = default;
|
||||
|
||||
bool ANIHandler::canRead() const
|
||||
{
|
||||
if (canRead(device())) {
|
||||
setFormat("ani");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if there's another frame coming
|
||||
const QByteArray nextFrame = device()->peek(sizeof(ChunkHeader));
|
||||
if (nextFrame.size() == sizeof(ChunkHeader)) {
|
||||
const auto *header = reinterpret_cast<const ChunkHeader *>(nextFrame.data());
|
||||
if (qstrncmp(header->magic, "icon", sizeof(header->magic)) == 0 && header->size > 0) {
|
||||
setFormat("ani");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ANIHandler::read(QImage *outImage)
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (device()->pos() < m_firstFrameOffset) {
|
||||
device()->seek(m_firstFrameOffset);
|
||||
}
|
||||
|
||||
const QByteArray frameType = device()->read(4);
|
||||
if (frameType != "icon") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray frameSizeData = device()->read(sizeof(quint32_le));
|
||||
if (frameSizeData.count() != sizeof(quint32_le)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto frameSize = *(reinterpret_cast<const quint32_le *>(frameSizeData.data()));
|
||||
if (!frameSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray frameData = device()->read(frameSize);
|
||||
|
||||
const bool ok = outImage->loadFromData(frameData, "cur");
|
||||
|
||||
++m_currentImageNumber;
|
||||
|
||||
// When we have a custom image sequence, seek to before the frame that would follow
|
||||
if (!m_imageSequence.isEmpty()) {
|
||||
if (m_currentImageNumber < m_imageSequence.count()) {
|
||||
const int nextFrame = m_imageSequence.at(m_currentImageNumber);
|
||||
if (nextFrame < 0 || nextFrame >= m_frameOffsets.count()) {
|
||||
return false;
|
||||
}
|
||||
const auto nextOffset = m_frameOffsets.at(nextFrame);
|
||||
device()->seek(nextOffset);
|
||||
} else if (m_currentImageNumber == m_imageSequence.count()) {
|
||||
const auto endOffset = m_frameOffsets.last();
|
||||
if (device()->pos() != endOffset) {
|
||||
device()->seek(endOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
int ANIHandler::currentImageNumber() const
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return 0;
|
||||
}
|
||||
return m_currentImageNumber;
|
||||
}
|
||||
|
||||
int ANIHandler::imageCount() const
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return 0;
|
||||
}
|
||||
return m_imageCount;
|
||||
}
|
||||
|
||||
bool ANIHandler::jumpToImage(int imageNumber)
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (imageNumber < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (imageNumber == m_currentImageNumber) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we have a custom image sequence we have a index of frames we can jump to
|
||||
if (!m_imageSequence.isEmpty()) {
|
||||
if (imageNumber >= m_imageSequence.count()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int targetFrame = m_imageSequence.at(imageNumber);
|
||||
|
||||
const auto targetOffset = m_frameOffsets.value(targetFrame, -1);
|
||||
|
||||
if (device()->seek(targetOffset)) {
|
||||
m_currentImageNumber = imageNumber;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (imageNumber >= m_frameCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// otherwise we need to jump from frame to frame
|
||||
const auto oldPos = device()->pos();
|
||||
|
||||
if (imageNumber < m_currentImageNumber) {
|
||||
// start from the beginning
|
||||
if (!device()->seek(m_firstFrameOffset)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
while (m_currentImageNumber < imageNumber) {
|
||||
if (!jumpToNextImage()) {
|
||||
device()->seek(oldPos);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_currentImageNumber = imageNumber;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ANIHandler::jumpToNextImage()
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have a custom image sequence we have a index of frames we can jump to
|
||||
// Delegate to jumpToImage
|
||||
if (!m_imageSequence.isEmpty()) {
|
||||
return jumpToImage(m_currentImageNumber + 1);
|
||||
}
|
||||
|
||||
if (device()->pos() < m_firstFrameOffset) {
|
||||
if (!device()->seek(m_firstFrameOffset)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const QByteArray nextFrame = device()->peek(sizeof(ChunkHeader));
|
||||
if (nextFrame.size() != sizeof(ChunkHeader)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto *header = reinterpret_cast<const ChunkHeader *>(nextFrame.data());
|
||||
if (qstrncmp(header->magic, "icon", sizeof(header->magic)) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const qint64 seekBy = sizeof(ChunkHeader) + header->size;
|
||||
|
||||
if (!device()->seek(device()->pos() + seekBy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
++m_currentImageNumber;
|
||||
return true;
|
||||
}
|
||||
|
||||
int ANIHandler::loopCount() const
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ANIHandler::nextImageDelay() const
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int rate = m_displayRate;
|
||||
|
||||
if (!m_displayRates.isEmpty()) {
|
||||
int previousImage = m_currentImageNumber - 1;
|
||||
if (previousImage < 0) {
|
||||
previousImage = m_displayRates.count() - 1;
|
||||
}
|
||||
rate = m_displayRates.at(previousImage);
|
||||
}
|
||||
|
||||
return rate * 1000 / 60;
|
||||
}
|
||||
|
||||
bool ANIHandler::supportsOption(ImageOption option) const
|
||||
{
|
||||
return option == Size || option == Name || option == Description || option == Animation;
|
||||
}
|
||||
|
||||
QVariant ANIHandler::option(ImageOption option) const
|
||||
{
|
||||
if (!supportsOption(option) || !ensureScanned()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (option) {
|
||||
case QImageIOHandler::Size:
|
||||
return m_size;
|
||||
// TODO QImageIOHandler::Format
|
||||
// but both iBitCount in AniHeader and bColorCount are just zero most of the time
|
||||
// so one would probably need to traverse even further down into IcoHeader and IconDirEntry...
|
||||
// but Qt's ICO/CUR handler always seems to give us a ARB
|
||||
case QImageIOHandler::Name:
|
||||
return m_name;
|
||||
case QImageIOHandler::Description: {
|
||||
QString description;
|
||||
if (!m_name.isEmpty()) {
|
||||
description += QStringLiteral("Title: %1\n\n").arg(m_name);
|
||||
}
|
||||
if (!m_artist.isEmpty()) {
|
||||
description += QStringLiteral("Author: %1\n\n").arg(m_artist);
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
case QImageIOHandler::Animation:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
bool ANIHandler::ensureScanned() const
|
||||
{
|
||||
if (m_scanned) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (device()->isSequential()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *mutableThis = const_cast<ANIHandler *>(this);
|
||||
|
||||
const auto oldPos = device()->pos();
|
||||
auto cleanup = qScopeGuard([this, oldPos] {
|
||||
device()->seek(oldPos);
|
||||
});
|
||||
|
||||
device()->seek(0);
|
||||
|
||||
const QByteArray riffIntro = device()->read(4);
|
||||
if (riffIntro != "RIFF") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto riffSizeData = device()->read(sizeof(quint32_le));
|
||||
if (riffSizeData.size() != sizeof(quint32_le)) {
|
||||
return false;
|
||||
}
|
||||
const auto riffSize = *(reinterpret_cast<const quint32_le *>(riffSizeData.data()));
|
||||
// TODO do a basic sanity check if the size is enough to hold some metadata and a frame?
|
||||
if (riffSize == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mutableThis->m_displayRates.clear();
|
||||
mutableThis->m_imageSequence.clear();
|
||||
|
||||
while (device()->pos() < riffSize) {
|
||||
const QByteArray chunkId = device()->read(4);
|
||||
if (chunkId.length() != 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (chunkId == "ACON") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QByteArray chunkSizeData = device()->read(sizeof(quint32_le));
|
||||
if (chunkSizeData.length() != sizeof(quint32_le)) {
|
||||
return false;
|
||||
}
|
||||
auto chunkSize = *(reinterpret_cast<const quint32_le *>(chunkSizeData.data()));
|
||||
|
||||
if (chunkId == "anih") {
|
||||
if (chunkSize != sizeof(AniHeader)) {
|
||||
qWarning() << "anih chunk size does not match ANIHEADER size";
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray anihData = device()->read(sizeof(AniHeader));
|
||||
if (anihData.size() != sizeof(AniHeader)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *aniHeader = reinterpret_cast<const AniHeader *>(anihData.data());
|
||||
|
||||
// The size in the ani header is usually 0 unfortunately,
|
||||
// so we'll also check the first frame for its size further below
|
||||
mutableThis->m_size = QSize(aniHeader->iWidth, aniHeader->iHeight);
|
||||
mutableThis->m_frameCount = aniHeader->nFrames;
|
||||
mutableThis->m_imageCount = aniHeader->nSteps;
|
||||
mutableThis->m_displayRate = aniHeader->iDispRate;
|
||||
} else if (chunkId == "rate" || chunkId == "seq ") {
|
||||
const QByteArray data = device()->read(chunkSize);
|
||||
if (static_cast<quint32_le>(data.size()) != chunkSize || data.size() % sizeof(quint32_le) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO should we check that the number of rate entries matches nSteps?
|
||||
auto *dataPtr = data.data();
|
||||
QVector<int> list;
|
||||
for (int i = 0; i < data.count(); i += sizeof(quint32_le)) {
|
||||
const auto entry = *(reinterpret_cast<const quint32_le *>(dataPtr + i));
|
||||
list.append(entry);
|
||||
}
|
||||
|
||||
if (chunkId == "rate") {
|
||||
// should we check that the number of rate entries matches nSteps?
|
||||
mutableThis->m_displayRates = list;
|
||||
} else if (chunkId == "seq ") {
|
||||
// Check if it's just an ascending sequence, don't bother with it then
|
||||
bool isAscending = true;
|
||||
for (int i = 0; i < list.count(); ++i) {
|
||||
if (list.at(i) != i) {
|
||||
isAscending = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAscending) {
|
||||
mutableThis->m_imageSequence = list;
|
||||
}
|
||||
}
|
||||
// IART and INAM are technically inside LIST->INFO but "INFO" is supposedly optional
|
||||
// so just handle those two attributes wherever we encounter them
|
||||
} else if (chunkId == "INAM" || chunkId == "IART") {
|
||||
const QByteArray value = device()->read(chunkSize);
|
||||
|
||||
if (static_cast<quint32_le>(value.size()) != chunkSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// DWORDs are aligned to even sizes
|
||||
if (chunkSize % 2 != 0) {
|
||||
device()->read(1);
|
||||
}
|
||||
|
||||
// FIXME encoding
|
||||
const QString stringValue = QString::fromLocal8Bit(value);
|
||||
if (chunkId == "INAM") {
|
||||
mutableThis->m_name = stringValue;
|
||||
} else if (chunkId == "IART") {
|
||||
mutableThis->m_artist = stringValue;
|
||||
}
|
||||
} else if (chunkId == "LIST") {
|
||||
const QByteArray listType = device()->read(4);
|
||||
|
||||
if (listType == "INFO") {
|
||||
// Technically would contain INAM and IART but we handle them anywhere above
|
||||
} else if (listType == "fram") {
|
||||
quint64 read = 0;
|
||||
while (read < chunkSize) {
|
||||
const QByteArray chunkType = device()->read(4);
|
||||
read += 4;
|
||||
if (chunkType != "icon") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!m_firstFrameOffset) {
|
||||
mutableThis->m_firstFrameOffset = device()->pos() - 4;
|
||||
mutableThis->m_currentImageNumber = 0;
|
||||
|
||||
// If size in header isn't valid, use the first frame's size instead
|
||||
if (!m_size.isValid() || m_size.isEmpty()) {
|
||||
const auto oldPos = device()->pos();
|
||||
|
||||
device()->read(sizeof(quint32_le));
|
||||
|
||||
const QByteArray curHeaderData = device()->read(sizeof(CurHeader));
|
||||
const QByteArray cursorDirEntryData = device()->read(sizeof(CursorDirEntry));
|
||||
|
||||
if (curHeaderData.length() == sizeof(CurHeader) && cursorDirEntryData.length() == sizeof(CursorDirEntry)) {
|
||||
auto *cursorDirEntry = reinterpret_cast<const CursorDirEntry *>(cursorDirEntryData.data());
|
||||
mutableThis->m_size = QSize(cursorDirEntry->bWidth, cursorDirEntry->bHeight);
|
||||
}
|
||||
|
||||
device()->seek(oldPos);
|
||||
}
|
||||
|
||||
// If we don't have a custom image sequence we can stop scanning right here
|
||||
if (m_imageSequence.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mutableThis->m_frameOffsets.append(device()->pos() - 4);
|
||||
|
||||
const QByteArray frameSizeData = device()->read(sizeof(quint32_le));
|
||||
if (frameSizeData.size() != sizeof(quint32_le)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto frameSize = *(reinterpret_cast<const quint32_le *>(frameSizeData.data()));
|
||||
device()->seek(device()->pos() + frameSize);
|
||||
|
||||
read += frameSize;
|
||||
|
||||
if (m_frameOffsets.count() == m_frameCount) {
|
||||
// Also record the end of frame data
|
||||
mutableThis->m_frameOffsets.append(device()->pos() - 4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_imageCount != m_frameCount && m_imageSequence.isEmpty()) {
|
||||
qWarning("ANIHandler: 'nSteps' is not equal to 'nFrames' but no 'seq' entries were provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_imageSequence.isEmpty() && m_imageSequence.count() != m_imageCount) {
|
||||
qWarning("ANIHandler: count of entries in 'seq' does not match 'nSteps' in anih");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_displayRates.isEmpty() && m_displayRates.count() != m_imageCount) {
|
||||
qWarning("ANIHandler: count of entries in 'rate' does not match 'nSteps' in anih");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_frameOffsets.isEmpty() && m_frameOffsets.count() - 1 != m_frameCount) {
|
||||
qWarning("ANIHandler: number of actual frames does not match 'nFrames' in anih");
|
||||
return false;
|
||||
}
|
||||
|
||||
mutableThis->m_scanned = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ANIHandler::canRead(QIODevice *device)
|
||||
{
|
||||
if (!device) {
|
||||
qWarning("ANIHandler::canRead() called with no device");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray riffIntro = device->peek(12);
|
||||
|
||||
if (riffIntro.length() != 12) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!riffIntro.startsWith("RIFF")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO sanity check chunk size?
|
||||
|
||||
if (riffIntro.mid(4 + 4, 4) != "ACON") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QImageIOPlugin::Capabilities ANIPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
if (format == "ani") {
|
||||
return Capabilities(CanRead);
|
||||
}
|
||||
if (!format.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
if (!device->isOpen()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Capabilities cap;
|
||||
if (device->isReadable() && ANIHandler::canRead(device)) {
|
||||
cap |= CanRead;
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
|
||||
QImageIOHandler *ANIPlugin::create(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
QImageIOHandler *handler = new ANIHandler;
|
||||
handler->setDevice(device);
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
7
src/imageformats/ani.desktop
Normal file
@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Service
|
||||
X-KDE-ServiceTypes=QImageIOPlugins
|
||||
X-KDE-ImageFormat=ani
|
||||
X-KDE-MimeType=application/x-navi-animation
|
||||
X-KDE-Read=true
|
||||
X-KDE-Write=false
|
4
src/imageformats/ani.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Keys": [ "ani" ],
|
||||
"MimeTypes": [ "application/x-navi-animation" ]
|
||||
}
|
68
src/imageformats/ani_p.h
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <kde@broulik.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KIMG_ANI_P_H
|
||||
#define KIMG_ANI_P_H
|
||||
|
||||
#include <QImageIOPlugin>
|
||||
#include <QSize>
|
||||
|
||||
class ANIHandler : public QImageIOHandler
|
||||
{
|
||||
public:
|
||||
ANIHandler();
|
||||
|
||||
bool canRead() const override;
|
||||
bool read(QImage *image) override;
|
||||
|
||||
int currentImageNumber() const override;
|
||||
int imageCount() const override;
|
||||
bool jumpToImage(int imageNumber) override;
|
||||
bool jumpToNextImage() override;
|
||||
|
||||
int loopCount() const override;
|
||||
int nextImageDelay() const override;
|
||||
|
||||
bool supportsOption(ImageOption option) const override;
|
||||
QVariant option(ImageOption option) const override;
|
||||
|
||||
static bool canRead(QIODevice *device);
|
||||
|
||||
private:
|
||||
bool ensureScanned() const;
|
||||
|
||||
bool m_scanned = false;
|
||||
|
||||
int m_currentImageNumber = 0;
|
||||
|
||||
int m_frameCount = 0; // "physical" frames
|
||||
int m_imageCount = 0; // logical images
|
||||
// Stores a custom sequence of images
|
||||
QVector<int> m_imageSequence;
|
||||
// and the corresponding offsets where they are
|
||||
// since we can't read the image data sequentally in this case then
|
||||
QVector<qint64> m_frameOffsets;
|
||||
qint64 m_firstFrameOffset = 0;
|
||||
|
||||
int m_displayRate = 0;
|
||||
QVector<int> m_displayRates;
|
||||
|
||||
QString m_name;
|
||||
QString m_artist;
|
||||
QSize m_size;
|
||||
};
|
||||
|
||||
class ANIPlugin : public QImageIOPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "ani.json")
|
||||
|
||||
public:
|
||||
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
|
||||
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||
};
|
||||
|
||||
#endif // KIMG_ANI_P_H
|
972
src/imageformats/avif.cpp
Normal file
@ -0,0 +1,972 @@
|
||||
/*
|
||||
AV1 Image File Format (AVIF) support for QImage.
|
||||
|
||||
SPDX-FileCopyrightText: 2020 Daniel Novomesky <dnovomesky@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <QThread>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <QColorSpace>
|
||||
|
||||
#include "avif_p.h"
|
||||
#include <cfloat>
|
||||
|
||||
QAVIFHandler::QAVIFHandler()
|
||||
: m_parseState(ParseAvifNotParsed)
|
||||
, m_quality(52)
|
||||
, m_container_width(0)
|
||||
, m_container_height(0)
|
||||
, m_rawAvifData(AVIF_DATA_EMPTY)
|
||||
, m_decoder(nullptr)
|
||||
, m_must_jump_to_next_image(false)
|
||||
{
|
||||
}
|
||||
|
||||
QAVIFHandler::~QAVIFHandler()
|
||||
{
|
||||
if (m_decoder) {
|
||||
avifDecoderDestroy(m_decoder);
|
||||
}
|
||||
}
|
||||
|
||||
bool QAVIFHandler::canRead() const
|
||||
{
|
||||
if (m_parseState == ParseAvifNotParsed && !canRead(device())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_parseState != ParseAvifError) {
|
||||
setFormat("avif");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::canRead(QIODevice *device)
|
||||
{
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
QByteArray header = device->peek(144);
|
||||
if (header.size() < 12) {
|
||||
return false;
|
||||
}
|
||||
|
||||
avifROData input;
|
||||
input.data = (const uint8_t *)header.constData();
|
||||
input.size = header.size();
|
||||
|
||||
if (avifPeekCompatibleFileType(&input)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::ensureParsed() const
|
||||
{
|
||||
if (m_parseState == ParseAvifSuccess) {
|
||||
return true;
|
||||
}
|
||||
if (m_parseState == ParseAvifError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QAVIFHandler *that = const_cast<QAVIFHandler *>(this);
|
||||
|
||||
return that->ensureDecoder();
|
||||
}
|
||||
|
||||
bool QAVIFHandler::ensureDecoder()
|
||||
{
|
||||
if (m_decoder) {
|
||||
return true;
|
||||
}
|
||||
|
||||
m_rawData = device()->readAll();
|
||||
|
||||
m_rawAvifData.data = (const uint8_t *)m_rawData.constData();
|
||||
m_rawAvifData.size = m_rawData.size();
|
||||
|
||||
if (avifPeekCompatibleFileType(&m_rawAvifData) == AVIF_FALSE) {
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_decoder = avifDecoderCreate();
|
||||
|
||||
#if AVIF_VERSION >= 90100
|
||||
m_decoder->strictFlags = AVIF_STRICT_DISABLED;
|
||||
#endif
|
||||
|
||||
avifResult decodeResult;
|
||||
|
||||
decodeResult = avifDecoderSetIOMemory(m_decoder, m_rawAvifData.data, m_rawAvifData.size);
|
||||
if (decodeResult != AVIF_RESULT_OK) {
|
||||
qWarning("ERROR: avifDecoderSetIOMemory failed: %s", avifResultToString(decodeResult));
|
||||
|
||||
avifDecoderDestroy(m_decoder);
|
||||
m_decoder = nullptr;
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
decodeResult = avifDecoderParse(m_decoder);
|
||||
if (decodeResult != AVIF_RESULT_OK) {
|
||||
qWarning("ERROR: Failed to parse input: %s", avifResultToString(decodeResult));
|
||||
|
||||
avifDecoderDestroy(m_decoder);
|
||||
m_decoder = nullptr;
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
decodeResult = avifDecoderNextImage(m_decoder);
|
||||
|
||||
if (decodeResult == AVIF_RESULT_OK) {
|
||||
m_container_width = m_decoder->image->width;
|
||||
m_container_height = m_decoder->image->height;
|
||||
|
||||
if ((m_container_width > 32768) || (m_container_height > 32768)) {
|
||||
qWarning("AVIF image (%dx%d) is too large!", m_container_width, m_container_height);
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((m_container_width == 0) || (m_container_height == 0)) {
|
||||
qWarning("Empty image, nothing to decode");
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_parseState = ParseAvifSuccess;
|
||||
if (decode_one_frame()) {
|
||||
return true;
|
||||
} else {
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
qWarning("ERROR: Failed to decode image: %s", avifResultToString(decodeResult));
|
||||
}
|
||||
|
||||
avifDecoderDestroy(m_decoder);
|
||||
m_decoder = nullptr;
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::decode_one_frame()
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool loadalpha;
|
||||
|
||||
if (m_decoder->image->alphaPlane) {
|
||||
loadalpha = true;
|
||||
} else {
|
||||
loadalpha = false;
|
||||
}
|
||||
|
||||
QImage::Format resultformat;
|
||||
|
||||
if (m_decoder->image->depth > 8) {
|
||||
if (loadalpha) {
|
||||
resultformat = QImage::Format_RGBA64;
|
||||
} else {
|
||||
resultformat = QImage::Format_RGBX64;
|
||||
}
|
||||
} else {
|
||||
if (loadalpha) {
|
||||
resultformat = QImage::Format_RGBA8888;
|
||||
} else {
|
||||
resultformat = QImage::Format_RGB888;
|
||||
}
|
||||
}
|
||||
QImage result(m_decoder->image->width, m_decoder->image->height, resultformat);
|
||||
|
||||
if (result.isNull()) {
|
||||
qWarning("Memory cannot be allocated");
|
||||
return false;
|
||||
}
|
||||
|
||||
QColorSpace colorspace;
|
||||
if (m_decoder->image->icc.data && (m_decoder->image->icc.size > 0)) {
|
||||
const QByteArray icc_data((const char *)m_decoder->image->icc.data, (int)m_decoder->image->icc.size);
|
||||
colorspace = QColorSpace::fromIccProfile(icc_data);
|
||||
if (!colorspace.isValid()) {
|
||||
qWarning("AVIF image has Qt-unsupported or invalid ICC profile!");
|
||||
}
|
||||
} else {
|
||||
float prim[8] = {0.64f, 0.33f, 0.3f, 0.6f, 0.15f, 0.06f, 0.3127f, 0.329f};
|
||||
// outPrimaries: rX, rY, gX, gY, bX, bY, wX, wY
|
||||
avifColorPrimariesGetValues(m_decoder->image->colorPrimaries, prim);
|
||||
|
||||
const QPointF redPoint(QAVIFHandler::CompatibleChromacity(prim[0], prim[1]));
|
||||
const QPointF greenPoint(QAVIFHandler::CompatibleChromacity(prim[2], prim[3]));
|
||||
const QPointF bluePoint(QAVIFHandler::CompatibleChromacity(prim[4], prim[5]));
|
||||
const QPointF whitePoint(QAVIFHandler::CompatibleChromacity(prim[6], prim[7]));
|
||||
|
||||
QColorSpace::TransferFunction q_trc = QColorSpace::TransferFunction::Custom;
|
||||
float q_trc_gamma = 0.0f;
|
||||
|
||||
switch (m_decoder->image->transferCharacteristics) {
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_BT470M */
|
||||
case 4:
|
||||
q_trc = QColorSpace::TransferFunction::Gamma;
|
||||
q_trc_gamma = 2.2f;
|
||||
break;
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_BT470BG */
|
||||
case 5:
|
||||
q_trc = QColorSpace::TransferFunction::Gamma;
|
||||
q_trc_gamma = 2.8f;
|
||||
break;
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_LINEAR */
|
||||
case 8:
|
||||
q_trc = QColorSpace::TransferFunction::Linear;
|
||||
break;
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_SRGB */
|
||||
case 0:
|
||||
case 2: /* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */
|
||||
case 13:
|
||||
q_trc = QColorSpace::TransferFunction::SRgb;
|
||||
break;
|
||||
default:
|
||||
qWarning("CICP colorPrimaries: %d, transferCharacteristics: %d\nThe colorspace is unsupported by this plug-in yet.",
|
||||
m_decoder->image->colorPrimaries,
|
||||
m_decoder->image->transferCharacteristics);
|
||||
q_trc = QColorSpace::TransferFunction::SRgb;
|
||||
break;
|
||||
}
|
||||
|
||||
if (q_trc != QColorSpace::TransferFunction::Custom) { // we create new colorspace using Qt
|
||||
switch (m_decoder->image->colorPrimaries) {
|
||||
/* AVIF_COLOR_PRIMARIES_BT709 */
|
||||
case 0:
|
||||
case 1:
|
||||
case 2: /* AVIF_COLOR_PRIMARIES_UNSPECIFIED */
|
||||
colorspace = QColorSpace(QColorSpace::Primaries::SRgb, q_trc, q_trc_gamma);
|
||||
break;
|
||||
/* AVIF_COLOR_PRIMARIES_SMPTE432 */
|
||||
case 12:
|
||||
colorspace = QColorSpace(QColorSpace::Primaries::DciP3D65, q_trc, q_trc_gamma);
|
||||
break;
|
||||
default:
|
||||
colorspace = QColorSpace(whitePoint, redPoint, greenPoint, bluePoint, q_trc, q_trc_gamma);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!colorspace.isValid()) {
|
||||
qWarning("AVIF plugin created invalid QColorSpace from NCLX/CICP!");
|
||||
}
|
||||
}
|
||||
|
||||
result.setColorSpace(colorspace);
|
||||
|
||||
avifRGBImage rgb;
|
||||
avifRGBImageSetDefaults(&rgb, m_decoder->image);
|
||||
|
||||
if (m_decoder->image->depth > 8) {
|
||||
rgb.depth = 16;
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
|
||||
if (!loadalpha) {
|
||||
rgb.ignoreAlpha = AVIF_TRUE;
|
||||
result.fill(Qt::black);
|
||||
if (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
|
||||
resultformat = QImage::Format_Grayscale16;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rgb.depth = 8;
|
||||
if (loadalpha) {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
resultformat = QImage::Format_ARGB32;
|
||||
} else {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGB;
|
||||
|
||||
if (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
|
||||
resultformat = QImage::Format_Grayscale8;
|
||||
} else {
|
||||
resultformat = QImage::Format_RGB32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rgb.rowBytes = result.bytesPerLine();
|
||||
rgb.pixels = result.bits();
|
||||
|
||||
avifResult res = avifImageYUVToRGB(m_decoder->image, &rgb);
|
||||
if (res != AVIF_RESULT_OK) {
|
||||
qWarning("ERROR in avifImageYUVToRGB: %s", avifResultToString(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_decoder->image->transformFlags & AVIF_TRANSFORM_CLAP) {
|
||||
if ((m_decoder->image->clap.widthD > 0) && (m_decoder->image->clap.heightD > 0) && (m_decoder->image->clap.horizOffD > 0)
|
||||
&& (m_decoder->image->clap.vertOffD > 0)) {
|
||||
int new_width, new_height, offx, offy;
|
||||
|
||||
new_width = (int)((double)(m_decoder->image->clap.widthN) / (m_decoder->image->clap.widthD) + 0.5);
|
||||
if (new_width > result.width()) {
|
||||
new_width = result.width();
|
||||
}
|
||||
|
||||
new_height = (int)((double)(m_decoder->image->clap.heightN) / (m_decoder->image->clap.heightD) + 0.5);
|
||||
if (new_height > result.height()) {
|
||||
new_height = result.height();
|
||||
}
|
||||
|
||||
if (new_width > 0 && new_height > 0) {
|
||||
offx = ((double)((int32_t)m_decoder->image->clap.horizOffN)) / (m_decoder->image->clap.horizOffD) + (result.width() - new_width) / 2.0 + 0.5;
|
||||
if (offx < 0) {
|
||||
offx = 0;
|
||||
} else if (offx > (result.width() - new_width)) {
|
||||
offx = result.width() - new_width;
|
||||
}
|
||||
|
||||
offy = ((double)((int32_t)m_decoder->image->clap.vertOffN)) / (m_decoder->image->clap.vertOffD) + (result.height() - new_height) / 2.0 + 0.5;
|
||||
if (offy < 0) {
|
||||
offy = 0;
|
||||
} else if (offy > (result.height() - new_height)) {
|
||||
offy = result.height() - new_height;
|
||||
}
|
||||
|
||||
result = result.copy(offx, offy, new_width, new_height);
|
||||
}
|
||||
}
|
||||
|
||||
else { // Zero values, we need to avoid 0 divide.
|
||||
qWarning("ERROR: Wrong values in avifCleanApertureBox");
|
||||
}
|
||||
}
|
||||
|
||||
if (m_decoder->image->transformFlags & AVIF_TRANSFORM_IROT) {
|
||||
QTransform transform;
|
||||
switch (m_decoder->image->irot.angle) {
|
||||
case 1:
|
||||
transform.rotate(-90);
|
||||
result = result.transformed(transform);
|
||||
break;
|
||||
case 2:
|
||||
transform.rotate(180);
|
||||
result = result.transformed(transform);
|
||||
break;
|
||||
case 3:
|
||||
transform.rotate(90);
|
||||
result = result.transformed(transform);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_decoder->image->transformFlags & AVIF_TRANSFORM_IMIR) {
|
||||
#if AVIF_VERSION > 90100
|
||||
switch (m_decoder->image->imir.mode) {
|
||||
#else
|
||||
switch (m_decoder->image->imir.axis) {
|
||||
#endif
|
||||
case 0: // top-to-bottom
|
||||
result = result.mirrored(false, true);
|
||||
break;
|
||||
case 1: // left-to-right
|
||||
result = result.mirrored(true, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (resultformat == result.format()) {
|
||||
m_current_image = result;
|
||||
} else {
|
||||
m_current_image = result.convertToFormat(resultformat);
|
||||
}
|
||||
|
||||
m_must_jump_to_next_image = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::read(QImage *image)
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_must_jump_to_next_image) {
|
||||
jumpToNextImage();
|
||||
}
|
||||
|
||||
*image = m_current_image;
|
||||
if (imageCount() >= 2) {
|
||||
m_must_jump_to_next_image = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::write(const QImage &image)
|
||||
{
|
||||
if (image.format() == QImage::Format_Invalid) {
|
||||
qWarning("No image data to save");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((image.width() > 32768) || (image.height() > 32768)) {
|
||||
qWarning("Image is too large");
|
||||
return false;
|
||||
}
|
||||
|
||||
int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100;
|
||||
int minQuantizer = 0;
|
||||
int maxQuantizerAlpha = 0;
|
||||
avifResult res;
|
||||
|
||||
bool save_grayscale; // true - monochrome, false - colors
|
||||
int save_depth; // 8 or 10bit per channel
|
||||
QImage::Format tmpformat; // format for temporary image
|
||||
|
||||
avifImage *avif = nullptr;
|
||||
|
||||
// grayscale detection
|
||||
switch (image.format()) {
|
||||
case QImage::Format_Mono:
|
||||
case QImage::Format_MonoLSB:
|
||||
case QImage::Format_Grayscale8:
|
||||
case QImage::Format_Grayscale16:
|
||||
save_grayscale = true;
|
||||
break;
|
||||
case QImage::Format_Indexed8:
|
||||
save_grayscale = image.isGrayscale();
|
||||
break;
|
||||
default:
|
||||
save_grayscale = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// depth detection
|
||||
switch (image.format()) {
|
||||
case QImage::Format_BGR30:
|
||||
case QImage::Format_A2BGR30_Premultiplied:
|
||||
case QImage::Format_RGB30:
|
||||
case QImage::Format_A2RGB30_Premultiplied:
|
||||
case QImage::Format_Grayscale16:
|
||||
case QImage::Format_RGBX64:
|
||||
case QImage::Format_RGBA64:
|
||||
case QImage::Format_RGBA64_Premultiplied:
|
||||
save_depth = 10;
|
||||
break;
|
||||
default:
|
||||
if (image.depth() > 32) {
|
||||
save_depth = 10;
|
||||
} else {
|
||||
save_depth = 8;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// quality settings
|
||||
if (maxQuantizer > 20) {
|
||||
minQuantizer = maxQuantizer - 20;
|
||||
if (maxQuantizer > 40) { // we decrease quality of alpha channel here
|
||||
maxQuantizerAlpha = maxQuantizer - 40;
|
||||
}
|
||||
}
|
||||
|
||||
if (save_grayscale && !image.hasAlphaChannel()) { // we are going to save grayscale image without alpha channel
|
||||
if (save_depth > 8) {
|
||||
tmpformat = QImage::Format_Grayscale16;
|
||||
} else {
|
||||
tmpformat = QImage::Format_Grayscale8;
|
||||
}
|
||||
QImage tmpgrayimage = image.convertToFormat(tmpformat);
|
||||
|
||||
avif = avifImageCreate(tmpgrayimage.width(), tmpgrayimage.height(), save_depth, AVIF_PIXEL_FORMAT_YUV400);
|
||||
avifImageAllocatePlanes(avif, AVIF_PLANES_YUV);
|
||||
|
||||
if (tmpgrayimage.colorSpace().isValid()) {
|
||||
avif->colorPrimaries = (avifColorPrimaries)1;
|
||||
avif->matrixCoefficients = (avifMatrixCoefficients)1;
|
||||
|
||||
switch (tmpgrayimage.colorSpace().transferFunction()) {
|
||||
case QColorSpace::TransferFunction::Linear:
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_LINEAR */
|
||||
avif->transferCharacteristics = (avifTransferCharacteristics)8;
|
||||
break;
|
||||
case QColorSpace::TransferFunction::SRgb:
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_SRGB */
|
||||
avif->transferCharacteristics = (avifTransferCharacteristics)13;
|
||||
break;
|
||||
default:
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (save_depth > 8) { // QImage::Format_Grayscale16
|
||||
for (int y = 0; y < tmpgrayimage.height(); y++) {
|
||||
const uint16_t *src16bit = reinterpret_cast<const uint16_t *>(tmpgrayimage.constScanLine(y));
|
||||
uint16_t *dest16bit = reinterpret_cast<uint16_t *>(avif->yuvPlanes[0] + y * avif->yuvRowBytes[0]);
|
||||
for (int x = 0; x < tmpgrayimage.width(); x++) {
|
||||
int tmp_pixelval = (int)(((float)(*src16bit) / 65535.0f) * 1023.0f + 0.5f); // downgrade to 10 bits
|
||||
*dest16bit = qBound(0, tmp_pixelval, 1023);
|
||||
dest16bit++;
|
||||
src16bit++;
|
||||
}
|
||||
}
|
||||
} else { // QImage::Format_Grayscale8
|
||||
for (int y = 0; y < tmpgrayimage.height(); y++) {
|
||||
const uchar *src8bit = tmpgrayimage.constScanLine(y);
|
||||
uint8_t *dest8bit = avif->yuvPlanes[0] + y * avif->yuvRowBytes[0];
|
||||
for (int x = 0; x < tmpgrayimage.width(); x++) {
|
||||
*dest8bit = *src8bit;
|
||||
dest8bit++;
|
||||
src8bit++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else { // we are going to save color image
|
||||
if (save_depth > 8) {
|
||||
if (image.hasAlphaChannel()) {
|
||||
tmpformat = QImage::Format_RGBA64;
|
||||
} else {
|
||||
tmpformat = QImage::Format_RGBX64;
|
||||
}
|
||||
} else { // 8bit depth
|
||||
if (image.hasAlphaChannel()) {
|
||||
tmpformat = QImage::Format_RGBA8888;
|
||||
} else {
|
||||
tmpformat = QImage::Format_RGB888;
|
||||
}
|
||||
}
|
||||
|
||||
QImage tmpcolorimage = image.convertToFormat(tmpformat);
|
||||
|
||||
avifPixelFormat pixel_format = AVIF_PIXEL_FORMAT_YUV420;
|
||||
if (maxQuantizer < 20) {
|
||||
if (maxQuantizer < 10) {
|
||||
pixel_format = AVIF_PIXEL_FORMAT_YUV444; // best quality
|
||||
} else {
|
||||
pixel_format = AVIF_PIXEL_FORMAT_YUV422; // high quality
|
||||
}
|
||||
}
|
||||
|
||||
avifMatrixCoefficients matrix_to_save = (avifMatrixCoefficients)1; // default for Qt 5.12 and 5.13;
|
||||
|
||||
avifColorPrimaries primaries_to_save = (avifColorPrimaries)2;
|
||||
avifTransferCharacteristics transfer_to_save = (avifTransferCharacteristics)2;
|
||||
QByteArray iccprofile;
|
||||
|
||||
if (tmpcolorimage.colorSpace().isValid()) {
|
||||
switch (tmpcolorimage.colorSpace().primaries()) {
|
||||
case QColorSpace::Primaries::SRgb:
|
||||
/* AVIF_COLOR_PRIMARIES_BT709 */
|
||||
primaries_to_save = (avifColorPrimaries)1;
|
||||
/* AVIF_MATRIX_COEFFICIENTS_BT709 */
|
||||
matrix_to_save = (avifMatrixCoefficients)1;
|
||||
break;
|
||||
case QColorSpace::Primaries::DciP3D65:
|
||||
/* AVIF_NCLX_COLOUR_PRIMARIES_P3, AVIF_NCLX_COLOUR_PRIMARIES_SMPTE432 */
|
||||
primaries_to_save = (avifColorPrimaries)12;
|
||||
/* AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_NCL */
|
||||
matrix_to_save = (avifMatrixCoefficients)12;
|
||||
break;
|
||||
default:
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */
|
||||
primaries_to_save = (avifColorPrimaries)2;
|
||||
/* AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED */
|
||||
matrix_to_save = (avifMatrixCoefficients)2;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (tmpcolorimage.colorSpace().transferFunction()) {
|
||||
case QColorSpace::TransferFunction::Linear:
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_LINEAR */
|
||||
transfer_to_save = (avifTransferCharacteristics)8;
|
||||
break;
|
||||
case QColorSpace::TransferFunction::Gamma:
|
||||
if (qAbs(tmpcolorimage.colorSpace().gamma() - 2.2f) < 0.1f) {
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_BT470M */
|
||||
transfer_to_save = (avifTransferCharacteristics)4;
|
||||
} else if (qAbs(tmpcolorimage.colorSpace().gamma() - 2.8f) < 0.1f) {
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_BT470BG */
|
||||
transfer_to_save = (avifTransferCharacteristics)5;
|
||||
} else {
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */
|
||||
transfer_to_save = (avifTransferCharacteristics)2;
|
||||
}
|
||||
break;
|
||||
case QColorSpace::TransferFunction::SRgb:
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_SRGB */
|
||||
transfer_to_save = (avifTransferCharacteristics)13;
|
||||
break;
|
||||
default:
|
||||
/* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */
|
||||
transfer_to_save = (avifTransferCharacteristics)2;
|
||||
break;
|
||||
}
|
||||
|
||||
// in case primaries or trc were not identified
|
||||
if ((primaries_to_save == 2) || (transfer_to_save == 2)) {
|
||||
// upgrade image to higher bit depth
|
||||
if (save_depth == 8) {
|
||||
save_depth = 10;
|
||||
if (tmpcolorimage.hasAlphaChannel()) {
|
||||
tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBA64);
|
||||
} else {
|
||||
tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBX64);
|
||||
}
|
||||
}
|
||||
|
||||
if ((primaries_to_save == 2) && (transfer_to_save != 2)) { // other primaries but known trc
|
||||
primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709
|
||||
matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709
|
||||
|
||||
switch (transfer_to_save) {
|
||||
case 8: // AVIF_TRANSFER_CHARACTERISTICS_LINEAR
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::Linear));
|
||||
break;
|
||||
case 4: // AVIF_TRANSFER_CHARACTERISTICS_BT470M
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.2f));
|
||||
break;
|
||||
case 5: // AVIF_TRANSFER_CHARACTERISTICS_BT470BG
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.8f));
|
||||
break;
|
||||
default: // AVIF_TRANSFER_CHARACTERISTICS_SRGB + any other
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb));
|
||||
transfer_to_save = (avifTransferCharacteristics)13;
|
||||
break;
|
||||
}
|
||||
} else if ((primaries_to_save != 2) && (transfer_to_save == 2)) { // recognized primaries but other trc
|
||||
transfer_to_save = (avifTransferCharacteristics)13;
|
||||
tmpcolorimage.convertToColorSpace(tmpcolorimage.colorSpace().withTransferFunction(QColorSpace::TransferFunction::SRgb));
|
||||
} else { // unrecognized profile
|
||||
primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709
|
||||
transfer_to_save = (avifTransferCharacteristics)13;
|
||||
matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb));
|
||||
}
|
||||
}
|
||||
} else { // profile is unsupported by Qt
|
||||
iccprofile = tmpcolorimage.colorSpace().iccProfile();
|
||||
if (iccprofile.size() > 0) {
|
||||
matrix_to_save = (avifMatrixCoefficients)6;
|
||||
}
|
||||
}
|
||||
|
||||
avif = avifImageCreate(tmpcolorimage.width(), tmpcolorimage.height(), save_depth, pixel_format);
|
||||
avif->matrixCoefficients = matrix_to_save;
|
||||
|
||||
avif->colorPrimaries = primaries_to_save;
|
||||
avif->transferCharacteristics = transfer_to_save;
|
||||
|
||||
if (iccprofile.size() > 0) {
|
||||
avifImageSetProfileICC(avif, (const uint8_t *)iccprofile.constData(), iccprofile.size());
|
||||
}
|
||||
|
||||
avifRGBImage rgb;
|
||||
avifRGBImageSetDefaults(&rgb, avif);
|
||||
rgb.rowBytes = tmpcolorimage.bytesPerLine();
|
||||
rgb.pixels = const_cast<uint8_t *>(tmpcolorimage.constBits());
|
||||
|
||||
if (save_depth > 8) { // 10bit depth
|
||||
rgb.depth = 16;
|
||||
|
||||
if (tmpcolorimage.hasAlphaChannel()) {
|
||||
avif->alphaRange = AVIF_RANGE_FULL;
|
||||
} else {
|
||||
rgb.ignoreAlpha = AVIF_TRUE;
|
||||
}
|
||||
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
} else { // 8bit depth
|
||||
rgb.depth = 8;
|
||||
|
||||
if (tmpcolorimage.hasAlphaChannel()) {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
avif->alphaRange = AVIF_RANGE_FULL;
|
||||
} else {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGB;
|
||||
}
|
||||
}
|
||||
|
||||
res = avifImageRGBToYUV(avif, &rgb);
|
||||
if (res != AVIF_RESULT_OK) {
|
||||
qWarning("ERROR in avifImageRGBToYUV: %s", avifResultToString(res));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
avifRWData raw = AVIF_DATA_EMPTY;
|
||||
avifEncoder *encoder = avifEncoderCreate();
|
||||
encoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
|
||||
encoder->minQuantizer = minQuantizer;
|
||||
encoder->maxQuantizer = maxQuantizer;
|
||||
|
||||
if (image.hasAlphaChannel()) {
|
||||
encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
|
||||
encoder->maxQuantizerAlpha = maxQuantizerAlpha;
|
||||
}
|
||||
|
||||
encoder->speed = 8;
|
||||
|
||||
res = avifEncoderWrite(encoder, avif, &raw);
|
||||
avifEncoderDestroy(encoder);
|
||||
avifImageDestroy(avif);
|
||||
|
||||
if (res == AVIF_RESULT_OK) {
|
||||
qint64 status = device()->write((const char *)raw.data, raw.size);
|
||||
avifRWDataFree(&raw);
|
||||
|
||||
if (status > 0) {
|
||||
return true;
|
||||
} else if (status == -1) {
|
||||
qWarning("Write error: %s", qUtf8Printable(device()->errorString()));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
qWarning("ERROR: Failed to encode: %s", avifResultToString(res));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QVariant QAVIFHandler::option(ImageOption option) const
|
||||
{
|
||||
if (option == Quality) {
|
||||
return m_quality;
|
||||
}
|
||||
|
||||
if (!supportsOption(option) || !ensureParsed()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (option) {
|
||||
case Size:
|
||||
return m_current_image.size();
|
||||
case Animation:
|
||||
if (imageCount() >= 2) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
void QAVIFHandler::setOption(ImageOption option, const QVariant &value)
|
||||
{
|
||||
switch (option) {
|
||||
case Quality:
|
||||
m_quality = value.toInt();
|
||||
if (m_quality > 100) {
|
||||
m_quality = 100;
|
||||
} else if (m_quality < 0) {
|
||||
m_quality = 52;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
QImageIOHandler::setOption(option, value);
|
||||
}
|
||||
|
||||
bool QAVIFHandler::supportsOption(ImageOption option) const
|
||||
{
|
||||
return option == Quality || option == Size || option == Animation;
|
||||
}
|
||||
|
||||
int QAVIFHandler::imageCount() const
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (m_decoder->imageCount >= 1) {
|
||||
return m_decoder->imageCount;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int QAVIFHandler::currentImageNumber() const
|
||||
{
|
||||
if (m_parseState == ParseAvifNotParsed) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (m_parseState == ParseAvifError || !m_decoder) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return m_decoder->imageIndex;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::jumpToNextImage()
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_decoder->imageCount < 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning
|
||||
avifDecoderReset(m_decoder);
|
||||
}
|
||||
|
||||
avifResult decodeResult = avifDecoderNextImage(m_decoder);
|
||||
|
||||
if (decodeResult != AVIF_RESULT_OK) {
|
||||
qWarning("ERROR: Failed to decode Next image in sequence: %s", avifResultToString(decodeResult));
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((m_container_width != m_decoder->image->width) || (m_container_height != m_decoder->image->height)) {
|
||||
qWarning("Decoded image sequence size (%dx%d) do not match first image size (%dx%d)!",
|
||||
m_decoder->image->width,
|
||||
m_decoder->image->height,
|
||||
m_container_width,
|
||||
m_container_height);
|
||||
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (decode_one_frame()) {
|
||||
return true;
|
||||
} else {
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool QAVIFHandler::jumpToImage(int imageNumber)
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_decoder->imageCount < 2) { // not an animation
|
||||
if (imageNumber == 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (imageNumber < 0 || imageNumber >= m_decoder->imageCount) { // wrong index
|
||||
return false;
|
||||
}
|
||||
|
||||
if (imageNumber == m_decoder->imageCount) { // we are here already
|
||||
return true;
|
||||
}
|
||||
|
||||
avifResult decodeResult = avifDecoderNthImage(m_decoder, imageNumber);
|
||||
|
||||
if (decodeResult != AVIF_RESULT_OK) {
|
||||
qWarning("ERROR: Failed to decode %d th Image in sequence: %s", imageNumber, avifResultToString(decodeResult));
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((m_container_width != m_decoder->image->width) || (m_container_height != m_decoder->image->height)) {
|
||||
qWarning("Decoded image sequence size (%dx%d) do not match declared container size (%dx%d)!",
|
||||
m_decoder->image->width,
|
||||
m_decoder->image->height,
|
||||
m_container_width,
|
||||
m_container_height);
|
||||
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (decode_one_frame()) {
|
||||
return true;
|
||||
} else {
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int QAVIFHandler::nextImageDelay() const
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (m_decoder->imageCount < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int delay_ms = 1000.0 * m_decoder->imageTiming.duration;
|
||||
if (delay_ms < 1) {
|
||||
delay_ms = 1;
|
||||
}
|
||||
return delay_ms;
|
||||
}
|
||||
|
||||
int QAVIFHandler::loopCount() const
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (m_decoder->imageCount < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
QPointF QAVIFHandler::CompatibleChromacity(qreal chrX, qreal chrY)
|
||||
{
|
||||
chrX = qBound(qreal(0.0), chrX, qreal(1.0));
|
||||
chrY = qBound(qreal(DBL_MIN), chrY, qreal(1.0));
|
||||
|
||||
if ((chrX + chrY) > qreal(1.0)) {
|
||||
chrX = qreal(1.0) - chrY;
|
||||
}
|
||||
|
||||
return QPointF(chrX, chrY);
|
||||
}
|
||||
|
||||
QImageIOPlugin::Capabilities QAVIFPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
if (format == "avif") {
|
||||
return Capabilities(CanRead | CanWrite);
|
||||
}
|
||||
|
||||
if (format == "avifs") {
|
||||
return Capabilities(CanRead);
|
||||
}
|
||||
|
||||
if (!format.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
if (!device->isOpen()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Capabilities cap;
|
||||
if (device->isReadable() && QAVIFHandler::canRead(device)) {
|
||||
cap |= CanRead;
|
||||
}
|
||||
if (device->isWritable()) {
|
||||
cap |= CanWrite;
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
|
||||
QImageIOHandler *QAVIFPlugin::create(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
QImageIOHandler *handler = new QAVIFHandler;
|
||||
handler->setDevice(device);
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
7
src/imageformats/avif.desktop
Normal file
@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Service
|
||||
X-KDE-ServiceTypes=QImageIOPlugins
|
||||
X-KDE-ImageFormat=avif
|
||||
X-KDE-MimeType=image/avif
|
||||
X-KDE-Read=true
|
||||
X-KDE-Write=true
|
4
src/imageformats/avif.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Keys": [ "avif", "avifs" ],
|
||||
"MimeTypes": [ "image/avif", "image/avif-sequence" ]
|
||||
}
|
82
src/imageformats/avif_p.h
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
AV1 Image File Format (AVIF) support for QImage.
|
||||
|
||||
SPDX-FileCopyrightText: 2020 Daniel Novomesky <dnovomesky@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#ifndef KIMG_AVIF_P_H
|
||||
#define KIMG_AVIF_P_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QImage>
|
||||
#include <QImageIOPlugin>
|
||||
#include <QPointF>
|
||||
#include <QVariant>
|
||||
#include <avif/avif.h>
|
||||
#include <qimageiohandler.h>
|
||||
|
||||
class QAVIFHandler : public QImageIOHandler
|
||||
{
|
||||
public:
|
||||
QAVIFHandler();
|
||||
~QAVIFHandler();
|
||||
|
||||
bool canRead() const override;
|
||||
bool read(QImage *image) override;
|
||||
bool write(const QImage &image) override;
|
||||
|
||||
static bool canRead(QIODevice *device);
|
||||
|
||||
QVariant option(ImageOption option) const override;
|
||||
void setOption(ImageOption option, const QVariant &value) override;
|
||||
bool supportsOption(ImageOption option) const override;
|
||||
|
||||
int imageCount() const override;
|
||||
int currentImageNumber() const override;
|
||||
bool jumpToNextImage() override;
|
||||
bool jumpToImage(int imageNumber) override;
|
||||
|
||||
int nextImageDelay() const override;
|
||||
|
||||
int loopCount() const override;
|
||||
|
||||
private:
|
||||
static QPointF CompatibleChromacity(qreal chrX, qreal chrY);
|
||||
bool ensureParsed() const;
|
||||
bool ensureDecoder();
|
||||
bool decode_one_frame();
|
||||
|
||||
enum ParseAvifState {
|
||||
ParseAvifError = -1,
|
||||
ParseAvifNotParsed = 0,
|
||||
ParseAvifSuccess = 1,
|
||||
};
|
||||
|
||||
ParseAvifState m_parseState;
|
||||
int m_quality;
|
||||
|
||||
uint32_t m_container_width;
|
||||
uint32_t m_container_height;
|
||||
|
||||
QByteArray m_rawData;
|
||||
avifROData m_rawAvifData;
|
||||
|
||||
avifDecoder *m_decoder;
|
||||
QImage m_current_image;
|
||||
|
||||
bool m_must_jump_to_next_image;
|
||||
};
|
||||
|
||||
class QAVIFPlugin : public QImageIOPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "avif.json")
|
||||
|
||||
public:
|
||||
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
|
||||
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||
};
|
||||
|
||||
#endif // KIMG_AVIF_P_H
|
@ -9,13 +9,13 @@
|
||||
*/
|
||||
#include "eps_p.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QImage>
|
||||
#include <QImageReader>
|
||||
#include <QPainter>
|
||||
#include <QPrinter>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryFile>
|
||||
#include <QCoreApplication>
|
||||
|
||||
// logging category for this framework, default: log stuff >= warning
|
||||
Q_LOGGING_CATEGORY(EPSPLUGIN, "kf.imageformats.plugins.eps", QtWarningMsg)
|
||||
@ -51,19 +51,13 @@ static bool seekToCodeStart(QIODevice *io, qint64 &ps_offset, qint64 &ps_size)
|
||||
return false;
|
||||
}
|
||||
ps_offset // Offset is in little endian
|
||||
= qint64(((unsigned char)buf[0])
|
||||
+ ((unsigned char)buf[1] << 8)
|
||||
+ ((unsigned char)buf[2] << 16)
|
||||
+ ((unsigned char)buf[3] << 24));
|
||||
= qint64(((unsigned char)buf[0]) + ((unsigned char)buf[1] << 8) + ((unsigned char)buf[2] << 16) + ((unsigned char)buf[3] << 24));
|
||||
if (io->read(buf, 4) != 4) { // Get size of PostScript code in the MS-DOS EPS file.
|
||||
qCDebug(EPSPLUGIN) << "cannot read size of MS-DOS EPS file";
|
||||
return false;
|
||||
}
|
||||
ps_size // Size is in little endian
|
||||
= qint64(((unsigned char)buf[0])
|
||||
+ ((unsigned char)buf[1] << 8)
|
||||
+ ((unsigned char)buf[2] << 16)
|
||||
+ ((unsigned char)buf[3] << 24));
|
||||
= qint64(((unsigned char)buf[0]) + ((unsigned char)buf[1] << 8) + ((unsigned char)buf[2] << 16) + ((unsigned char)buf[3] << 24));
|
||||
qCDebug(EPSPLUGIN) << "Offset: " << ps_offset << " Size: " << ps_size;
|
||||
if (!io->seek(ps_offset)) { // Get offset of PostScript code in the MS-DOS EPS file.
|
||||
qCDebug(EPSPLUGIN) << "cannot seek in MS-DOS EPS file";
|
||||
@ -100,9 +94,11 @@ static bool bbox(QIODevice *io, int *x1, int *y1, int *x2, int *y2)
|
||||
if (strncmp(buf, BBOX, BBOX_LEN) == 0) {
|
||||
// Some EPS files have non-integer values for the bbox
|
||||
// We don't support that currently, but at least we parse it
|
||||
float _x1, _y1, _x2, _y2;
|
||||
if (sscanf(buf, "%*s %f %f %f %f",
|
||||
&_x1, &_y1, &_x2, &_y2) == 4) {
|
||||
float _x1;
|
||||
float _y1;
|
||||
float _x2;
|
||||
float _y2;
|
||||
if (sscanf(buf, "%*s %f %f %f %f", &_x1, &_y1, &_x2, &_y2) == 4) {
|
||||
qCDebug(EPSPLUGIN) << "BBOX: " << _x1 << " " << _y1 << " " << _x2 << " " << _y2;
|
||||
*x1 = int(_x1);
|
||||
*y1 = int(_y1);
|
||||
@ -134,14 +130,18 @@ bool EPSHandler::read(QImage *image)
|
||||
{
|
||||
qCDebug(EPSPLUGIN) << "starting...";
|
||||
|
||||
int x1, y1, x2, y2;
|
||||
int x1;
|
||||
int y1;
|
||||
int x2;
|
||||
int y2;
|
||||
#ifdef EPS_PERFORMANCE_DEBUG
|
||||
QTime dt;
|
||||
dt.start();
|
||||
#endif
|
||||
|
||||
QIODevice *io = device();
|
||||
qint64 ps_offset, ps_size;
|
||||
qint64 ps_offset;
|
||||
qint64 ps_size;
|
||||
|
||||
// find start of PostScript code
|
||||
if (!seekToCodeStart(io, ps_offset, ps_size)) {
|
||||
@ -177,23 +177,17 @@ bool EPSHandler::read(QImage *image)
|
||||
// create GS command line
|
||||
|
||||
QStringList gsArgs;
|
||||
gsArgs << QLatin1String("-sOutputFile=") + tmpFile.fileName()
|
||||
<< QStringLiteral("-q")
|
||||
<< QStringLiteral("-g%1x%2").arg(wantedWidth).arg(wantedHeight)
|
||||
<< QStringLiteral("-dSAFER")
|
||||
<< QStringLiteral("-dPARANOIDSAFER")
|
||||
<< QStringLiteral("-dNOPAUSE")
|
||||
<< QStringLiteral("-sDEVICE=ppm")
|
||||
gsArgs << QLatin1String("-sOutputFile=") + tmpFile.fileName() << QStringLiteral("-q") << QStringLiteral("-g%1x%2").arg(wantedWidth).arg(wantedHeight)
|
||||
<< QStringLiteral("-dSAFER") << QStringLiteral("-dPARANOIDSAFER") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-sDEVICE=ppm")
|
||||
<< QStringLiteral("-c")
|
||||
<< QStringLiteral("0 0 moveto "
|
||||
<< QStringLiteral(
|
||||
"0 0 moveto "
|
||||
"1000 0 lineto "
|
||||
"1000 1000 lineto "
|
||||
"0 1000 lineto "
|
||||
"1 1 254 255 div setrgbcolor fill "
|
||||
"0 0 0 setrgbcolor")
|
||||
<< QStringLiteral("-")
|
||||
<< QStringLiteral("-c")
|
||||
<< QStringLiteral("showpage quit");
|
||||
<< QStringLiteral("-") << QStringLiteral("-c") << QStringLiteral("showpage quit");
|
||||
qCDebug(EPSPLUGIN) << "Running gs with args" << gsArgs;
|
||||
|
||||
QProcess converter;
|
||||
@ -264,7 +258,8 @@ bool EPSHandler::write(const QImage &image)
|
||||
psOut.setOutputFileName(tmpFile.fileName());
|
||||
psOut.setOutputFormat(QPrinter::PdfFormat);
|
||||
psOut.setFullPage(true);
|
||||
psOut.setPaperSize(image.size(), QPrinter::DevicePixel);
|
||||
const double multiplier = psOut.resolution() <= 0 ? 1.0 : 72.0 / psOut.resolution();
|
||||
psOut.setPageSize(QPageSize(image.size() * multiplier, QPageSize::Point));
|
||||
|
||||
// painting the pixmap to the "printer" which is a file
|
||||
p.begin(&psOut);
|
||||
@ -277,24 +272,16 @@ bool EPSHandler::write(const QImage &image)
|
||||
|
||||
// pdftops comes with Poppler and produces much smaller EPS files than GhostScript
|
||||
QStringList pdftopsArgs;
|
||||
pdftopsArgs << QStringLiteral("-eps")
|
||||
<< tmpFile.fileName()
|
||||
<< QStringLiteral("-");
|
||||
pdftopsArgs << QStringLiteral("-eps") << tmpFile.fileName() << QStringLiteral("-");
|
||||
qCDebug(EPSPLUGIN) << "Running pdftops with args" << pdftopsArgs;
|
||||
converter.start(QStringLiteral("pdftops"), pdftopsArgs);
|
||||
|
||||
if (!converter.waitForStarted()) {
|
||||
// GhostScript produces huge files, and takes a long time doing so
|
||||
QStringList gsArgs;
|
||||
gsArgs << QStringLiteral("-q") << QStringLiteral("-P-")
|
||||
<< QStringLiteral("-dNOPAUSE") << QStringLiteral("-dBATCH")
|
||||
<< QStringLiteral("-dSAFER")
|
||||
<< QStringLiteral("-sDEVICE=epswrite")
|
||||
<< QStringLiteral("-sOutputFile=-")
|
||||
<< QStringLiteral("-c")
|
||||
<< QStringLiteral("save") << QStringLiteral("pop")
|
||||
<< QStringLiteral("-f")
|
||||
<< tmpFile.fileName();
|
||||
gsArgs << QStringLiteral("-q") << QStringLiteral("-P-") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-dBATCH") << QStringLiteral("-dSAFER")
|
||||
<< QStringLiteral("-sDEVICE=epswrite") << QStringLiteral("-sOutputFile=-") << QStringLiteral("-c") << QStringLiteral("save")
|
||||
<< QStringLiteral("pop") << QStringLiteral("-f") << tmpFile.fileName();
|
||||
qCDebug(EPSPLUGIN) << "Failed to start pdftops; trying gs with args" << gsArgs;
|
||||
converter.start(QStringLiteral("gs"), gsArgs);
|
||||
|
||||
|
@ -35,4 +35,3 @@ public:
|
||||
Q_DECLARE_LOGGING_CATEGORY(EPSPLUGIN)
|
||||
|
||||
#endif // KIMG_EPS_P_H
|
||||
|
||||
|
@ -9,41 +9,48 @@
|
||||
|
||||
#include "exr_p.h"
|
||||
|
||||
#include <ImfRgbaFile.h>
|
||||
#include <ImfStandardAttributes.h>
|
||||
#include <IexThrowErrnoExc.h>
|
||||
#include <ImathBox.h>
|
||||
#include <ImfInputFile.h>
|
||||
#include <ImfArray.h>
|
||||
#include <ImfBoxAttribute.h>
|
||||
#include <ImfChannelListAttribute.h>
|
||||
#include <ImfCompressionAttribute.h>
|
||||
#include <ImfConvert.h>
|
||||
#include <ImfFloatAttribute.h>
|
||||
#include <ImfInputFile.h>
|
||||
#include <ImfInt64.h>
|
||||
#include <ImfIntAttribute.h>
|
||||
#include <ImfLineOrderAttribute.h>
|
||||
#include <ImfRgbaFile.h>
|
||||
#include <ImfStandardAttributes.h>
|
||||
#include <ImfStringAttribute.h>
|
||||
#include <ImfVecAttribute.h>
|
||||
#include <ImfArray.h>
|
||||
#include <ImfConvert.h>
|
||||
#include <ImfVersion.h>
|
||||
#include <IexThrowErrnoExc.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <QImage>
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
#include <QImage>
|
||||
#include <QImageIOPlugin>
|
||||
|
||||
class K_IStream : public Imf::IStream
|
||||
{
|
||||
public:
|
||||
K_IStream(QIODevice *dev, const QByteArray &fileName):
|
||||
IStream(fileName.data()), m_dev(dev)
|
||||
K_IStream(QIODevice *dev, const QByteArray &fileName)
|
||||
: IStream(fileName.data())
|
||||
, m_dev(dev)
|
||||
{
|
||||
}
|
||||
|
||||
bool read(char c[], int n) override;
|
||||
#if OPENEXR_VERSION_MAJOR > 2
|
||||
uint64_t tellg() override;
|
||||
void seekg(uint64_t pos) override;
|
||||
#else
|
||||
Imf::Int64 tellg() override;
|
||||
void seekg(Imf::Int64 pos) override;
|
||||
#endif
|
||||
void clear() override;
|
||||
|
||||
private:
|
||||
@ -63,12 +70,20 @@ bool K_IStream::read(char c[], int n)
|
||||
return false;
|
||||
}
|
||||
|
||||
#if OPENEXR_VERSION_MAJOR > 2
|
||||
uint64_t K_IStream::tellg()
|
||||
#else
|
||||
Imf::Int64 K_IStream::tellg()
|
||||
#endif
|
||||
{
|
||||
return m_dev->pos();
|
||||
}
|
||||
|
||||
#if OPENEXR_VERSION_MAJOR > 2
|
||||
void K_IStream::seekg(uint64_t pos)
|
||||
#else
|
||||
void K_IStream::seekg(Imf::Int64 pos)
|
||||
#endif
|
||||
{
|
||||
m_dev->seek(pos);
|
||||
}
|
||||
@ -84,7 +99,10 @@ void K_IStream::clear()
|
||||
*/
|
||||
QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
|
||||
{
|
||||
float r, g, b, a;
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float a;
|
||||
|
||||
// 1) Compensate for fogging by subtracting defog
|
||||
// from the raw pixel values.
|
||||
@ -117,24 +135,24 @@ QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
|
||||
// maximum intensity).
|
||||
// Response: kneeLow = 0.0 (2^0.0 => 1); kneeHigh = 5.0 (2^5 =>32)
|
||||
if (r > 1.0) {
|
||||
r = 1.0 + Imath::Math<float>::log((r - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
r = 1.0 + std::log((r - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
}
|
||||
if (g > 1.0) {
|
||||
g = 1.0 + Imath::Math<float>::log((g - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
g = 1.0 + std::log((g - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
}
|
||||
if (b > 1.0) {
|
||||
b = 1.0 + Imath::Math<float>::log((b - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
b = 1.0 + std::log((b - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
}
|
||||
if (a > 1.0) {
|
||||
a = 1.0 + Imath::Math<float>::log((a - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
a = 1.0 + std::log((a - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
}
|
||||
//
|
||||
// 5) Gamma-correct the pixel values, assuming that the
|
||||
// screen's gamma is 0.4545 (or 1/2.2).
|
||||
r = Imath::Math<float>::pow(r, 0.4545);
|
||||
g = Imath::Math<float>::pow(g, 0.4545);
|
||||
b = Imath::Math<float>::pow(b, 0.4545);
|
||||
a = Imath::Math<float>::pow(a, 0.4545);
|
||||
r = std::pow(r, 0.4545);
|
||||
g = std::pow(g, 0.4545);
|
||||
b = std::pow(b, 0.4545);
|
||||
a = std::pow(a, 0.4545);
|
||||
|
||||
// 6) Scale the values such that pixels middle gray
|
||||
// pixels are mapped to 84.66 (or 3.5 f-stops below
|
||||
@ -163,7 +181,8 @@ bool EXRHandler::canRead() const
|
||||
bool EXRHandler::read(QImage *outImage)
|
||||
{
|
||||
try {
|
||||
int width, height;
|
||||
int width;
|
||||
int height;
|
||||
|
||||
K_IStream istr(device(), QByteArray());
|
||||
Imf::RgbaInputFile file(istr);
|
||||
|
@ -41,10 +41,9 @@ const uchar OPAQUE_OPACITY = 255; //!< Opaque value for 8-bit alpha component.
|
||||
typedef enum {
|
||||
RGB,
|
||||
GRAY,
|
||||
INDEXED
|
||||
INDEXED,
|
||||
} GimpImageBaseType;
|
||||
|
||||
|
||||
// From GIMP "libgimp/gimpenums.h" v2.4
|
||||
|
||||
//! Effect to apply when layers are merged together.
|
||||
@ -74,7 +73,6 @@ typedef enum {
|
||||
GRAIN_MERGE_MODE
|
||||
} LayerModeEffects;
|
||||
|
||||
|
||||
// From GIMP "paint_funcs.c" v1.2
|
||||
|
||||
/*!
|
||||
|
@ -8,8 +8,8 @@
|
||||
|
||||
#include "hdr_p.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <QDataStream>
|
||||
#include <QImage>
|
||||
#include <QLoggingCategory>
|
||||
#include <QRegularExpressionMatch>
|
||||
|
||||
@ -21,7 +21,6 @@ Q_LOGGING_CATEGORY(HDRPLUGIN, "kf.imageformats.plugins.hdr", QtWarningMsg)
|
||||
|
||||
namespace // Private.
|
||||
{
|
||||
|
||||
#define MAXLINE 1024
|
||||
#define MINELEN 8 // minimum scanline length for encoding
|
||||
#define MAXELEN 0x7fff // maximum scanline length for encoding
|
||||
@ -81,9 +80,7 @@ static void RGBE_To_QRgbLine(uchar *image, QRgb *scanline, int width)
|
||||
v = 1.0f / float(1 << -e);
|
||||
}
|
||||
|
||||
scanline[j] = qRgb(ClipToByte(float(image[0]) * v),
|
||||
ClipToByte(float(image[1]) * v),
|
||||
ClipToByte(float(image[2]) * v));
|
||||
scanline[j] = qRgb(ClipToByte(float(image[0]) * v), ClipToByte(float(image[1]) * v), ClipToByte(float(image[2]) * v));
|
||||
|
||||
image += 4;
|
||||
}
|
||||
@ -92,7 +89,8 @@ static void RGBE_To_QRgbLine(uchar *image, QRgb *scanline, int width)
|
||||
// Load the HDR image.
|
||||
static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &img)
|
||||
{
|
||||
uchar val, code;
|
||||
uchar val;
|
||||
uchar code;
|
||||
|
||||
// Create dst image.
|
||||
img = QImage(width, height, QImage::Format_RGB32);
|
||||
@ -227,8 +225,14 @@ bool HDRHandler::read(QImage *outImage)
|
||||
qCDebug(HDRPLUGIN) << "Invalid HDR file, the first line after the header didn't have the expected format:" << line;
|
||||
return false;
|
||||
}
|
||||
const int width = match.captured(2).toInt();
|
||||
const int height = match.captured(4).toInt();
|
||||
|
||||
if ((match.captured(1).at(1) != u'Y') || (match.captured(3).at(1) != u'X')) {
|
||||
qCDebug(HDRPLUGIN) << "Unsupported image orientation in HDR file.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const int width = match.captured(4).toInt();
|
||||
const int height = match.captured(2).toInt();
|
||||
|
||||
QDataStream s(device());
|
||||
|
||||
|
734
src/imageformats/heif.cpp
Normal file
@ -0,0 +1,734 @@
|
||||
/*
|
||||
High Efficiency Image File Format (HEIF) support for QImage.
|
||||
|
||||
SPDX-FileCopyrightText: 2020 Sirius Bakke <sirius@bakke.co>
|
||||
SPDX-FileCopyrightText: 2021 Daniel Novomesky <dnovomesky@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "heif_p.h"
|
||||
#include "libheif/heif_cxx.h"
|
||||
|
||||
#include <QColorSpace>
|
||||
#include <QDebug>
|
||||
#include <QPointF>
|
||||
#include <QSysInfo>
|
||||
#include <string.h>
|
||||
|
||||
namespace // Private.
|
||||
{
|
||||
struct HeifQIODeviceWriter : public heif::Context::Writer {
|
||||
HeifQIODeviceWriter(QIODevice *device)
|
||||
: m_ioDevice(device)
|
||||
{
|
||||
}
|
||||
|
||||
heif_error write(const void *data, size_t size) override
|
||||
{
|
||||
heif_error error;
|
||||
error.code = heif_error_Ok;
|
||||
error.subcode = heif_suberror_Unspecified;
|
||||
error.message = errorOkMessage;
|
||||
|
||||
qint64 bytesWritten = m_ioDevice->write(static_cast<const char *>(data), size);
|
||||
|
||||
if (bytesWritten < static_cast<qint64>(size)) {
|
||||
error.code = heif_error_Encoding_error;
|
||||
error.message = QIODeviceWriteErrorMessage;
|
||||
error.subcode = heif_suberror_Cannot_write_output_data;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
static constexpr const char *errorOkMessage = "Success";
|
||||
static constexpr const char *QIODeviceWriteErrorMessage = "Bytes written to QIODevice are smaller than input data size";
|
||||
|
||||
private:
|
||||
QIODevice *m_ioDevice;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
HEIFHandler::HEIFHandler()
|
||||
: m_parseState(ParseHeicNotParsed)
|
||||
, m_quality(100)
|
||||
{
|
||||
}
|
||||
|
||||
bool HEIFHandler::canRead() const
|
||||
{
|
||||
if (m_parseState == ParseHeicNotParsed && !canRead(device())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_parseState != ParseHeicError) {
|
||||
setFormat("heif");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HEIFHandler::read(QImage *outImage)
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*outImage = m_current_image;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HEIFHandler::write(const QImage &image)
|
||||
{
|
||||
if (image.format() == QImage::Format_Invalid || image.isNull()) {
|
||||
qWarning("No image data to save");
|
||||
return false;
|
||||
}
|
||||
|
||||
int save_depth; // 8 or 10bit per channel
|
||||
QImage::Format tmpformat; // format for temporary image
|
||||
const bool save_alpha = image.hasAlphaChannel();
|
||||
|
||||
switch (image.format()) {
|
||||
case QImage::Format_BGR30:
|
||||
case QImage::Format_A2BGR30_Premultiplied:
|
||||
case QImage::Format_RGB30:
|
||||
case QImage::Format_A2RGB30_Premultiplied:
|
||||
case QImage::Format_Grayscale16:
|
||||
case QImage::Format_RGBX64:
|
||||
case QImage::Format_RGBA64:
|
||||
case QImage::Format_RGBA64_Premultiplied:
|
||||
save_depth = 10;
|
||||
break;
|
||||
default:
|
||||
if (image.depth() > 32) {
|
||||
save_depth = 10;
|
||||
} else {
|
||||
save_depth = 8;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
heif_chroma chroma;
|
||||
if (save_depth > 8) {
|
||||
if (save_alpha) {
|
||||
tmpformat = QImage::Format_RGBA64;
|
||||
chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBBAA_BE;
|
||||
} else {
|
||||
tmpformat = QImage::Format_RGBX64;
|
||||
chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RRGGBB_BE;
|
||||
}
|
||||
} else {
|
||||
if (save_alpha) {
|
||||
tmpformat = QImage::Format_RGBA8888;
|
||||
chroma = heif_chroma_interleaved_RGBA;
|
||||
} else {
|
||||
tmpformat = QImage::Format_RGB888;
|
||||
chroma = heif_chroma_interleaved_RGB;
|
||||
}
|
||||
}
|
||||
|
||||
const QImage tmpimage = image.convertToFormat(tmpformat);
|
||||
|
||||
try {
|
||||
heif::Context ctx;
|
||||
heif::Image heifImage;
|
||||
heifImage.create(tmpimage.width(), tmpimage.height(), heif_colorspace_RGB, chroma);
|
||||
|
||||
QByteArray iccprofile = tmpimage.colorSpace().iccProfile();
|
||||
if (iccprofile.size() > 0) {
|
||||
std::vector<uint8_t> rawProfile(iccprofile.begin(), iccprofile.end());
|
||||
heifImage.set_raw_color_profile(heif_color_profile_type_prof, rawProfile);
|
||||
}
|
||||
|
||||
heifImage.add_plane(heif_channel_interleaved, image.width(), image.height(), save_depth);
|
||||
int stride = 0;
|
||||
uint8_t *const dst = heifImage.get_plane(heif_channel_interleaved, &stride);
|
||||
size_t rowbytes;
|
||||
|
||||
switch (save_depth) {
|
||||
case 10:
|
||||
if (save_alpha) {
|
||||
for (int y = 0; y < tmpimage.height(); y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
|
||||
uint16_t *dest_word = reinterpret_cast<uint16_t *>(dst + (y * stride));
|
||||
for (int x = 0; x < tmpimage.width(); x++) {
|
||||
int tmp_pixelval;
|
||||
// R
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// G
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// B
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// A
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
}
|
||||
}
|
||||
} else { // no alpha channel
|
||||
for (int y = 0; y < tmpimage.height(); y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
|
||||
uint16_t *dest_word = reinterpret_cast<uint16_t *>(dst + (y * stride));
|
||||
for (int x = 0; x < tmpimage.width(); x++) {
|
||||
int tmp_pixelval;
|
||||
// R
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// G
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// B
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// X
|
||||
src_word++;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
rowbytes = save_alpha ? (tmpimage.width() * 4) : (tmpimage.width() * 3);
|
||||
for (int y = 0; y < tmpimage.height(); y++) {
|
||||
memcpy(dst + (y * stride), tmpimage.constScanLine(y), rowbytes);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
qWarning() << "Unsupported depth:" << save_depth;
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
heif::Encoder encoder(heif_compression_HEVC);
|
||||
|
||||
encoder.set_lossy_quality(m_quality);
|
||||
if (m_quality > 90) {
|
||||
if (m_quality == 100) {
|
||||
encoder.set_lossless(true);
|
||||
}
|
||||
encoder.set_string_parameter("chroma", "444");
|
||||
}
|
||||
|
||||
heif::Context::EncodingOptions encodingOptions;
|
||||
encodingOptions.save_alpha_channel = save_alpha;
|
||||
|
||||
if ((tmpimage.width() % 2 == 1) || (tmpimage.height() % 2 == 1)) {
|
||||
qWarning() << "Image has odd dimension!\nUse even-numbered dimension(s) for better compatibility with other HEIF implementations.";
|
||||
if (save_alpha) {
|
||||
// This helps to save alpha channel when image has odd dimension
|
||||
encodingOptions.macOS_compatibility_workaround = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.encode_image(heifImage, encoder, encodingOptions);
|
||||
|
||||
HeifQIODeviceWriter writer(device());
|
||||
|
||||
ctx.write(writer);
|
||||
|
||||
} catch (const heif::Error &err) {
|
||||
qWarning() << "libheif error:" << err.get_message().c_str();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HEIFHandler::canRead(QIODevice *device)
|
||||
{
|
||||
if (!device) {
|
||||
qWarning("HEIFHandler::canRead() called with no device");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray header = device->peek(28);
|
||||
return HEIFHandler::isSupportedBMFFType(header);
|
||||
}
|
||||
|
||||
bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
|
||||
{
|
||||
if (header.size() < 28) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *buffer = header.constData();
|
||||
if (qstrncmp(buffer + 4, "ftyp", 4) == 0) {
|
||||
if (qstrncmp(buffer + 8, "heic", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (qstrncmp(buffer + 8, "heis", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (qstrncmp(buffer + 8, "heix", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* we want to avoid loading AVIF files via this plugin */
|
||||
if (qstrncmp(buffer + 8, "mif1", 4) == 0) {
|
||||
for (int offset = 16; offset <= 24; offset += 4) {
|
||||
if (qstrncmp(buffer + offset, "avif", 4) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (qstrncmp(buffer + 8, "mif2", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (qstrncmp(buffer + 8, "msf1", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QVariant HEIFHandler::option(ImageOption option) const
|
||||
{
|
||||
if (option == Quality) {
|
||||
return m_quality;
|
||||
}
|
||||
|
||||
if (!supportsOption(option) || !ensureParsed()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (option) {
|
||||
case Size:
|
||||
return m_current_image.size();
|
||||
break;
|
||||
default:
|
||||
return QVariant();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HEIFHandler::setOption(ImageOption option, const QVariant &value)
|
||||
{
|
||||
switch (option) {
|
||||
case Quality:
|
||||
m_quality = value.toInt();
|
||||
if (m_quality > 100) {
|
||||
m_quality = 100;
|
||||
} else if (m_quality < 0) {
|
||||
m_quality = 100;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
QImageIOHandler::setOption(option, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool HEIFHandler::supportsOption(ImageOption option) const
|
||||
{
|
||||
return option == Quality || option == Size;
|
||||
}
|
||||
|
||||
bool HEIFHandler::ensureParsed() const
|
||||
{
|
||||
if (m_parseState == ParseHeicSuccess) {
|
||||
return true;
|
||||
}
|
||||
if (m_parseState == ParseHeicError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HEIFHandler *that = const_cast<HEIFHandler *>(this);
|
||||
|
||||
return that->ensureDecoder();
|
||||
}
|
||||
bool HEIFHandler::ensureDecoder()
|
||||
{
|
||||
if (m_parseState != ParseHeicNotParsed) {
|
||||
if (m_parseState == ParseHeicSuccess) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray buffer = device()->readAll();
|
||||
if (!HEIFHandler::isSupportedBMFFType(buffer)) {
|
||||
m_parseState = ParseHeicError;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
heif::Context ctx;
|
||||
ctx.read_from_memory_without_copy((const void *)(buffer.constData()), buffer.size());
|
||||
|
||||
heif::ImageHandle handle = ctx.get_primary_image_handle();
|
||||
|
||||
const bool hasAlphaChannel = handle.has_alpha_channel();
|
||||
const int bit_depth = handle.get_luma_bits_per_pixel();
|
||||
heif_chroma chroma;
|
||||
|
||||
QImage::Format target_image_format;
|
||||
|
||||
if (bit_depth == 10 || bit_depth == 12) {
|
||||
if (hasAlphaChannel) {
|
||||
chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBBAA_BE;
|
||||
target_image_format = QImage::Format_RGBA64;
|
||||
} else {
|
||||
chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RRGGBB_BE;
|
||||
target_image_format = QImage::Format_RGBX64;
|
||||
}
|
||||
} else if (bit_depth == 8) {
|
||||
if (hasAlphaChannel) {
|
||||
chroma = heif_chroma_interleaved_RGBA;
|
||||
target_image_format = QImage::Format_ARGB32;
|
||||
} else {
|
||||
chroma = heif_chroma_interleaved_RGB;
|
||||
target_image_format = QImage::Format_RGB32;
|
||||
}
|
||||
} else {
|
||||
m_parseState = ParseHeicError;
|
||||
if (bit_depth > 0) {
|
||||
qWarning() << "Unsupported bit depth:" << bit_depth;
|
||||
} else {
|
||||
qWarning() << "Undefined bit depth.";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
heif::Image img = handle.decode_image(heif_colorspace_RGB, chroma);
|
||||
|
||||
const int imageWidth = img.get_width(heif_channel_interleaved);
|
||||
const int imageHeight = img.get_height(heif_channel_interleaved);
|
||||
|
||||
QSize imageSize(imageWidth, imageHeight);
|
||||
|
||||
if (!imageSize.isValid()) {
|
||||
m_parseState = ParseHeicError;
|
||||
qWarning() << "HEIC image size invalid:" << imageSize;
|
||||
return false;
|
||||
}
|
||||
|
||||
int stride = 0;
|
||||
const uint8_t *const src = img.get_plane(heif_channel_interleaved, &stride);
|
||||
|
||||
if (!src || stride <= 0) {
|
||||
m_parseState = ParseHeicError;
|
||||
qWarning() << "HEIC data pixels information not valid!";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_current_image = QImage(imageSize, target_image_format);
|
||||
if (m_current_image.isNull()) {
|
||||
m_parseState = ParseHeicError;
|
||||
qWarning() << "Unable to allocate memory!";
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (bit_depth) {
|
||||
case 12:
|
||||
if (hasAlphaChannel) {
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
|
||||
uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int tmpvalue;
|
||||
// R
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// G
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// B
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// A
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
}
|
||||
}
|
||||
} else { // no alpha channel
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
|
||||
uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int tmpvalue;
|
||||
// R
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// G
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// B
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// X = 0xffff
|
||||
*dest_data = 0xffff;
|
||||
dest_data++;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 10:
|
||||
if (hasAlphaChannel) {
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
|
||||
uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int tmpvalue;
|
||||
// R
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// G
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// B
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// A
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
}
|
||||
}
|
||||
} else { // no alpha channel
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
|
||||
uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int tmpvalue;
|
||||
// R
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// G
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// B
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// X = 0xffff
|
||||
*dest_data = 0xffff;
|
||||
dest_data++;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
if (hasAlphaChannel) {
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint8_t *src_byte = src + (y * stride);
|
||||
uint32_t *dest_pixel = reinterpret_cast<uint32_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int red = *src_byte++;
|
||||
int green = *src_byte++;
|
||||
int blue = *src_byte++;
|
||||
int alpha = *src_byte++;
|
||||
*dest_pixel = qRgba(red, green, blue, alpha);
|
||||
dest_pixel++;
|
||||
}
|
||||
}
|
||||
} else { // no alpha channel
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint8_t *src_byte = src + (y * stride);
|
||||
uint32_t *dest_pixel = reinterpret_cast<uint32_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int red = *src_byte++;
|
||||
int green = *src_byte++;
|
||||
int blue = *src_byte++;
|
||||
*dest_pixel = qRgb(red, green, blue);
|
||||
dest_pixel++;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
m_parseState = ParseHeicError;
|
||||
qWarning() << "Unsupported bit depth:" << bit_depth;
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
heif_color_profile_type profileType = heif_image_handle_get_color_profile_type(handle.get_raw_image_handle());
|
||||
struct heif_error err;
|
||||
if (profileType == heif_color_profile_type_prof || profileType == heif_color_profile_type_rICC) {
|
||||
int rawProfileSize = (int)heif_image_handle_get_raw_color_profile_size(handle.get_raw_image_handle());
|
||||
if (rawProfileSize > 0) {
|
||||
QByteArray ba(rawProfileSize, 0);
|
||||
err = heif_image_handle_get_raw_color_profile(handle.get_raw_image_handle(), ba.data());
|
||||
if (err.code) {
|
||||
qWarning() << "icc profile loading failed";
|
||||
} else {
|
||||
m_current_image.setColorSpace(QColorSpace::fromIccProfile(ba));
|
||||
if (!m_current_image.colorSpace().isValid()) {
|
||||
qWarning() << "HEIC image has Qt-unsupported or invalid ICC profile!";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qWarning() << "icc profile is empty";
|
||||
}
|
||||
|
||||
} else if (profileType == heif_color_profile_type_nclx) {
|
||||
struct heif_color_profile_nclx *nclx = nullptr;
|
||||
err = heif_image_handle_get_nclx_color_profile(handle.get_raw_image_handle(), &nclx);
|
||||
if (err.code || !nclx) {
|
||||
qWarning() << "nclx profile loading failed";
|
||||
} else {
|
||||
const QPointF redPoint(nclx->color_primary_red_x, nclx->color_primary_red_y);
|
||||
const QPointF greenPoint(nclx->color_primary_green_x, nclx->color_primary_green_y);
|
||||
const QPointF bluePoint(nclx->color_primary_blue_x, nclx->color_primary_blue_y);
|
||||
const QPointF whitePoint(nclx->color_primary_white_x, nclx->color_primary_white_y);
|
||||
|
||||
QColorSpace::TransferFunction q_trc = QColorSpace::TransferFunction::Custom;
|
||||
float q_trc_gamma = 0.0f;
|
||||
|
||||
switch (nclx->transfer_characteristics) {
|
||||
case 4:
|
||||
q_trc = QColorSpace::TransferFunction::Gamma;
|
||||
q_trc_gamma = 2.2f;
|
||||
break;
|
||||
case 5:
|
||||
q_trc = QColorSpace::TransferFunction::Gamma;
|
||||
q_trc_gamma = 2.8f;
|
||||
break;
|
||||
case 8:
|
||||
q_trc = QColorSpace::TransferFunction::Linear;
|
||||
break;
|
||||
case 2:
|
||||
case 13:
|
||||
q_trc = QColorSpace::TransferFunction::SRgb;
|
||||
break;
|
||||
default:
|
||||
qWarning("CICP color_primaries: %d, transfer_characteristics: %d\nThe colorspace is unsupported by this plug-in yet.",
|
||||
nclx->color_primaries,
|
||||
nclx->transfer_characteristics);
|
||||
q_trc = QColorSpace::TransferFunction::SRgb;
|
||||
break;
|
||||
}
|
||||
|
||||
if (q_trc != QColorSpace::TransferFunction::Custom) { // we create new colorspace using Qt
|
||||
switch (nclx->color_primaries) {
|
||||
case 1:
|
||||
case 2:
|
||||
m_current_image.setColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, q_trc, q_trc_gamma));
|
||||
break;
|
||||
case 12:
|
||||
m_current_image.setColorSpace(QColorSpace(QColorSpace::Primaries::DciP3D65, q_trc, q_trc_gamma));
|
||||
break;
|
||||
default:
|
||||
m_current_image.setColorSpace(QColorSpace(whitePoint, redPoint, greenPoint, bluePoint, q_trc, q_trc_gamma));
|
||||
break;
|
||||
}
|
||||
}
|
||||
heif_nclx_color_profile_free(nclx);
|
||||
|
||||
if (!m_current_image.colorSpace().isValid()) {
|
||||
qWarning() << "HEIC plugin created invalid QColorSpace from NCLX!";
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
m_current_image.setColorSpace(QColorSpace(QColorSpace::SRgb));
|
||||
}
|
||||
|
||||
} catch (const heif::Error &err) {
|
||||
m_parseState = ParseHeicError;
|
||||
qWarning() << "libheif error:" << err.get_message().c_str();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_parseState = ParseHeicSuccess;
|
||||
return true;
|
||||
}
|
||||
|
||||
QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
if (format == "heif" || format == "heic") {
|
||||
Capabilities format_cap;
|
||||
if (heif_have_decoder_for_format(heif_compression_HEVC)) {
|
||||
format_cap |= CanRead;
|
||||
}
|
||||
if (heif_have_encoder_for_format(heif_compression_HEVC)) {
|
||||
format_cap |= CanWrite;
|
||||
}
|
||||
return format_cap;
|
||||
}
|
||||
if (!format.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
if (!device->isOpen()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Capabilities cap;
|
||||
if (device->isReadable() && HEIFHandler::canRead(device) && heif_have_decoder_for_format(heif_compression_HEVC)) {
|
||||
cap |= CanRead;
|
||||
}
|
||||
|
||||
if (device->isWritable() && heif_have_encoder_for_format(heif_compression_HEVC)) {
|
||||
cap |= CanWrite;
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
|
||||
QImageIOHandler *HEIFPlugin::create(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
QImageIOHandler *handler = new HEIFHandler;
|
||||
handler->setDevice(device);
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
7
src/imageformats/heif.desktop
Normal file
@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Service
|
||||
X-KDE-ServiceTypes=QImageIOPlugins
|
||||
X-KDE-ImageFormat=heif
|
||||
X-KDE-MimeType=image/heif
|
||||
X-KDE-Read=true
|
||||
X-KDE-Write=true
|
4
src/imageformats/heif.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Keys": [ "heif", "heic" ],
|
||||
"MimeTypes": [ "image/heif", "image/heif" ]
|
||||
}
|
58
src/imageformats/heif_p.h
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
High Efficiency Image File Format (HEIF) support for QImage.
|
||||
|
||||
SPDX-FileCopyrightText: 2020 Sirius Bakke <sirius@bakke.co>
|
||||
SPDX-FileCopyrightText: 2021 Daniel Novomesky <dnovomesky@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KIMG_HEIF_P_H
|
||||
#define KIMG_HEIF_P_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QImage>
|
||||
#include <QImageIOPlugin>
|
||||
|
||||
class HEIFHandler : public QImageIOHandler
|
||||
{
|
||||
public:
|
||||
HEIFHandler();
|
||||
|
||||
bool canRead() const override;
|
||||
bool read(QImage *image) override;
|
||||
bool write(const QImage &image) override;
|
||||
|
||||
static bool canRead(QIODevice *device);
|
||||
|
||||
QVariant option(ImageOption option) const override;
|
||||
void setOption(ImageOption option, const QVariant &value) override;
|
||||
bool supportsOption(ImageOption option) const override;
|
||||
|
||||
private:
|
||||
static bool isSupportedBMFFType(const QByteArray &header);
|
||||
bool ensureParsed() const;
|
||||
bool ensureDecoder();
|
||||
|
||||
enum ParseHeicState {
|
||||
ParseHeicError = -1,
|
||||
ParseHeicNotParsed = 0,
|
||||
ParseHeicSuccess = 1,
|
||||
};
|
||||
|
||||
ParseHeicState m_parseState;
|
||||
int m_quality;
|
||||
QImage m_current_image;
|
||||
};
|
||||
|
||||
class HEIFPlugin : public QImageIOPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "heif.json")
|
||||
|
||||
public:
|
||||
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
|
||||
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||
};
|
||||
|
||||
#endif // KIMG_HEIF_P_H
|
@ -12,9 +12,9 @@
|
||||
|
||||
#include <kzip.h>
|
||||
|
||||
#include <QImage>
|
||||
#include <QIODevice>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QImage>
|
||||
|
||||
static constexpr char s_magic[] = "application/x-krita";
|
||||
static constexpr int s_magic_size = sizeof(s_magic) - 1; // -1 to remove the last \0
|
||||
@ -35,10 +35,14 @@ bool KraHandler::canRead() const
|
||||
bool KraHandler::read(QImage *image)
|
||||
{
|
||||
KZip zip(device());
|
||||
if (!zip.open(QIODevice::ReadOnly)) return false;
|
||||
if (!zip.open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const KArchiveEntry *entry = zip.directory()->entry(QStringLiteral("mergedimage.png"));
|
||||
if (!entry || !entry->isFile()) return false;
|
||||
if (!entry || !entry->isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const KZipFileEntry *fileZipEntry = static_cast<const KZipFileEntry *>(entry);
|
||||
|
||||
@ -55,8 +59,9 @@ bool KraHandler::canRead(QIODevice *device)
|
||||
}
|
||||
|
||||
char buff[57];
|
||||
if (device->peek(buff, sizeof(buff)) == sizeof(buff))
|
||||
if (device->peek(buff, sizeof(buff)) == sizeof(buff)) {
|
||||
return memcmp(buff + 0x26, s_magic, s_magic_size) == 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -31,6 +31,4 @@ public:
|
||||
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
|
@ -34,10 +34,14 @@ bool OraHandler::canRead() const
|
||||
bool OraHandler::read(QImage *image)
|
||||
{
|
||||
KZip zip(device());
|
||||
if (!zip.open(QIODevice::ReadOnly)) return false;
|
||||
if (!zip.open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const KArchiveEntry *entry = zip.directory()->entry(QStringLiteral("mergedimage.png"));
|
||||
if (!entry || !entry->isFile()) return false;
|
||||
if (!entry || !entry->isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const KZipFileEntry *fileZipEntry = static_cast<const KZipFileEntry *>(entry);
|
||||
|
||||
@ -54,8 +58,9 @@ bool OraHandler::canRead(QIODevice *device)
|
||||
}
|
||||
|
||||
char buff[54];
|
||||
if (device->peek(buff, sizeof(buff)) == sizeof(buff))
|
||||
if (device->peek(buff, sizeof(buff)) == sizeof(buff)) {
|
||||
return memcmp(buff + 0x26, s_magic, s_magic_size) == 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ public:
|
||||
static bool canRead(QIODevice *device);
|
||||
};
|
||||
|
||||
|
||||
class OraPlugin : public QImageIOPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -31,6 +30,4 @@ public:
|
||||
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
|
@ -12,7 +12,6 @@
|
||||
#include <QDebug>
|
||||
#include <QImage>
|
||||
|
||||
|
||||
#pragma pack(push, 1)
|
||||
class RGB
|
||||
{
|
||||
@ -29,7 +28,6 @@ public:
|
||||
c.b = qBlue(color);
|
||||
return c;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
class Palette
|
||||
@ -107,7 +105,9 @@ public:
|
||||
|
||||
static QDataStream &operator>>(QDataStream &s, RGB &rgb)
|
||||
{
|
||||
quint8 r, g, b;
|
||||
quint8 r;
|
||||
quint8 g;
|
||||
quint8 b;
|
||||
|
||||
s >> r >> g >> b;
|
||||
rgb.r = r;
|
||||
@ -128,35 +128,48 @@ static QDataStream &operator>>(QDataStream &s, Palette &pal)
|
||||
|
||||
static QDataStream &operator>>(QDataStream &s, PCXHEADER &ph)
|
||||
{
|
||||
quint8 m, ver, enc, bpp;
|
||||
quint8 m;
|
||||
quint8 ver;
|
||||
quint8 enc;
|
||||
quint8 bpp;
|
||||
s >> m >> ver >> enc >> bpp;
|
||||
ph.Manufacturer = m;
|
||||
ph.Version = ver;
|
||||
ph.Encoding = enc;
|
||||
ph.Bpp = bpp;
|
||||
quint16 xmin, ymin, xmax, ymax;
|
||||
quint16 xmin;
|
||||
quint16 ymin;
|
||||
quint16 xmax;
|
||||
quint16 ymax;
|
||||
s >> xmin >> ymin >> xmax >> ymax;
|
||||
ph.XMin = xmin;
|
||||
ph.YMin = ymin;
|
||||
ph.XMax = xmax;
|
||||
ph.YMax = ymax;
|
||||
quint16 hdpi, ydpi;
|
||||
quint16 hdpi;
|
||||
quint16 ydpi;
|
||||
s >> hdpi >> ydpi;
|
||||
ph.HDpi = hdpi;
|
||||
ph.YDpi = ydpi;
|
||||
Palette colorMap;
|
||||
quint8 res, np;
|
||||
quint8 res;
|
||||
quint8 np;
|
||||
s >> colorMap >> res >> np;
|
||||
ph.ColorMap = colorMap;
|
||||
ph.Reserved = res;
|
||||
ph.NPlanes = np;
|
||||
quint16 bytesperline;
|
||||
s >> bytesperline; ph.BytesPerLine = bytesperline;
|
||||
s >> bytesperline;
|
||||
ph.BytesPerLine = bytesperline;
|
||||
quint16 paletteinfo;
|
||||
s >> paletteinfo; ph.PaletteInfo = paletteinfo;
|
||||
quint16 hscreensize, vscreensize;
|
||||
s >> hscreensize; ph.HScreenSize = hscreensize;
|
||||
s >> vscreensize; ph.VScreenSize = vscreensize;
|
||||
s >> paletteinfo;
|
||||
ph.PaletteInfo = paletteinfo;
|
||||
quint16 hscreensize;
|
||||
quint16 vscreensize;
|
||||
s >> hscreensize;
|
||||
ph.HScreenSize = hscreensize;
|
||||
s >> vscreensize;
|
||||
ph.VScreenSize = vscreensize;
|
||||
|
||||
// Skip the rest of the header
|
||||
quint8 byte;
|
||||
@ -220,7 +233,8 @@ static void readLine(QDataStream &s, QByteArray &buf, const PCXHEADER &header)
|
||||
{
|
||||
quint32 i = 0;
|
||||
quint32 size = buf.size();
|
||||
quint8 byte, count;
|
||||
quint8 byte;
|
||||
quint8 count;
|
||||
|
||||
if (header.isCompressed()) {
|
||||
// Uncompress the image data
|
||||
@ -298,11 +312,12 @@ static void readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
quint32 offset = i * header.BytesPerLine;
|
||||
for (int x = 0; x < header.width(); ++x)
|
||||
for (int x = 0; x < header.width(); ++x) {
|
||||
if (buf[offset + (x / 8)] & (128 >> (x % 8))) {
|
||||
pixbuf[x] = (int)(pixbuf[x]) + (1 << i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uchar *p = img.scanLine(y);
|
||||
if (!p) {
|
||||
@ -341,8 +356,9 @@ static void readImage8(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
|
||||
uchar *p = img.scanLine(y);
|
||||
|
||||
if (!p)
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned int bpl = qMin(header.BytesPerLine, (quint16)header.width());
|
||||
for (unsigned int x = 0; x < bpl; ++x) {
|
||||
@ -356,7 +372,9 @@ static void readImage8(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
|
||||
if (flag == 12 && (header.Version == 5 || header.Version == 2)) {
|
||||
// Read the palette
|
||||
quint8 r, g, b;
|
||||
quint8 r;
|
||||
quint8 g;
|
||||
quint8 b;
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
s >> r >> g >> b;
|
||||
img.setColor(i, qRgb(r, g, b));
|
||||
@ -398,7 +416,8 @@ static void writeLine(QDataStream &s, QByteArray &buf)
|
||||
{
|
||||
quint32 i = 0;
|
||||
quint32 size = buf.size();
|
||||
quint8 count, data;
|
||||
quint8 count;
|
||||
quint8 data;
|
||||
char byte;
|
||||
|
||||
while (i < size) {
|
||||
@ -471,11 +490,12 @@ static void writeImage4(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
}
|
||||
|
||||
for (int x = 0; x < header.width(); ++x) {
|
||||
for (int i = 0; i < 4; ++i)
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (*(p + x) & (1 << i)) {
|
||||
buf[i][x / 8] = (int)(buf[i][x / 8]) | 1 << (7 - x % 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
writeLine(s, buf[i]);
|
||||
@ -616,8 +636,12 @@ bool PCXHandler::write(const QImage &image)
|
||||
|
||||
QImage img = image;
|
||||
|
||||
int w = img.width();
|
||||
int h = img.height();
|
||||
const int w = img.width();
|
||||
const int h = img.height();
|
||||
|
||||
if (w > 65536 || h > 65536) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// qDebug() << "Width: " << w;
|
||||
// qDebug() << "Height: " << h;
|
||||
|
@ -21,9 +21,10 @@
|
||||
#include <QDebug>
|
||||
#include <QImage>
|
||||
#include <QVariant>
|
||||
#include <qendian.h>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <qendian.h>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* Reads a PIC file header from a data stream.
|
||||
@ -181,15 +182,13 @@ static bool readRow(QDataStream &stream, QRgb *row, quint16 width, const QList<P
|
||||
return qRgba(red, green, blue, alpha);
|
||||
};
|
||||
auto updatePixel = [&](QRgb oldPixel, QRgb newPixel) -> QRgb {
|
||||
return qRgba(
|
||||
qRed((channel.code & RED) ? newPixel : oldPixel),
|
||||
return qRgba(qRed((channel.code & RED) ? newPixel : oldPixel),
|
||||
qGreen((channel.code & GREEN) ? newPixel : oldPixel),
|
||||
qBlue((channel.code & BLUE) ? newPixel : oldPixel),
|
||||
qAlpha((channel.code & ALPHA) ? newPixel : oldPixel));
|
||||
};
|
||||
if (channel.encoding == MixedRLE) {
|
||||
bool success = decodeRLEData(RLEVariant::PIC, stream, row, width,
|
||||
readPixel, updatePixel);
|
||||
bool success = decodeRLEData(RLEVariant::PIC, stream, row, width, readPixel, updatePixel);
|
||||
if (!success) {
|
||||
qDebug() << "decodeRLEData failed";
|
||||
return false;
|
||||
@ -227,7 +226,7 @@ bool SoftimagePICHandler::read(QImage *image)
|
||||
}
|
||||
|
||||
QImage::Format fmt = QImage::Format_RGB32;
|
||||
for (const PicChannel &channel : qAsConst(m_channels)) {
|
||||
for (const PicChannel &channel : std::as_const(m_channels)) {
|
||||
if (channel.size != 8) {
|
||||
// we cannot read images that do not come in bytes
|
||||
qDebug() << "Channel size was" << channel.size;
|
||||
@ -265,9 +264,7 @@ bool SoftimagePICHandler::read(QImage *image)
|
||||
bool SoftimagePICHandler::write(const QImage &_image)
|
||||
{
|
||||
bool alpha = _image.hasAlphaChannel();
|
||||
const QImage image = _image.convertToFormat(
|
||||
alpha ? QImage::Format_ARGB32
|
||||
: QImage::Format_RGB32);
|
||||
const QImage image = _image.convertToFormat(alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||
|
||||
if (image.width() < 0 || image.height() < 0) {
|
||||
qDebug() << "Image size invalid:" << image.width() << image.height();
|
||||
@ -296,18 +293,13 @@ bool SoftimagePICHandler::write(const QImage &_image)
|
||||
|
||||
/* Write the RGB part of the scanline */
|
||||
auto rgbEqual = [](QRgb p1, QRgb p2) -> bool {
|
||||
return qRed(p1) == qRed(p2) &&
|
||||
qGreen(p1) == qGreen(p2) &&
|
||||
qBlue(p1) == qBlue(p2);
|
||||
return qRed(p1) == qRed(p2) && qGreen(p1) == qGreen(p2) && qBlue(p1) == qBlue(p2);
|
||||
};
|
||||
auto writeRgb = [](QDataStream &str, QRgb pixel) -> void {
|
||||
str << quint8(qRed(pixel))
|
||||
<< quint8(qGreen(pixel))
|
||||
<< quint8(qBlue(pixel));
|
||||
str << quint8(qRed(pixel)) << quint8(qGreen(pixel)) << quint8(qBlue(pixel));
|
||||
};
|
||||
if (m_compression) {
|
||||
encodeRLEData(RLEVariant::PIC, stream, row, image.width(),
|
||||
rgbEqual, writeRgb);
|
||||
encodeRLEData(RLEVariant::PIC, stream, row, image.width(), rgbEqual, writeRgb);
|
||||
} else {
|
||||
for (int i = 0; i < image.width(); ++i) {
|
||||
writeRgb(stream, row[i]);
|
||||
@ -323,8 +315,7 @@ bool SoftimagePICHandler::write(const QImage &_image)
|
||||
str << quint8(qAlpha(pixel));
|
||||
};
|
||||
if (m_compression) {
|
||||
encodeRLEData(RLEVariant::PIC, stream, row, image.width(),
|
||||
alphaEqual, writeAlpha);
|
||||
encodeRLEData(RLEVariant::PIC, stream, row, image.width(), alphaEqual, writeAlpha);
|
||||
} else {
|
||||
for (int i = 0; i < image.width(); ++i) {
|
||||
writeAlpha(stream, row[i]);
|
||||
@ -408,15 +399,13 @@ QVariant SoftimagePICHandler::option(ImageOption option) const
|
||||
if (const_cast<SoftimagePICHandler *>(this)->readHeader()) {
|
||||
QString descStr = QString::fromUtf8(m_header.comment);
|
||||
if (!descStr.isEmpty()) {
|
||||
return QString(QStringLiteral("Description: ") +
|
||||
descStr +
|
||||
QStringLiteral("\n\n"));
|
||||
return QString(QStringLiteral("Description: ") + descStr + QStringLiteral("\n\n"));
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
case ImageFormat:
|
||||
if (const_cast<SoftimagePICHandler *>(this)->readChannels()) {
|
||||
for (const PicChannel &channel : qAsConst(m_channels)) {
|
||||
for (const PicChannel &channel : std::as_const(m_channels)) {
|
||||
if (channel.code & ALPHA) {
|
||||
return QImage::Format_ARGB32;
|
||||
}
|
||||
@ -431,10 +420,7 @@ QVariant SoftimagePICHandler::option(ImageOption option) const
|
||||
|
||||
bool SoftimagePICHandler::supportsOption(ImageOption option) const
|
||||
{
|
||||
return (option == CompressionRatio ||
|
||||
option == Description ||
|
||||
option == ImageFormat ||
|
||||
option == Size);
|
||||
return (option == CompressionRatio || option == Description || option == ImageFormat || option == Size);
|
||||
}
|
||||
|
||||
QImageIOPlugin::Capabilities SoftimagePICPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||
|
@ -8,8 +8,8 @@
|
||||
#ifndef KIMG_PIC_P_H
|
||||
#define KIMG_PIC_P_H
|
||||
|
||||
#include <QImageIOPlugin>
|
||||
#include <QDataStream>
|
||||
#include <QImageIOPlugin>
|
||||
|
||||
/**
|
||||
* The magic number at the start of a SoftImage PIC file.
|
||||
@ -25,7 +25,7 @@ enum PicFields {
|
||||
NoPicture = 0, /**< No picture */
|
||||
OddScanlines = 1, /**< Odd scanlines */
|
||||
EvenScanlines = 2, /**< Even scanlines */
|
||||
BothScanlines = 3 /**< Every scanline */
|
||||
BothScanlines = 3, /**< Every scanline */
|
||||
};
|
||||
|
||||
/**
|
||||
@ -33,7 +33,7 @@ enum PicFields {
|
||||
*/
|
||||
enum PicChannelEncoding {
|
||||
Uncompressed = 0, /**< Image is uncompressed */
|
||||
MixedRLE = 2 /**< Run length compression */
|
||||
MixedRLE = 2, /**< Run length compression */
|
||||
};
|
||||
|
||||
/**
|
||||
@ -43,7 +43,7 @@ enum PicChannelCode {
|
||||
RED = 0x80, /**< Red channel */
|
||||
GREEN = 0x40, /**< Green channel */
|
||||
BLUE = 0x20, /**< Blue channel */
|
||||
ALPHA = 0x10 /**< Alpha channel */
|
||||
ALPHA = 0x10, /**< Alpha channel */
|
||||
};
|
||||
|
||||
/**
|
||||
@ -70,9 +70,12 @@ struct PicHeader {
|
||||
, height(_height)
|
||||
, ratio(1.0f)
|
||||
, fields(BothScanlines)
|
||||
{}
|
||||
{
|
||||
}
|
||||
/** Construct an invalid header. */
|
||||
PicHeader() {}
|
||||
PicHeader()
|
||||
{
|
||||
}
|
||||
|
||||
quint32 magic; /**< Should be PIC_MAGIC_NUMBER */
|
||||
float version; /**< Version of something (header? file format?) (ignored) */
|
||||
@ -88,9 +91,9 @@ struct PicHeader {
|
||||
/**
|
||||
* Returns true if the @p magic and @p id fields are set correctly.
|
||||
*/
|
||||
bool isValid() const {
|
||||
return magic == PIC_MAGIC_NUMBER
|
||||
&& id == "PICT";
|
||||
bool isValid() const
|
||||
{
|
||||
return magic == PIC_MAGIC_NUMBER && id == "PICT";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,7 +126,8 @@ struct PicChannel {
|
||||
: size(_size)
|
||||
, encoding(_encoding)
|
||||
, code(_code)
|
||||
{}
|
||||
{
|
||||
}
|
||||
/**
|
||||
* Constructs a default channel description for a SoftImage PIC file.
|
||||
*
|
||||
@ -135,7 +139,8 @@ struct PicChannel {
|
||||
*/
|
||||
PicChannel()
|
||||
: size(8)
|
||||
{}
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class SoftimagePICHandler : public QImageIOHandler
|
||||
@ -155,13 +160,14 @@ public:
|
||||
Error,
|
||||
Ready,
|
||||
ReadHeader,
|
||||
ReadChannels
|
||||
ReadChannels,
|
||||
};
|
||||
|
||||
SoftimagePICHandler()
|
||||
: m_state(Ready)
|
||||
, m_compression(true)
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
bool readHeader();
|
||||
bool readChannels();
|
||||
|
@ -31,7 +31,6 @@ typedef quint8 uchar;
|
||||
|
||||
namespace // Private.
|
||||
{
|
||||
|
||||
enum ColorMode {
|
||||
CM_BITMAP = 0,
|
||||
CM_GRAYSCALE = 1,
|
||||
@ -40,7 +39,7 @@ enum ColorMode {
|
||||
CM_CMYK = 4,
|
||||
CM_MULTICHANNEL = 7,
|
||||
CM_DUOTONE = 8,
|
||||
CM_LABCOLOR = 9
|
||||
CM_LABCOLOR = 9,
|
||||
};
|
||||
|
||||
struct PSDHeader {
|
||||
@ -87,7 +86,7 @@ static bool IsSupported(const PSDHeader &header)
|
||||
if (header.channel_count > 16) {
|
||||
return false;
|
||||
}
|
||||
if (header.depth != 8) {
|
||||
if (header.depth != 8 && header.depth != 16) {
|
||||
return false;
|
||||
}
|
||||
if (header.color_mode != CM_RGB) {
|
||||
@ -104,21 +103,28 @@ static void skip_section(QDataStream &s)
|
||||
s.skipRawData(section_length);
|
||||
}
|
||||
|
||||
static quint8 readPixel(QDataStream &stream) {
|
||||
quint8 pixel;
|
||||
template<class Trait>
|
||||
static Trait readPixel(QDataStream &stream)
|
||||
{
|
||||
Trait pixel;
|
||||
stream >> pixel;
|
||||
return pixel;
|
||||
}
|
||||
static QRgb updateRed(QRgb oldPixel, quint8 redPixel) {
|
||||
|
||||
static QRgb updateRed(QRgb oldPixel, quint8 redPixel)
|
||||
{
|
||||
return qRgba(redPixel, qGreen(oldPixel), qBlue(oldPixel), qAlpha(oldPixel));
|
||||
}
|
||||
static QRgb updateGreen(QRgb oldPixel, quint8 greenPixel) {
|
||||
static QRgb updateGreen(QRgb oldPixel, quint8 greenPixel)
|
||||
{
|
||||
return qRgba(qRed(oldPixel), greenPixel, qBlue(oldPixel), qAlpha(oldPixel));
|
||||
}
|
||||
static QRgb updateBlue(QRgb oldPixel, quint8 bluePixel) {
|
||||
static QRgb updateBlue(QRgb oldPixel, quint8 bluePixel)
|
||||
{
|
||||
return qRgba(qRed(oldPixel), qGreen(oldPixel), bluePixel, qAlpha(oldPixel));
|
||||
}
|
||||
static QRgb updateAlpha(QRgb oldPixel, quint8 alphaPixel) {
|
||||
static QRgb updateAlpha(QRgb oldPixel, quint8 alphaPixel)
|
||||
{
|
||||
return qRgba(qRed(oldPixel), qGreen(oldPixel), qBlue(oldPixel), alphaPixel);
|
||||
}
|
||||
typedef QRgb (*channelUpdater)(QRgb, quint8);
|
||||
@ -149,15 +155,16 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
|
||||
quint32 channel_num = header.channel_count;
|
||||
|
||||
QImage::Format fmt = QImage::Format_RGB32;
|
||||
QImage::Format fmt = header.depth == 8 ? QImage::Format_RGB32 : QImage::Format_RGBX64;
|
||||
// Clear the image.
|
||||
if (channel_num >= 4) {
|
||||
// Enable alpha.
|
||||
fmt = QImage::Format_ARGB32;
|
||||
fmt = header.depth == 8 ? QImage::Format_ARGB32 : QImage::Format_RGBA64;
|
||||
|
||||
// Ignore the other channels.
|
||||
channel_num = 4;
|
||||
}
|
||||
|
||||
img = QImage(header.width, header.height, fmt);
|
||||
if (img.isNull()) {
|
||||
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width, header.height);
|
||||
@ -166,9 +173,10 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
img.fill(qRgb(0, 0, 0));
|
||||
|
||||
const quint32 pixel_count = header.height * header.width;
|
||||
const quint32 channel_size = pixel_count * header.depth / 8;
|
||||
|
||||
// Verify this, as this is used to write into the memory of the QImage
|
||||
if (pixel_count > img.sizeInBytes() / sizeof(QRgb)) {
|
||||
if (pixel_count > img.sizeInBytes() / (header.depth == 8 ? sizeof(QRgb) : sizeof(QRgba64))) {
|
||||
qWarning() << "Invalid pixel count!" << pixel_count << "bytes available:" << img.sizeInBytes();
|
||||
return false;
|
||||
}
|
||||
@ -179,12 +187,21 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
return false;
|
||||
}
|
||||
|
||||
static const channelUpdater updaters[4] = {
|
||||
updateRed,
|
||||
updateGreen,
|
||||
updateBlue,
|
||||
updateAlpha
|
||||
};
|
||||
static const channelUpdater updaters[4] = {updateRed, updateGreen, updateBlue, updateAlpha};
|
||||
|
||||
typedef QRgba64 (*channelUpdater16)(QRgba64, quint16);
|
||||
static const channelUpdater16 updaters64[4] = {[](QRgba64 oldPixel, quint16 redPixel) {
|
||||
return qRgba64((oldPixel & ~(0xFFFFull << 0)) | (quint64(redPixel) << 0));
|
||||
},
|
||||
[](QRgba64 oldPixel, quint16 greenPixel) {
|
||||
return qRgba64((oldPixel & ~(0xFFFFull << 16)) | (quint64(greenPixel) << 16));
|
||||
},
|
||||
[](QRgba64 oldPixel, quint16 bluePixel) {
|
||||
return qRgba64((oldPixel & ~(0xFFFFull << 32)) | (quint64(bluePixel) << 32));
|
||||
},
|
||||
[](QRgba64 oldPixel, quint16 alphaPixel) {
|
||||
return qRgba64((oldPixel & ~(0xFFFFull << 48)) | (quint64(alphaPixel) << 48));
|
||||
}};
|
||||
|
||||
if (compression) {
|
||||
// Skip row lengths.
|
||||
@ -194,9 +211,14 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
}
|
||||
|
||||
for (unsigned short channel = 0; channel < channel_num; channel++) {
|
||||
bool success = decodeRLEData(RLEVariant::PackBits, stream,
|
||||
image_data, pixel_count,
|
||||
&readPixel, updaters[channel]);
|
||||
bool success = false;
|
||||
if (header.depth == 8) {
|
||||
success = decodeRLEData(RLEVariant::PackBits, stream, image_data, channel_size, &readPixel<quint8>, updaters[channel]);
|
||||
} else if (header.depth == 16) {
|
||||
QRgba64 *image_data = reinterpret_cast<QRgba64 *>(img.bits());
|
||||
success = decodeRLEData(RLEVariant::PackBits16, stream, image_data, channel_size, &readPixel<quint8>, updaters64[channel]);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
qDebug() << "decodeRLEData on channel" << channel << "failed";
|
||||
return false;
|
||||
@ -204,8 +226,15 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
}
|
||||
} else {
|
||||
for (unsigned short channel = 0; channel < channel_num; channel++) {
|
||||
if (header.depth == 8) {
|
||||
for (unsigned i = 0; i < pixel_count; ++i) {
|
||||
image_data[i] = updaters[channel](image_data[i], readPixel(stream));
|
||||
image_data[i] = updaters[channel](image_data[i], readPixel<quint8>(stream));
|
||||
}
|
||||
} else if (header.depth == 16) {
|
||||
QRgba64 *image_data = reinterpret_cast<QRgba64 *>(img.bits());
|
||||
for (unsigned i = 0; i < pixel_count; ++i) {
|
||||
image_data[i] = updaters64[channel](image_data[i], readPixel<quint16>(stream));
|
||||
}
|
||||
}
|
||||
// make sure we didn't try to read past the end of the stream
|
||||
if (stream.status() != QDataStream::Ok) {
|
||||
|
@ -32,4 +32,3 @@ public:
|
||||
};
|
||||
|
||||
#endif // KIMG_PSD_P_H
|
||||
|
||||
|
@ -9,9 +9,9 @@
|
||||
|
||||
#include "ras_p.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
#include <QImage>
|
||||
|
||||
namespace // Private.
|
||||
{
|
||||
@ -28,13 +28,13 @@ enum RASType {
|
||||
RAS_TYPE_RGB_FORMAT = 0x3,
|
||||
RAS_TYPE_TIFF_FORMAT = 0x4,
|
||||
RAS_TYPE_IFF_FORMAT = 0x5,
|
||||
RAS_TYPE_EXPERIMENTAL = 0xFFFF
|
||||
RAS_TYPE_EXPERIMENTAL = 0xFFFF,
|
||||
};
|
||||
|
||||
enum RASColorMapType {
|
||||
RAS_COLOR_MAP_TYPE_NONE = 0x0,
|
||||
RAS_COLOR_MAP_TYPE_RGB = 0x1,
|
||||
RAS_COLOR_MAP_TYPE_RAW = 0x2
|
||||
RAS_COLOR_MAP_TYPE_RAW = 0x2,
|
||||
};
|
||||
|
||||
struct RasHeader {
|
||||
@ -46,7 +46,9 @@ struct RasHeader {
|
||||
quint32 Type;
|
||||
quint32 ColorMapType;
|
||||
quint32 ColorMapLength;
|
||||
enum { SIZE = 32 }; // 8 fields of four bytes each
|
||||
enum {
|
||||
SIZE = 32,
|
||||
}; // 8 fields of four bytes each
|
||||
};
|
||||
|
||||
static QDataStream &operator>>(QDataStream &s, RasHeader &head)
|
||||
@ -79,8 +81,7 @@ static bool IsSupported(const RasHeader &head)
|
||||
// check for an appropriate depth
|
||||
// we support 8bit+palette, 24bit and 32bit ONLY!
|
||||
// TODO: add support for 1bit
|
||||
if (!((head.Depth == 8 && head.ColorMapType == 1)
|
||||
|| head.Depth == 24 || head.Depth == 32)) {
|
||||
if (!((head.Depth == 8 && head.ColorMapType == 1) || head.Depth == 24 || head.Depth == 32)) {
|
||||
return false;
|
||||
}
|
||||
// the Type field adds support for RLE(BGR), RGB and other encodings
|
||||
@ -153,13 +154,16 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
// Allocate image
|
||||
img = QImage(ras.Width, ras.Height, QImage::Format_ARGB32);
|
||||
|
||||
if (img.isNull())
|
||||
if (img.isNull()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reconstruct image from RGB palette if we have a palette
|
||||
// TODO: make generic so it works with 24bit or 32bit palettes
|
||||
if (ras.ColorMapType == 1 && ras.Depth == 8) {
|
||||
quint8 red, green, blue;
|
||||
quint8 red;
|
||||
quint8 green;
|
||||
quint8 blue;
|
||||
for (quint32 y = 0; y < ras.Height; y++) {
|
||||
for (quint32 x = 0; x < ras.Width; x++) {
|
||||
red = palette.value((int)input[y * ras.Width + x]);
|
||||
@ -168,11 +172,12 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
img.setPixel(x, y, qRgb(red, green, blue));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (ras.ColorMapType == 0 && ras.Depth == 24 && (ras.Type == 1 || ras.Type == 2)) {
|
||||
quint8 red, green, blue;
|
||||
quint8 red;
|
||||
quint8 green;
|
||||
quint8 blue;
|
||||
for (quint32 y = 0; y < ras.Height; y++) {
|
||||
for (quint32 x = 0; x < ras.Width; x++) {
|
||||
red = input[y * 3 * ras.Width + x * 3 + 2];
|
||||
@ -184,7 +189,9 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
}
|
||||
|
||||
if (ras.ColorMapType == 0 && ras.Depth == 24 && ras.Type == 3) {
|
||||
quint8 red, green, blue;
|
||||
quint8 red;
|
||||
quint8 green;
|
||||
quint8 blue;
|
||||
for (quint32 y = 0; y < ras.Height; y++) {
|
||||
for (quint32 x = 0; x < ras.Width; x++) {
|
||||
red = input[y * 3 * ras.Width + x * 3];
|
||||
@ -196,7 +203,9 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
}
|
||||
|
||||
if (ras.ColorMapType == 0 && ras.Depth == 32 && (ras.Type == 1 || ras.Type == 2)) {
|
||||
quint8 red, green, blue;
|
||||
quint8 red;
|
||||
quint8 green;
|
||||
quint8 blue;
|
||||
for (quint32 y = 0; y < ras.Height; y++) {
|
||||
for (quint32 x = 0; x < ras.Width; x++) {
|
||||
red = input[y * 4 * ras.Width + x * 4 + 3];
|
||||
@ -208,7 +217,9 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
}
|
||||
|
||||
if (ras.ColorMapType == 0 && ras.Depth == 32 && ras.Type == 3) {
|
||||
quint8 red, green, blue;
|
||||
quint8 red;
|
||||
quint8 green;
|
||||
quint8 blue;
|
||||
for (quint32 y = 0; y < ras.Height; y++) {
|
||||
for (quint32 x = 0; x < ras.Width; x++) {
|
||||
red = input[y * 4 * ras.Width + x * 4 + 1];
|
||||
@ -274,8 +285,9 @@ bool RASHandler::read(QImage *outImage)
|
||||
RasHeader ras;
|
||||
s >> ras;
|
||||
|
||||
if (ras.ColorMapLength > std::numeric_limits<int>::max())
|
||||
if (ras.ColorMapLength > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: add support for old versions of RAS where Length may be zero in header
|
||||
s.device()->seek(RasHeader::SIZE + ras.Length + ras.ColorMapLength);
|
||||
@ -306,7 +318,6 @@ bool RASHandler::read(QImage *outImage)
|
||||
|
||||
QImageIOPlugin::Capabilities RASPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
|
||||
if (format == "ras") {
|
||||
return Capabilities(CanRead);
|
||||
}
|
||||
|
@ -24,14 +24,17 @@
|
||||
#include <QMap>
|
||||
#include <QVector>
|
||||
|
||||
#include <QImage>
|
||||
#include <QDebug>
|
||||
#include <QImage>
|
||||
|
||||
class RLEData : public QVector<uchar>
|
||||
{
|
||||
public:
|
||||
RLEData() {}
|
||||
RLEData(const uchar *d, uint l, uint o) : _offset(o)
|
||||
RLEData()
|
||||
{
|
||||
}
|
||||
RLEData(const uchar *d, uint l, uint o)
|
||||
: _offset(o)
|
||||
{
|
||||
for (uint i = 0; i < l; i++) {
|
||||
append(d[i]);
|
||||
@ -51,7 +54,11 @@ private:
|
||||
class RLEMap : public QMap<RLEData, uint>
|
||||
{
|
||||
public:
|
||||
RLEMap() : _counter(0), _offset(0) {}
|
||||
RLEMap()
|
||||
: _counter(0)
|
||||
, _offset(0)
|
||||
{
|
||||
}
|
||||
uint insert(const uchar *d, uint l);
|
||||
QVector<const RLEData *> vector();
|
||||
void setBaseOffset(uint o)
|
||||
@ -74,7 +81,12 @@ public:
|
||||
bool writeImage(const QImage &);
|
||||
|
||||
private:
|
||||
enum { NORMAL, DITHERED, SCREEN, COLORMAP }; // colormap
|
||||
enum {
|
||||
NORMAL,
|
||||
DITHERED,
|
||||
SCREEN,
|
||||
COLORMAP,
|
||||
}; // colormap
|
||||
QIODevice *_dev;
|
||||
QDataStream _stream;
|
||||
|
||||
@ -108,9 +120,9 @@ private:
|
||||
uchar intensity(uchar);
|
||||
};
|
||||
|
||||
SGIImage::SGIImage(QIODevice *io) :
|
||||
_starttab(nullptr),
|
||||
_lengthtab(nullptr)
|
||||
SGIImage::SGIImage(QIODevice *io)
|
||||
: _starttab(nullptr)
|
||||
, _lengthtab(nullptr)
|
||||
{
|
||||
_dev = io;
|
||||
_stream.setDevice(_dev);
|
||||
@ -126,7 +138,8 @@ SGIImage::~SGIImage()
|
||||
|
||||
bool SGIImage::getRow(uchar *dest)
|
||||
{
|
||||
int n, i;
|
||||
int n;
|
||||
int i;
|
||||
if (!_rle) {
|
||||
for (i = 0; i < _xsize; i++) {
|
||||
if (_pos >= _data.end()) {
|
||||
@ -172,7 +185,8 @@ bool SGIImage::readData(QImage &img)
|
||||
quint32 *start = _starttab;
|
||||
QByteArray lguard(_xsize, 0);
|
||||
uchar *line = (uchar *)lguard.data();
|
||||
unsigned x, y;
|
||||
unsigned x;
|
||||
unsigned y;
|
||||
|
||||
if (!_rle) {
|
||||
_pos = _data.begin();
|
||||
@ -267,9 +281,9 @@ bool SGIImage::readImage(QImage &img)
|
||||
// bytes per channel
|
||||
_stream >> _bpc;
|
||||
// qDebug() << "bytes per channel: " << int(_bpc);
|
||||
if (_bpc == 1)
|
||||
if (_bpc == 1) {
|
||||
;
|
||||
else if (_bpc == 2) {
|
||||
} else if (_bpc == 2) {
|
||||
// qDebug() << "dropping least significant byte";
|
||||
} else {
|
||||
return false;
|
||||
@ -316,8 +330,9 @@ bool SGIImage::readImage(QImage &img)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_zsize == 0 )
|
||||
if (_zsize == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_zsize == 2 || _zsize == 4) {
|
||||
img = img.convertToFormat(QImage::Format_ARGB32);
|
||||
@ -325,9 +340,10 @@ bool SGIImage::readImage(QImage &img)
|
||||
// qDebug() << "using first 4 of " << _zsize << " channels";
|
||||
// Only let this continue if it won't cause a int overflow later
|
||||
// this is most likely a broken file anyway
|
||||
if (_ysize > std::numeric_limits<int>::max() / _zsize)
|
||||
if (_ysize > std::numeric_limits<int>::max() / _zsize) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_numrows = _ysize * _zsize;
|
||||
|
||||
@ -351,13 +367,15 @@ bool SGIImage::readImage(QImage &img)
|
||||
_data = _dev->readAll();
|
||||
|
||||
// sanity check
|
||||
if (_rle)
|
||||
for (uint o = 0; o < _numrows; o++)
|
||||
if (_rle) {
|
||||
for (uint o = 0; o < _numrows; o++) {
|
||||
// don't change to greater-or-equal!
|
||||
if (_starttab[o] + _lengthtab[o] > (uint)_data.size()) {
|
||||
// qDebug() << "image corrupt (sanity check failed)";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!readData(img)) {
|
||||
// qDebug() << "image corrupt (incomplete scanline)";
|
||||
@ -378,7 +396,8 @@ void RLEData::write(QDataStream &s)
|
||||
|
||||
bool RLEData::operator<(const RLEData &b) const
|
||||
{
|
||||
uchar ac, bc;
|
||||
uchar ac;
|
||||
uchar bc;
|
||||
for (int i = 0; i < qMin(size(), b.size()); i++) {
|
||||
ac = at(i);
|
||||
bc = b[i];
|
||||
@ -424,8 +443,13 @@ uchar SGIImage::intensity(uchar c)
|
||||
|
||||
uint SGIImage::compact(uchar *d, uchar *s)
|
||||
{
|
||||
uchar *dest = d, *src = s, patt, *t, *end = s + _xsize;
|
||||
int i, n;
|
||||
uchar *dest = d;
|
||||
uchar *src = s;
|
||||
uchar patt;
|
||||
uchar *t;
|
||||
uchar *end = s + _xsize;
|
||||
int i;
|
||||
int n;
|
||||
while (src < end) {
|
||||
for (n = 0, t = src; t + 2 < end && !(*t == t[1] && *t == t[2]); t++) {
|
||||
n++;
|
||||
@ -468,7 +492,8 @@ bool SGIImage::scanData(const QImage &img)
|
||||
uchar *line = (uchar *)lineguard.data();
|
||||
uchar *buf = (uchar *)bufguard.data();
|
||||
const QRgb *c;
|
||||
unsigned x, y;
|
||||
unsigned x;
|
||||
unsigned y;
|
||||
uint len;
|
||||
|
||||
for (y = 0; y < _ysize; y++) {
|
||||
@ -594,7 +619,8 @@ void SGIImage::writeVerbatim(const QImage &img)
|
||||
writeHeader();
|
||||
|
||||
const QRgb *c;
|
||||
unsigned x, y;
|
||||
unsigned x;
|
||||
unsigned y;
|
||||
|
||||
for (y = 0; y < _ysize; y++) {
|
||||
c = reinterpret_cast<const QRgb *>(img.scanLine(_ysize - y - 1));
|
||||
@ -655,9 +681,16 @@ bool SGIImage::writeImage(const QImage &image)
|
||||
return false;
|
||||
}
|
||||
|
||||
const int w = img.width();
|
||||
const int h = img.height();
|
||||
|
||||
if (w > 65536 || h > 65536) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_bpc = 1;
|
||||
_xsize = img.width();
|
||||
_ysize = img.height();
|
||||
_xsize = w;
|
||||
_ysize = h;
|
||||
_pixmin = ~0u;
|
||||
_pixmax = 0;
|
||||
_colormap = NORMAL;
|
||||
@ -746,8 +779,7 @@ bool RGBHandler::canRead(QIODevice *device)
|
||||
|
||||
QImageIOPlugin::Capabilities RGBPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
if (format == "rgb" || format == "rgba" ||
|
||||
format == "bw" || format == "sgi") {
|
||||
if (format == "rgb" || format == "rgba" || format == "bw" || format == "sgi") {
|
||||
return Capabilities(CanRead | CanWrite);
|
||||
}
|
||||
if (!format.isEmpty()) {
|
||||
|
@ -33,4 +33,3 @@ public:
|
||||
};
|
||||
|
||||
#endif // KIMG_RGB_P_H
|
||||
|
||||
|
@ -8,8 +8,8 @@
|
||||
#ifndef KIMAGEFORMATS_RLE_P_H
|
||||
#define KIMAGEFORMATS_RLE_P_H
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
|
||||
/**
|
||||
* The RLEVariant to use.
|
||||
@ -24,6 +24,11 @@ enum class RLEVariant {
|
||||
* of size 2, 130 of size 3, up to 255 of size 128.
|
||||
*/
|
||||
PackBits,
|
||||
/**
|
||||
* Same as PackBits, but treat unpacked data as
|
||||
* 16-bit integers.
|
||||
*/
|
||||
PackBits16,
|
||||
/**
|
||||
* PIC-style RLE
|
||||
*
|
||||
@ -32,7 +37,7 @@ enum class RLEVariant {
|
||||
* of size 128, 130 of size 127, down to 255 of
|
||||
* size 2.
|
||||
*/
|
||||
PIC
|
||||
PIC,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -59,14 +64,11 @@ enum class RLEVariant {
|
||||
* into @p buf, @c false otherwise.
|
||||
*/
|
||||
template<typename Item, typename Func1, typename Func2>
|
||||
static inline bool decodeRLEData(RLEVariant variant,
|
||||
QDataStream &stream,
|
||||
Item *dest,
|
||||
quint32 length,
|
||||
Func1 readData,
|
||||
Func2 updateItem)
|
||||
static inline bool decodeRLEData(RLEVariant variant, QDataStream &stream, Item *dest, quint32 length, Func1 readData, Func2 updateItem)
|
||||
{
|
||||
unsigned offset = 0; // in dest
|
||||
bool is_msb = true; // only used for 16-bit PackBits, data is big-endian
|
||||
quint16 temp_data = 0;
|
||||
while (offset < length) {
|
||||
unsigned remaining = length - offset;
|
||||
quint8 count1;
|
||||
@ -85,7 +87,7 @@ static inline bool decodeRLEData(RLEVariant variant,
|
||||
// 2 to 128 repetitions
|
||||
length = count1 - 127u;
|
||||
}
|
||||
} else if (variant == RLEVariant::PackBits) {
|
||||
} else if (variant == RLEVariant::PackBits || variant == RLEVariant::PackBits16) {
|
||||
if (count1 == 128u) {
|
||||
// Ignore value 128
|
||||
continue;
|
||||
@ -102,8 +104,19 @@ static inline bool decodeRLEData(RLEVariant variant,
|
||||
}
|
||||
auto datum = readData(stream);
|
||||
for (unsigned i = offset; i < offset + length; ++i) {
|
||||
if (variant == RLEVariant::PackBits16) {
|
||||
if (is_msb) {
|
||||
temp_data = datum << 8;
|
||||
is_msb = false;
|
||||
} else {
|
||||
temp_data |= datum;
|
||||
dest[i >> 1] = updateItem(dest[i >> 1], temp_data);
|
||||
is_msb = true;
|
||||
}
|
||||
} else {
|
||||
dest[i] = updateItem(dest[i], datum);
|
||||
}
|
||||
}
|
||||
offset += length;
|
||||
} else {
|
||||
// No repetitions
|
||||
@ -114,8 +127,19 @@ static inline bool decodeRLEData(RLEVariant variant,
|
||||
}
|
||||
for (unsigned i = offset; i < offset + length; ++i) {
|
||||
auto datum = readData(stream);
|
||||
if (variant == RLEVariant::PackBits16) {
|
||||
if (is_msb) {
|
||||
temp_data = datum << 8;
|
||||
is_msb = false;
|
||||
} else {
|
||||
temp_data |= datum;
|
||||
dest[i >> 1] = updateItem(dest[i >> 1], temp_data);
|
||||
is_msb = true;
|
||||
}
|
||||
} else {
|
||||
dest[i] = updateItem(dest[i], datum);
|
||||
}
|
||||
}
|
||||
offset += length;
|
||||
}
|
||||
}
|
||||
@ -125,7 +149,6 @@ static inline bool decodeRLEData(RLEVariant variant,
|
||||
return stream.status() == QDataStream::Ok;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes data in run-length encoding format.
|
||||
*
|
||||
@ -141,18 +164,10 @@ static inline bool decodeRLEData(RLEVariant variant,
|
||||
* and writes the item to the data stream.
|
||||
*/
|
||||
template<typename Item, typename Func1, typename Func2>
|
||||
static inline void encodeRLEData(RLEVariant variant,
|
||||
QDataStream &stream,
|
||||
const Item *data,
|
||||
unsigned length,
|
||||
Func1 itemsEqual,
|
||||
Func2 writeItem)
|
||||
static inline void encodeRLEData(RLEVariant variant, QDataStream &stream, const Item *data, unsigned length, Func1 itemsEqual, Func2 writeItem)
|
||||
{
|
||||
unsigned offset = 0;
|
||||
const unsigned maxEncodableChunk =
|
||||
(variant == RLEVariant::PIC)
|
||||
? 65535u
|
||||
: 128;
|
||||
const unsigned maxEncodableChunk = (variant == RLEVariant::PIC) ? 65535u : 128;
|
||||
while (offset < length) {
|
||||
const Item *chunkStart = data + offset;
|
||||
unsigned maxChunk = qMin(length - offset, maxEncodableChunk);
|
||||
@ -191,10 +206,7 @@ static inline void encodeRLEData(RLEVariant variant,
|
||||
}
|
||||
chunkLength = 1;
|
||||
chunkEnd = chunkStart + 1;
|
||||
while (chunkLength < maxChunk &&
|
||||
(chunkLength + 1u == maxChunk ||
|
||||
!itemsEqual(*chunkEnd, *(chunkEnd+1))))
|
||||
{
|
||||
while (chunkLength < maxChunk && (chunkLength + 1u == maxChunk || !itemsEqual(*chunkEnd, *(chunkEnd + 1)))) {
|
||||
++chunkEnd;
|
||||
++chunkLength;
|
||||
}
|
||||
|
@ -20,9 +20,9 @@
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <QImage>
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
#include <QImage>
|
||||
|
||||
typedef quint32 uint;
|
||||
typedef quint16 ushort;
|
||||
@ -30,7 +30,6 @@ typedef quint8 uchar;
|
||||
|
||||
namespace // Private.
|
||||
{
|
||||
|
||||
// Header format of saved files.
|
||||
uchar targaMagic[12] = {0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
|
||||
@ -40,7 +39,7 @@ enum TGAType {
|
||||
TGA_TYPE_GREY = 3,
|
||||
TGA_TYPE_RLE_INDEXED = 9,
|
||||
TGA_TYPE_RLE_RGB = 10,
|
||||
TGA_TYPE_RLE_GREY = 11
|
||||
TGA_TYPE_RLE_GREY = 11,
|
||||
};
|
||||
|
||||
#define TGA_INTERLEAVE_MASK 0xc0
|
||||
@ -69,7 +68,9 @@ struct TgaHeader {
|
||||
uchar pixel_size;
|
||||
uchar flags;
|
||||
|
||||
enum { SIZE = 18 }; // const static int SIZE = 18;
|
||||
enum {
|
||||
SIZE = 18,
|
||||
}; // const static int SIZE = 18;
|
||||
};
|
||||
|
||||
static QDataStream &operator>>(QDataStream &s, TgaHeader &head)
|
||||
@ -88,30 +89,23 @@ static QDataStream &operator>> (QDataStream &s, TgaHeader &head)
|
||||
s >> head.flags;
|
||||
/*qDebug() << "id_length: " << head.id_length << " - colormap_type: " << head.colormap_type << " - image_type: " << head.image_type;
|
||||
qDebug() << "colormap_index: " << head.colormap_index << " - colormap_length: " << head.colormap_length << " - colormap_size: " << head.colormap_size;
|
||||
qDebug() << "x_origin: " << head.x_origin << " - y_origin: " << head.y_origin << " - width:" << head.width << " - height:" << head.height << " - pixelsize: " << head.pixel_size << " - flags: " << head.flags;*/
|
||||
qDebug() << "x_origin: " << head.x_origin << " - y_origin: " << head.y_origin << " - width:" << head.width << " - height:" << head.height << " - pixelsize:
|
||||
" << head.pixel_size << " - flags: " << head.flags;*/
|
||||
return s;
|
||||
}
|
||||
|
||||
static bool IsSupported(const TgaHeader &head)
|
||||
{
|
||||
if (head.image_type != TGA_TYPE_INDEXED &&
|
||||
head.image_type != TGA_TYPE_RGB &&
|
||||
head.image_type != TGA_TYPE_GREY &&
|
||||
head.image_type != TGA_TYPE_RLE_INDEXED &&
|
||||
head.image_type != TGA_TYPE_RLE_RGB &&
|
||||
head.image_type != TGA_TYPE_RLE_GREY) {
|
||||
if (head.image_type != TGA_TYPE_INDEXED && head.image_type != TGA_TYPE_RGB && head.image_type != TGA_TYPE_GREY && head.image_type != TGA_TYPE_RLE_INDEXED
|
||||
&& head.image_type != TGA_TYPE_RLE_RGB && head.image_type != TGA_TYPE_RLE_GREY) {
|
||||
return false;
|
||||
}
|
||||
if (head.image_type == TGA_TYPE_INDEXED ||
|
||||
head.image_type == TGA_TYPE_RLE_INDEXED) {
|
||||
if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) {
|
||||
if (head.colormap_length > 256 || head.colormap_size != 24 || head.colormap_type != 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (head.image_type == TGA_TYPE_RGB ||
|
||||
head.image_type == TGA_TYPE_GREY ||
|
||||
head.image_type == TGA_TYPE_RLE_RGB ||
|
||||
head.image_type == TGA_TYPE_RLE_GREY) {
|
||||
if (head.image_type == TGA_TYPE_RGB || head.image_type == TGA_TYPE_GREY || head.image_type == TGA_TYPE_RLE_RGB || head.image_type == TGA_TYPE_RLE_GREY) {
|
||||
if (head.colormap_type != 0) {
|
||||
return false;
|
||||
}
|
||||
@ -119,8 +113,7 @@ static bool IsSupported(const TgaHeader &head)
|
||||
if (head.width == 0 || head.height == 0) {
|
||||
return false;
|
||||
}
|
||||
if (head.pixel_size != 8 && head.pixel_size != 16 &&
|
||||
head.pixel_size != 24 && head.pixel_size != 32) {
|
||||
if (head.pixel_size != 8 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -138,7 +131,11 @@ struct TgaHeaderInfo {
|
||||
bool rgb;
|
||||
bool grey;
|
||||
|
||||
TgaHeaderInfo(const TgaHeader &tga) : rle(false), pal(false), rgb(false), grey(false)
|
||||
TgaHeaderInfo(const TgaHeader &tga)
|
||||
: rle(false)
|
||||
, pal(false)
|
||||
, rgb(false)
|
||||
, grey(false)
|
||||
{
|
||||
switch (tga.image_type) {
|
||||
case TGA_TYPE_RLE_INDEXED:
|
||||
@ -282,11 +279,11 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if ((uint)dataRead < count) {
|
||||
const size_t toCopy = count - dataRead;
|
||||
if (&dst[dataRead] + toCopy > imgEnd) {
|
||||
qWarning() << "Trying to write out of bounds!" << ptrdiff_t(image) << ptrdiff_t(&dst[dataRead]);;
|
||||
qWarning() << "Trying to write out of bounds!" << ptrdiff_t(image) << ptrdiff_t(&dst[dataRead]);
|
||||
;
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
@ -314,7 +311,9 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
|
||||
}
|
||||
|
||||
// Convert image to internal format.
|
||||
int y_start, y_step, y_end;
|
||||
int y_start;
|
||||
int y_step;
|
||||
int y_end;
|
||||
if (tga.flags & TGA_ORIGIN_UPPER) {
|
||||
y_start = 0;
|
||||
y_step = 1;
|
||||
@ -440,7 +439,7 @@ bool TGAHandler::write(const QImage &image)
|
||||
s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha)
|
||||
s << quint8(hasAlpha ? 0x24 : 0x20); // top left image (0x20) + 8 bit alpha (0x4)
|
||||
|
||||
for (int y = 0; y < img.height(); y++)
|
||||
for (int y = 0; y < img.height(); y++) {
|
||||
for (int x = 0; x < img.width(); x++) {
|
||||
const QRgb color = img.pixel(x, y);
|
||||
s << quint8(qBlue(color));
|
||||
@ -450,6 +449,7 @@ bool TGAHandler::write(const QImage &image)
|
||||
s << quint8(qAlpha(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -8,55 +8,23 @@
|
||||
#define FORMAT_ENUM_H
|
||||
|
||||
#include <QImage>
|
||||
|
||||
// Generated from QImage::Format enum
|
||||
static const char * qimage_format_enum_names[] = {
|
||||
"Invalid",
|
||||
"Mono",
|
||||
"MonoLSB",
|
||||
"Indexed8",
|
||||
"RGB32",
|
||||
"ARGB32",
|
||||
"ARGB32_Premultiplied",
|
||||
"RGB16",
|
||||
"ARGB8565_Premultiplied",
|
||||
"RGB666",
|
||||
"ARGB6666_Premultiplied",
|
||||
"RGB555",
|
||||
"ARGB8555_Premultiplied",
|
||||
"RGB888",
|
||||
"RGB444",
|
||||
"ARGB4444_Premultiplied",
|
||||
"RGBX8888",
|
||||
"RGBA8888",
|
||||
"RGBA8888_Premultiplied"
|
||||
};
|
||||
// Never claim there are more than QImage::NImageFormats supported formats.
|
||||
// This is future-proofing against the above list being extended.
|
||||
static const int qimage_format_enum_names_count =
|
||||
(sizeof(qimage_format_enum_names) / sizeof(*qimage_format_enum_names) > int(QImage::NImageFormats))
|
||||
? int(QImage::NImageFormats)
|
||||
: (sizeof(qimage_format_enum_names) / sizeof(*qimage_format_enum_names));
|
||||
#include <QMetaEnum>
|
||||
|
||||
QImage::Format formatFromString(const QString &str)
|
||||
{
|
||||
for (int i = 0; i < qimage_format_enum_names_count; ++i) {
|
||||
if (str.compare(QLatin1String(qimage_format_enum_names[i]), Qt::CaseInsensitive) == 0) {
|
||||
return (QImage::Format)(i);
|
||||
}
|
||||
}
|
||||
return QImage::Format_Invalid;
|
||||
const QMetaEnum metaEnum = QMetaEnum::fromType<QImage::Format>();
|
||||
const QString enumString = QStringLiteral("Format_") + str;
|
||||
|
||||
bool ok;
|
||||
const int res = metaEnum.keyToValue(enumString.toLatin1().constData(), &ok);
|
||||
|
||||
return ok ? static_cast<QImage::Format>(res) : QImage::Format_Invalid;
|
||||
}
|
||||
|
||||
QString formatToString(QImage::Format format)
|
||||
{
|
||||
int index = int(format);
|
||||
if (index > 0 && index < qimage_format_enum_names_count) {
|
||||
return QLatin1String(qimage_format_enum_names[index]);
|
||||
}
|
||||
return QLatin1String("<unknown:") +
|
||||
QString::number(index) +
|
||||
QLatin1String(">");
|
||||
const QMetaEnum metaEnum = QMetaEnum::fromType<QImage::Format>();
|
||||
return QString::fromLatin1(metaEnum.valueToKey(format)).remove(QStringLiteral("Format_"));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -27,21 +27,20 @@ int main(int argc, char **argv)
|
||||
parser.addVersionOption();
|
||||
parser.addPositionalArgument(QStringLiteral("in"), QStringLiteral("input image file"));
|
||||
parser.addPositionalArgument(QStringLiteral("out"), QStringLiteral("output image file"));
|
||||
QCommandLineOption informat(
|
||||
QStringList() << QStringLiteral("i") << QStringLiteral("informat"),
|
||||
QCommandLineOption informat(QStringList() << QStringLiteral("i") << QStringLiteral("informat"),
|
||||
QStringLiteral("Image format for input file"),
|
||||
QStringLiteral("format"));
|
||||
parser.addOption(informat);
|
||||
QCommandLineOption outformat(
|
||||
QStringList() << QStringLiteral("o") << QStringLiteral("outformat"),
|
||||
QCommandLineOption outformat(QStringList() << QStringLiteral("o") << QStringLiteral("outformat"),
|
||||
QStringLiteral("Image format for output file"),
|
||||
QStringLiteral("format"));
|
||||
parser.addOption(outformat);
|
||||
QCommandLineOption listformats(
|
||||
QStringList() << QStringLiteral("l") << QStringLiteral("list"),
|
||||
QStringLiteral("List supported image formats"));
|
||||
QCommandLineOption listformats(QStringList() << QStringLiteral("l") << QStringLiteral("list"), QStringLiteral("List supported image formats"));
|
||||
parser.addOption(listformats);
|
||||
|
||||
QCommandLineOption listmimes(QStringList() << QStringLiteral("m") << QStringLiteral("listmime"), QStringLiteral("List supported image mime formats"));
|
||||
parser.addOption(listmimes);
|
||||
|
||||
parser.process(app);
|
||||
|
||||
const QStringList files = parser.positionalArguments();
|
||||
@ -61,6 +60,21 @@ int main(int argc, char **argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (parser.isSet(listmimes)) {
|
||||
QTextStream out(stdout);
|
||||
out << "Input mime formats:\n";
|
||||
const auto lstReaderSupportedMimes = QImageReader::supportedMimeTypes();
|
||||
for (const QByteArray &fmt : lstReaderSupportedMimes) {
|
||||
out << " " << fmt << '\n';
|
||||
}
|
||||
out << "Output mime formats:\n";
|
||||
const auto lstWriterSupportedMimes = QImageWriter::supportedMimeTypes();
|
||||
for (const QByteArray &fmt : lstWriterSupportedMimes) {
|
||||
out << " " << fmt << '\n';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (files.count() != 2) {
|
||||
QTextStream(stdout) << "Must provide exactly two files\n";
|
||||
parser.showHelp(1);
|
||||
|
@ -10,10 +10,10 @@
|
||||
#include <QCommandLineParser>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QImageReader>
|
||||
#include <QFile>
|
||||
#include <QMetaObject>
|
||||
#include <QImageReader>
|
||||
#include <QMetaEnum>
|
||||
#include <QMetaObject>
|
||||
#include <QTextStream>
|
||||
|
||||
#include "format-enum.h"
|
||||
@ -31,22 +31,21 @@ int main(int argc, char **argv)
|
||||
parser.addVersionOption();
|
||||
parser.addPositionalArgument(QStringLiteral("image"), QStringLiteral("image file"));
|
||||
parser.addPositionalArgument(QStringLiteral("datafile"), QStringLiteral("file QImage data should be written to"));
|
||||
QCommandLineOption informat(
|
||||
QStringList() << QStringLiteral("f") << QStringLiteral("file-format"),
|
||||
QCommandLineOption informat(QStringList() << QStringLiteral("f") << QStringLiteral("file-format"),
|
||||
QStringLiteral("Image file format"),
|
||||
QStringLiteral("format"));
|
||||
parser.addOption(informat);
|
||||
QCommandLineOption qimgformat(
|
||||
QStringList() << QStringLiteral("q") << QStringLiteral("qimage-format"),
|
||||
QCommandLineOption qimgformat(QStringList() << QStringLiteral("q") << QStringLiteral("qimage-format"),
|
||||
QStringLiteral("QImage data format"),
|
||||
QStringLiteral("format"));
|
||||
parser.addOption(qimgformat);
|
||||
QCommandLineOption listformats(
|
||||
QStringList() << QStringLiteral("l") << QStringLiteral("list-file-formats"),
|
||||
QCommandLineOption listformats(QStringList() << QStringLiteral("l") << QStringLiteral("list-file-formats"),
|
||||
QStringLiteral("List supported image file formats"));
|
||||
parser.addOption(listformats);
|
||||
QCommandLineOption listqformats(
|
||||
QStringList() << QStringLiteral("p") << QStringLiteral("list-qimage-formats"),
|
||||
QCommandLineOption listmimetypes(QStringList() << QStringLiteral("m") << QStringLiteral("list-mime-types"),
|
||||
QStringLiteral("List supported image mime types"));
|
||||
parser.addOption(listmimetypes);
|
||||
QCommandLineOption listqformats(QStringList() << QStringLiteral("p") << QStringLiteral("list-qimage-formats"),
|
||||
QStringLiteral("List supported QImage data formats"));
|
||||
parser.addOption(listqformats);
|
||||
|
||||
@ -63,12 +62,21 @@ int main(int argc, char **argv)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (parser.isSet(listmimetypes)) {
|
||||
QTextStream out(stdout);
|
||||
out << "MIME types:\n";
|
||||
const auto lstSupportedMimeTypes = QImageReader::supportedMimeTypes();
|
||||
for (const auto &fmt : lstSupportedMimeTypes) {
|
||||
out << " " << fmt << '\n';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (parser.isSet(listqformats)) {
|
||||
QTextStream out(stdout);
|
||||
out << "QImage formats:\n";
|
||||
// skip QImage::Format_Invalid
|
||||
for (int i = 1; i < qimage_format_enum_names_count; ++i) {
|
||||
out << " " << qimage_format_enum_names[i] << '\n';
|
||||
for (int i = 1; i < QImage::NImageFormats; ++i) {
|
||||
out << " " << formatToString(static_cast<QImage::Format>(i)) << '\n';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@ -80,35 +88,29 @@ int main(int argc, char **argv)
|
||||
QImageReader reader(files.at(0), parser.value(informat).toLatin1());
|
||||
QImage img = reader.read();
|
||||
if (img.isNull()) {
|
||||
QTextStream(stderr) << "Could not read image: "
|
||||
<< reader.errorString() << '\n';
|
||||
QTextStream(stderr) << "Could not read image: " << reader.errorString() << '\n';
|
||||
return 2;
|
||||
}
|
||||
|
||||
QFile output(files.at(1));
|
||||
if (!output.open(QIODevice::WriteOnly)) {
|
||||
QTextStream(stderr) << "Could not open " << files.at(1)
|
||||
<< " for writing: "
|
||||
<< output.errorString() << '\n';
|
||||
QTextStream(stderr) << "Could not open " << files.at(1) << " for writing: " << output.errorString() << '\n';
|
||||
return 3;
|
||||
}
|
||||
if (parser.isSet(qimgformat)) {
|
||||
QImage::Format qformat = formatFromString(parser.value(qimgformat));
|
||||
if (qformat == QImage::Format_Invalid) {
|
||||
QTextStream(stderr) << "Unknown QImage data format "
|
||||
<< parser.value(qimgformat) << '\n';
|
||||
QTextStream(stderr) << "Unknown QImage data format " << parser.value(qimgformat) << '\n';
|
||||
return 4;
|
||||
}
|
||||
img = img.convertToFormat(qformat);
|
||||
}
|
||||
qint64 written = output.write(reinterpret_cast<const char *>(img.bits()), img.sizeInBytes());
|
||||
if (written != img.sizeInBytes()) {
|
||||
QTextStream(stderr) << "Could not write image data to " << files.at(1)
|
||||
<< ":" << output.errorString() << "\n";
|
||||
QTextStream(stderr) << "Could not write image data to " << files.at(1) << ":" << output.errorString() << "\n";
|
||||
return 5;
|
||||
}
|
||||
QTextStream(stdout) << "Created " << files.at(1) << " with data format "
|
||||
<< formatToString(img.format()) << "\n";
|
||||
QTextStream(stdout) << "Created " << files.at(1) << " with data format " << formatToString(img.format()) << "\n";
|
||||
|
||||
return 0;
|
||||
}
|
||||
|