Compare commits

...

42 Commits

Author SHA1 Message Date
2aec1d3926 GIT_SILENT Upgrade ECM and KF version requirements for 5.95.0 release. 2022-06-04 08:19:33 +00:00
2a84dd677d psd: Fix segfault on architectures where char is unsigned (like ARM) 2022-05-27 12:26:56 +03:00
ebcc34519c avif: prepare for breaking change in libavif 2022-05-02 11:46:37 +02:00
cff2604cf9 XCF: Support to QImageIOHandler::Size option 2022-04-29 13:23:20 +00:00
f8a251e268 Support to QImageIOHandler::Size option 2022-04-28 08:52:18 +02:00
52134fc2e9 QByteArray resize removal
- Removed QByteArray resize with potentially large numbers as in merge request !66
2022-04-14 23:04:58 +00:00
343954ca98 psd: Fix crash on broken files
Instead of resizing the bytearray to the potential size and then reading
into it, ask the device to read into a bytearray, this way instead of a
crash because we're trying to resize to a too big number we get a nice
  maxSize argument exceeds QByteArray size limit
warning

oss-fuzz/46664
2022-04-13 23:07:22 +02:00
44fd6b7bc0 psd: duotone read
- New format added: Duotone
- Fix float to int conversion round issue
2022-04-11 21:07:23 +00:00
c8a0806aab psd: Don't crash with broken images
Found by oss-fuzz but still with an unfiled bug number
2022-04-10 12:19:52 +02:00
bb475dedd1 psd: Header depth has to be 8 for CM_INDEXED color_mode
As suggested by Mirco Miranda
2022-04-07 23:50:15 +02:00
9e28aae868 psd: Protect against broken images
If you have an image that says it's Mono but has 16 as header.depth we
end up doing invalid memory accesses

oss-fuzz/46437
2022-04-07 21:46:08 +00:00
5c47a97b79 psd: Don't abort on broken images
oss-fuzz/46418
2022-04-06 22:58:31 +00:00
84d56d00cf avif: lossless support 2022-04-06 16:13:10 +00:00
384f78a13c psd: Don't assert on broken files
oss-fuzz/46407
2022-04-06 00:16:38 +02:00
72fc32aefc Add windows CI 2022-04-05 15:45:38 +02:00
98f19c60ae PSD: Performance improvements and support to missing common formats
- Supersedes merge request !55 (PSB support, XMP metadata, ICC color profile, image resolution read)
- Performance improvements: 5 time faster than previous version (tested on a 3.9GB PSB: 9sec instead 47sec)
- New formats support added: INDEXED (8bps), BITMAP (1bps), GRAYSCALE (8, 16, 32bps), RGB (32bps)
- Should fix Bug https://bugs.kde.org/show_bug.cgi?id=397610
- Fix Bug https://bugs.kde.org/show_bug.cgi?id=428238
2022-04-04 17:22:45 +00:00
ae6b724824 GIT_SILENT Upgrade ECM and KF version requirements for 5.93.0 release. 2022-04-02 10:00:12 +00:00
3e751dd80d Fix XCF parasites metadata in QImage and support to ICC profile
- Fix parasite "gimp-comment" not set due to null QImage
- Support to parasite "icc-profile" using Qt 5.14+ API
- Added parasite "gimp-image-metadata" as QImage metadata "XML:org.gimp.xml"
- Added a XCF with XML metadata and icc prifile embedded in autotest folder (generated by GIMP 2.10.30)
- Tested with Qt 5.15.2 and Qt 6.2.3 under Windows and Qt 6.2.3 under macOS
2022-03-23 23:34:33 +00:00
e69dff73e6 avif: encoder speed 7->6 2022-03-10 09:44:50 +01:00
64cfe52bee avif: fix jumpToImage 2022-03-10 09:39:53 +01:00
8732fc8487 avif: warn about non-recommended libavif configuration 2022-03-10 09:35:08 +01:00
d9729b7190 GIT_SILENT Upgrade ECM and KF version requirements for 5.92.0 release. 2022-03-05 11:15:00 +00:00
55d3c568b2 Add Qt6 Android CI 2022-03-01 16:04:48 +00:00
4afafee6c1 Add write tests for heif/avif/jxl
Unfortunately none of them pass since it seems they can't load a png,
save it to their format with loseless quality and read it back and get
exactly the same contents than the png
2022-02-18 00:05:01 +01:00
f04084e175 jxl: encoding improvements
Plug-in can save in 8bit depth now,
previously only 16bit was supported.
Memory and dimension limits were adjusted.
2022-02-16 10:26:56 +01:00
9911c9c2ea avif: adjust dimension and memory limits
With or height can be above 32k now (up to 64k), but
image should not have more than 256megapixels
(Default memory limit of libavif)
2022-02-11 16:01:07 +01:00
4ceef5164d GIT_SILENT Upgrade ECM and KF version requirements for 5.91.0 release. 2022-02-05 15:14:16 +00:00
3d2d91a08a Fix typo, should be qCWarning
GIT_SILENT
2022-02-03 16:34:06 +02:00
0a02458560 Check executables exist in PATH before passing them to QProcess
See:
https://kde.org/info/security/advisory-20220131-1.txt
https://mail.kde.org/pipermail/kde-devel/2022-January/000943.html
2022-02-03 11:32:24 +02:00
96836e849f Fix handling of null terminated ANI metadata with Qt6
In Qt5 converting a QByteArray to a QString stops at the first null byte,
in Qt6 the QByteArray size is respected, and trailing null bytes are
therefore included in the final QString. Explicitly determine the length
of the string data to deal with that.
2022-01-22 22:03:21 +01:00
f4edb7296f Add CI qt6 support 2022-01-08 09:27:14 +01:00
56376ffd66 GIT_SILENT Upgrade ECM and KF version requirements for 5.90.0 release. 2022-01-01 12:16:00 +00:00
f534254063 GIT_SILENT: It compiles fine without deprecated methods 2021-12-26 11:35:54 +01:00
32347725cb Fix typo 2021-12-21 20:28:05 +01:00
56e762c563 Make it compile against qt6 2021-12-21 20:10:11 +01:00
81603ed908 GIT_SILENT Upgrade ECM and KF version requirements for 5.89.0 release. 2021-12-04 17:00:59 +00:00
957c199c68 GIT_SILENT: add missing licences 2021-12-02 07:14:54 +01:00
f445e5dd0a avif: limit scope of variables 2021-11-29 17:35:51 +01:00
d3da56bba3 Add JXL to the list of supported formats 2021-11-17 20:06:11 +01:00
41c4b5930c Add plugin for JPEG XL (JXL) 2021-11-10 16:01:58 +01:00
fb66044714 Add FreeBSD CI 2021-10-07 00:28:35 +02:00
a43394a759 Add Android CI 2021-10-07 00:04:08 +02:00
52 changed files with 2314 additions and 263 deletions

View File

@ -3,3 +3,8 @@
include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml

View File

