mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-07-16 03:24:17 -04:00
Compare commits
57 Commits
v5.82.0
...
v5.93.0-rc
Author | SHA1 | Date | |
---|---|---|---|
ae6b724824 | |||
3e751dd80d | |||
e69dff73e6 | |||
64cfe52bee | |||
8732fc8487 | |||
d9729b7190 | |||
55d3c568b2 | |||
4afafee6c1 | |||
f04084e175 | |||
9911c9c2ea | |||
4ceef5164d | |||
3d2d91a08a | |||
0a02458560 | |||
96836e849f | |||
f4edb7296f | |||
56376ffd66 | |||
f534254063 | |||
32347725cb | |||
56e762c563 | |||
81603ed908 | |||
957c199c68 | |||
f445e5dd0a | |||
d3da56bba3 | |||
41c4b5930c | |||
fb66044714 | |||
a43394a759 | |||
904c251f50 | |||
2f9e09f04a | |||
6458c9ae52 | |||
45cd128f73 | |||
e89d21f12d | |||
1d2b51ddf1 | |||
1080976abe | |||
abd550c60c | |||
aade392da3 | |||
7642633551 | |||
9f2c5061c8 | |||
28099eed71 | |||
f5d574b3ad | |||
a8f92e5525 | |||
fbeef559b7 | |||
7f56d835f0 | |||
8f87ce4cb2 | |||
5aa03c12ad | |||
3266a9c466 | |||
1b2bf6e931 | |||
894391b000 | |||
ef6be2c077 | |||
96b40da089 | |||
bf3f94da76 | |||
318dacda75 | |||
e358bb0feb | |||
dca6e87c89 | |||
62f9af9a35 | |||
ff53d3d7e9 | |||
780f342825 | |||
297ed9a2fe |
@ -1,2 +1,3 @@
|
||||
#clang-format
|
||||
#clang-format/tidy
|
||||
1169859b07f25c865ee0bfc2a7dc97a431651776
|
||||
eaca33bec8b19498c3621fc3a20a8c55a2812462
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,3 +25,4 @@ CMakeLists.txt.user*
|
||||
.clangd
|
||||
.idea
|
||||
/cmake-build*
|
||||
.cache
|
||||
|
9
.gitlab-ci.yml
Normal file
9
.gitlab-ci.yml
Normal file
@ -0,0 +1,9 @@
|
||||
# SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
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
|
8
.kde-ci.yml
Normal file
8
.kde-ci.yml
Normal file
@ -0,0 +1,8 @@
|
||||
Dependencies:
|
||||
- 'on': ['Linux', 'FreeBSD', 'macOS', 'Windows']
|
||||
'require':
|
||||
'frameworks/extra-cmake-modules': '@same'
|
||||
'frameworks/karchive' : '@same'
|
||||
|
||||
Options:
|
||||
test-before-installing: True
|
@ -1,11 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.6)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(KImageFormats)
|
||||
|
||||
set (CMAKE_CXX_STANDARD 14)
|
||||
|
||||
include(FeatureSummary)
|
||||
find_package(ECM 5.82.0 NO_MODULE)
|
||||
find_package(ECM 5.93.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)
|
||||
|
||||
@ -19,9 +17,10 @@ include(KDEGitCommitHooks)
|
||||
|
||||
|
||||
include(CheckIncludeFiles)
|
||||
include(FindPkgConfig)
|
||||
|
||||
set(REQUIRED_QT_VERSION 5.15.0)
|
||||
find_package(Qt5Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
|
||||
set(REQUIRED_QT_VERSION 5.15.2)
|
||||
find_package(Qt${QT_MAJOR_VERSION}Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
|
||||
|
||||
find_package(KF5Archive)
|
||||
set_package_properties(KF5Archive PROPERTIES
|
||||
@ -33,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()
|
||||
@ -60,16 +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")
|
||||
|
||||
add_definitions(-DQT_NO_FOREACH)
|
||||
# 050d00 (5.13) triggers a BIC in qimageiohandler.h, in Qt 5.13, so do not enable that until we can require 5.14
|
||||
# https://codereview.qt-project.org/c/qt/qtbase/+/279215
|
||||
add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f00)
|
||||
add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x055100)
|
||||
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=0x055900)
|
||||
add_subdirectory(src)
|
||||
if (BUILD_TESTING)
|
||||
add_subdirectory(autotests)
|
||||
|
121
LICENSES/CC0-1.0.txt
Normal file
121
LICENSES/CC0-1.0.txt
Normal 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.
|
@ -23,6 +23,7 @@ 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)
|
||||
|
@ -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,29 @@ if (TARGET avif)
|
||||
kimageformats_read_tests(
|
||||
avif
|
||||
)
|
||||
# because the plug-ins use RGB->YUV conversion which sometimes results in 1 value difference.
|
||||
kimageformats_write_tests(FUZZ 1
|
||||
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 +140,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
37
autotests/fuzzyeq.cpp
Normal 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
BIN
autotests/read/jxl/rgb.jxl
Normal file
Binary file not shown.
BIN
autotests/read/jxl/rgb.png
Normal file
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
BIN
autotests/read/jxl/rgba.jxl
Normal file
Binary file not shown.
BIN
autotests/read/jxl/rgba.png
Normal file
BIN
autotests/read/jxl/rgba.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 117 KiB |
BIN
autotests/read/xcf/fruktpilot_icc.png
Normal file
BIN
autotests/read/xcf/fruktpilot_icc.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 115 KiB |
BIN
autotests/read/xcf/fruktpilot_icc.xcf
Normal file
BIN
autotests/read/xcf/fruktpilot_icc.xcf
Normal file
Binary file not shown.
@ -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,36 +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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ type: functional
|
||||
platforms:
|
||||
- name: Linux
|
||||
- name: FreeBSD
|
||||
- name: MacOSX
|
||||
- name: macOS
|
||||
- name: Windows
|
||||
note: No EPS support on Windows
|
||||
- name: Android
|
||||
|
@ -4,129 +4,136 @@
|
||||
|
||||
function(kimageformats_add_plugin plugin)
|
||||
set(options)
|
||||
set(oneValueArgs JSON)
|
||||
set(multiValueArgs SOURCES)
|
||||
cmake_parse_arguments(KIF_ADD_PLUGIN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
||||
if(NOT KIF_ADD_PLUGIN_SOURCES)
|
||||
message(FATAL_ERROR "kimageformats_add_plugin called without SOURCES parameter")
|
||||
endif()
|
||||
get_filename_component(json "${KIF_ADD_PLUGIN_JSON}" REALPATH)
|
||||
if(NOT KIF_ADD_PLUGIN_JSON OR NOT EXISTS ${json})
|
||||
message(FATAL_ERROR "JSON file doesn't exist: ${json}")
|
||||
endif()
|
||||
|
||||
add_library(${plugin} MODULE ${KIF_ADD_PLUGIN_SOURCES})
|
||||
set_property(TARGET ${plugin} APPEND PROPERTY AUTOGEN_TARGET_DEPENDS ${json})
|
||||
set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/imageformats")
|
||||
target_link_libraries(${plugin} Qt5::Gui)
|
||||
target_link_libraries(${plugin} Qt${QT_MAJOR_VERSION}::Gui)
|
||||
install(TARGETS ${plugin} DESTINATION ${KDE_INSTALL_QTPLUGINDIR}/imageformats)
|
||||
endfunction()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_ani JSON "ani.json" SOURCES ani.cpp)
|
||||
install(FILES ani.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
kimageformats_add_plugin(kimg_ani SOURCES ani.cpp)
|
||||
install(FILES ani.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
if (TARGET avif)
|
||||
kimageformats_add_plugin(kimg_avif JSON "avif.json" SOURCES "avif.cpp")
|
||||
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)
|
||||
kimageformats_add_plugin(kimg_eps JSON "eps.json" SOURCES eps.cpp)
|
||||
target_link_libraries(kimg_eps Qt5::PrintSupport)
|
||||
install(FILES eps.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
if (TARGET Qt${QT_MAJOR_VERSION}::PrintSupport)
|
||||
kimageformats_add_plugin(kimg_eps SOURCES eps.cpp)
|
||||
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/)
|
||||
|
||||
##################################
|
||||
|
||||
if(OpenEXR_FOUND)
|
||||
kimageformats_add_plugin(kimg_exr JSON "exr.json" SOURCES exr.cpp)
|
||||
kimageformats_add_plugin(kimg_exr SOURCES exr.cpp)
|
||||
if(TARGET OpenEXR::OpenEXR)
|
||||
target_link_libraries(kimg_exr OpenEXR::OpenEXR)
|
||||
else()
|
||||
if(OpenEXR_VERSION_STRING VERSION_LESS 2.3.0)
|
||||
# Older OpenEXR versions use dynamic exception specifications, so
|
||||
# cannot use C++17 with them
|
||||
set_target_properties(kimg_exr PROPERTIES CXX_STANDARD 14)
|
||||
endif()
|
||||
target_link_libraries(kimg_exr OpenEXR::IlmImf)
|
||||
endif()
|
||||
kde_target_enable_exceptions(kimg_exr PRIVATE)
|
||||
|
||||
install(FILES exr.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES exr.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
endif()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_hdr JSON "hdr.json" SOURCES hdr.cpp)
|
||||
install(FILES hdr.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
kimageformats_add_plugin(kimg_hdr SOURCES hdr.cpp)
|
||||
install(FILES hdr.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
if (LibHeif_FOUND)
|
||||
kimageformats_add_plugin(kimg_heif JSON "heif.json" SOURCES heif.cpp)
|
||||
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()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_pcx JSON "pcx.json" SOURCES pcx.cpp)
|
||||
install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
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_pic JSON "pic.json" SOURCES pic.cpp)
|
||||
install(FILES pic.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
kimageformats_add_plugin(kimg_pcx SOURCES pcx.cpp)
|
||||
install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_psd JSON "psd.json" SOURCES psd.cpp)
|
||||
install(FILES psd.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
kimageformats_add_plugin(kimg_pic SOURCES pic.cpp)
|
||||
install(FILES pic.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_ras JSON "ras.json" SOURCES ras.cpp)
|
||||
install(FILES ras.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
kimageformats_add_plugin(kimg_psd SOURCES psd.cpp)
|
||||
install(FILES psd.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_rgb JSON "rgb.json" SOURCES rgb.cpp)
|
||||
install(FILES rgb.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
kimageformats_add_plugin(kimg_ras SOURCES ras.cpp)
|
||||
install(FILES ras.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_tga JSON "tga.json" SOURCES tga.cpp)
|
||||
install(FILES tga.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
kimageformats_add_plugin(kimg_rgb SOURCES rgb.cpp)
|
||||
install(FILES rgb.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_xcf JSON "xcf.json" SOURCES xcf.cpp)
|
||||
install(FILES xcf.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
kimageformats_add_plugin(kimg_tga SOURCES tga.cpp)
|
||||
install(FILES tga.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_xcf SOURCES xcf.cpp)
|
||||
install(FILES xcf.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
if (KF5Archive_FOUND)
|
||||
|
||||
kimageformats_add_plugin(kimg_kra JSON "kra.json" SOURCES kra.cpp)
|
||||
kimageformats_add_plugin(kimg_kra SOURCES kra.cpp)
|
||||
target_link_libraries(kimg_kra KF5::Archive)
|
||||
install(FILES kra.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES kra.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
kimageformats_add_plugin(kimg_ora JSON "ora.json" SOURCES ora.cpp)
|
||||
kimageformats_add_plugin(kimg_ora SOURCES ora.cpp)
|
||||
target_link_libraries(kimg_ora KF5::Archive)
|
||||
install(FILES ora.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES ora.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
endif()
|
||||
|
@ -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") {
|
||||
|
@ -97,6 +97,14 @@ bool QAVIFHandler::ensureDecoder()
|
||||
|
||||
m_decoder = avifDecoderCreate();
|
||||
|
||||
#if AVIF_VERSION >= 80400
|
||||
m_decoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
|
||||
#endif
|
||||
|
||||
#if AVIF_VERSION >= 90100
|
||||
m_decoder->strictFlags = AVIF_STRICT_DISABLED;
|
||||
#endif
|
||||
|
||||
avifResult decodeResult;
|
||||
|
||||
decodeResult = avifDecoderSetIOMemory(m_decoder, m_rawAvifData.data, m_rawAvifData.size);
|
||||
@ -125,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;
|
||||
@ -137,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;
|
||||
@ -180,7 +194,7 @@ bool QAVIFHandler::decode_one_frame()
|
||||
if (loadalpha) {
|
||||
resultformat = QImage::Format_RGBA8888;
|
||||
} else {
|
||||
resultformat = QImage::Format_RGB888;
|
||||
resultformat = QImage::Format_RGBX8888;
|
||||
}
|
||||
}
|
||||
QImage result(m_decoder->image->width, m_decoder->image->height, resultformat);
|
||||
@ -190,10 +204,12 @@ bool QAVIFHandler::decode_one_frame()
|
||||
return false;
|
||||
}
|
||||
|
||||
QColorSpace colorspace;
|
||||
if (m_decoder->image->icc.data && (m_decoder->image->icc.size > 0)) {
|
||||
result.setColorSpace(QColorSpace::fromIccProfile(QByteArray::fromRawData((const char *)m_decoder->image->icc.data, (int)m_decoder->image->icc.size)));
|
||||
if (!result.colorSpace().isValid()) {
|
||||
qWarning("Invalid QColorSpace created from ICC!");
|
||||
const QByteArray icc_data((const char *)m_decoder->image->icc.data, (int)m_decoder->image->icc.size);
|
||||
colorspace = QColorSpace::fromIccProfile(icc_data);
|
||||
if (!colorspace.isValid()) {
|
||||
qWarning("AVIF image has Qt-unsupported or invalid ICC profile!");
|
||||
}
|
||||
} else {
|
||||
float prim[8] = {0.64f, 0.33f, 0.3f, 0.6f, 0.15f, 0.06f, 0.3127f, 0.329f};
|
||||
@ -243,23 +259,25 @@ bool QAVIFHandler::decode_one_frame()
|
||||
case 0:
|
||||
case 1:
|
||||
case 2: /* AVIF_COLOR_PRIMARIES_UNSPECIFIED */
|
||||
result.setColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, q_trc, q_trc_gamma));
|
||||
colorspace = QColorSpace(QColorSpace::Primaries::SRgb, q_trc, q_trc_gamma);
|
||||
break;
|
||||
/* AVIF_COLOR_PRIMARIES_SMPTE432 */
|
||||
case 12:
|
||||
result.setColorSpace(QColorSpace(QColorSpace::Primaries::DciP3D65, q_trc, q_trc_gamma));
|
||||
colorspace = QColorSpace(QColorSpace::Primaries::DciP3D65, q_trc, q_trc_gamma);
|
||||
break;
|
||||
default:
|
||||
result.setColorSpace(QColorSpace(whitePoint, redPoint, greenPoint, bluePoint, q_trc, q_trc_gamma));
|
||||
colorspace = QColorSpace(whitePoint, redPoint, greenPoint, bluePoint, q_trc, q_trc_gamma);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.colorSpace().isValid()) {
|
||||
qWarning("Invalid QColorSpace created from NCLX/CICP!");
|
||||
if (!colorspace.isValid()) {
|
||||
qWarning("AVIF plugin created invalid QColorSpace from NCLX/CICP!");
|
||||
}
|
||||
}
|
||||
|
||||
result.setColorSpace(colorspace);
|
||||
|
||||
avifRGBImage rgb;
|
||||
avifRGBImageSetDefaults(&rgb, m_decoder->image);
|
||||
|
||||
@ -268,20 +286,24 @@ bool QAVIFHandler::decode_one_frame()
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
|
||||
if (!loadalpha) {
|
||||
rgb.ignoreAlpha = AVIF_TRUE;
|
||||
result.fill(Qt::black);
|
||||
if (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
|
||||
resultformat = QImage::Format_Grayscale16;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rgb.depth = 8;
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
|
||||
#if AVIF_VERSION >= 80400
|
||||
if (m_decoder->imageCount > 1) {
|
||||
/* accelerate animated AVIF */
|
||||
rgb.chromaUpsampling = AVIF_CHROMA_UPSAMPLING_FASTEST;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (loadalpha) {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
resultformat = QImage::Format_ARGB32;
|
||||
} else {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGB;
|
||||
|
||||
if (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
|
||||
resultformat = QImage::Format_Grayscale8;
|
||||
} else {
|
||||
@ -302,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)) {
|
||||
@ -357,11 +379,15 @@ bool QAVIFHandler::decode_one_frame()
|
||||
}
|
||||
|
||||
if (m_decoder->image->transformFlags & AVIF_TRANSFORM_IMIR) {
|
||||
#if AVIF_VERSION > 90100
|
||||
switch (m_decoder->image->imir.mode) {
|
||||
#else
|
||||
switch (m_decoder->image->imir.axis) {
|
||||
case 0: // vertical
|
||||
#endif
|
||||
case 0: // top-to-bottom
|
||||
result = result.mirrored(false, true);
|
||||
break;
|
||||
case 1: // horizontal
|
||||
case 1: // left-to-right
|
||||
result = result.mirrored(true, false);
|
||||
break;
|
||||
}
|
||||
@ -397,15 +423,39 @@ 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;
|
||||
}
|
||||
|
||||
if (m_quality >= 100 && !avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE)) {
|
||||
qWarning("You are using %s encoder. It is recommended to enable libAOM encoder in libavif for better near-lossless compression.", encoder_name);
|
||||
}
|
||||
|
||||
int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100;
|
||||
int minQuantizer = 0;
|
||||
int maxQuantizerAlpha = 0;
|
||||
@ -545,6 +595,7 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
|
||||
avifColorPrimaries primaries_to_save = (avifColorPrimaries)2;
|
||||
avifTransferCharacteristics transfer_to_save = (avifTransferCharacteristics)2;
|
||||
QByteArray iccprofile;
|
||||
|
||||
if (tmpcolorimage.colorSpace().isValid()) {
|
||||
switch (tmpcolorimage.colorSpace().primaries()) {
|
||||
@ -636,13 +687,23 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb));
|
||||
}
|
||||
}
|
||||
} else { // profile is unsupported by Qt
|
||||
iccprofile = tmpcolorimage.colorSpace().iccProfile();
|
||||
if (iccprofile.size() > 0) {
|
||||
matrix_to_save = (avifMatrixCoefficients)6;
|
||||
}
|
||||
}
|
||||
|
||||
avif = avifImageCreate(tmpcolorimage.width(), tmpcolorimage.height(), save_depth, pixel_format);
|
||||
avif->matrixCoefficients = matrix_to_save;
|
||||
|
||||
avif->colorPrimaries = primaries_to_save;
|
||||
avif->transferCharacteristics = transfer_to_save;
|
||||
|
||||
if (iccprofile.size() > 0) {
|
||||
avifImageSetProfileICC(avif, (const uint8_t *)iccprofile.constData(), iccprofile.size());
|
||||
}
|
||||
|
||||
avifRGBImage rgb;
|
||||
avifRGBImageSetDefaults(&rgb, avif);
|
||||
rgb.rowBytes = tmpcolorimage.bytesPerLine();
|
||||
@ -687,7 +748,7 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
encoder->maxQuantizerAlpha = maxQuantizerAlpha;
|
||||
}
|
||||
|
||||
encoder->speed = 8;
|
||||
encoder->speed = 6;
|
||||
|
||||
res = avifEncoderWrite(encoder, avif, &raw);
|
||||
avifEncoderDestroy(encoder);
|
||||
@ -791,7 +852,7 @@ bool QAVIFHandler::jumpToNextImage()
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from begining
|
||||
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning
|
||||
avifDecoderReset(m_decoder);
|
||||
}
|
||||
|
||||
@ -840,7 +901,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;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"Keys": [ "avif", "avifs" ],
|
||||
"MimeTypes": [ "image/avif", "image/avif-sequence" ]
|
||||
"MimeTypes": [ "image/avif", "image/avif" ]
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
#include <QPainter>
|
||||
#include <QPrinter>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
// logging category for this framework, default: log stuff >= warning
|
||||
@ -94,7 +95,10 @@ static bool bbox(QIODevice *io, int *x1, int *y1, int *x2, int *y2)
|
||||
if (strncmp(buf, BBOX, BBOX_LEN) == 0) {
|
||||
// Some EPS files have non-integer values for the bbox
|
||||
// We don't support that currently, but at least we parse it
|
||||
float _x1, _y1, _x2, _y2;
|
||||
float _x1;
|
||||
float _y1;
|
||||
float _x2;
|
||||
float _y2;
|
||||
if (sscanf(buf, "%*s %f %f %f %f", &_x1, &_y1, &_x2, &_y2) == 4) {
|
||||
qCDebug(EPSPLUGIN) << "BBOX: " << _x1 << " " << _y1 << " " << _x2 << " " << _y2;
|
||||
*x1 = int(_x1);
|
||||
@ -127,14 +131,18 @@ bool EPSHandler::read(QImage *image)
|
||||
{
|
||||
qCDebug(EPSPLUGIN) << "starting...";
|
||||
|
||||
int x1, y1, x2, y2;
|
||||
int x1;
|
||||
int y1;
|
||||
int x2;
|
||||
int y2;
|
||||
#ifdef EPS_PERFORMANCE_DEBUG
|
||||
QTime dt;
|
||||
dt.start();
|
||||
#endif
|
||||
|
||||
QIODevice *io = device();
|
||||
qint64 ps_offset, ps_size;
|
||||
qint64 ps_offset;
|
||||
qint64 ps_size;
|
||||
|
||||
// find start of PostScript code
|
||||
if (!seekToCodeStart(io, ps_offset, ps_size)) {
|
||||
@ -169,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")
|
||||
@ -185,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;
|
||||
|
@ -44,8 +44,13 @@ public:
|
||||
}
|
||||
|
||||
bool read(char c[], int n) override;
|
||||
#if OPENEXR_VERSION_MAJOR > 2
|
||||
uint64_t tellg() override;
|
||||
void seekg(uint64_t pos) override;
|
||||
#else
|
||||
Imf::Int64 tellg() override;
|
||||
void seekg(Imf::Int64 pos) override;
|
||||
#endif
|
||||
void clear() override;
|
||||
|
||||
private:
|
||||
@ -65,12 +70,20 @@ bool K_IStream::read(char c[], int n)
|
||||
return false;
|
||||
}
|
||||
|
||||
#if OPENEXR_VERSION_MAJOR > 2
|
||||
uint64_t K_IStream::tellg()
|
||||
#else
|
||||
Imf::Int64 K_IStream::tellg()
|
||||
#endif
|
||||
{
|
||||
return m_dev->pos();
|
||||
}
|
||||
|
||||
#if OPENEXR_VERSION_MAJOR > 2
|
||||
void K_IStream::seekg(uint64_t pos)
|
||||
#else
|
||||
void K_IStream::seekg(Imf::Int64 pos)
|
||||
#endif
|
||||
{
|
||||
m_dev->seek(pos);
|
||||
}
|
||||
@ -86,7 +99,10 @@ void K_IStream::clear()
|
||||
*/
|
||||
QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
|
||||
{
|
||||
float r, g, b, a;
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float a;
|
||||
|
||||
// 1) Compensate for fogging by subtracting defog
|
||||
// from the raw pixel values.
|
||||
@ -119,24 +135,24 @@ QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
|
||||
// maximum intensity).
|
||||
// Response: kneeLow = 0.0 (2^0.0 => 1); kneeHigh = 5.0 (2^5 =>32)
|
||||
if (r > 1.0) {
|
||||
r = 1.0 + Imath::Math<float>::log((r - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
r = 1.0 + std::log((r - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
}
|
||||
if (g > 1.0) {
|
||||
g = 1.0 + Imath::Math<float>::log((g - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
g = 1.0 + std::log((g - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
}
|
||||
if (b > 1.0) {
|
||||
b = 1.0 + Imath::Math<float>::log((b - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
b = 1.0 + std::log((b - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
}
|
||||
if (a > 1.0) {
|
||||
a = 1.0 + Imath::Math<float>::log((a - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
a = 1.0 + std::log((a - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
}
|
||||
//
|
||||
// 5) Gamma-correct the pixel values, assuming that the
|
||||
// screen's gamma is 0.4545 (or 1/2.2).
|
||||
r = Imath::Math<float>::pow(r, 0.4545);
|
||||
g = Imath::Math<float>::pow(g, 0.4545);
|
||||
b = Imath::Math<float>::pow(b, 0.4545);
|
||||
a = Imath::Math<float>::pow(a, 0.4545);
|
||||
r = std::pow(r, 0.4545);
|
||||
g = std::pow(g, 0.4545);
|
||||
b = std::pow(b, 0.4545);
|
||||
a = std::pow(a, 0.4545);
|
||||
|
||||
// 6) Scale the values such that pixels middle gray
|
||||
// pixels are mapped to 84.66 (or 3.5 f-stops below
|
||||
@ -165,7 +181,8 @@ bool EXRHandler::canRead() const
|
||||
bool EXRHandler::read(QImage *outImage)
|
||||
{
|
||||
try {
|
||||
int width, height;
|
||||
int width;
|
||||
int height;
|
||||
|
||||
K_IStream istr(device(), QByteArray());
|
||||
Imf::RgbaInputFile file(istr);
|
||||
|
@ -89,7 +89,8 @@ static void RGBE_To_QRgbLine(uchar *image, QRgb *scanline, int width)
|
||||
// Load the HDR image.
|
||||
static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &img)
|
||||
{
|
||||
uchar val, code;
|
||||
uchar val;
|
||||
uchar code;
|
||||
|
||||
// Create dst image.
|
||||
img = QImage(width, height, QImage::Format_RGB32);
|
||||
|
@ -137,12 +137,10 @@ bool HEIFHandler::write(const QImage &image)
|
||||
heif::Image heifImage;
|
||||
heifImage.create(tmpimage.width(), tmpimage.height(), heif_colorspace_RGB, chroma);
|
||||
|
||||
if (tmpimage.colorSpace().isValid()) {
|
||||
QByteArray iccprofile = tmpimage.colorSpace().iccProfile();
|
||||
if (iccprofile.size() > 0) {
|
||||
std::vector<uint8_t> rawProfile(iccprofile.begin(), iccprofile.end());
|
||||
heifImage.set_raw_color_profile(heif_color_profile_type_prof, rawProfile);
|
||||
}
|
||||
QByteArray iccprofile = tmpimage.colorSpace().iccProfile();
|
||||
if (iccprofile.size() > 0) {
|
||||
std::vector<uint8_t> rawProfile(iccprofile.begin(), iccprofile.end());
|
||||
heifImage.set_raw_color_profile(heif_color_profile_type_prof, rawProfile);
|
||||
}
|
||||
|
||||
heifImage.add_plane(heif_channel_interleaved, image.width(), image.height(), save_depth);
|
||||
@ -617,7 +615,7 @@ bool HEIFHandler::ensureDecoder()
|
||||
} else {
|
||||
m_current_image.setColorSpace(QColorSpace::fromIccProfile(ba));
|
||||
if (!m_current_image.colorSpace().isValid()) {
|
||||
qWarning() << "icc profile is invalid";
|
||||
qWarning() << "HEIC image has Qt-unsupported or invalid ICC profile!";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -679,7 +677,7 @@ bool HEIFHandler::ensureDecoder()
|
||||
heif_nclx_color_profile_free(nclx);
|
||||
|
||||
if (!m_current_image.colorSpace().isValid()) {
|
||||
qWarning() << "invalid color profile created from NCLX";
|
||||
qWarning() << "HEIC plugin created invalid QColorSpace from NCLX!";
|
||||
}
|
||||
}
|
||||
|
||||
|
977
src/imageformats/jxl.cpp
Normal file
977
src/imageformats/jxl.cpp
Normal 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;
|
||||
}
|
7
src/imageformats/jxl.desktop
Normal file
7
src/imageformats/jxl.desktop
Normal 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
|
4
src/imageformats/jxl.json
Normal file
4
src/imageformats/jxl.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Keys": [ "jxl" ],
|
||||
"MimeTypes": [ "image/jxl" ]
|
||||
}
|
96
src/imageformats/jxl_p.h
Normal file
96
src/imageformats/jxl_p.h
Normal 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
|
@ -35,12 +35,14 @@ bool KraHandler::canRead() const
|
||||
bool KraHandler::read(QImage *image)
|
||||
{
|
||||
KZip zip(device());
|
||||
if (!zip.open(QIODevice::ReadOnly))
|
||||
if (!zip.open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const KArchiveEntry *entry = zip.directory()->entry(QStringLiteral("mergedimage.png"));
|
||||
if (!entry || !entry->isFile())
|
||||
if (!entry || !entry->isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const KZipFileEntry *fileZipEntry = static_cast<const KZipFileEntry *>(entry);
|
||||
|
||||
@ -57,8 +59,9 @@ bool KraHandler::canRead(QIODevice *device)
|
||||
}
|
||||
|
||||
char buff[57];
|
||||
if (device->peek(buff, sizeof(buff)) == sizeof(buff))
|
||||
if (device->peek(buff, sizeof(buff)) == sizeof(buff)) {
|
||||
return memcmp(buff + 0x26, s_magic, s_magic_size) == 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -34,12 +34,14 @@ bool OraHandler::canRead() const
|
||||
bool OraHandler::read(QImage *image)
|
||||
{
|
||||
KZip zip(device());
|
||||
if (!zip.open(QIODevice::ReadOnly))
|
||||
if (!zip.open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const KArchiveEntry *entry = zip.directory()->entry(QStringLiteral("mergedimage.png"));
|
||||
if (!entry || !entry->isFile())
|
||||
if (!entry || !entry->isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const KZipFileEntry *fileZipEntry = static_cast<const KZipFileEntry *>(entry);
|
||||
|
||||
@ -56,8 +58,9 @@ bool OraHandler::canRead(QIODevice *device)
|
||||
}
|
||||
|
||||
char buff[54];
|
||||
if (device->peek(buff, sizeof(buff)) == sizeof(buff))
|
||||
if (device->peek(buff, sizeof(buff)) == sizeof(buff)) {
|
||||
return memcmp(buff + 0x26, s_magic, s_magic_size) == 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -105,7 +105,9 @@ public:
|
||||
|
||||
static QDataStream &operator>>(QDataStream &s, RGB &rgb)
|
||||
{
|
||||
quint8 r, g, b;
|
||||
quint8 r;
|
||||
quint8 g;
|
||||
quint8 b;
|
||||
|
||||
s >> r >> g >> b;
|
||||
rgb.r = r;
|
||||
@ -126,24 +128,32 @@ static QDataStream &operator>>(QDataStream &s, Palette &pal)
|
||||
|
||||
static QDataStream &operator>>(QDataStream &s, PCXHEADER &ph)
|
||||
{
|
||||
quint8 m, ver, enc, bpp;
|
||||
quint8 m;
|
||||
quint8 ver;
|
||||
quint8 enc;
|
||||
quint8 bpp;
|
||||
s >> m >> ver >> enc >> bpp;
|
||||
ph.Manufacturer = m;
|
||||
ph.Version = ver;
|
||||
ph.Encoding = enc;
|
||||
ph.Bpp = bpp;
|
||||
quint16 xmin, ymin, xmax, ymax;
|
||||
quint16 xmin;
|
||||
quint16 ymin;
|
||||
quint16 xmax;
|
||||
quint16 ymax;
|
||||
s >> xmin >> ymin >> xmax >> ymax;
|
||||
ph.XMin = xmin;
|
||||
ph.YMin = ymin;
|
||||
ph.XMax = xmax;
|
||||
ph.YMax = ymax;
|
||||
quint16 hdpi, ydpi;
|
||||
quint16 hdpi;
|
||||
quint16 ydpi;
|
||||
s >> hdpi >> ydpi;
|
||||
ph.HDpi = hdpi;
|
||||
ph.YDpi = ydpi;
|
||||
Palette colorMap;
|
||||
quint8 res, np;
|
||||
quint8 res;
|
||||
quint8 np;
|
||||
s >> colorMap >> res >> np;
|
||||
ph.ColorMap = colorMap;
|
||||
ph.Reserved = res;
|
||||
@ -154,7 +164,8 @@ static QDataStream &operator>>(QDataStream &s, PCXHEADER &ph)
|
||||
quint16 paletteinfo;
|
||||
s >> paletteinfo;
|
||||
ph.PaletteInfo = paletteinfo;
|
||||
quint16 hscreensize, vscreensize;
|
||||
quint16 hscreensize;
|
||||
quint16 vscreensize;
|
||||
s >> hscreensize;
|
||||
ph.HScreenSize = hscreensize;
|
||||
s >> vscreensize;
|
||||
@ -222,7 +233,8 @@ static void readLine(QDataStream &s, QByteArray &buf, const PCXHEADER &header)
|
||||
{
|
||||
quint32 i = 0;
|
||||
quint32 size = buf.size();
|
||||
quint8 byte, count;
|
||||
quint8 byte;
|
||||
quint8 count;
|
||||
|
||||
if (header.isCompressed()) {
|
||||
// Uncompress the image data
|
||||
@ -300,10 +312,11 @@ static void readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
quint32 offset = i * header.BytesPerLine;
|
||||
for (int x = 0; x < header.width(); ++x)
|
||||
for (int x = 0; x < header.width(); ++x) {
|
||||
if (buf[offset + (x / 8)] & (128 >> (x % 8))) {
|
||||
pixbuf[x] = (int)(pixbuf[x]) + (1 << i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uchar *p = img.scanLine(y);
|
||||
@ -343,8 +356,9 @@ static void readImage8(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
|
||||
uchar *p = img.scanLine(y);
|
||||
|
||||
if (!p)
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned int bpl = qMin(header.BytesPerLine, (quint16)header.width());
|
||||
for (unsigned int x = 0; x < bpl; ++x) {
|
||||
@ -358,7 +372,9 @@ static void readImage8(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
|
||||
if (flag == 12 && (header.Version == 5 || header.Version == 2)) {
|
||||
// Read the palette
|
||||
quint8 r, g, b;
|
||||
quint8 r;
|
||||
quint8 g;
|
||||
quint8 b;
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
s >> r >> g >> b;
|
||||
img.setColor(i, qRgb(r, g, b));
|
||||
@ -400,7 +416,8 @@ static void writeLine(QDataStream &s, QByteArray &buf)
|
||||
{
|
||||
quint32 i = 0;
|
||||
quint32 size = buf.size();
|
||||
quint8 count, data;
|
||||
quint8 count;
|
||||
quint8 data;
|
||||
char byte;
|
||||
|
||||
while (i < size) {
|
||||
@ -473,10 +490,11 @@ static void writeImage4(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
}
|
||||
|
||||
for (int x = 0; x < header.width(); ++x) {
|
||||
for (int i = 0; i < 4; ++i)
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (*(p + x) & (1 << i)) {
|
||||
buf[i][x / 8] = (int)(buf[i][x / 8]) | 1 << (7 - x % 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
@ -618,8 +636,12 @@ bool PCXHandler::write(const QImage &image)
|
||||
|
||||
QImage img = image;
|
||||
|
||||
int w = img.width();
|
||||
int h = img.height();
|
||||
const int w = img.width();
|
||||
const int h = img.height();
|
||||
|
||||
if (w > 65536 || h > 65536) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// qDebug() << "Width: " << w;
|
||||
// qDebug() << "Height: " << h;
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <qendian.h>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* Reads a PIC file header from a data stream.
|
||||
@ -225,7 +226,7 @@ bool SoftimagePICHandler::read(QImage *image)
|
||||
}
|
||||
|
||||
QImage::Format fmt = QImage::Format_RGB32;
|
||||
for (const PicChannel &channel : qAsConst(m_channels)) {
|
||||
for (const PicChannel &channel : std::as_const(m_channels)) {
|
||||
if (channel.size != 8) {
|
||||
// we cannot read images that do not come in bytes
|
||||
qDebug() << "Channel size was" << channel.size;
|
||||
@ -404,7 +405,7 @@ QVariant SoftimagePICHandler::option(ImageOption option) const
|
||||
return QString();
|
||||
case ImageFormat:
|
||||
if (const_cast<SoftimagePICHandler *>(this)->readChannels()) {
|
||||
for (const PicChannel &channel : qAsConst(m_channels)) {
|
||||
for (const PicChannel &channel : std::as_const(m_channels)) {
|
||||
if (channel.code & ALPHA) {
|
||||
return QImage::Format_ARGB32;
|
||||
}
|
||||
|
@ -154,13 +154,16 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
// Allocate image
|
||||
img = QImage(ras.Width, ras.Height, QImage::Format_ARGB32);
|
||||
|
||||
if (img.isNull())
|
||||
if (img.isNull()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reconstruct image from RGB palette if we have a palette
|
||||
// TODO: make generic so it works with 24bit or 32bit palettes
|
||||
if (ras.ColorMapType == 1 && ras.Depth == 8) {
|
||||
quint8 red, green, blue;
|
||||
quint8 red;
|
||||
quint8 green;
|
||||
quint8 blue;
|
||||
for (quint32 y = 0; y < ras.Height; y++) {
|
||||
for (quint32 x = 0; x < ras.Width; x++) {
|
||||
red = palette.value((int)input[y * ras.Width + x]);
|
||||
@ -172,7 +175,9 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
}
|
||||
|
||||
if (ras.ColorMapType == 0 && ras.Depth == 24 && (ras.Type == 1 || ras.Type == 2)) {
|
||||
quint8 red, green, blue;
|
||||
quint8 red;
|
||||
quint8 green;
|
||||
quint8 blue;
|
||||
for (quint32 y = 0; y < ras.Height; y++) {
|
||||
for (quint32 x = 0; x < ras.Width; x++) {
|
||||
red = input[y * 3 * ras.Width + x * 3 + 2];
|
||||
@ -184,7 +189,9 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
}
|
||||
|
||||
if (ras.ColorMapType == 0 && ras.Depth == 24 && ras.Type == 3) {
|
||||
quint8 red, green, blue;
|
||||
quint8 red;
|
||||
quint8 green;
|
||||
quint8 blue;
|
||||
for (quint32 y = 0; y < ras.Height; y++) {
|
||||
for (quint32 x = 0; x < ras.Width; x++) {
|
||||
red = input[y * 3 * ras.Width + x * 3];
|
||||
@ -196,7 +203,9 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
}
|
||||
|
||||
if (ras.ColorMapType == 0 && ras.Depth == 32 && (ras.Type == 1 || ras.Type == 2)) {
|
||||
quint8 red, green, blue;
|
||||
quint8 red;
|
||||
quint8 green;
|
||||
quint8 blue;
|
||||
for (quint32 y = 0; y < ras.Height; y++) {
|
||||
for (quint32 x = 0; x < ras.Width; x++) {
|
||||
red = input[y * 4 * ras.Width + x * 4 + 3];
|
||||
@ -208,7 +217,9 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
}
|
||||
|
||||
if (ras.ColorMapType == 0 && ras.Depth == 32 && ras.Type == 3) {
|
||||
quint8 red, green, blue;
|
||||
quint8 red;
|
||||
quint8 green;
|
||||
quint8 blue;
|
||||
for (quint32 y = 0; y < ras.Height; y++) {
|
||||
for (quint32 x = 0; x < ras.Width; x++) {
|
||||
red = input[y * 4 * ras.Width + x * 4 + 1];
|
||||
@ -274,8 +285,9 @@ bool RASHandler::read(QImage *outImage)
|
||||
RasHeader ras;
|
||||
s >> ras;
|
||||
|
||||
if (ras.ColorMapLength > std::numeric_limits<int>::max())
|
||||
if (ras.ColorMapLength > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: add support for old versions of RAS where Length may be zero in header
|
||||
s.device()->seek(RasHeader::SIZE + ras.Length + ras.ColorMapLength);
|
||||
|
@ -138,7 +138,8 @@ SGIImage::~SGIImage()
|
||||
|
||||
bool SGIImage::getRow(uchar *dest)
|
||||
{
|
||||
int n, i;
|
||||
int n;
|
||||
int i;
|
||||
if (!_rle) {
|
||||
for (i = 0; i < _xsize; i++) {
|
||||
if (_pos >= _data.end()) {
|
||||
@ -184,7 +185,8 @@ bool SGIImage::readData(QImage &img)
|
||||
quint32 *start = _starttab;
|
||||
QByteArray lguard(_xsize, 0);
|
||||
uchar *line = (uchar *)lguard.data();
|
||||
unsigned x, y;
|
||||
unsigned x;
|
||||
unsigned y;
|
||||
|
||||
if (!_rle) {
|
||||
_pos = _data.begin();
|
||||
@ -279,9 +281,9 @@ bool SGIImage::readImage(QImage &img)
|
||||
// bytes per channel
|
||||
_stream >> _bpc;
|
||||
// qDebug() << "bytes per channel: " << int(_bpc);
|
||||
if (_bpc == 1)
|
||||
if (_bpc == 1) {
|
||||
;
|
||||
else if (_bpc == 2) {
|
||||
} else if (_bpc == 2) {
|
||||
// qDebug() << "dropping least significant byte";
|
||||
} else {
|
||||
return false;
|
||||
@ -328,8 +330,9 @@ bool SGIImage::readImage(QImage &img)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_zsize == 0)
|
||||
if (_zsize == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_zsize == 2 || _zsize == 4) {
|
||||
img = img.convertToFormat(QImage::Format_ARGB32);
|
||||
@ -337,8 +340,9 @@ bool SGIImage::readImage(QImage &img)
|
||||
// qDebug() << "using first 4 of " << _zsize << " channels";
|
||||
// Only let this continue if it won't cause a int overflow later
|
||||
// this is most likely a broken file anyway
|
||||
if (_ysize > std::numeric_limits<int>::max() / _zsize)
|
||||
if (_ysize > std::numeric_limits<int>::max() / _zsize) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_numrows = _ysize * _zsize;
|
||||
@ -363,13 +367,15 @@ bool SGIImage::readImage(QImage &img)
|
||||
_data = _dev->readAll();
|
||||
|
||||
// sanity check
|
||||
if (_rle)
|
||||
for (uint o = 0; o < _numrows; o++)
|
||||
if (_rle) {
|
||||
for (uint o = 0; o < _numrows; o++) {
|
||||
// don't change to greater-or-equal!
|
||||
if (_starttab[o] + _lengthtab[o] > (uint)_data.size()) {
|
||||
// qDebug() << "image corrupt (sanity check failed)";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!readData(img)) {
|
||||
// qDebug() << "image corrupt (incomplete scanline)";
|
||||
@ -390,7 +396,8 @@ void RLEData::write(QDataStream &s)
|
||||
|
||||
bool RLEData::operator<(const RLEData &b) const
|
||||
{
|
||||
uchar ac, bc;
|
||||
uchar ac;
|
||||
uchar bc;
|
||||
for (int i = 0; i < qMin(size(), b.size()); i++) {
|
||||
ac = at(i);
|
||||
bc = b[i];
|
||||
@ -436,8 +443,13 @@ uchar SGIImage::intensity(uchar c)
|
||||
|
||||
uint SGIImage::compact(uchar *d, uchar *s)
|
||||
{
|
||||
uchar *dest = d, *src = s, patt, *t, *end = s + _xsize;
|
||||
int i, n;
|
||||
uchar *dest = d;
|
||||
uchar *src = s;
|
||||
uchar patt;
|
||||
uchar *t;
|
||||
uchar *end = s + _xsize;
|
||||
int i;
|
||||
int n;
|
||||
while (src < end) {
|
||||
for (n = 0, t = src; t + 2 < end && !(*t == t[1] && *t == t[2]); t++) {
|
||||
n++;
|
||||
@ -480,7 +492,8 @@ bool SGIImage::scanData(const QImage &img)
|
||||
uchar *line = (uchar *)lineguard.data();
|
||||
uchar *buf = (uchar *)bufguard.data();
|
||||
const QRgb *c;
|
||||
unsigned x, y;
|
||||
unsigned x;
|
||||
unsigned y;
|
||||
uint len;
|
||||
|
||||
for (y = 0; y < _ysize; y++) {
|
||||
@ -606,7 +619,8 @@ void SGIImage::writeVerbatim(const QImage &img)
|
||||
writeHeader();
|
||||
|
||||
const QRgb *c;
|
||||
unsigned x, y;
|
||||
unsigned x;
|
||||
unsigned y;
|
||||
|
||||
for (y = 0; y < _ysize; y++) {
|
||||
c = reinterpret_cast<const QRgb *>(img.scanLine(_ysize - y - 1));
|
||||
@ -667,9 +681,16 @@ bool SGIImage::writeImage(const QImage &image)
|
||||
return false;
|
||||
}
|
||||
|
||||
const int w = img.width();
|
||||
const int h = img.height();
|
||||
|
||||
if (w > 65536 || h > 65536) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_bpc = 1;
|
||||
_xsize = img.width();
|
||||
_ysize = img.height();
|
||||
_xsize = w;
|
||||
_ysize = h;
|
||||
_pixmin = ~0u;
|
||||
_pixmax = 0;
|
||||
_colormap = NORMAL;
|
||||
|
@ -311,7 +311,9 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
|
||||
}
|
||||
|
||||
// Convert image to internal format.
|
||||
int y_start, y_step, y_end;
|
||||
int y_start;
|
||||
int y_step;
|
||||
int y_end;
|
||||
if (tga.flags & TGA_ORIGIN_UPPER) {
|
||||
y_start = 0;
|
||||
y_step = 1;
|
||||
@ -437,7 +439,7 @@ bool TGAHandler::write(const QImage &image)
|
||||
s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha)
|
||||
s << quint8(hasAlpha ? 0x24 : 0x20); // top left image (0x20) + 8 bit alpha (0x4)
|
||||
|
||||
for (int y = 0; y < img.height(); y++)
|
||||
for (int y = 0; y < img.height(); y++) {
|
||||
for (int x = 0; x < img.width(); x++) {
|
||||
const QRgb color = img.pixel(x, y);
|
||||
s << quint8(qBlue(color));
|
||||
@ -447,6 +449,7 @@ bool TGAHandler::write(const QImage &image)
|
||||
s << quint8(qAlpha(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -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);
|
||||
@ -537,6 +541,7 @@ inline QRgb qRgba(const QRgb rgb, int a)
|
||||
*/
|
||||
XCFImageFormat::XCFImageFormat()
|
||||
{
|
||||
static_assert(sizeof(QRgb) == 4, "the code assumes sizeof(QRgb) == 4, if that's not your case, help us fix it :)");
|
||||
}
|
||||
|
||||
/*!
|
||||
@ -626,12 +631,17 @@ bool XCFImageFormat::readXCF(QIODevice *device, QImage *outImage)
|
||||
QStack<qint64> layer_offsets;
|
||||
|
||||
while (true) {
|
||||
qint64 layer_offset = readOffsetPtr(xcf_io);
|
||||
const qint64 layer_offset = readOffsetPtr(xcf_io);
|
||||
|
||||
if (layer_offset == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (layer_offset < 0) {
|
||||
qCDebug(XCFPLUGIN) << "XCF: negative layer offset";
|
||||
return false;
|
||||
}
|
||||
|
||||
layer_offsets.push(layer_offset);
|
||||
}
|
||||
|
||||
@ -659,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;
|
||||
}
|
||||
@ -709,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;
|
||||
|
||||
@ -741,7 +754,9 @@ bool XCFImageFormat::loadImageProperties(QDataStream &xcf_io, XCFImage &xcf_imag
|
||||
xcf_image.palette.reserve(xcf_image.num_colors);
|
||||
|
||||
for (int i = 0; i < xcf_image.num_colors; i++) {
|
||||
uchar r, g, b;
|
||||
uchar r;
|
||||
uchar g;
|
||||
uchar b;
|
||||
property >> r >> g >> b;
|
||||
xcf_image.palette.push_back(qRgb(r, g, b));
|
||||
}
|
||||
@ -888,6 +903,16 @@ bool XCFImageFormat::loadLayer(QDataStream &xcf_io, XCFImage &xcf_image)
|
||||
layer.hierarchy_offset = readOffsetPtr(xcf_io);
|
||||
layer.mask_offset = readOffsetPtr(xcf_io);
|
||||
|
||||
if (layer.hierarchy_offset < 0) {
|
||||
qCDebug(XCFPLUGIN) << "XCF: negative layer hierarchy_offset";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (layer.mask_offset < 0) {
|
||||
qCDebug(XCFPLUGIN) << "XCF: negative layer mask_offset";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate the individual tile QImages based on the size and type
|
||||
// of this layer.
|
||||
|
||||
@ -1216,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.
|
||||
@ -1303,10 +1399,15 @@ bool XCFImageFormat::loadHierarchy(QDataStream &xcf_io, Layer &layer)
|
||||
quint32 bpp;
|
||||
|
||||
xcf_io >> width >> height >> bpp;
|
||||
qint64 offset = readOffsetPtr(xcf_io);
|
||||
const qint64 offset = readOffsetPtr(xcf_io);
|
||||
|
||||
qCDebug(XCFPLUGIN) << "width" << width << "height" << height << "bpp" << bpp << "offset" << offset;
|
||||
|
||||
if (offset < 0) {
|
||||
qCDebug(XCFPLUGIN) << "XCF: negative hierarchy offset";
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool isMask = layer.assignBytes == assignMaskBytes;
|
||||
|
||||
// make sure bpp is correct and complain if it is not
|
||||
@ -1361,6 +1462,11 @@ bool XCFImageFormat::loadHierarchy(QDataStream &xcf_io, Layer &layer)
|
||||
break;
|
||||
}
|
||||
|
||||
if (bpp > 4) {
|
||||
qCDebug(XCFPLUGIN) << "bpp is" << bpp << "We don't support layers with bpp > 4";
|
||||
return false;
|
||||
}
|
||||
|
||||
// GIMP stores images in a "mipmap"-like format (multiple levels of
|
||||
// increasingly lower resolution). Only the top level is used here,
|
||||
// however.
|
||||
@ -1402,6 +1508,11 @@ bool XCFImageFormat::loadLevel(QDataStream &xcf_io, Layer &layer, qint32 bpp)
|
||||
xcf_io >> width >> height;
|
||||
qint64 offset = readOffsetPtr(xcf_io);
|
||||
|
||||
if (offset < 0) {
|
||||
qCDebug(XCFPLUGIN) << "XCF: negative level offset";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
// offset 0 with rowsxcols != 0 is probably an error since it means we have tiles
|
||||
// without data but just clear the bits for now instead of returning false
|
||||
@ -1426,6 +1537,11 @@ bool XCFImageFormat::loadLevel(QDataStream &xcf_io, Layer &layer, qint32 bpp)
|
||||
qint64 saved_pos = xcf_io.device()->pos();
|
||||
qint64 offset2 = readOffsetPtr(xcf_io);
|
||||
|
||||
if (offset2 < 0) {
|
||||
qCDebug(XCFPLUGIN) << "XCF: negative level offset";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Evidently, RLE can occasionally expand a tile instead of compressing it!
|
||||
if (offset2 == 0) {
|
||||
offset2 = offset + (uint)(TILE_WIDTH * TILE_HEIGHT * 4 * 1.5);
|
||||
@ -1471,6 +1587,11 @@ bool XCFImageFormat::loadLevel(QDataStream &xcf_io, Layer &layer, qint32 bpp)
|
||||
|
||||
xcf_io.device()->seek(saved_pos);
|
||||
offset = readOffsetPtr(xcf_io);
|
||||
|
||||
if (offset < 0) {
|
||||
qCDebug(XCFPLUGIN) << "XCF: negative level offset";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1497,7 +1618,12 @@ bool XCFImageFormat::loadMask(QDataStream &xcf_io, Layer &layer)
|
||||
return false;
|
||||
}
|
||||
|
||||
qint64 hierarchy_offset = readOffsetPtr(xcf_io);
|
||||
const qint64 hierarchy_offset = readOffsetPtr(xcf_io);
|
||||
|
||||
if (hierarchy_offset < 0) {
|
||||
qCDebug(XCFPLUGIN) << "XCF: negative mask hierarchy_offset";
|
||||
return false;
|
||||
}
|
||||
|
||||
xcf_io.device()->seek(hierarchy_offset);
|
||||
layer.assignBytes = assignMaskBytes;
|
||||
@ -1905,11 +2031,13 @@ bool XCFImageFormat::initializeImage(XCFImage &xcf_image)
|
||||
|
||||
if (xcf_image.x_resolution > 0 && xcf_image.y_resolution > 0) {
|
||||
const float dpmx = xcf_image.x_resolution * INCHESPERMETER;
|
||||
if (dpmx > std::numeric_limits<int>::max())
|
||||
if (dpmx > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
const float dpmy = xcf_image.y_resolution * INCHESPERMETER;
|
||||
if (dpmy > std::numeric_limits<int>::max())
|
||||
if (dpmy > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
image.setDotsPerMeterX((int)dpmx);
|
||||
image.setDotsPerMeterY((int)dpmy);
|
||||
}
|
||||
@ -2593,7 +2721,8 @@ void XCFImageFormat::mergeRGBToRGB(const Layer &layer, uint i, uint j, int k, in
|
||||
src_a = qMin(src_a, dst_a);
|
||||
} break;
|
||||
case GIMP_LAYER_MODE_SOFTLIGHT_LEGACY: {
|
||||
uint tmpS, tmpM;
|
||||
uint tmpS;
|
||||
uint tmpM;
|
||||
|
||||
tmpM = INT_MULT(dst_r, src_r);
|
||||
tmpS = 255 - INT_MULT((255 - dst_r), (255 - src_r));
|
||||
@ -2661,7 +2790,10 @@ void XCFImageFormat::mergeRGBToRGB(const Layer &layer, uint i, uint j, int k, in
|
||||
src_a = INT_MULT(src_a, layer.mask_tiles[j][i].pixelIndex(k, l));
|
||||
}
|
||||
|
||||
uchar new_r, new_g, new_b, new_a;
|
||||
uchar new_r;
|
||||
uchar new_g;
|
||||
uchar new_b;
|
||||
uchar new_a;
|
||||
new_a = dst_a + INT_MULT(OPAQUE_OPACITY - dst_a, src_a);
|
||||
|
||||
const float src_ratio = new_a == 0 ? 1.0 : (float)src_a / new_a;
|
||||
@ -2767,7 +2899,8 @@ void XCFImageFormat::mergeGrayAToGray(const Layer &layer, uint i, uint j, int k,
|
||||
}
|
||||
} break;
|
||||
case SOFTLIGHT_MODE: {
|
||||
uint tmpS, tmpM;
|
||||
uint tmpS;
|
||||
uint tmpM;
|
||||
|
||||
tmpM = INT_MULT(dst, src);
|
||||
tmpS = 255 - INT_MULT((255 - dst), (255 - src));
|
||||
@ -2919,7 +3052,8 @@ void XCFImageFormat::mergeGrayAToRGB(const Layer &layer, uint i, uint j, int k,
|
||||
src_a = qMin(src_a, dst_a);
|
||||
} break;
|
||||
case SOFTLIGHT_MODE: {
|
||||
uint tmpS, tmpM;
|
||||
uint tmpS;
|
||||
uint tmpM;
|
||||
|
||||
tmpM = INT_MULT(dst, src);
|
||||
tmpS = 255 - INT_MULT((255 - dst), (255 - src));
|
||||
|
@ -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()
|
||||
|
Reference in New Issue
Block a user