@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16)
project(KImageFormats)
include(FeatureSummary)
find_package(ECM 5.87.0 NO_MODULE)
find_package(ECM 5.95.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)
@ -17,9 +17,10 @@ include(KDEGitCommitHooks)
include(CheckIncludeFiles)
include(FindPkgConfig)
set(REQUIRED_QT_VERSION 5.15.2)
find_package(Qt5Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
find_package(Qt${QT_MAJOR_VERSION}Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
find_package(KF5Archive)
set_package_properties(KF5Archive PROPERTIES
@ -31,12 +32,12 @@ set_package_properties(KF5Archive PROPERTIES
# this available in PATH
set(BUILD_EPS_PLUGIN FALSE)
if (UNIX)
find_package(Qt5PrintSupport ${REQUIRED_QT_VERSION} NO_MODULE)
set_package_properties(Qt5PrintSupport PROPERTIES
find_package(Qt${QT_MAJOR_VERSION}PrintSupport ${REQUIRED_QT_VERSION} NO_MODULE)
set_package_properties(Qt${QT_MAJOR_VERSION}PrintSupport PROPERTIES
PURPOSE "Required for the QImage plugin for EPS images"
TYPE OPTIONAL
)
if (Qt5PrintSupport_FOUND)
if (TARGET Qt${QT_MAJOR_VERSION}::PrintSupport)
set(BUILD_EPS_PLUGIN TRUE)
endif()
endif()
@ -58,15 +59,19 @@ set_package_properties(libavif PROPERTIES
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
option(KIMAGEFORMATS_JXL "Enable plugin for JPEG XL format" ON)
if(KIMAGEFORMATS_JXL)
pkg_check_modules(LibJXL IMPORTED_TARGET libjxl>=0.6.1)
pkg_check_modules(LibJXLThreads IMPORTED_TARGET libjxl_threads>=0.6.1)
endif()
add_feature_info(LibJXL LibJXL_FOUND "required for the QImage plugin for JPEG XL images")
add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f02)
add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x055100)
add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x055900)
add_subdirectory(src)
if (BUILD_TESTING)
add_subdirectory(autotests)

121
LICENSES/CC0-1.0.txt Normal file
View File

@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

View File

@ -16,13 +16,14 @@ The following image formats have read-only support:
- Animated Windows cursors (ani)
- Gimp (xcf)
- OpenEXR (exr)
- Photoshop documents (psd)
- Photoshop documents (psd, psb, pdd, psdt)
- Sun Raster (ras)
The following image formats have read and write support:
- AV1 Image File Format (AVIF)
- Encapsulated PostScript (eps)
- JPEG XL (jxl)
- Personal Computer Exchange (pcx)
- SGI images (rgb, rgba, sgi, bw)
- Softimage PIC (pic)

View File

@ -15,7 +15,7 @@ macro(kimageformats_read_tests)
if (NOT TARGET readtest)
add_executable(readtest readtest.cpp)
target_link_libraries(readtest Qt5::Gui)
target_link_libraries(readtest Qt${QT_MAJOR_VERSION}::Gui)
target_compile_definitions(readtest
PRIVATE IMAGEDIR="${CMAKE_CURRENT_SOURCE_DIR}/read")
ecm_mark_as_test(readtest)
@ -30,23 +30,35 @@ macro(kimageformats_read_tests)
endmacro()
macro(kimageformats_write_tests)
cmake_parse_arguments(KIF_RT "" "FUZZ" "" ${ARGN})
set(_fuzzarg)
if (KIF_RT_FUZZ)
set(_fuzzarg -f ${KIF_RT_FUZZ})
endif()
if (NOT TARGET writetest)
add_executable(writetest writetest.cpp)
target_link_libraries(writetest Qt5::Gui)
target_link_libraries(writetest Qt${QT_MAJOR_VERSION}::Gui)
target_compile_definitions(writetest
PRIVATE IMAGEDIR="${CMAKE_CURRENT_SOURCE_DIR}/write")
ecm_mark_as_test(writetest)
endif()
foreach(_testname ${ARGN})
foreach(_testname ${KIF_RT_UNPARSED_ARGUMENTS})
string(REGEX MATCH "-lossless$" _is_lossless "${_testname}")
string(REGEX MATCH "-nodatacheck" _is_no_data_check "${_testname}")
unset(lossless_arg)
unset(no_data_check_arg)
if (_is_lossless)
set(lossless_arg "--lossless")
string(REGEX REPLACE "-lossless$" "" _testname "${_testname}")
endif()
if (_is_no_data_check)
set(no_data_check_arg "--no-data-check")
string(REGEX REPLACE "-nodatacheck$" "" _testname "${_testname}")
endif()
add_test(
NAME kimageformats-write-${_testname}
COMMAND writetest ${lossless_arg} ${_testname}
COMMAND writetest ${lossless_arg} ${no_data_check_arg} ${_fuzzarg} ${_testname}
)
endforeach(_testname)
endmacro()
@ -74,12 +86,28 @@ if (TARGET avif)
kimageformats_read_tests(
avif
)
kimageformats_write_tests(
avif-nodatacheck-lossless
)
endif()
if (LibHeif_FOUND)
kimageformats_read_tests(
heif
)
# because the plug-ins use RGB->YUV conversion which sometimes results in 1 value difference.
kimageformats_write_tests(FUZZ 1
heif-nodatacheck-lossless
)
endif()
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
kimageformats_read_tests(
jxl
)
kimageformats_write_tests(
jxl-nodatacheck-lossless
)
endif()
# Allow some fuzziness when reading this formats, to allow for
@ -111,19 +139,19 @@ if (OpenEXR_FOUND)
# FIXME: OpenEXR tests
endif()
find_package(Qt5Test ${REQUIRED_QT_VERSION} CONFIG QUIET)
find_package(Qt${QT_MAJOR_VERSION}Test ${REQUIRED_QT_VERSION} CONFIG QUIET)
if(NOT Qt5Test_FOUND)
message(STATUS "Qt5Test not found, some autotests will not be built.")
if(NOT TARGET Qt${QT_MAJOR_VERSION}::Test)
message(STATUS "Qt${QT_MAJOR_VERSION}Test not found, some autotests will not be built.")
return()
endif()
add_executable(pictest pictest.cpp)
target_link_libraries(pictest Qt5::Gui Qt5::Test)
target_link_libraries(pictest Qt${QT_MAJOR_VERSION}::Gui Qt${QT_MAJOR_VERSION}::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)
target_link_libraries(anitest Qt${QT_MAJOR_VERSION}::Gui Qt${QT_MAJOR_VERSION}::Test)
ecm_mark_as_test(anitest)
add_test(NAME kimageformats-ani COMMAND anitest)

37
autotests/fuzzyeq.cpp Normal file
View File

@ -0,0 +1,37 @@
/*
SPDX-FileCopyrightText: 2014 Alex Merry <alex.merry@kdemail.net>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
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)
{
return (im1.depth() == 64) ? fuzzyeq<quint16>(im1, im2, fuzziness) : fuzzyeq<quint8>(im1, im2, fuzziness);
}

BIN
autotests/read/jxl/rgb.jxl Normal file

Binary file not shown.

BIN
autotests/read/jxl/rgb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

BIN
autotests/read/jxl/rgba.jxl Normal file

Binary file not shown.

BIN
autotests/read/jxl/rgba.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

View File

@ -16,6 +16,8 @@
#include "../tests/format-enum.h"
#include "fuzzyeq.cpp"
static void writeImageData(const char *name, const QString &filename, const QImage &image)
{
QFile file(filename);
@ -31,38 +33,6 @@ static void writeImageData(const char *name, const QString &filename, const QIma
}
}
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)
{
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.

View File

@ -16,6 +16,8 @@
#include <QImageWriter>
#include <QTextStream>
#include "fuzzyeq.cpp"
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
@ -31,7 +33,13 @@ int main(int argc, char **argv)
parser.addPositionalArgument(QStringLiteral("format"), QStringLiteral("format to test."));
QCommandLineOption lossless(QStringList() << QStringLiteral("l") << QStringLiteral("lossless"),
QStringLiteral("Check that reading back the data gives the same image."));
QCommandLineOption ignoreDataCheck({QStringLiteral("no-data-check")}, QStringLiteral("Don't check that write data is exactly the same."));
QCommandLineOption fuzz(QStringList() << QStringLiteral("f") << QStringLiteral("fuzz"),
QStringLiteral("Allow for some deviation in ARGB data."),
QStringLiteral("max"));
parser.addOption(lossless);
parser.addOption(ignoreDataCheck);
parser.addOption(fuzz);
parser.process(app);
@ -44,11 +52,26 @@ int main(int argc, char **argv)
parser.showHelp(1);
}
uchar fuzziness = 0;
if (parser.isSet(fuzz)) {
bool ok;
uint fuzzarg = parser.value(fuzz).toUInt(&ok);
if (!ok || fuzzarg > 255) {
QTextStream(stderr) << "Error: max fuzz argument must be a number between 0 and 255\n";
parser.showHelp(1);
}
fuzziness = uchar(fuzzarg);
}
QString suffix = args.at(0);
QByteArray format = suffix.toLatin1();
QDir imgdir(QStringLiteral(IMAGEDIR));
imgdir.setNameFilters(QStringList(QLatin1String("*.") + suffix));
if (parser.isSet(ignoreDataCheck)) {
imgdir.setNameFilters({QLatin1String("*.png")});
} else {
imgdir.setNameFilters(QStringList(QLatin1String("*.") + suffix));
}
imgdir.setFilter(QDir::Files);
int passed = 0;
@ -58,8 +81,13 @@ int main(int argc, char **argv)
<< "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();
QString pngfile = fi.filePath().replace(suffixPos, suffix.count(), QStringLiteral("png"));
QString pngfile;
if (parser.isSet(ignoreDataCheck)) {
pngfile = fi.filePath();
} else {
int suffixPos = fi.filePath().count() - suffix.count();
pngfile = fi.filePath().replace(suffixPos, suffix.count(), QStringLiteral("png"));
}
QString pngfilename = QFileInfo(pngfile).fileName();
QImageReader pngReader(pngfile, "png");
@ -70,29 +98,13 @@ int main(int argc, char **argv)
continue;
}
QFile expFile(fi.filePath());
if (!expFile.open(QIODevice::ReadOnly)) {
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not open " << fi.fileName() << ": " << expFile.errorString() << "\n";
++failed;
continue;
}
QByteArray expData = expFile.readAll();
if (expData.isEmpty()) {
// check if there was actually anything to read
expFile.reset();
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";
++failed;
continue;
}
}
QByteArray writtenData;
{
QBuffer buffer(&writtenData);
QImageWriter imgWriter(&buffer, format.constData());
if (parser.isSet(lossless)) {
imgWriter.setQuality(100);
}
if (!imgWriter.write(pngImage)) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed to write image data\n";
++failed;
@ -100,10 +112,31 @@ int main(int argc, char **argv)
}
}
if (expData != writtenData) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": written data differs from " << fi.fileName() << "\n";
++failed;
continue;
if (!parser.isSet(ignoreDataCheck)) {
QFile expFile(fi.filePath());
if (!expFile.open(QIODevice::ReadOnly)) {
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not open " << fi.fileName() << ": " << expFile.errorString() << "\n";
++failed;
continue;
}
QByteArray expData = expFile.readAll();
if (expData.isEmpty()) {
// check if there was actually anything to read
expFile.reset();
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";
++failed;
continue;
}
}
if (expData != writtenData) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": written data differs from " << fi.fileName() << "\n";
++failed;
continue;
}
}
QImage reReadImage;
@ -119,8 +152,18 @@ int main(int argc, char **argv)
}
if (parser.isSet(lossless)) {
if (pngImage != reReadImage) {
if (!fuzzyeq(pngImage, reReadImage, fuzziness)) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": re-reading the data resulted in a different image\n";
if (pngImage.size() == reReadImage.size()) {
for (int i = 0; i < pngImage.width(); ++i) {
for (int j = 0; j < pngImage.height(); ++j) {
if (pngImage.pixel(i, j) != reReadImage.pixel(i, j)) {
QTextStream(stdout) << "Pixel is different " << i << ',' << j << ' ' << pngImage.pixel(i, j) << ' ' << reReadImage.pixel(i, j)
<< '\n';
}
}
}
}
++failed;
continue;
}

View File

@ -12,41 +12,41 @@ function(kimageformats_add_plugin plugin)
add_library(${plugin} MODULE ${KIF_ADD_PLUGIN_SOURCES})
set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/imageformats")
target_link_libraries(${plugin} Qt5::Gui)
target_link_libraries(${plugin} Qt${QT_MAJOR_VERSION}::Gui)
install(TARGETS ${plugin} DESTINATION ${KDE_INSTALL_QTPLUGINDIR}/imageformats)
endfunction()
##################################
kimageformats_add_plugin(kimg_ani SOURCES ani.cpp)
install(FILES ani.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES ani.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/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/)
install(FILES avif.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()
##################################
install(FILES dds-qt.desktop RENAME dds.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES dds-qt.desktop RENAME dds.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
if (BUILD_EPS_PLUGIN)
if (Qt5PrintSupport_FOUND)
if (TARGET Qt${QT_MAJOR_VERSION}::PrintSupport)
kimageformats_add_plugin(kimg_eps SOURCES eps.cpp)
target_link_libraries(kimg_eps Qt5::PrintSupport)
install(FILES eps.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
target_link_libraries(kimg_eps Qt${QT_MAJOR_VERSION}::PrintSupport)
install(FILES eps.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()
endif()
##################################
# need this for Qt's version of the plugin
install(FILES jp2.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES jp2.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
@ -64,13 +64,13 @@ if(OpenEXR_FOUND)
endif()
kde_target_enable_exceptions(kimg_exr PRIVATE)
install(FILES exr.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES exr.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()
##################################
kimageformats_add_plugin(kimg_hdr SOURCES hdr.cpp)
install(FILES hdr.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES hdr.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
@ -78,43 +78,51 @@ 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/)
install(FILES heif.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()
##################################
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp)
target_link_libraries(kimg_jxl PkgConfig::LibJXL PkgConfig::LibJXLThreads)
install(FILES jxl.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()
##################################
kimageformats_add_plugin(kimg_pcx SOURCES pcx.cpp)
install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_pic SOURCES pic.cpp)
install(FILES pic.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES pic.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_psd SOURCES psd.cpp)
install(FILES psd.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES psd.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_ras SOURCES ras.cpp)
install(FILES ras.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES ras.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_rgb SOURCES rgb.cpp)
install(FILES rgb.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES rgb.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_tga SOURCES tga.cpp)
install(FILES tga.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES tga.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_xcf SOURCES xcf.cpp)
install(FILES xcf.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES xcf.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
@ -122,10 +130,10 @@ if (KF5Archive_FOUND)
kimageformats_add_plugin(kimg_kra SOURCES kra.cpp)
target_link_libraries(kimg_kra KF5::Archive)
install(FILES kra.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES kra.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
kimageformats_add_plugin(kimg_ora SOURCES ora.cpp)
target_link_libraries(kimg_ora KF5::Archive)
install(FILES ora.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES ora.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()

View File

@ -12,6 +12,8 @@
#include <QVariant>
#include <QtEndian>
#include <cstring>
namespace
{
struct ChunkHeader {
@ -419,7 +421,7 @@ bool ANIHandler::ensureScanned() const
}
// FIXME encoding
const QString stringValue = QString::fromLocal8Bit(value);
const QString stringValue = QString::fromLocal8Bit(value.constData(), std::strlen(value.constData()));
if (chunkId == "INAM") {
mutableThis->m_name = stringValue;
} else if (chunkId == "IART") {

View File

@ -133,7 +133,7 @@ bool QAVIFHandler::ensureDecoder()
m_container_width = m_decoder->image->width;
m_container_height = m_decoder->image->height;
if ((m_container_width > 32768) || (m_container_height > 32768)) {
if ((m_container_width > 65535) || (m_container_height > 65535)) {
qWarning("AVIF image (%dx%d) is too large!", m_container_width, m_container_height);
m_parseState = ParseAvifError;
return false;
@ -145,6 +145,12 @@ bool QAVIFHandler::ensureDecoder()
return false;
}
if (m_container_width > ((16384 * 16384) / m_container_height)) {
qWarning("AVIF image (%dx%d) has more than 256 megapixels!", m_container_width, m_container_height);
m_parseState = ParseAvifError;
return false;
}
m_parseState = ParseAvifSuccess;
if (decode_one_frame()) {
return true;
@ -318,27 +324,27 @@ bool QAVIFHandler::decode_one_frame()
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);
int 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);
int 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;
int 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;
int 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)) {
@ -417,15 +423,44 @@ bool QAVIFHandler::read(QImage *image)
bool QAVIFHandler::write(const QImage &image)
{
if (image.format() == QImage::Format_Invalid) {
qWarning("No image data to save");
qWarning("No image data to save!");
return false;
}
if ((image.width() > 32768) || (image.height() > 32768)) {
qWarning("Image is too large");
if ((image.width() > 0) && (image.height() > 0)) {
if ((image.width() > 65535) || (image.height() > 65535)) {
qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
return false;
}
if (image.width() > ((16384 * 16384) / image.height())) {
qWarning("Image (%dx%d) will not be saved because it has more than 256 megapixels!", image.width(), image.height());
return false;
}
if ((image.width() > 32768) || (image.height() > 32768)) {
qWarning("Image (%dx%d) has a dimension above 32768 pixels, saved AVIF may not work in other software!", image.width(), image.height());
}
} else {
qWarning("Image has zero dimension!");
return false;
}
const char *encoder_name = avifCodecName(AVIF_CODEC_CHOICE_AUTO, AVIF_CODEC_FLAG_CAN_ENCODE);
if (!encoder_name) {
qWarning("Cannot save AVIF images because libavif was built without AV1 encoders!");
return false;
}
bool lossless = false;
if (m_quality >= 100) {
if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE)) {
lossless = true;
} else {
qWarning("You are using %s encoder. It is recommended to enable libAOM encoder in libavif to use lossless compression.", encoder_name);
}
}
int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100;
int minQuantizer = 0;
int maxQuantizerAlpha = 0;
@ -618,43 +653,47 @@ bool QAVIFHandler::write(const QImage &image)
// 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 (lossless) {
iccprofile = tmpcolorimage.colorSpace().iccProfile();
} else {
// 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
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));
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;
break;
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 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
@ -664,6 +703,9 @@ bool QAVIFHandler::write(const QImage &image)
}
}
if (lossless && pixel_format == AVIF_PIXEL_FORMAT_YUV444) {
matrix_to_save = (avifMatrixCoefficients)0;
}
avif = avifImageCreate(tmpcolorimage.width(), tmpcolorimage.height(), save_depth, pixel_format);
avif->matrixCoefficients = matrix_to_save;
@ -682,9 +724,7 @@ bool QAVIFHandler::write(const QImage &image)
if (save_depth > 8) { // 10bit depth
rgb.depth = 16;
if (tmpcolorimage.hasAlphaChannel()) {
avif->alphaRange = AVIF_RANGE_FULL;
} else {
if (!tmpcolorimage.hasAlphaChannel()) {
rgb.ignoreAlpha = AVIF_TRUE;
}
@ -694,7 +734,6 @@ bool QAVIFHandler::write(const QImage &image)
if (tmpcolorimage.hasAlphaChannel()) {
rgb.format = AVIF_RGB_FORMAT_RGBA;
avif->alphaRange = AVIF_RANGE_FULL;
} else {
rgb.format = AVIF_RGB_FORMAT_RGB;
}
@ -718,7 +757,7 @@ bool QAVIFHandler::write(const QImage &image)
encoder->maxQuantizerAlpha = maxQuantizerAlpha;
}
encoder->speed = 7;
encoder->speed = 6;
res = avifEncoderWrite(encoder, avif, &raw);
avifEncoderDestroy(encoder);
@ -871,7 +910,8 @@ bool QAVIFHandler::jumpToImage(int imageNumber)
return false;
}
if (imageNumber == m_decoder->imageCount) { // we are here already
if (imageNumber == m_decoder->imageIndex) { // we are here already
m_must_jump_to_next_image = false;
return true;
}

View File

@ -15,6 +15,7 @@
#include <QPainter>
#include <QPrinter>
#include <QProcess>
#include <QStandardPaths>
#include <QTemporaryFile>
// logging category for this framework, default: log stuff >= warning
@ -176,6 +177,12 @@ bool EPSHandler::read(QImage *image)
// create GS command line
const QString gsExec = QStandardPaths::findExecutable(QStringLiteral("gs"));
if (gsExec.isEmpty()) {
qCWarning(EPSPLUGIN) << "Couldn't find gs exectuable (from GhostScript) in PATH.";
return false;
}
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")
@ -192,7 +199,7 @@ bool EPSHandler::read(QImage *image)
QProcess converter;
converter.setProcessChannelMode(QProcess::ForwardedErrorChannel);
converter.start(QStringLiteral("gs"), gsArgs);
converter.start(gsExec, gsArgs);
if (!converter.waitForStarted(3000)) {
qCWarning(EPSPLUGIN) << "Reading EPS files requires gs (from GhostScript)";
return false;

977
src/imageformats/jxl.cpp Normal file
View File

@ -0,0 +1,977 @@
/*
JPEG XL (JXL) support for QImage.
SPDX-FileCopyrightText: 2021 Daniel Novomesky <dnovomesky@gmail.com>
SPDX-License-Identifier: BSD-2-Clause
*/
#include <QThread>
#include <QtGlobal>
#include "jxl_p.h"
#include <jxl/encode.h>
#include <jxl/thread_parallel_runner.h>
#include <string.h>
QJpegXLHandler::QJpegXLHandler()
: m_parseState(ParseJpegXLNotParsed)
, m_quality(90)
, m_currentimage_index(0)
, m_previousimage_index(-1)
, m_decoder(nullptr)
, m_runner(nullptr)
, m_next_image_delay(0)
, m_input_image_format(QImage::Format_Invalid)
, m_target_image_format(QImage::Format_Invalid)
, m_buffer_size(0)
{
}
QJpegXLHandler::~QJpegXLHandler()
{
if (m_runner) {
JxlThreadParallelRunnerDestroy(m_runner);
}
if (m_decoder) {
JxlDecoderDestroy(m_decoder);
}
}
bool QJpegXLHandler::canRead() const
{
if (m_parseState == ParseJpegXLNotParsed && !canRead(device())) {
return false;
}
if (m_parseState != ParseJpegXLError) {
setFormat("jxl");
return true;
}
return false;
}
bool QJpegXLHandler::canRead(QIODevice *device)
{
if (!device) {
return false;
}
QByteArray header = device->peek(32);
if (header.size() < 12) {
return false;
}
JxlSignature signature = JxlSignatureCheck((const uint8_t *)header.constData(), header.size());
if (signature == JXL_SIG_CODESTREAM || signature == JXL_SIG_CONTAINER) {
return true;
}
return false;
}
bool QJpegXLHandler::ensureParsed() const
{
if (m_parseState == ParseJpegXLSuccess || m_parseState == ParseJpegXLBasicInfoParsed) {
return true;
}
if (m_parseState == ParseJpegXLError) {
return false;
}
QJpegXLHandler *that = const_cast<QJpegXLHandler *>(this);
return that->ensureDecoder();
}
bool QJpegXLHandler::ensureALLCounted() const
{
if (!ensureParsed()) {
return false;
}
if (m_parseState == ParseJpegXLSuccess) {
return true;
}
QJpegXLHandler *that = const_cast<QJpegXLHandler *>(this);
return that->countALLFrames();
}
bool QJpegXLHandler::ensureDecoder()
{
if (m_decoder) {
return true;
}
m_rawData = device()->readAll();
if (m_rawData.isEmpty()) {
return false;
}
JxlSignature signature = JxlSignatureCheck((const uint8_t *)m_rawData.constData(), m_rawData.size());
if (signature != JXL_SIG_CODESTREAM && signature != JXL_SIG_CONTAINER) {
m_parseState = ParseJpegXLError;
return false;
}
m_decoder = JxlDecoderCreate(nullptr);
if (!m_decoder) {
qWarning("ERROR: JxlDecoderCreate failed");
m_parseState = ParseJpegXLError;
return false;
}
int num_worker_threads = QThread::idealThreadCount();
if (!m_runner && num_worker_threads >= 4) {
/* use half of the threads because plug-in is usually used in environment
* where application performs another tasks in backround (pre-load other images) */
num_worker_threads = num_worker_threads / 2;
num_worker_threads = qBound(2, num_worker_threads, 64);
m_runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) {
qWarning("ERROR: JxlDecoderSetParallelRunner failed");
m_parseState = ParseJpegXLError;
return false;
}
}
if (JxlDecoderSetInput(m_decoder, (const uint8_t *)m_rawData.constData(), m_rawData.size()) != JXL_DEC_SUCCESS) {
qWarning("ERROR: JxlDecoderSetInput failed");
m_parseState = ParseJpegXLError;
return false;
}
JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME);
if (status == JXL_DEC_ERROR) {
qWarning("ERROR: JxlDecoderSubscribeEvents failed");
m_parseState = ParseJpegXLError;
return false;
}
status = JxlDecoderProcessInput(m_decoder);
if (status == JXL_DEC_ERROR) {
qWarning("ERROR: JXL decoding failed");
m_parseState = ParseJpegXLError;
return false;
}
if (status == JXL_DEC_NEED_MORE_INPUT) {
qWarning("ERROR: JXL data incomplete");
m_parseState = ParseJpegXLError;
return false;
}
status = JxlDecoderGetBasicInfo(m_decoder, &m_basicinfo);
if (status != JXL_DEC_SUCCESS) {
qWarning("ERROR: JXL basic info not available");
m_parseState = ParseJpegXLError;
return false;
}
if (m_basicinfo.xsize == 0 || m_basicinfo.ysize == 0) {
qWarning("ERROR: JXL image has zero dimensions");
m_parseState = ParseJpegXLError;
return false;
}
if (m_basicinfo.xsize > 65535 || m_basicinfo.ysize > 65535) {
qWarning("JXL image (%dx%d) is too large", m_basicinfo.xsize, m_basicinfo.ysize);
m_parseState = ParseJpegXLError;
return false;
}
if (sizeof(void *) <= 4) {
/* On 32bit systems, there is limited address space.
* We skip imagess bigger than 8192 x 8192 pixels.
* If we don't do it, abort() in libjxl may close whole application */
if (m_basicinfo.xsize > ((8192 * 8192) / m_basicinfo.ysize)) {
qWarning("JXL image (%dx%d) is too large for 32bit build of the plug-in", m_basicinfo.xsize, m_basicinfo.ysize);
m_parseState = ParseJpegXLError;
return false;
}
} else {
/* On 64bit systems
* We skip images bigger than 16384 x 16384 pixels.
* It is an artificial limit not to use extreme amount of memory */
if (m_basicinfo.xsize > ((16384 * 16384) / m_basicinfo.ysize)) {
qWarning("JXL image (%dx%d) is bigger than security limit 256 megapixels", m_basicinfo.xsize, m_basicinfo.ysize);
m_parseState = ParseJpegXLError;
return false;
}
}
m_parseState = ParseJpegXLBasicInfoParsed;
return true;
}
bool QJpegXLHandler::countALLFrames()
{
if (m_parseState != ParseJpegXLBasicInfoParsed) {
return false;
}
JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
if (status != JXL_DEC_COLOR_ENCODING) {
qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status);
m_parseState = ParseJpegXLError;
return false;
}
JxlColorEncoding color_encoding;
if (m_basicinfo.uses_original_profile == JXL_FALSE) {
JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE);
JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
}
bool loadalpha;
if (m_basicinfo.alpha_bits > 0) {
loadalpha = true;
} else {
loadalpha = false;
}
m_input_pixel_format.endianness = JXL_NATIVE_ENDIAN;
m_input_pixel_format.align = 0;
m_input_pixel_format.num_channels = 4;
if (m_basicinfo.bits_per_sample > 8) { // high bit depth
m_input_pixel_format.data_type = JXL_TYPE_UINT16;
m_buffer_size = 8 * (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize;
m_input_image_format = QImage::Format_RGBA64;
if (loadalpha) {
m_target_image_format = QImage::Format_RGBA64;
} else {
m_target_image_format = QImage::Format_RGBX64;
}
} else { // 8bit depth
m_input_pixel_format.data_type = JXL_TYPE_UINT8;
m_buffer_size = 4 * (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize;
m_input_image_format = QImage::Format_RGBA8888;
if (loadalpha) {
m_target_image_format = QImage::Format_ARGB32;
} else {
m_target_image_format = QImage::Format_RGB32;
}
}
status = JxlDecoderGetColorAsEncodedProfile(m_decoder, &m_input_pixel_format, JXL_COLOR_PROFILE_TARGET_DATA, &color_encoding);
if (status == JXL_DEC_SUCCESS && color_encoding.color_space == JXL_COLOR_SPACE_RGB && color_encoding.white_point == JXL_WHITE_POINT_D65
&& color_encoding.primaries == JXL_PRIMARIES_SRGB && color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_SRGB) {
m_colorspace = QColorSpace(QColorSpace::SRgb);
} else {
size_t icc_size = 0;
if (JxlDecoderGetICCProfileSize(m_decoder, &m_input_pixel_format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size) == JXL_DEC_SUCCESS) {
if (icc_size > 0) {
QByteArray icc_data((int)icc_size, 0);
if (JxlDecoderGetColorAsICCProfile(m_decoder, &m_input_pixel_format, JXL_COLOR_PROFILE_TARGET_DATA, (uint8_t *)icc_data.data(), icc_data.size())
== JXL_DEC_SUCCESS) {
m_colorspace = QColorSpace::fromIccProfile(icc_data);
if (!m_colorspace.isValid()) {
qWarning("JXL image has Qt-unsupported or invalid ICC profile!");
}
} else {
qWarning("Failed to obtain data from JPEG XL decoder");
}
} else {
qWarning("Empty ICC data");
}
} else {
qWarning("no ICC, other color profile");
}
}
if (m_basicinfo.have_animation) { // count all frames
JxlFrameHeader frame_header;
int delay;
for (status = JxlDecoderProcessInput(m_decoder); status != JXL_DEC_SUCCESS; status = JxlDecoderProcessInput(m_decoder)) {
if (status != JXL_DEC_FRAME) {
switch (status) {
case JXL_DEC_ERROR:
qWarning("ERROR: JXL decoding failed");
break;
case JXL_DEC_NEED_MORE_INPUT:
qWarning("ERROR: JXL data incomplete");
break;
default:
qWarning("Unexpected event %d instead of JXL_DEC_FRAME", status);
break;
}
m_parseState = ParseJpegXLError;
return false;
}
if (JxlDecoderGetFrameHeader(m_decoder, &frame_header) != JXL_DEC_SUCCESS) {
qWarning("ERROR: JxlDecoderGetFrameHeader failed");
m_parseState = ParseJpegXLError;
return false;
}
if (m_basicinfo.animation.tps_denominator > 0 && m_basicinfo.animation.tps_numerator > 0) {
delay = (int)(0.5 + 1000.0 * frame_header.duration * m_basicinfo.animation.tps_denominator / m_basicinfo.animation.tps_numerator);
} else {
delay = 0;
}
m_framedelays.append(delay);
}
if (m_framedelays.isEmpty()) {
qWarning("no frames loaded by the JXL plug-in");
m_parseState = ParseJpegXLError;
return false;
}
if (m_framedelays.count() == 1) {
qWarning("JXL file was marked as animation but it has only one frame.");
m_basicinfo.have_animation = JXL_FALSE;
}
} else { // static picture
m_framedelays.resize(1);
m_framedelays[0] = 0;
}
if (!rewind()) {
return false;
}
m_next_image_delay = m_framedelays[0];
m_parseState = ParseJpegXLSuccess;
return true;
}
bool QJpegXLHandler::decode_one_frame()
{
JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
qWarning("Unexpected event %d instead of JXL_DEC_NEED_IMAGE_OUT_BUFFER", status);
m_parseState = ParseJpegXLError;
return false;
}
m_current_image = QImage(m_basicinfo.xsize, m_basicinfo.ysize, m_input_image_format);
if (m_current_image.isNull()) {
qWarning("Memory cannot be allocated");
m_parseState = ParseJpegXLError;
return false;
}
m_current_image.setColorSpace(m_colorspace);
if (JxlDecoderSetImageOutBuffer(m_decoder, &m_input_pixel_format, m_current_image.bits(), m_buffer_size) != JXL_DEC_SUCCESS) {
qWarning("ERROR: JxlDecoderSetImageOutBuffer failed");
m_parseState = ParseJpegXLError;
return false;
}
status = JxlDecoderProcessInput(m_decoder);
if (status != JXL_DEC_FULL_IMAGE) {
qWarning("Unexpected event %d instead of JXL_DEC_FULL_IMAGE", status);
m_parseState = ParseJpegXLError;
return false;
}
if (m_target_image_format != m_input_image_format) {
m_current_image.convertTo(m_target_image_format);
}
m_next_image_delay = m_framedelays[m_currentimage_index];
m_previousimage_index = m_currentimage_index;
if (m_framedelays.count() > 1) {
m_currentimage_index++;
if (m_currentimage_index >= m_framedelays.count()) {
if (!rewind()) {
return false;
}
}
}
return true;
}
bool QJpegXLHandler::read(QImage *image)
{
if (!ensureALLCounted()) {
return false;
}
if (m_currentimage_index == m_previousimage_index) {
*image = m_current_image;
return jumpToNextImage();
}
if (decode_one_frame()) {
*image = m_current_image;
return true;
} else {
return false;
}
}
bool QJpegXLHandler::write(const QImage &image)
{
if (image.format() == QImage::Format_Invalid) {
qWarning("No image data to save");
return false;
}
if ((image.width() > 0) && (image.height() > 0)) {
if ((image.width() > 65535) || (image.height() > 65535)) {
qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
return false;
}
if (sizeof(void *) <= 4) {
if (image.width() > ((8192 * 8192) / image.height())) {
qWarning("Image (%dx%d) is too large save via 32bit build of JXL plug-in", image.width(), image.height());
return false;
}
} else {
if (image.width() > ((16384 * 16384) / image.height())) {
qWarning("Image (%dx%d) will not be saved because it has more than 256 megapixels", image.width(), image.height());
return false;
}
}
} else {
qWarning("Image has zero dimension!");
return false;
}
int save_depth = 8; // 8 or 16
// 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 = 16;
break;
case QImage::Format_RGB32:
case QImage::Format_ARGB32:
case QImage::Format_ARGB32_Premultiplied:
case QImage::Format_RGB888:
case QImage::Format_RGBX8888:
case QImage::Format_RGBA8888:
case QImage::Format_RGBA8888_Premultiplied:
save_depth = 8;
break;
default:
if (image.depth() > 32) {
save_depth = 16;
} else {
save_depth = 8;
}
break;
}
JxlEncoder *encoder = JxlEncoderCreate(nullptr);
if (!encoder) {
qWarning("Failed to create Jxl encoder");
return false;
}
void *runner = nullptr;
int num_worker_threads = qBound(1, QThread::idealThreadCount(), 64);
if (num_worker_threads > 1) {
runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
if (JxlEncoderSetParallelRunner(encoder, JxlThreadParallelRunner, runner) != JXL_ENC_SUCCESS) {
qWarning("JxlEncoderSetParallelRunner failed");
JxlThreadParallelRunnerDestroy(runner);
JxlEncoderDestroy(encoder);
return false;
}
}
JxlEncoderOptions *encoder_options = JxlEncoderOptionsCreate(encoder, nullptr);
if (m_quality > 100) {
m_quality = 100;
} else if (m_quality < 0) {
m_quality = 90;
}
JxlEncoderOptionsSetDistance(encoder_options, (100.0f - m_quality) / 10.0f);
JxlEncoderOptionsSetLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE);
JxlBasicInfo output_info;
JxlEncoderInitBasicInfo(&output_info);
JxlColorEncoding color_profile;
JxlColorEncodingSetToSRGB(&color_profile, JXL_FALSE);
bool convert_color_profile;
QByteArray iccprofile;
if (image.colorSpace().isValid() && (m_quality < 100)) {
if (image.colorSpace().primaries() != QColorSpace::Primaries::SRgb || image.colorSpace().transferFunction() != QColorSpace::TransferFunction::SRgb) {
convert_color_profile = true;
} else {
convert_color_profile = false;
}
} else { // lossless or no profile or Qt-unsupported ICC profile
convert_color_profile = false;
iccprofile = image.colorSpace().iccProfile();
if (iccprofile.size() > 0 || m_quality == 100) {
output_info.uses_original_profile = 1;
}
}
JxlPixelFormat pixel_format;
QImage::Format tmpformat;
JxlEncoderStatus status;
pixel_format.endianness = JXL_NATIVE_ENDIAN;
pixel_format.align = 0;
output_info.intensity_target = 255.0f;
output_info.orientation = JXL_ORIENT_IDENTITY;
output_info.num_color_channels = 3;
output_info.animation.tps_numerator = 10;
output_info.animation.tps_denominator = 1;
if (save_depth > 8) { // 16bit depth
pixel_format.data_type = JXL_TYPE_UINT16;
output_info.bits_per_sample = 16;
if (image.hasAlphaChannel()) {
tmpformat = QImage::Format_RGBA64;
pixel_format.num_channels = 4;
output_info.alpha_bits = 16;
output_info.num_extra_channels = 1;
} else {
tmpformat = QImage::Format_RGBX64;
pixel_format.num_channels = 3;
output_info.alpha_bits = 0;
}
} else { // 8bit depth
pixel_format.data_type = JXL_TYPE_UINT8;
output_info.bits_per_sample = 8;
if (image.hasAlphaChannel()) {
tmpformat = QImage::Format_RGBA8888;
pixel_format.num_channels = 4;
output_info.alpha_bits = 8;
output_info.num_extra_channels = 1;
} else {
tmpformat = QImage::Format_RGB888;
pixel_format.num_channels = 3;
output_info.alpha_bits = 0;
}
}
const QImage tmpimage =
convert_color_profile ? image.convertToFormat(tmpformat).convertedToColorSpace(QColorSpace(QColorSpace::SRgb)) : image.convertToFormat(tmpformat);
const size_t xsize = tmpimage.width();
const size_t ysize = tmpimage.height();
const size_t buffer_size = (save_depth > 8) ? (2 * pixel_format.num_channels * xsize * ysize) : (pixel_format.num_channels * xsize * ysize);
if (xsize == 0 || ysize == 0 || tmpimage.isNull()) {
qWarning("Unable to allocate memory for output image");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
output_info.xsize = tmpimage.width();
output_info.ysize = tmpimage.height();
status = JxlEncoderSetBasicInfo(encoder, &output_info);
if (status != JXL_ENC_SUCCESS) {
qWarning("JxlEncoderSetBasicInfo failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
if (!convert_color_profile && iccprofile.size() > 0) {
status = JxlEncoderSetICCProfile(encoder, (const uint8_t *)iccprofile.constData(), iccprofile.size());
if (status != JXL_ENC_SUCCESS) {
qWarning("JxlEncoderSetICCProfile failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
} else {
status = JxlEncoderSetColorEncoding(encoder, &color_profile);
if (status != JXL_ENC_SUCCESS) {
qWarning("JxlEncoderSetColorEncoding failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
}
if (image.hasAlphaChannel() || ((save_depth == 8) && (xsize % 4 == 0))) {
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, (void *)tmpimage.constBits(), buffer_size);
} else {
if (save_depth > 8) { // 16bit depth without alpha channel
uint16_t *tmp_buffer = new (std::nothrow) uint16_t[3 * xsize * ysize];
if (!tmp_buffer) {
qWarning("Memory allocation error");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
uint16_t *dest_pixels = tmp_buffer;
for (int y = 0; y < tmpimage.height(); y++) {
const uint16_t *src_pixels = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
for (int x = 0; x < tmpimage.width(); x++) {
// R
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels++;
// G
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels++;
// B
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels += 2; // skipalpha
}
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, (void *)tmp_buffer, buffer_size);
delete[] tmp_buffer;
} else { // 8bit depth without alpha channel
uchar *tmp_buffer8 = new (std::nothrow) uchar[3 * xsize * ysize];
if (!tmp_buffer8) {
qWarning("Memory allocation error");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
uchar *dest_pixels8 = tmp_buffer8;
const size_t rowbytes = 3 * xsize;
for (int y = 0; y < tmpimage.height(); y++) {
memcpy(dest_pixels8, tmpimage.constScanLine(y), rowbytes);
dest_pixels8 += rowbytes;
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, (void *)tmp_buffer8, buffer_size);
delete[] tmp_buffer8;
}
}
if (status == JXL_ENC_ERROR) {
qWarning("JxlEncoderAddImageFrame failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
JxlEncoderCloseInput(encoder);
std::vector<uint8_t> compressed;
compressed.resize(4096);
size_t offset = 0;
uint8_t *next_out;
size_t avail_out;
do {
next_out = compressed.data() + offset;
avail_out = compressed.size() - offset;
status = JxlEncoderProcessOutput(encoder, &next_out, &avail_out);
if (status == JXL_ENC_NEED_MORE_OUTPUT) {
offset = next_out - compressed.data();
compressed.resize(compressed.size() * 2);
} else if (status == JXL_ENC_ERROR) {
qWarning("JxlEncoderProcessOutput failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
} while (status != JXL_ENC_SUCCESS);
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
compressed.resize(next_out - compressed.data());
if (compressed.size() > 0) {
qint64 write_status = device()->write((const char *)compressed.data(), compressed.size());
if (write_status > 0) {
return true;
} else if (write_status == -1) {
qWarning("Write error: %s\n", qUtf8Printable(device()->errorString()));
}
}
return false;
}
QVariant QJpegXLHandler::option(ImageOption option) const
{
if (option == Quality) {
return m_quality;
}
if (!supportsOption(option) || !ensureParsed()) {
return QVariant();
}
switch (option) {
case Size:
return QSize(m_basicinfo.xsize, m_basicinfo.ysize);
case Animation:
if (m_basicinfo.have_animation) {
return true;
} else {
return false;
}
default:
return QVariant();
}
}
void QJpegXLHandler::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 = 90;
}
return;
default:
break;
}
QImageIOHandler::setOption(option, value);
}
bool QJpegXLHandler::supportsOption(ImageOption option) const
{
return option == Quality || option == Size || option == Animation;
}
int QJpegXLHandler::imageCount() const
{
if (!ensureParsed()) {
return 0;
}
if (m_parseState == ParseJpegXLBasicInfoParsed) {
if (!m_basicinfo.have_animation) {
return 1;
}
if (!ensureALLCounted()) {
return 0;
}
}
if (!m_framedelays.isEmpty()) {
return m_framedelays.count();
}
return 0;
}
int QJpegXLHandler::currentImageNumber() const
{
if (m_parseState == ParseJpegXLNotParsed) {
return -1;
}
if (m_parseState == ParseJpegXLError || m_parseState == ParseJpegXLBasicInfoParsed || !m_decoder) {
return 0;
}
return m_currentimage_index;
}
bool QJpegXLHandler::jumpToNextImage()
{
if (!ensureALLCounted()) {
return false;
}
if (m_framedelays.count() > 1) {
m_currentimage_index++;
if (m_currentimage_index >= m_framedelays.count()) {
if (!rewind()) {
return false;
}
} else {
JxlDecoderSkipFrames(m_decoder, 1);
}
}
return true;
}
bool QJpegXLHandler::jumpToImage(int imageNumber)
{
if (!ensureALLCounted()) {
return false;
}
if (imageNumber < 0 || imageNumber >= m_framedelays.count()) {
return false;
}
if (imageNumber == m_currentimage_index) {
return true;
}
if (imageNumber > m_currentimage_index) {
JxlDecoderSkipFrames(m_decoder, imageNumber - m_currentimage_index);
m_currentimage_index = imageNumber;
return true;
}
if (!rewind()) {
return false;
}
if (imageNumber > 0) {
JxlDecoderSkipFrames(m_decoder, imageNumber);
}
m_currentimage_index = imageNumber;
return true;
}
int QJpegXLHandler::nextImageDelay() const
{
if (!ensureALLCounted()) {
return 0;
}
if (m_framedelays.count() < 2) {
return 0;
}
return m_next_image_delay;
}
int QJpegXLHandler::loopCount() const
{
if (!ensureParsed()) {
return 0;
}
if (m_basicinfo.have_animation) {
return 1;
} else {
return 0;
}
}
bool QJpegXLHandler::rewind()
{
m_currentimage_index = 0;
JxlDecoderReleaseInput(m_decoder);
JxlDecoderRewind(m_decoder);
if (m_runner) {
if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) {
qWarning("ERROR: JxlDecoderSetParallelRunner failed");
m_parseState = ParseJpegXLError;
return false;
}
}
if (JxlDecoderSetInput(m_decoder, (const uint8_t *)m_rawData.constData(), m_rawData.size()) != JXL_DEC_SUCCESS) {
qWarning("ERROR: JxlDecoderSetInput failed");
m_parseState = ParseJpegXLError;
return false;
}
if (m_basicinfo.uses_original_profile) {
if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
qWarning("ERROR: JxlDecoderSubscribeEvents failed");
m_parseState = ParseJpegXLError;
return false;
}
} else {
if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
qWarning("ERROR: JxlDecoderSubscribeEvents failed");
m_parseState = ParseJpegXLError;
return false;
}
JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
if (status != JXL_DEC_COLOR_ENCODING) {
qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status);
m_parseState = ParseJpegXLError;
return false;
}
JxlColorEncoding color_encoding;
JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE);
JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
}
return true;
}
QImageIOPlugin::Capabilities QJpegXLPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (format == "jxl") {
return Capabilities(CanRead | CanWrite);
}
if (!format.isEmpty()) {
return {};
}
if (!device->isOpen()) {
return {};
}
Capabilities cap;
if (device->isReadable() && QJpegXLHandler::canRead(device)) {
cap |= CanRead;
}
if (device->isWritable()) {
cap |= CanWrite;
}
return cap;
}
QImageIOHandler *QJpegXLPlugin::create(QIODevice *device, const QByteArray &format) const
{
QImageIOHandler *handler = new QJpegXLHandler;
handler->setDevice(device);
handler->setFormat(format);
return handler;
}

View File

@ -0,0 +1,7 @@
[Desktop Entry]
Type=Service
X-KDE-ServiceTypes=QImageIOPlugins
X-KDE-ImageFormat=jxl
X-KDE-MimeType=image/jxl
X-KDE-Read=true
X-KDE-Write=true

View File

@ -0,0 +1,4 @@
{
"Keys": [ "jxl" ],
"MimeTypes": [ "image/jxl" ]
}

96
src/imageformats/jxl_p.h Normal file
View File

@ -0,0 +1,96 @@
/*
JPEG XL (JXL) support for QImage.
SPDX-FileCopyrightText: 2021 Daniel Novomesky <dnovomesky@gmail.com>
SPDX-License-Identifier: BSD-2-Clause
*/
#ifndef KIMG_JXL_P_H
#define KIMG_JXL_P_H
#include <QByteArray>
#include <QColorSpace>
#include <QImage>
#include <QImageIOHandler>
#include <QImageIOPlugin>
#include <QVariant>
#include <QVector>
#include <jxl/decode.h>
class QJpegXLHandler : public QImageIOHandler
{
public:
QJpegXLHandler();
~QJpegXLHandler();
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:
bool ensureParsed() const;
bool ensureALLCounted() const;
bool ensureDecoder();
bool countALLFrames();
bool decode_one_frame();
bool rewind();
enum ParseJpegXLState {
ParseJpegXLError = -1,
ParseJpegXLNotParsed = 0,
ParseJpegXLSuccess = 1,
ParseJpegXLBasicInfoParsed = 2,
};
ParseJpegXLState m_parseState;
int m_quality;
int m_currentimage_index;
int m_previousimage_index;
QByteArray m_rawData;
JxlDecoder *m_decoder;
void *m_runner;
JxlBasicInfo m_basicinfo;
QVector<int> m_framedelays;
int m_next_image_delay;
QImage m_current_image;
QColorSpace m_colorspace;
QImage::Format m_input_image_format;
QImage::Format m_target_image_format;
JxlPixelFormat m_input_pixel_format;
size_t m_buffer_size;
};
class QJpegXLPlugin : public QImageIOPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "jxl.json")
public:
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
};
#endif // KIMG_JXL_P_H

View File

@ -3,6 +3,7 @@
SPDX-FileCopyrightText: 2003 Ignacio Castaño <castano@ludicon.com>
SPDX-FileCopyrightText: 2015 Alex Merry <alex.merry@kde.org>
SPDX-FileCopyrightText: 2022 Mirco Miranda <mirco.miranda@systemceramics.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
@ -17,13 +18,27 @@
* http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
*/
/*
* Limitations of the current code:
* - 32-bit float image are converted to 16-bit integer image.
* NOTE: Qt 6.2 allow 32-bit float images (RGB only)
* - Other color spaces cannot be read due to lack of QImage support for
* color spaces other than RGB (and Grayscale): a conversion to
* RGB must be done.
* - The best way to convert between different color spaces is to use a
* color management engine (e.g. LittleCMS).
* - An approximate way is to ignore the color information and use
* literature formulas (possible but not recommended).
*/
#include "psd_p.h"
#include "rle_p.h"
#include "util_p.h"
#include <QDataStream>
#include <QDebug>
#include <QImage>
#include <QColorSpace>
typedef quint32 uint;
typedef quint16 ushort;
@ -42,6 +57,14 @@ enum ColorMode {
CM_LABCOLOR = 9,
};
enum ImageResourceId : quint16 {
IRI_RESOLUTIONINFO = 0x03ED,
IRI_ICCPROFILE = 0x040F,
IRI_TRANSPARENCYINDEX = 0x0417,
IRI_VERSIONINFO = 0x0421,
IRI_XMPMETADATA = 0x0424
};
struct PSDHeader {
uint signature;
ushort version;
@ -53,6 +76,339 @@ struct PSDHeader {
ushort color_mode;
};
struct PSDImageResourceBlock {
QString name;
QByteArray data;
};
/*!
* \brief The PSDDuotoneOptions struct
* \note You can decode the duotone data using the "Duotone Options"
* file format found in the "Photoshop File Format" specs.
*/
struct PSDDuotoneOptions {
QByteArray data;
};
/*!
* \brief The PSDColorModeDataSection struct
* Only indexed color and duotone have color mode data.
*/
struct PSDColorModeDataSection {
PSDDuotoneOptions duotone;
QVector<QRgb> palette;
};
using PSDImageResourceSection = QHash<quint16, PSDImageResourceBlock>;
/*!
* \brief fixedPointToDouble
* Converts a fixed point number to floating point one.
*/
static double fixedPointToDouble(qint32 fixedPoint)
{
auto i = double(fixedPoint >> 16);
auto d = double((fixedPoint & 0x0000FFFF) / 65536.0);
return (i+d);
}
/*!
* \brief readPascalString
* Reads the Pascal string as defined in the PSD specification.
* \param s The stream.
* \param alignBytes Alignment of the string.
* \param size Number of stream bytes used.
* \return The string read.
*/
static QString readPascalString(QDataStream &s, qint32 alignBytes = 1, qint32 *size = nullptr)
{
qint32 tmp = 0;
if (size == nullptr)
size = &tmp;
quint8 stringSize;
s >> stringSize;
*size = sizeof(stringSize);
QString str;
if (stringSize > 0) {
QByteArray ba;
ba.resize(stringSize);
auto read = s.readRawData(ba.data(), ba.size());
if (read > 0) {
*size += read;
str = QString::fromLatin1(ba);
}
}
// align
if (alignBytes > 1)
if (auto pad = *size % alignBytes)
*size += s.skipRawData(alignBytes - pad);
return str;
}
/*!
* \brief readImageResourceSection
* Reads the image resource section.
* \param s The stream.
* \param ok Pointer to the operation result variable.
* \return The image resource section raw data.
*/
static PSDImageResourceSection readImageResourceSection(QDataStream &s, bool *ok = nullptr)
{
PSDImageResourceSection irs;
bool tmp = true;
if (ok == nullptr)
ok = &tmp;
*ok = true;
// Section size
qint32 sectioSize;
s >> sectioSize;
#ifdef QT_DEBUG
auto pos = qint64();
if (auto dev = s.device())
pos = dev->pos();
#endif
// Reading Image resource block
for (auto size = sectioSize; size > 0;) {
// Length Description
// -------------------------------------------------------------------
// 4 Signature: '8BIM'
// 2 Unique identifier for the resource. Image resource IDs
// contains a list of resource IDs used by Photoshop.
// Variable Name: Pascal string, padded to make the size even
// (a null name consists of two bytes of 0)
// 4 Actual size of resource data that follows
// Variable The resource data, described in the sections on the
// individual resource types. It is padded to make the size
// even.
quint32 signature;
s >> signature;
size -= sizeof(signature);
// NOTE: MeSa signature is not documented but found in some old PSD take from Photoshop 7.0 CD.
if (signature != 0x3842494D && signature != 0x4D655361) { // 8BIM and MeSa
qDebug() << "Invalid Image Resource Block Signature!";
*ok = false;
break;
}
// id
quint16 id;
s >> id;
size -= sizeof(id);
// getting data
PSDImageResourceBlock irb;
// name
qint32 bytes = 0;
irb.name = readPascalString(s, 2, &bytes);
size -= bytes;
// data read
quint32 dataSize;
s >> dataSize;
size -= sizeof(dataSize);
// NOTE: Qt device::read() and QDataStream::readRawData() could read less data than specified.
// The read code should be improved.
if(auto dev = s.device())
irb.data = dev->read(dataSize);
auto read = irb.data.size();
if (read > 0)
size -= read;
if (read != dataSize) {
qDebug() << "Image Resource Block Read Error!";
*ok = false;
break;
}
if (auto pad = dataSize % 2) {
auto skipped = s.skipRawData(pad);
if (skipped > 0)
size -= skipped;
}
// insert IRB
irs.insert(id, irb);
}
#ifdef QT_DEBUG
if (auto dev = s.device()) {
if ((dev->pos() - pos) != sectioSize) {
*ok = false;
}
}
#endif
return irs;
}
/*!
* \brief readColorModeDataSection
* Read the color mode section
* \param s The stream.
* \param ok Pointer to the operation result variable.
* \return The color mode section.
*/
PSDColorModeDataSection readColorModeDataSection(QDataStream &s, bool *ok = nullptr)
{
PSDColorModeDataSection cms;
bool tmp = false;
if (ok == nullptr)
ok = &tmp;
*ok = true;
qint32 size;
s >> size;
if (size != 768) { // read the duotone data (524 bytes)
// NOTE: A RGB/Gray float image has a 112 bytes ColorModeData that could be
// the "32-bit Toning Options" of Photoshop (starts with 'hdrt').
// Official Adobe specification tells "Only indexed color and duotone
// (see the mode field in the File header section) have color mode data.".
// See test case images 32bit_grayscale.psd and 32bit-rgb.psd
cms.duotone.data = s.device()->read(size);
if (cms.duotone.data.size() != size)
*ok = false;
}
else { // read the palette (768 bytes)
auto&& palette = cms.palette;
QVector<quint8> vect(size);
for (auto&& v : vect)
s >> v;
for (qsizetype i = 0, n = vect.size()/3; i < n; ++i)
palette.append(qRgb(vect.at(i), vect.at(n+i), vect.at(n+n+i)));
}
return cms;
}
/*!
* \brief setColorSpace
* Set the color space to the image.
* \param img The image.
* \param irs The image resource section.
* \return True on success, otherwise false.
*/
static bool setColorSpace(QImage& img, const PSDImageResourceSection& irs)
{
if (!irs.contains(IRI_ICCPROFILE))
return false;
auto irb = irs.value(IRI_ICCPROFILE);
auto cs = QColorSpace::fromIccProfile(irb.data);
if (!cs.isValid())
return false;
img.setColorSpace(cs);
return true;
}
/*!
* \brief setXmpData
* Adds XMP metadata to QImage.
* \param img The image.
* \param irs The image resource section.
* \return True on success, otherwise false.
*/
static bool setXmpData(QImage& img, const PSDImageResourceSection& irs)
{
if (!irs.contains(IRI_XMPMETADATA))
return false;
auto irb = irs.value(IRI_XMPMETADATA);
auto xmp = QString::fromUtf8(irb.data);
if (xmp.isEmpty())
return false;
// NOTE: "XML:com.adobe.xmp" is the meta set by Qt reader when an
// XMP packet is found (e.g. when reading a PNG saved by Photoshop).
// I'm reusing the same key because a programs could search for it.
img.setText(QStringLiteral("XML:com.adobe.xmp"), xmp);
return true;
}
/*!
* \brief hasMergedData
* Checks if merged image data are available.
* \param irs The image resource section.
* \return True on success or if the block does not exist, otherwise false.
*/
static bool hasMergedData(const PSDImageResourceSection& irs)
{
if (!irs.contains(IRI_VERSIONINFO))
return true;
auto irb = irs.value(IRI_VERSIONINFO);
if (irb.data.size() > 4)
return irb.data.at(4) != 0;
return false;
}
/*!
* \brief setResolution
* Set the image resolution.
* \param img The image.
* \param irs The image resource section.
* \return True on success, otherwise false.
*/
static bool setResolution(QImage& img, const PSDImageResourceSection& irs)
{
if (!irs.contains(IRI_RESOLUTIONINFO))
return false;
auto irb = irs.value(IRI_RESOLUTIONINFO);
QDataStream s(irb.data);
s.setByteOrder(QDataStream::BigEndian);
qint32 i32;
s >> i32; // Horizontal resolution in pixels per inch.
if (i32 <= 0)
return false;
auto hres = fixedPointToDouble(i32);
s.skipRawData(4); // Display data (not used here)
s >> i32; // Vertial resolution in pixels per inch.
if (i32 <= 0)
return false;
auto vres = fixedPointToDouble(i32);
img.setDotsPerMeterX(hres * 1000 / 25.4);
img.setDotsPerMeterY(vres * 1000 / 25.4);
return true;
}
/*!
* \brief setTransparencyIndex
* Search for transparency index block and, if found, changes the alpha of the value at the given index.
* \param img The image.
* \param irs The image resource section.
* \return True on success, otherwise false.
*/
static bool setTransparencyIndex(QImage& img, const PSDImageResourceSection& irs)
{
if (!irs.contains(IRI_TRANSPARENCYINDEX))
return false;
auto irb = irs.value(IRI_TRANSPARENCYINDEX);
QDataStream s(irb.data);
s.setByteOrder(QDataStream::BigEndian);
quint16 idx;
s >> idx;
auto palette = img.colorTable();
if (idx < palette.size()) {
auto&& v = palette[idx];
v = QRgb(v & ~0xFF000000);
img.setColorTable(palette);
return true;
}
return false;
}
static QDataStream &operator>>(QDataStream &s, PSDHeader &header)
{
s >> header.signature;
@ -80,66 +436,227 @@ static bool IsValid(const PSDHeader &header)
// Check that the header is supported.
static bool IsSupported(const PSDHeader &header)
{
if (header.version != 1) {
if (header.version != 1 && header.version != 2) {
return false;
}
if (header.channel_count > 16) {
if (header.depth != 8 &&
header.depth != 16 &&
header.depth != 32 &&
header.depth != 1) {
return false;
}
if (header.depth != 8 && header.depth != 16) {
return false;
}
if (header.color_mode != CM_RGB) {
if (header.color_mode != CM_RGB &&
header.color_mode != CM_GRAYSCALE &&
header.color_mode != CM_INDEXED &&
header.color_mode != CM_DUOTONE &&
header.color_mode != CM_BITMAP) {
return false;
}
return true;
}
static void skip_section(QDataStream &s)
static bool skip_section(QDataStream &s, bool psb = false)
{
quint32 section_length;
qint64 section_length;
if (!psb) {
quint32 tmp;
s >> tmp;
section_length = tmp;
}
else {
s >> section_length;
}
// Skip mode data.
s >> section_length;
s.skipRawData(section_length);
for (qint32 i32 = 0; section_length; section_length -= i32) {
i32 = std::min(section_length, qint64(std::numeric_limits<qint32>::max()));
i32 = s.skipRawData(i32);
if (i32 < 1)
return false;
}
return true;
}
template<class Trait>
static Trait readPixel(QDataStream &stream)
/*!
* \brief decompress
* Fast PackBits decompression.
* \param input The compressed input buffer.
* \param ilen The input buffer size.
* \param output The uncompressed target buffer.
* \param olen The target buffer size.
* \return The number of valid bytes in the target buffer.
*/
qint64 decompress(const char *input, qint64 ilen, char *output, qint64 olen)
{
Trait pixel;
stream >> pixel;
return pixel;
qint64 j = 0;
for (qint64 ip = 0, rr = 0, available = olen; j < olen && ip < ilen; available = olen - j) {
signed char n = static_cast<signed char>(input[ip++]);
if (n == -128)
continue;
if (n >= 0) {
rr = qint64(n) + 1;
if (available < rr) {
ip--;
break;
}
if (ip + rr > ilen)
return -1;
memcpy(output + j, input + ip, size_t(rr));
ip += rr;
}
else if (ip < ilen) {
rr = qint64(1-n);
if (available < rr) {
ip--;
break;
}
memset(output + j, input[ip++], size_t(rr));
}
j += rr;
}
return j;
}
static QRgb updateRed(QRgb oldPixel, quint8 redPixel)
/*!
* \brief imageFormat
* \param header The PSD header.
* \return The Qt image format.
*/
static QImage::Format imageFormat(const PSDHeader &header)
{
return qRgba(redPixel, qGreen(oldPixel), qBlue(oldPixel), qAlpha(oldPixel));
if (header.channel_count == 0) {
return QImage::Format_Invalid;
}
auto format = QImage::Format_Invalid;
switch(header.color_mode) {
case CM_RGB:
if (header.depth == 16 || header.depth == 32)
format = header.channel_count < 4 ? QImage::Format_RGBX64 : QImage::Format_RGBA64;
else
format = header.channel_count < 4 ? QImage::Format_RGB888 : QImage::Format_RGBA8888;
break;
case CM_GRAYSCALE:
case CM_DUOTONE:
format = header.depth == 8 ? QImage::Format_Grayscale8 : QImage::Format_Grayscale16;
break;
case CM_INDEXED:
format = header.depth == 8 ? QImage::Format_Indexed8 : QImage::Format_Invalid;
break;
case CM_BITMAP:
format = header.depth == 1 ? QImage::Format_Mono : QImage::Format_Invalid;
break;
}
return format;
}
static QRgb updateGreen(QRgb oldPixel, quint8 greenPixel)
/*!
* \brief imageChannels
* \param format The Qt image format.
* \return The number of channels of the image format.
*/
static qint32 imageChannels(const QImage::Format& format)
{
return qRgba(qRed(oldPixel), greenPixel, qBlue(oldPixel), qAlpha(oldPixel));
qint32 c = 4;
switch(format) {
case QImage::Format_RGB888:
c = 3;
break;
case QImage::Format_Grayscale8:
case QImage::Format_Grayscale16:
case QImage::Format_Indexed8:
case QImage::Format_Mono:
c = 1;
break;
default:
break;
}
return c;
}
static QRgb updateBlue(QRgb oldPixel, quint8 bluePixel)
inline quint8 xchg(quint8 v) {
return v;
}
inline quint16 xchg(quint16 v) {
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
return quint16( (v>>8) | (v<<8) );
#else
return v; // never tested
#endif
}
inline quint32 xchg(quint32 v) {
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
return quint32( (v>>24) | ((v & 0x00FF0000)>>8) | ((v & 0x0000FF00)<<8) | (v<<24) );
#else
return v; // never tested
#endif
}
template<class T>
inline void planarToChunchy(uchar *target, const char* source, qint32 width, qint32 c, qint32 cn)
{
return qRgba(qRed(oldPixel), qGreen(oldPixel), bluePixel, qAlpha(oldPixel));
auto s = reinterpret_cast<const T*>(source);
auto t = reinterpret_cast<T*>(target);
for (qint32 x = 0; x < width; ++x)
t[x*cn+c] = xchg(s[x]);
}
static QRgb updateAlpha(QRgb oldPixel, quint8 alphaPixel)
template<class T>
inline void planarToChunchyFloat(uchar *target, const char* source, qint32 width, qint32 c, qint32 cn)
{
return qRgba(qRed(oldPixel), qGreen(oldPixel), qBlue(oldPixel), alphaPixel);
auto s = reinterpret_cast<const T*>(source);
auto t = reinterpret_cast<quint16*>(target);
for (qint32 x = 0; x < width; ++x) {
auto tmp = xchg(s[x]);
t[x*cn+c] = std::min(quint16(*reinterpret_cast<float*>(&tmp) * std::numeric_limits<quint16>::max() + 0.5),
std::numeric_limits<quint16>::max());
}
}
inline void monoInvert(uchar *target, const char* source, qint32 bytes)
{
auto s = reinterpret_cast<const quint8*>(source);
auto t = reinterpret_cast<quint8*>(target);
for (qint32 x = 0; x < bytes; ++x)
t[x] = ~s[x];
}
typedef QRgb (*channelUpdater)(QRgb, quint8);
// Load the PSD image.
static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
{
// Mode data
skip_section(stream);
// Checking for PSB
auto isPsb = header.version == 2;
bool ok = false;
// Image resources
skip_section(stream);
// Color Mode Data section
auto cmds = readColorModeDataSection(stream, &ok);
if (!ok) {
qDebug() << "Error while skipping Color Mode Data section";
return false;
}
// Reserved data
skip_section(stream);
// Image Resources Section
auto irs = readImageResourceSection(stream, &ok);
if (!ok) {
qDebug() << "Error while reading Image Resources Section";
return false;
}
// Checking for merged image (Photoshop compatibility data)
if (!hasMergedData(irs)) {
qDebug() << "No merged data found";
return false;
}
// Layer and Mask section
if (!skip_section(stream, isPsb)) {
qDebug() << "Error while skipping Layer and Mask section";
return false;
}
// Find out if the data is compressed.
// Known values:
@ -147,103 +664,114 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
// 1: RLE compressed
quint16 compression;
stream >> compression;
if (compression > 1) {
qDebug() << "Unknown compression type";
return false;
}
quint32 channel_num = header.channel_count;
QImage::Format fmt = header.depth == 8 ? QImage::Format_RGB32 : QImage::Format_RGBX64;
// Clear the image.
if (channel_num >= 4) {
// Enable alpha.
fmt = header.depth == 8 ? QImage::Format_ARGB32 : QImage::Format_RGBA64;
// Ignore the other channels.
channel_num = 4;
const QImage::Format format = imageFormat(header);
if (format == QImage::Format_Invalid) {
qWarning() << "Unsupported image format. color_mode:" << header.color_mode << "depth:" << header.depth << "channel_count:" << header.channel_count;
return false;
}
img = QImage(header.width, header.height, fmt);
img = QImage(header.width, header.height, format);
if (img.isNull()) {
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width, header.height);
return false;
}
img.fill(qRgb(0, 0, 0));
if (!cmds.palette.isEmpty()) {
img.setColorTable(cmds.palette);
setTransparencyIndex(img, irs);
}
const quint32 pixel_count = header.height * header.width;
const quint32 channel_size = pixel_count * header.depth / 8;
auto imgChannels = imageChannels(img.format());
auto channel_num = std::min(qint32(header.channel_count), imgChannels);
auto raw_count = qsizetype(header.width * header.depth + 7) / 8;
// Verify this, as this is used to write into the memory of the QImage
if (pixel_count > img.sizeInBytes() / (header.depth == 8 ? sizeof(QRgb) : sizeof(QRgba64))) {
qWarning() << "Invalid pixel count!" << pixel_count << "bytes available:" << img.sizeInBytes();
if (header.height > kMaxQVectorSize / header.channel_count / sizeof(quint32)) {
qWarning() << "LoadPSD() header height/channel_count too big" << header.height << header.channel_count;
return false;
}
QRgb *image_data = reinterpret_cast<QRgb *>(img.bits());
if (!image_data) {
return false;
}
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.
int skip_count = header.height * header.channel_count * sizeof(quint16);
if (stream.skipRawData(skip_count) != skip_count) {
return false;
QVector<quint32> strides(header.height * header.channel_count, raw_count);
// Read the compressed stride sizes
if (compression)
for (auto&& v : strides) {
if (isPsb) {
stream >> v;
continue;
}
quint16 tmp;
stream >> tmp;
v = tmp;
}
for (unsigned short channel = 0; channel < channel_num; 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]);
// Read the image
QByteArray rawStride;
rawStride.resize(raw_count);
for (qint32 c = 0; c < channel_num; ++c) {
for(qint32 y = 0, h = header.height; y < h; ++y) {
auto&& strideSize = strides.at(c*qsizetype(h)+y);
if (compression) {
QByteArray tmp;
tmp.resize(strideSize);
if (stream.readRawData(tmp.data(), tmp.size()) != tmp.size()) {
qDebug() << "Error while reading the stream of channel" << c << "line" << y;
return false;
}
if (decompress(tmp.data(), tmp.size(), rawStride.data(), rawStride.size()) < 0) {
qDebug() << "Error while decompressing the channel" << c << "line" << y;
return false;
}
}
else {
if (stream.readRawData(rawStride.data(), rawStride.size()) != rawStride.size()) {
qDebug() << "Error while reading the stream of channel" << c << "line" << y;
return false;
}
}
if (!success) {
qDebug() << "decodeRLEData on channel" << channel << "failed";
return false;
}
}
} 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<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) {
qDebug() << "DataStream status was" << stream.status();
qDebug() << "Stream read error" << stream.status();
return false;
}
auto scanLine = img.scanLine(y);
if (header.depth == 1) // Bitmap
monoInvert(scanLine, rawStride.data(), std::min(rawStride.size(), img.bytesPerLine()));
else if (header.depth == 8) // 8-bits images: Indexed, Grayscale, RGB/RGBA
planarToChunchy<quint8>(scanLine, rawStride.data(), header.width, c, imgChannels);
else if (header.depth == 16) // 16-bits integer images: Grayscale, RGB/RGBA
planarToChunchy<quint16>(scanLine, rawStride.data(), header.width, c, imgChannels);
else if (header.depth == 32) // 32-bits float images: Grayscale, RGB/RGBA (coverted to equivalent integer 16-bits)
planarToChunchyFloat<quint32>(scanLine, rawStride.data(), header.width, c, imgChannels);
}
}
// Resolution info
if (!setResolution(img, irs)) {
// qDebug() << "No resolution info found!";
}
// ICC profile
if (!setColorSpace(img, irs)) {
// qDebug() << "No colorspace info set!";
}
// XMP data
if (!setXmpData(img, irs)) {
// qDebug() << "No XMP data found!";
}
// Duotone images: color data contains the duotone specification (not documented).
// Other applications that read Photoshop files can treat a duotone image as a gray image,
// and just preserve the contents of the duotone information when reading and writing the file.
if (!cmds.duotone.data.isEmpty()) {
img.setText(QStringLiteral("PSDDuotoneOptions"), QString::fromUtf8(cmds.duotone.data.toHex()));
}
return true;
}
@ -292,6 +820,38 @@ bool PSDHandler::read(QImage *image)
return true;
}
bool PSDHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size)
return true;
return false;
}
QVariant PSDHandler::option(ImageOption option) const
{
QVariant v;
if (option == QImageIOHandler::Size) {
if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto ba = d->read(sizeof(PSDHeader));
d->rollbackTransaction();
QDataStream s(ba);
s.setByteOrder(QDataStream::BigEndian);
PSDHeader header;
s >> header;
if (s.status() == QDataStream::Ok && IsValid(header))
v = QVariant::fromValue(QSize(header.width, header.height));
}
}
return v;
}
bool PSDHandler::canRead(QIODevice *device)
{
if (!device) {
@ -332,7 +892,7 @@ bool PSDHandler::canRead(QIODevice *device)
QImageIOPlugin::Capabilities PSDPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (format == "psd") {
if (format == "psd" || format == "psb" || format == "pdd" || format == "psdt") {
return Capabilities(CanRead);
}
if (!format.isEmpty()) {

View File

@ -1,4 +1,4 @@
{
"Keys": [ "psd" ],
"MimeTypes": [ "image/vnd.adobe.photoshop" ]
"Keys": [ "psd", "psb", "pdd", "psdt" ],
"MimeTypes": [ "image/vnd.adobe.photoshop", "image/vnd.adobe.photoshop", "image/vnd.adobe.photoshop", "image/vnd.adobe.photoshop" ]
}

View File

@ -18,6 +18,9 @@ public:
bool canRead() const override;
bool read(QImage *image) override;
bool supportsOption(QImageIOHandler::ImageOption option) const override;
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
};

View File

@ -9,6 +9,8 @@
#include "ras_p.h"
#include "util_p.h"
#include <QDataStream>
#include <QDebug>
#include <QImage>
@ -102,8 +104,7 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
{
s.device()->seek(RasHeader::SIZE);
// QVector uses some extra space for stuff, hence the 32 here suggested by thiago
if (ras.ColorMapLength > std::numeric_limits<int>::max() - 32) {
if (ras.ColorMapLength > kMaxQVectorSize) {
qWarning() << "LoadRAS() unsupported image color map length in file header" << ras.ColorMapLength;
return false;
}
@ -127,8 +128,7 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
qWarning() << "LoadRAS() mistmatch between height and width" << ras.Width << ras.Height << ras.Length << ras.Depth;
return false;
}
// QVector uses some extra space for stuff, hence the 32 here suggested by thiago
if (ras.Length > std::numeric_limits<int>::max() - 32) {
if (ras.Length > kMaxQVectorSize) {
qWarning() << "LoadRAS() unsupported image length in file header" << ras.Length;
return false;
}

10
src/imageformats/util_p.h Normal file
View File

@ -0,0 +1,10 @@
/*
SPDX-FileCopyrightText: 2022 Albert Astals Cid <aacid@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <limits>
// QVector uses some extra space for stuff, hence the 32 here suggested by Thiago Macieira
static constexpr int kMaxQVectorSize = std::numeric_limits<int>::max() - 32;

View File

@ -16,8 +16,9 @@
#include <QStack>
#include <QVector>
#include <QtEndian>
#include <stdlib.h>
#include <QColorSpace>
#include <stdlib.h>
#include <string.h>
#include "gimp_p.h"
@ -351,6 +352,8 @@ private:
bool initialized; //!< Is the QImage initialized?
QImage image; //!< final QImage
QHash<QString,QByteArray> parasites; //!< parasites data
XCFImage(void)
: initialized(false)
{
@ -405,6 +408,7 @@ private:
bool composeTiles(XCFImage &xcf_image);
void setGrayPalette(QImage &image);
void setPalette(XCFImage &xcf_image, QImage &image);
void setImageParasites(const XCFImage &xcf_image, QImage &image);
static void assignImageBytes(Layer &layer, uint i, uint j);
bool loadHierarchy(QDataStream &xcf_io, Layer &layer);
bool loadLevel(QDataStream &xcf_io, Layer &layer, qint32 bpp);
@ -665,6 +669,9 @@ bool XCFImageFormat::readXCF(QIODevice *device, QImage *outImage)
return false;
}
// The image was created: now I can set metadata and ICC color profile inside it.
setImageParasites(xcf_image, xcf_image.image);
*outImage = xcf_image.image;
return true;
}
@ -715,15 +722,15 @@ bool XCFImageFormat::loadImageProperties(QDataStream &xcf_io, XCFImage &xcf_imag
property.readBytes(tag, size);
quint32 flags;
char *data = nullptr;
QByteArray data;
property >> flags >> data;
if (tag && strncmp(tag, "gimp-comment", strlen("gimp-comment")) == 0) {
xcf_image.image.setText(QStringLiteral("Comment"), QString::fromUtf8(data));
}
// WARNING: you cannot add metadata to QImage here because it can be null.
// Adding a metadata to a QImage when it is null, does nothing (metas are lost).
if(tag) // store metadata for future use
xcf_image.parasites.insert(QString::fromUtf8(tag), data);
delete[] tag;
delete[] data;
}
break;
@ -1234,6 +1241,77 @@ void XCFImageFormat::setPalette(XCFImage &xcf_image, QImage &image)
image.setColorTable(xcf_image.palette);
}
/*!
* Copy the parasites info to QImage.
* \param xcf_image XCF image containing the parasites read from the data stream.
* \param image image to apply the parasites data.
* \note Some comment taken from https://gitlab.gnome.org/GNOME/gimp/-/blob/master/devel-docs/parasites.txt
*/
void XCFImageFormat::setImageParasites(const XCFImage &xcf_image, QImage &image)
{
auto&& p = xcf_image.parasites;
auto keys = p.keys();
for (auto&& key : qAsConst(keys)) {
auto value = p.value(key);
if(value.isEmpty())
continue;
// "icc-profile" (IMAGE, PERSISTENT | UNDOABLE)
// This contains an ICC profile describing the color space the
// image was produced in. TIFF images stored in PhotoShop do
// oftentimes contain embedded profiles. An experimental color
// manager exists to use this parasite, and it will be used
// for interchange between TIFF and PNG (identical profiles)
if (key == QStringLiteral("icc-profile")) {
auto cs = QColorSpace::fromIccProfile(value);
if(cs.isValid())
image.setColorSpace(cs);
continue;
}
// "gimp-comment" (IMAGE, PERSISTENT)
// Standard GIF-style image comments. This parasite should be
// human-readable text in UTF-8 encoding. A trailing \0 might
// be included and is not part of the comment. Note that image
// comments may also be present in the "gimp-metadata" parasite.
if (key == QStringLiteral("gimp-comment")) {
value.replace('\0', QByteArray());
image.setText(QStringLiteral("Comment"), QString::fromUtf8(value));
continue;
}
// "gimp-image-metadata"
// Saved by GIMP 2.10.30 but it is not mentioned in the specification.
// It is an XML block with the properties set using GIMP.
if (key == QStringLiteral("gimp-image-metadata")) {
// NOTE: I arbitrary defined the metadata "XML:org.gimp.xml" because it seems
// a GIMP proprietary XML format (no xmlns defined)
value.replace('\0', QByteArray());
image.setText(QStringLiteral("XML:org.gimp.xml"), QString::fromUtf8(value));
continue;
}
#if 0 // Unable to generate it using latest GIMP version
// "gimp-metadata" (IMAGE, PERSISTENT)
// The metadata associated with the image, serialized as one XMP
// packet. This metadata includes the contents of any XMP, EXIF
// and IPTC blocks from the original image, as well as
// user-specified values such as image comment, copyright,
// license, etc.
if (key == QStringLiteral("gimp-metadata")) {
// NOTE: "XML:com.adobe.xmp" is the meta set by Qt reader when an
// XMP packet is found (e.g. when reading a PNG saved by Photoshop).
// I reused the same key because some programs could search for it.
value.replace('\0', QByteArray());
image.setText(QStringLiteral("XML:com.adobe.xmp"), QString::fromUtf8(value));
continue;
}
#endif
}
}
/*!
* Copy the bytes from the tile buffer into the image tile QImage, taking into
* account all the myriad different modes.
@ -3192,6 +3270,52 @@ bool XCFHandler::write(const QImage &)
return false;
}
bool XCFHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size)
return true;
return false;
}
QVariant XCFHandler::option(ImageOption option) const
{
QVariant v;
if (option == QImageIOHandler::Size) {
/*
* The image structure always starts at offset 0 in the XCF file.
* byte[9] "gimp xcf " File type identification
* byte[4] version XCF version
* "file": version 0
* "v001": version 1
* "v002": version 2
* "v003": version 3
* byte 0 Zero marks the end of the version tag.
* uint32 width Width of canvas
* uint32 height Height of canvas
*/
if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto ba9 = d->read(9); // "gimp xcf "
auto ba5 = d->read(4+1); // version + null terminator
auto ba = d->read(8); // width and height
d->rollbackTransaction();
if (ba9 == QByteArray("gimp xcf ") && ba5.size() == 5) {
QDataStream ds(ba);
quint32 width;
ds >> width;
quint32 height;
ds >> height;
if (ds.status() == QDataStream::Ok)
v = QVariant::fromValue(QSize(width, height));
}
}
}
return v;
}
bool XCFHandler::canRead(QIODevice *device)
{
if (!device) {

View File

@ -20,6 +20,9 @@ public:
bool read(QImage *image) override;
bool write(const QImage &image) override;
bool supportsOption(QImageIOHandler::ImageOption option) const override;
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
};

View File

@ -5,7 +5,7 @@ include(ECMMarkAsTest)
macro(kimageformats_executable_tests)
foreach(_testname ${ARGN})
add_executable(${_testname} ${_testname}.cpp)
target_link_libraries(${_testname} Qt5::Gui)
target_link_libraries(${_testname} Qt${QT_MAJOR_VERSION}::Gui)
ecm_mark_as_test(${_testname})
endforeach(_testname)
endmacro()