Compare commits
60 Commits
Author | SHA1 | Date | |
---|---|---|---|
9b3133ac92 | |||
b0a0bb1294 | |||
3d5090593c | |||
d4966d169b | |||
bf52896347 | |||
c52ffa2227 | |||
e4e386babf | |||
b47a9d7022 | |||
2cbf815d1f | |||
6cd0056f3b | |||
83374f390e | |||
5e59d950bd | |||
de320447f6 | |||
cf375a207f | |||
2aec1d3926 | |||
2a84dd677d | |||
ebcc34519c | |||
cff2604cf9 | |||
f8a251e268 | |||
52134fc2e9 | |||
343954ca98 | |||
44fd6b7bc0 | |||
c8a0806aab | |||
bb475dedd1 | |||
9e28aae868 | |||
5c47a97b79 | |||
84d56d00cf | |||
384f78a13c | |||
72fc32aefc | |||
98f19c60ae | |||
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 |
10
.gitlab-ci.yml
Normal file
@ -0,0 +1,10 @@
|
||||
# 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
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
|
9
.kde-ci.yml
Normal file
@ -0,0 +1,9 @@
|
||||
Dependencies:
|
||||
- 'on': ['Linux', 'FreeBSD', 'macOS', 'Windows']
|
||||
'require':
|
||||
'frameworks/extra-cmake-modules': '@same'
|
||||
'frameworks/karchive' : '@same'
|
||||
|
||||
Options:
|
||||
test-before-installing: True
|
||||
require-passing-tests-on: [ 'Linux', 'FreeBSD', 'Windows' ]
|
@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16)
|
||||
project(KImageFormats)
|
||||
|
||||
include(FeatureSummary)
|
||||
find_package(ECM 5.86.0 NO_MODULE)
|
||||
find_package(ECM 5.96.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)
|
||||
|
||||
@ -13,13 +13,14 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||
include(KDEInstallDirs)
|
||||
include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE)
|
||||
include(KDECMakeSettings)
|
||||
|
||||
include(KDEGitCommitHooks)
|
||||
|
||||
|
||||
include(ECMDeprecationSettings)
|
||||
include(CheckIncludeFiles)
|
||||
include(FindPkgConfig)
|
||||
|
||||
set(REQUIRED_QT_VERSION 5.15.2)
|
||||
find_package(Qt5Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
|
||||
find_package(Qt${QT_MAJOR_VERSION}Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
|
||||
|
||||
find_package(KF5Archive)
|
||||
set_package_properties(KF5Archive PROPERTIES
|
||||
@ -31,12 +32,12 @@ set_package_properties(KF5Archive PROPERTIES
|
||||
# this available in PATH
|
||||
set(BUILD_EPS_PLUGIN FALSE)
|
||||
if (UNIX)
|
||||
find_package(Qt5PrintSupport ${REQUIRED_QT_VERSION} NO_MODULE)
|
||||
set_package_properties(Qt5PrintSupport PROPERTIES
|
||||
find_package(Qt${QT_MAJOR_VERSION}PrintSupport ${REQUIRED_QT_VERSION} NO_MODULE)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}PrintSupport PROPERTIES
|
||||
PURPOSE "Required for the QImage plugin for EPS images"
|
||||
TYPE OPTIONAL
|
||||
)
|
||||
if (Qt5PrintSupport_FOUND)
|
||||
if (TARGET Qt${QT_MAJOR_VERSION}::PrintSupport)
|
||||
set(BUILD_EPS_PLUGIN TRUE)
|
||||
endif()
|
||||
endif()
|
||||
@ -58,15 +59,22 @@ set_package_properties(libavif PROPERTIES
|
||||
|
||||
option(KIMAGEFORMATS_HEIF "Enable plugin for HEIF format" OFF)
|
||||
if(KIMAGEFORMATS_HEIF)
|
||||
include(FindPkgConfig)
|
||||
pkg_check_modules(LibHeif IMPORTED_TARGET libheif>=1.10.0)
|
||||
endif()
|
||||
add_feature_info(LibHeif LibHeif_FOUND "required for the QImage plugin for HEIF/HEIC images")
|
||||
|
||||
# 050d00 (5.13) triggers a BIC in qimageiohandler.h, in Qt 5.13, so do not enable that until we can require 5.14
|
||||
# https://codereview.qt-project.org/c/qt/qtbase/+/279215
|
||||
add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f02)
|
||||
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")
|
||||
|
||||
ecm_set_disabled_deprecation_versions(
|
||||
QT 5.15.2
|
||||
KF 5.95
|
||||
)
|
||||
|
||||
add_subdirectory(src)
|
||||
if (BUILD_TESTING)
|
||||
add_subdirectory(autotests)
|
||||
|
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.
|
@ -16,13 +16,14 @@ The following image formats have read-only support:
|
||||
- Animated Windows cursors (ani)
|
||||
- Gimp (xcf)
|
||||
- OpenEXR (exr)
|
||||
- Photoshop documents (psd)
|
||||
- Photoshop documents (psd, psb, pdd, psdt)
|
||||
- Sun Raster (ras)
|
||||
|
||||
The following image formats have read and write support:
|
||||
|
||||
- AV1 Image File Format (AVIF)
|
||||
- Encapsulated PostScript (eps)
|
||||
- JPEG XL (jxl)
|
||||
- Personal Computer Exchange (pcx)
|
||||
- SGI images (rgb, rgba, sgi, bw)
|
||||
- Softimage PIC (pic)
|
||||
|
@ -15,7 +15,7 @@ macro(kimageformats_read_tests)
|
||||
|
||||
if (NOT TARGET readtest)
|
||||
add_executable(readtest readtest.cpp)
|
||||
target_link_libraries(readtest Qt5::Gui)
|
||||
target_link_libraries(readtest Qt${QT_MAJOR_VERSION}::Gui)
|
||||
target_compile_definitions(readtest
|
||||
PRIVATE IMAGEDIR="${CMAKE_CURRENT_SOURCE_DIR}/read")
|
||||
ecm_mark_as_test(readtest)
|
||||
@ -30,23 +30,35 @@ macro(kimageformats_read_tests)
|
||||
endmacro()
|
||||
|
||||
macro(kimageformats_write_tests)
|
||||
cmake_parse_arguments(KIF_RT "" "FUZZ" "" ${ARGN})
|
||||
set(_fuzzarg)
|
||||
if (KIF_RT_FUZZ)
|
||||
set(_fuzzarg -f ${KIF_RT_FUZZ})
|
||||
endif()
|
||||
|
||||
if (NOT TARGET writetest)
|
||||
add_executable(writetest writetest.cpp)
|
||||
target_link_libraries(writetest Qt5::Gui)
|
||||
target_link_libraries(writetest Qt${QT_MAJOR_VERSION}::Gui)
|
||||
target_compile_definitions(writetest
|
||||
PRIVATE IMAGEDIR="${CMAKE_CURRENT_SOURCE_DIR}/write")
|
||||
ecm_mark_as_test(writetest)
|
||||
endif()
|
||||
foreach(_testname ${ARGN})
|
||||
foreach(_testname ${KIF_RT_UNPARSED_ARGUMENTS})
|
||||
string(REGEX MATCH "-lossless$" _is_lossless "${_testname}")
|
||||
string(REGEX MATCH "-nodatacheck" _is_no_data_check "${_testname}")
|
||||
unset(lossless_arg)
|
||||
unset(no_data_check_arg)
|
||||
if (_is_lossless)
|
||||
set(lossless_arg "--lossless")
|
||||
string(REGEX REPLACE "-lossless$" "" _testname "${_testname}")
|
||||
endif()
|
||||
if (_is_no_data_check)
|
||||
set(no_data_check_arg "--no-data-check")
|
||||
string(REGEX REPLACE "-nodatacheck$" "" _testname "${_testname}")
|
||||
endif()
|
||||
add_test(
|
||||
NAME kimageformats-write-${_testname}
|
||||
COMMAND writetest ${lossless_arg} ${_testname}
|
||||
COMMAND writetest ${lossless_arg} ${no_data_check_arg} ${_fuzzarg} ${_testname}
|
||||
)
|
||||
endforeach(_testname)
|
||||
endmacro()
|
||||
@ -74,12 +86,28 @@ if (TARGET avif)
|
||||
kimageformats_read_tests(
|
||||
avif
|
||||
)
|
||||
kimageformats_write_tests(
|
||||
avif-nodatacheck-lossless
|
||||
)
|
||||
endif()
|
||||
|
||||
if (LibHeif_FOUND)
|
||||
kimageformats_read_tests(
|
||||
heif
|
||||
)
|
||||
# because the plug-ins use RGB->YUV conversion which sometimes results in 1 value difference.
|
||||
kimageformats_write_tests(FUZZ 1
|
||||
heif-nodatacheck-lossless
|
||||
)
|
||||
endif()
|
||||
|
||||
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
|
||||
kimageformats_read_tests(
|
||||
jxl
|
||||
)
|
||||
kimageformats_write_tests(
|
||||
jxl-nodatacheck-lossless
|
||||
)
|
||||
endif()
|
||||
|
||||
# Allow some fuzziness when reading this formats, to allow for
|
||||
@ -111,19 +139,19 @@ if (OpenEXR_FOUND)
|
||||
# FIXME: OpenEXR tests
|
||||
endif()
|
||||
|
||||
find_package(Qt5Test ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
find_package(Qt${QT_MAJOR_VERSION}Test ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
|
||||
if(NOT Qt5Test_FOUND)
|
||||
message(STATUS "Qt5Test not found, some autotests will not be built.")
|
||||
if(NOT TARGET Qt${QT_MAJOR_VERSION}::Test)
|
||||
message(STATUS "Qt${QT_MAJOR_VERSION}Test not found, some autotests will not be built.")
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_executable(pictest pictest.cpp)
|
||||
target_link_libraries(pictest Qt5::Gui Qt5::Test)
|
||||
target_link_libraries(pictest Qt${QT_MAJOR_VERSION}::Gui Qt${QT_MAJOR_VERSION}::Test)
|
||||
ecm_mark_as_test(pictest)
|
||||
add_test(NAME kimageformats-pic COMMAND pictest)
|
||||
|
||||
add_executable(anitest anitest.cpp)
|
||||
target_link_libraries(anitest Qt5::Gui Qt5::Test)
|
||||
target_link_libraries(anitest Qt${QT_MAJOR_VERSION}::Gui Qt${QT_MAJOR_VERSION}::Test)
|
||||
ecm_mark_as_test(anitest)
|
||||
add_test(NAME kimageformats-ani COMMAND anitest)
|
||||
|
37
autotests/fuzzyeq.cpp
Normal file
@ -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.png
Normal file
After Width: | Height: | Size: 528 KiB |
BIN
autotests/read/jxl/rgba.jxl
Normal file
BIN
autotests/read/jxl/rgba.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
autotests/read/psd/16bit_grayscale.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
autotests/read/psd/16bit_grayscale.psd
Normal file
BIN
autotests/read/psd/16bit_photoshop.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
autotests/read/psd/16bit_photoshop.psb
Normal file
BIN
autotests/read/psd/32bit-rgb.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
autotests/read/psd/32bit-rgb.psd
Normal file
BIN
autotests/read/psd/32bit_grayscale.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
autotests/read/psd/32bit_grayscale.psd
Normal file
BIN
autotests/read/psd/8bit-grayscale.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
autotests/read/psd/8bit-grayscale.psd
Normal file
BIN
autotests/read/psd/8bit-photoshop.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
autotests/read/psd/8bit-photoshop.psb
Normal file
BIN
autotests/read/psd/adobehq-2_5.png
Normal file
After Width: | Height: | Size: 298 KiB |
BIN
autotests/read/psd/adobehq-2_5.psd
Normal file
BIN
autotests/read/psd/birthday.pdd
Normal file
BIN
autotests/read/psd/birthday.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
autotests/read/psd/bitmap.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
autotests/read/psd/bitmap.psd
Normal file
BIN
autotests/read/psd/cmyka-16bits.png
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
autotests/read/psd/cmyka-16bits.psd
Normal file
BIN
autotests/read/psd/cmyka-8bits.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
autotests/read/psd/cmyka-8bits.psd
Normal file
BIN
autotests/read/psd/duotone.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
autotests/read/psd/duotone.psb
Normal file
BIN
autotests/read/psd/indexed.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
autotests/read/psd/indexed.psd
Normal file
BIN
autotests/read/xcf/fruktpilot_icc.png
Normal file
After Width: | Height: | Size: 115 KiB |
BIN
autotests/read/xcf/fruktpilot_icc.xcf
Normal file
@ -16,6 +16,8 @@
|
||||
|
||||
#include "../tests/format-enum.h"
|
||||
|
||||
#include "fuzzyeq.cpp"
|
||||
|
||||
static void writeImageData(const char *name, const QString &filename, const QImage &image)
|
||||
{
|
||||
QFile file(filename);
|
||||
@ -31,38 +33,6 @@ static void writeImageData(const char *name, const QString &filename, const QIma
|
||||
}
|
||||
}
|
||||
|
||||
template<class Trait>
|
||||
static bool fuzzyeq(const QImage &im1, const QImage &im2, uchar fuzziness)
|
||||
{
|
||||
Q_ASSERT(im1.format() == im2.format());
|
||||
Q_ASSERT(im1.depth() == 24 || im1.depth() == 32 || im1.depth() == 64);
|
||||
|
||||
const int height = im1.height();
|
||||
const int width = im1.width();
|
||||
for (int i = 0; i < height; ++i) {
|
||||
const Trait *line1 = reinterpret_cast<const Trait *>(im1.scanLine(i));
|
||||
const Trait *line2 = reinterpret_cast<const Trait *>(im2.scanLine(i));
|
||||
for (int j = 0; j < width; ++j) {
|
||||
if (line1[j] > line2[j]) {
|
||||
if (line1[j] - line2[j] > fuzziness) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (line2[j] - line1[j] > fuzziness) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// allow each byte to be different by up to 1, to allow for rounding errors
|
||||
static bool fuzzyeq(const QImage &im1, const QImage &im2, uchar fuzziness)
|
||||
{
|
||||
return (im1.depth() == 64) ? fuzzyeq<quint16>(im1, im2, fuzziness) : fuzzyeq<quint8>(im1, im2, fuzziness);
|
||||
}
|
||||
|
||||
// Returns the original format if we support, or returns
|
||||
// format which we preferred to use for `fuzzyeq()`.
|
||||
// We do only support formats with 8-bits/16-bits pre pixel.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
function(kimageformats_add_plugin plugin)
|
||||
set(options)
|
||||
set(oneValueArgs)
|
||||
set(multiValueArgs SOURCES)
|
||||
cmake_parse_arguments(KIF_ADD_PLUGIN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
||||
if(NOT KIF_ADD_PLUGIN_SOURCES)
|
||||
@ -12,41 +13,41 @@ function(kimageformats_add_plugin plugin)
|
||||
|
||||
add_library(${plugin} MODULE ${KIF_ADD_PLUGIN_SOURCES})
|
||||
set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/imageformats")
|
||||
target_link_libraries(${plugin} Qt5::Gui)
|
||||
target_link_libraries(${plugin} Qt${QT_MAJOR_VERSION}::Gui)
|
||||
install(TARGETS ${plugin} DESTINATION ${KDE_INSTALL_QTPLUGINDIR}/imageformats)
|
||||
endfunction()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_ani SOURCES ani.cpp)
|
||||
install(FILES ani.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES ani.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
if (TARGET avif)
|
||||
kimageformats_add_plugin(kimg_avif SOURCES "avif.cpp")
|
||||
target_link_libraries(kimg_avif "avif")
|
||||
install(FILES avif.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES avif.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
endif()
|
||||
|
||||
##################################
|
||||
|
||||
install(FILES dds-qt.desktop RENAME dds.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES dds-qt.desktop RENAME dds.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
if (BUILD_EPS_PLUGIN)
|
||||
if (Qt5PrintSupport_FOUND)
|
||||
if (TARGET Qt${QT_MAJOR_VERSION}::PrintSupport)
|
||||
kimageformats_add_plugin(kimg_eps SOURCES eps.cpp)
|
||||
target_link_libraries(kimg_eps Qt5::PrintSupport)
|
||||
install(FILES eps.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
target_link_libraries(kimg_eps Qt${QT_MAJOR_VERSION}::PrintSupport)
|
||||
install(FILES eps.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
##################################
|
||||
|
||||
# need this for Qt's version of the plugin
|
||||
install(FILES jp2.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES jp2.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
@ -64,13 +65,13 @@ if(OpenEXR_FOUND)
|
||||
endif()
|
||||
kde_target_enable_exceptions(kimg_exr PRIVATE)
|
||||
|
||||
install(FILES exr.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES exr.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
endif()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_hdr SOURCES hdr.cpp)
|
||||
install(FILES hdr.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES hdr.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
@ -78,43 +79,54 @@ if (LibHeif_FOUND)
|
||||
kimageformats_add_plugin(kimg_heif SOURCES heif.cpp)
|
||||
target_link_libraries(kimg_heif PkgConfig::LibHeif)
|
||||
kde_target_enable_exceptions(kimg_heif PRIVATE)
|
||||
install(FILES heif.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES heif.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
endif()
|
||||
|
||||
##################################
|
||||
|
||||
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
|
||||
kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp)
|
||||
target_link_libraries(kimg_jxl PkgConfig::LibJXL PkgConfig::LibJXLThreads)
|
||||
if (LibJXL_VERSION VERSION_GREATER_EQUAL "0.7.0")
|
||||
target_compile_definitions(kimg_jxl PRIVATE KIMG_JXL_API_VERSION=70)
|
||||
endif()
|
||||
install(FILES jxl.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
endif()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_pcx SOURCES pcx.cpp)
|
||||
install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_pic SOURCES pic.cpp)
|
||||
install(FILES pic.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES pic.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_psd SOURCES psd.cpp)
|
||||
install(FILES psd.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES psd.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_ras SOURCES ras.cpp)
|
||||
install(FILES ras.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES ras.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_rgb SOURCES rgb.cpp)
|
||||
install(FILES rgb.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES rgb.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_tga SOURCES tga.cpp)
|
||||
install(FILES tga.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES tga.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_xcf SOURCES xcf.cpp)
|
||||
install(FILES xcf.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES xcf.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
##################################
|
||||
|
||||
@ -122,10 +134,10 @@ if (KF5Archive_FOUND)
|
||||
|
||||
kimageformats_add_plugin(kimg_kra SOURCES kra.cpp)
|
||||
target_link_libraries(kimg_kra KF5::Archive)
|
||||
install(FILES kra.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES kra.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
kimageformats_add_plugin(kimg_ora SOURCES ora.cpp)
|
||||
target_link_libraries(kimg_ora KF5::Archive)
|
||||
install(FILES ora.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
install(FILES ora.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
||||
endif()
|
||||
|
@ -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") {
|
||||
|
@ -67,7 +67,7 @@ bool QAVIFHandler::canRead(QIODevice *device)
|
||||
|
||||
bool QAVIFHandler::ensureParsed() const
|
||||
{
|
||||
if (m_parseState == ParseAvifSuccess) {
|
||||
if (m_parseState == ParseAvifSuccess || m_parseState == ParseAvifMetadata) {
|
||||
return true;
|
||||
}
|
||||
if (m_parseState == ParseAvifError) {
|
||||
@ -79,6 +79,28 @@ bool QAVIFHandler::ensureParsed() const
|
||||
return that->ensureDecoder();
|
||||
}
|
||||
|
||||
bool QAVIFHandler::ensureOpened() const
|
||||
{
|
||||
if (m_parseState == ParseAvifSuccess) {
|
||||
return true;
|
||||
}
|
||||
if (m_parseState == ParseAvifError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QAVIFHandler *that = const_cast<QAVIFHandler *>(this);
|
||||
if (ensureParsed()) {
|
||||
if (m_parseState == ParseAvifMetadata) {
|
||||
bool success = that->jumpToNextImage();
|
||||
that->m_parseState = success ? ParseAvifSuccess : ParseAvifError;
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
that->m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::ensureDecoder()
|
||||
{
|
||||
if (m_decoder) {
|
||||
@ -97,6 +119,13 @@ bool QAVIFHandler::ensureDecoder()
|
||||
|
||||
m_decoder = avifDecoderCreate();
|
||||
|
||||
m_decoder->ignoreExif = AVIF_TRUE;
|
||||
m_decoder->ignoreXMP = AVIF_TRUE;
|
||||
|
||||
#if AVIF_VERSION >= 80400
|
||||
m_decoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
|
||||
#endif
|
||||
|
||||
#if AVIF_VERSION >= 90100
|
||||
m_decoder->strictFlags = AVIF_STRICT_DISABLED;
|
||||
#endif
|
||||
@ -123,39 +152,58 @@ bool QAVIFHandler::ensureDecoder()
|
||||
return false;
|
||||
}
|
||||
|
||||
decodeResult = avifDecoderNextImage(m_decoder);
|
||||
m_container_width = m_decoder->image->width;
|
||||
m_container_height = m_decoder->image->height;
|
||||
|
||||
if (decodeResult == AVIF_RESULT_OK) {
|
||||
m_container_width = m_decoder->image->width;
|
||||
m_container_height = m_decoder->image->height;
|
||||
|
||||
if ((m_container_width > 32768) || (m_container_height > 32768)) {
|
||||
qWarning("AVIF image (%dx%d) is too large!", m_container_width, m_container_height);
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((m_container_width == 0) || (m_container_height == 0)) {
|
||||
qWarning("Empty image, nothing to decode");
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_parseState = ParseAvifSuccess;
|
||||
if (decode_one_frame()) {
|
||||
return true;
|
||||
} else {
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
qWarning("ERROR: Failed to decode image: %s", avifResultToString(decodeResult));
|
||||
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;
|
||||
}
|
||||
|
||||
avifDecoderDestroy(m_decoder);
|
||||
m_decoder = nullptr;
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
if ((m_container_width == 0) || (m_container_height == 0)) {
|
||||
qWarning("Empty image, nothing to decode");
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// calculate final dimensions with crop and rotate operations applied
|
||||
int new_width = m_container_width;
|
||||
int new_height = m_container_height;
|
||||
|
||||
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 crop_width = (int)((double)(m_decoder->image->clap.widthN) / (m_decoder->image->clap.widthD) + 0.5);
|
||||
if (crop_width < new_width && crop_width > 0) {
|
||||
new_width = crop_width;
|
||||
}
|
||||
int crop_height = (int)((double)(m_decoder->image->clap.heightN) / (m_decoder->image->clap.heightD) + 0.5);
|
||||
if (crop_height < new_height && crop_height > 0) {
|
||||
new_height = crop_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_decoder->image->transformFlags & AVIF_TRANSFORM_IROT) {
|
||||
if (m_decoder->image->irot.angle == 1 || m_decoder->image->irot.angle == 3) {
|
||||
int tmp = new_width;
|
||||
new_width = new_height;
|
||||
new_height = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
m_estimated_dimensions.setWidth(new_width);
|
||||
m_estimated_dimensions.setHeight(new_height);
|
||||
|
||||
m_parseState = ParseAvifMetadata;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::decode_one_frame()
|
||||
@ -182,9 +230,9 @@ bool QAVIFHandler::decode_one_frame()
|
||||
}
|
||||
} else {
|
||||
if (loadalpha) {
|
||||
resultformat = QImage::Format_RGBA8888;
|
||||
resultformat = QImage::Format_ARGB32;
|
||||
} else {
|
||||
resultformat = QImage::Format_RGB888;
|
||||
resultformat = QImage::Format_RGB32;
|
||||
}
|
||||
}
|
||||
QImage result(m_decoder->image->width, m_decoder->image->height, resultformat);
|
||||
@ -275,26 +323,26 @@ bool QAVIFHandler::decode_one_frame()
|
||||
rgb.depth = 16;
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
|
||||
if (!loadalpha) {
|
||||
rgb.ignoreAlpha = AVIF_TRUE;
|
||||
result.fill(Qt::black);
|
||||
if (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
|
||||
resultformat = QImage::Format_Grayscale16;
|
||||
}
|
||||
if (!loadalpha && (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) {
|
||||
resultformat = QImage::Format_Grayscale16;
|
||||
}
|
||||
} else {
|
||||
rgb.depth = 8;
|
||||
if (loadalpha) {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
resultformat = QImage::Format_ARGB32;
|
||||
} else {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGB;
|
||||
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||||
rgb.format = AVIF_RGB_FORMAT_BGRA;
|
||||
#else
|
||||
rgb.format = AVIF_RGB_FORMAT_ARGB;
|
||||
#endif
|
||||
|
||||
if (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
|
||||
resultformat = QImage::Format_Grayscale8;
|
||||
} else {
|
||||
resultformat = QImage::Format_RGB32;
|
||||
}
|
||||
#if AVIF_VERSION >= 80400
|
||||
if (m_decoder->imageCount > 1) {
|
||||
/* accelerate animated AVIF */
|
||||
rgb.chromaUpsampling = AVIF_CHROMA_UPSAMPLING_FASTEST;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!loadalpha && (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) {
|
||||
resultformat = QImage::Format_Grayscale8;
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,27 +358,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)) {
|
||||
@ -385,13 +433,15 @@ bool QAVIFHandler::decode_one_frame()
|
||||
m_current_image = result.convertToFormat(resultformat);
|
||||
}
|
||||
|
||||
m_estimated_dimensions = m_current_image.size();
|
||||
|
||||
m_must_jump_to_next_image = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::read(QImage *image)
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
if (!ensureOpened()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -409,15 +459,44 @@ bool QAVIFHandler::read(QImage *image)
|
||||
bool QAVIFHandler::write(const QImage &image)
|
||||
{
|
||||
if (image.format() == QImage::Format_Invalid) {
|
||||
qWarning("No image data to save");
|
||||
qWarning("No image data to save!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((image.width() > 32768) || (image.height() > 32768)) {
|
||||
qWarning("Image is too large");
|
||||
if ((image.width() > 0) && (image.height() > 0)) {
|
||||
if ((image.width() > 65535) || (image.height() > 65535)) {
|
||||
qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (image.width() > ((16384 * 16384) / image.height())) {
|
||||
qWarning("Image (%dx%d) will not be saved because it has more than 256 megapixels!", image.width(), image.height());
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((image.width() > 32768) || (image.height() > 32768)) {
|
||||
qWarning("Image (%dx%d) has a dimension above 32768 pixels, saved AVIF may not work in other software!", image.width(), image.height());
|
||||
}
|
||||
} else {
|
||||
qWarning("Image has zero dimension!");
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *encoder_name = avifCodecName(AVIF_CODEC_CHOICE_AUTO, AVIF_CODEC_FLAG_CAN_ENCODE);
|
||||
if (!encoder_name) {
|
||||
qWarning("Cannot save AVIF images because libavif was built without AV1 encoders!");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool lossless = false;
|
||||
if (m_quality >= 100) {
|
||||
if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE)) {
|
||||
lossless = true;
|
||||
} else {
|
||||
qWarning("You are using %s encoder. It is recommended to enable libAOM encoder in libavif to use lossless compression.", encoder_name);
|
||||
}
|
||||
}
|
||||
|
||||
int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100;
|
||||
int minQuantizer = 0;
|
||||
int maxQuantizerAlpha = 0;
|
||||
@ -610,43 +689,47 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
|
||||
// in case primaries or trc were not identified
|
||||
if ((primaries_to_save == 2) || (transfer_to_save == 2)) {
|
||||
// upgrade image to higher bit depth
|
||||
if (save_depth == 8) {
|
||||
save_depth = 10;
|
||||
if (tmpcolorimage.hasAlphaChannel()) {
|
||||
tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBA64);
|
||||
} else {
|
||||
tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBX64);
|
||||
if (lossless) {
|
||||
iccprofile = tmpcolorimage.colorSpace().iccProfile();
|
||||
} else {
|
||||
// upgrade image to higher bit depth
|
||||
if (save_depth == 8) {
|
||||
save_depth = 10;
|
||||
if (tmpcolorimage.hasAlphaChannel()) {
|
||||
tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBA64);
|
||||
} else {
|
||||
tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBX64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((primaries_to_save == 2) && (transfer_to_save != 2)) { // other primaries but known trc
|
||||
primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709
|
||||
matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709
|
||||
if ((primaries_to_save == 2) && (transfer_to_save != 2)) { // other primaries but known trc
|
||||
primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709
|
||||
matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709
|
||||
|
||||
switch (transfer_to_save) {
|
||||
case 8: // AVIF_TRANSFER_CHARACTERISTICS_LINEAR
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::Linear));
|
||||
break;
|
||||
case 4: // AVIF_TRANSFER_CHARACTERISTICS_BT470M
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.2f));
|
||||
break;
|
||||
case 5: // AVIF_TRANSFER_CHARACTERISTICS_BT470BG
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.8f));
|
||||
break;
|
||||
default: // AVIF_TRANSFER_CHARACTERISTICS_SRGB + any other
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb));
|
||||
switch (transfer_to_save) {
|
||||
case 8: // AVIF_TRANSFER_CHARACTERISTICS_LINEAR
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::Linear));
|
||||
break;
|
||||
case 4: // AVIF_TRANSFER_CHARACTERISTICS_BT470M
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.2f));
|
||||
break;
|
||||
case 5: // AVIF_TRANSFER_CHARACTERISTICS_BT470BG
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.8f));
|
||||
break;
|
||||
default: // AVIF_TRANSFER_CHARACTERISTICS_SRGB + any other
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb));
|
||||
transfer_to_save = (avifTransferCharacteristics)13;
|
||||
break;
|
||||
}
|
||||
} else if ((primaries_to_save != 2) && (transfer_to_save == 2)) { // recognized primaries but other trc
|
||||
transfer_to_save = (avifTransferCharacteristics)13;
|
||||
break;
|
||||
tmpcolorimage.convertToColorSpace(tmpcolorimage.colorSpace().withTransferFunction(QColorSpace::TransferFunction::SRgb));
|
||||
} else { // unrecognized profile
|
||||
primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709
|
||||
transfer_to_save = (avifTransferCharacteristics)13;
|
||||
matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb));
|
||||
}
|
||||
} else if ((primaries_to_save != 2) && (transfer_to_save == 2)) { // recognized primaries but other trc
|
||||
transfer_to_save = (avifTransferCharacteristics)13;
|
||||
tmpcolorimage.convertToColorSpace(tmpcolorimage.colorSpace().withTransferFunction(QColorSpace::TransferFunction::SRgb));
|
||||
} else { // unrecognized profile
|
||||
primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709
|
||||
transfer_to_save = (avifTransferCharacteristics)13;
|
||||
matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709
|
||||
tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb));
|
||||
}
|
||||
}
|
||||
} else { // profile is unsupported by Qt
|
||||
@ -656,6 +739,9 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
}
|
||||
}
|
||||
|
||||
if (lossless && pixel_format == AVIF_PIXEL_FORMAT_YUV444) {
|
||||
matrix_to_save = (avifMatrixCoefficients)0;
|
||||
}
|
||||
avif = avifImageCreate(tmpcolorimage.width(), tmpcolorimage.height(), save_depth, pixel_format);
|
||||
avif->matrixCoefficients = matrix_to_save;
|
||||
|
||||
@ -674,9 +760,7 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
if (save_depth > 8) { // 10bit depth
|
||||
rgb.depth = 16;
|
||||
|
||||
if (tmpcolorimage.hasAlphaChannel()) {
|
||||
avif->alphaRange = AVIF_RANGE_FULL;
|
||||
} else {
|
||||
if (!tmpcolorimage.hasAlphaChannel()) {
|
||||
rgb.ignoreAlpha = AVIF_TRUE;
|
||||
}
|
||||
|
||||
@ -686,7 +770,6 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
|
||||
if (tmpcolorimage.hasAlphaChannel()) {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
avif->alphaRange = AVIF_RANGE_FULL;
|
||||
} else {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGB;
|
||||
}
|
||||
@ -710,7 +793,7 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
encoder->maxQuantizerAlpha = maxQuantizerAlpha;
|
||||
}
|
||||
|
||||
encoder->speed = 8;
|
||||
encoder->speed = 6;
|
||||
|
||||
res = avifEncoderWrite(encoder, avif, &raw);
|
||||
avifEncoderDestroy(encoder);
|
||||
@ -745,7 +828,7 @@ QVariant QAVIFHandler::option(ImageOption option) const
|
||||
|
||||
switch (option) {
|
||||
case Size:
|
||||
return m_current_image.size();
|
||||
return m_estimated_dimensions;
|
||||
case Animation:
|
||||
if (imageCount() >= 2) {
|
||||
return true;
|
||||
@ -801,6 +884,14 @@ int QAVIFHandler::currentImageNumber() const
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (m_parseState == ParseAvifMetadata) {
|
||||
if (m_decoder->imageCount >= 2) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return m_decoder->imageIndex;
|
||||
}
|
||||
|
||||
@ -810,12 +901,14 @@ bool QAVIFHandler::jumpToNextImage()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_decoder->imageCount < 2) {
|
||||
return true;
|
||||
}
|
||||
if (m_decoder->imageIndex >= 0) {
|
||||
if (m_decoder->imageCount < 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning
|
||||
avifDecoderReset(m_decoder);
|
||||
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning
|
||||
avifDecoderReset(m_decoder);
|
||||
}
|
||||
}
|
||||
|
||||
avifResult decodeResult = avifDecoderNextImage(m_decoder);
|
||||
@ -838,6 +931,7 @@ bool QAVIFHandler::jumpToNextImage()
|
||||
}
|
||||
|
||||
if (decode_one_frame()) {
|
||||
m_parseState = ParseAvifSuccess;
|
||||
return true;
|
||||
} else {
|
||||
m_parseState = ParseAvifError;
|
||||
@ -853,7 +947,7 @@ bool QAVIFHandler::jumpToImage(int imageNumber)
|
||||
|
||||
if (m_decoder->imageCount < 2) { // not an animation
|
||||
if (imageNumber == 0) {
|
||||
return true;
|
||||
return ensureOpened();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@ -863,7 +957,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;
|
||||
}
|
||||
|
||||
@ -887,6 +982,7 @@ bool QAVIFHandler::jumpToImage(int imageNumber)
|
||||
}
|
||||
|
||||
if (decode_one_frame()) {
|
||||
m_parseState = ParseAvifSuccess;
|
||||
return true;
|
||||
} else {
|
||||
m_parseState = ParseAvifError;
|
||||
@ -896,7 +992,7 @@ bool QAVIFHandler::jumpToImage(int imageNumber)
|
||||
|
||||
int QAVIFHandler::nextImageDelay() const
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
if (!ensureOpened()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"Keys": [ "avif", "avifs" ],
|
||||
"MimeTypes": [ "image/avif", "image/avif-sequence" ]
|
||||
"MimeTypes": [ "image/avif", "image/avif" ]
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include <QImage>
|
||||
#include <QImageIOPlugin>
|
||||
#include <QPointF>
|
||||
#include <QSize>
|
||||
#include <QVariant>
|
||||
#include <avif/avif.h>
|
||||
#include <qimageiohandler.h>
|
||||
@ -45,6 +46,7 @@ public:
|
||||
private:
|
||||
static QPointF CompatibleChromacity(qreal chrX, qreal chrY);
|
||||
bool ensureParsed() const;
|
||||
bool ensureOpened() const;
|
||||
bool ensureDecoder();
|
||||
bool decode_one_frame();
|
||||
|
||||
@ -52,6 +54,7 @@ private:
|
||||
ParseAvifError = -1,
|
||||
ParseAvifNotParsed = 0,
|
||||
ParseAvifSuccess = 1,
|
||||
ParseAvifMetadata = 2,
|
||||
};
|
||||
|
||||
ParseAvifState m_parseState;
|
||||
@ -59,6 +62,7 @@ private:
|
||||
|
||||
uint32_t m_container_width;
|
||||
uint32_t m_container_height;
|
||||
QSize m_estimated_dimensions;
|
||||
|
||||
QByteArray m_rawData;
|
||||
avifROData m_rawAvifData;
|
||||
|
@ -15,6 +15,7 @@
|
||||
#include <QPainter>
|
||||
#include <QPrinter>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
// logging category for this framework, default: log stuff >= warning
|
||||
@ -176,6 +177,12 @@ bool EPSHandler::read(QImage *image)
|
||||
|
||||
// create GS command line
|
||||
|
||||
const QString gsExec = QStandardPaths::findExecutable(QStringLiteral("gs"));
|
||||
if (gsExec.isEmpty()) {
|
||||
qCWarning(EPSPLUGIN) << "Couldn't find gs exectuable (from GhostScript) in PATH.";
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList gsArgs;
|
||||
gsArgs << QLatin1String("-sOutputFile=") + tmpFile.fileName() << QStringLiteral("-q") << QStringLiteral("-g%1x%2").arg(wantedWidth).arg(wantedHeight)
|
||||
<< QStringLiteral("-dSAFER") << QStringLiteral("-dPARANOIDSAFER") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-sDEVICE=ppm")
|
||||
@ -192,7 +199,7 @@ bool EPSHandler::read(QImage *image)
|
||||
|
||||
QProcess converter;
|
||||
converter.setProcessChannelMode(QProcess::ForwardedErrorChannel);
|
||||
converter.start(QStringLiteral("gs"), gsArgs);
|
||||
converter.start(gsExec, gsArgs);
|
||||
if (!converter.waitForStarted(3000)) {
|
||||
qCWarning(EPSPLUGIN) << "Reading EPS files requires gs (from GhostScript)";
|
||||
return false;
|
||||
|
1000
src/imageformats/jxl.cpp
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
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Keys": [ "jxl" ],
|
||||
"MimeTypes": [ "image/jxl" ]
|
||||
}
|
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
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"Keys": [ "psd" ],
|
||||
"MimeTypes": [ "image/vnd.adobe.photoshop" ]
|
||||
"Keys": [ "psd", "psb", "pdd", "psdt" ],
|
||||
"MimeTypes": [ "image/vnd.adobe.photoshop", "image/vnd.adobe.photoshop", "image/vnd.adobe.photoshop", "image/vnd.adobe.photoshop" ]
|
||||
}
|
||||
|
@ -18,6 +18,9 @@ public:
|
||||
bool canRead() const override;
|
||||
bool read(QImage *image) override;
|
||||
|
||||
bool supportsOption(QImageIOHandler::ImageOption option) const override;
|
||||
QVariant option(QImageIOHandler::ImageOption option) const override;
|
||||
|
||||
static bool canRead(QIODevice *device);
|
||||
};
|
||||
|
||||
|
@ -9,6 +9,8 @@
|
||||
|
||||
#include "ras_p.h"
|
||||
|
||||
#include "util_p.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
#include <QImage>
|
||||
@ -102,8 +104,7 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
{
|
||||
s.device()->seek(RasHeader::SIZE);
|
||||
|
||||
// QVector uses some extra space for stuff, hence the 32 here suggested by thiago
|
||||
if (ras.ColorMapLength > std::numeric_limits<int>::max() - 32) {
|
||||
if (ras.ColorMapLength > kMaxQVectorSize) {
|
||||
qWarning() << "LoadRAS() unsupported image color map length in file header" << ras.ColorMapLength;
|
||||
return false;
|
||||
}
|
||||
@ -127,8 +128,7 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
|
||||
qWarning() << "LoadRAS() mistmatch between height and width" << ras.Width << ras.Height << ras.Length << ras.Depth;
|
||||
return false;
|
||||
}
|
||||
// QVector uses some extra space for stuff, hence the 32 here suggested by thiago
|
||||
if (ras.Length > std::numeric_limits<int>::max() - 32) {
|
||||
if (ras.Length > kMaxQVectorSize) {
|
||||
qWarning() << "LoadRAS() unsupported image length in file header" << ras.Length;
|
||||
return false;
|
||||
}
|
||||
|
10
src/imageformats/util_p.h
Normal file
@ -0,0 +1,10 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2022 Albert Astals Cid <aacid@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include <limits>
|
||||
|
||||
// QVector uses some extra space for stuff, hence the 32 here suggested by Thiago Macieira
|
||||
static constexpr int kMaxQVectorSize = std::numeric_limits<int>::max() - 32;
|
@ -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"
|
||||
@ -134,7 +135,7 @@ public:
|
||||
PROP_SAMPLE_POINTS = 39,
|
||||
MAX_SUPPORTED_PROPTYPE, // should always be at the end so its value is last + 1
|
||||
};
|
||||
Q_ENUM(PropType);
|
||||
Q_ENUM(PropType)
|
||||
|
||||
//! Compression type used in layer tiles.
|
||||
enum XcfCompressionType {
|
||||
@ -144,7 +145,7 @@ public:
|
||||
COMPRESS_ZLIB = 2, /* unused */
|
||||
COMPRESS_FRACTAL = 3, /* unused */
|
||||
};
|
||||
Q_ENUM(XcfCompressionType);
|
||||
Q_ENUM(XcfCompressionType)
|
||||
|
||||
enum LayerModeType {
|
||||
GIMP_LAYER_MODE_NORMAL_LEGACY,
|
||||
@ -211,7 +212,7 @@ public:
|
||||
GIMP_LAYER_MODE_PASS_THROUGH,
|
||||
GIMP_LAYER_MODE_COUNT,
|
||||
};
|
||||
Q_ENUM(LayerModeType);
|
||||
Q_ENUM(LayerModeType)
|
||||
|
||||
//! Type of individual layers in an XCF file.
|
||||
enum GimpImageType {
|
||||
@ -222,7 +223,7 @@ public:
|
||||
INDEXED_GIMAGE,
|
||||
INDEXEDA_GIMAGE,
|
||||
};
|
||||
Q_ENUM(GimpImageType);
|
||||
Q_ENUM(GimpImageType)
|
||||
|
||||
//! Type of individual layers in an XCF file.
|
||||
enum GimpColorSpace {
|
||||
@ -351,6 +352,8 @@ private:
|
||||
bool initialized; //!< Is the QImage initialized?
|
||||
QImage image; //!< final QImage
|
||||
|
||||
QHash<QString,QByteArray> parasites; //!< parasites data
|
||||
|
||||
XCFImage(void)
|
||||
: initialized(false)
|
||||
{
|
||||
@ -405,6 +408,7 @@ private:
|
||||
bool composeTiles(XCFImage &xcf_image);
|
||||
void setGrayPalette(QImage &image);
|
||||
void setPalette(XCFImage &xcf_image, QImage &image);
|
||||
void setImageParasites(const XCFImage &xcf_image, QImage &image);
|
||||
static void assignImageBytes(Layer &layer, uint i, uint j);
|
||||
bool loadHierarchy(QDataStream &xcf_io, Layer &layer);
|
||||
bool loadLevel(QDataStream &xcf_io, Layer &layer, qint32 bpp);
|
||||
@ -665,6 +669,9 @@ bool XCFImageFormat::readXCF(QIODevice *device, QImage *outImage)
|
||||
return false;
|
||||
}
|
||||
|
||||
// The image was created: now I can set metadata and ICC color profile inside it.
|
||||
setImageParasites(xcf_image, xcf_image.image);
|
||||
|
||||
*outImage = xcf_image.image;
|
||||
return true;
|
||||
}
|
||||
@ -715,15 +722,15 @@ bool XCFImageFormat::loadImageProperties(QDataStream &xcf_io, XCFImage &xcf_imag
|
||||
property.readBytes(tag, size);
|
||||
|
||||
quint32 flags;
|
||||
char *data = nullptr;
|
||||
QByteArray data;
|
||||
property >> flags >> data;
|
||||
|
||||
if (tag && strncmp(tag, "gimp-comment", strlen("gimp-comment")) == 0) {
|
||||
xcf_image.image.setText(QStringLiteral("Comment"), QString::fromUtf8(data));
|
||||
}
|
||||
// WARNING: you cannot add metadata to QImage here because it can be null.
|
||||
// Adding a metadata to a QImage when it is null, does nothing (metas are lost).
|
||||
if(tag) // store metadata for future use
|
||||
xcf_image.parasites.insert(QString::fromUtf8(tag), data);
|
||||
|
||||
delete[] tag;
|
||||
delete[] data;
|
||||
}
|
||||
break;
|
||||
|
||||
@ -1234,6 +1241,77 @@ void XCFImageFormat::setPalette(XCFImage &xcf_image, QImage &image)
|
||||
image.setColorTable(xcf_image.palette);
|
||||
}
|
||||
|
||||
/*!
|
||||
* Copy the parasites info to QImage.
|
||||
* \param xcf_image XCF image containing the parasites read from the data stream.
|
||||
* \param image image to apply the parasites data.
|
||||
* \note Some comment taken from https://gitlab.gnome.org/GNOME/gimp/-/blob/master/devel-docs/parasites.txt
|
||||
*/
|
||||
void XCFImageFormat::setImageParasites(const XCFImage &xcf_image, QImage &image)
|
||||
{
|
||||
auto&& p = xcf_image.parasites;
|
||||
auto keys = p.keys();
|
||||
for (auto&& key : qAsConst(keys)) {
|
||||
auto value = p.value(key);
|
||||
if(value.isEmpty())
|
||||
continue;
|
||||
|
||||
// "icc-profile" (IMAGE, PERSISTENT | UNDOABLE)
|
||||
// This contains an ICC profile describing the color space the
|
||||
// image was produced in. TIFF images stored in PhotoShop do
|
||||
// oftentimes contain embedded profiles. An experimental color
|
||||
// manager exists to use this parasite, and it will be used
|
||||
// for interchange between TIFF and PNG (identical profiles)
|
||||
if (key == QStringLiteral("icc-profile")) {
|
||||
auto cs = QColorSpace::fromIccProfile(value);
|
||||
if(cs.isValid())
|
||||
image.setColorSpace(cs);
|
||||
continue;
|
||||
}
|
||||
|
||||
// "gimp-comment" (IMAGE, PERSISTENT)
|
||||
// Standard GIF-style image comments. This parasite should be
|
||||
// human-readable text in UTF-8 encoding. A trailing \0 might
|
||||
// be included and is not part of the comment. Note that image
|
||||
// comments may also be present in the "gimp-metadata" parasite.
|
||||
if (key == QStringLiteral("gimp-comment")) {
|
||||
value.replace('\0', QByteArray());
|
||||
image.setText(QStringLiteral("Comment"), QString::fromUtf8(value));
|
||||
continue;
|
||||
}
|
||||
|
||||
// "gimp-image-metadata"
|
||||
// Saved by GIMP 2.10.30 but it is not mentioned in the specification.
|
||||
// It is an XML block with the properties set using GIMP.
|
||||
if (key == QStringLiteral("gimp-image-metadata")) {
|
||||
// NOTE: I arbitrary defined the metadata "XML:org.gimp.xml" because it seems
|
||||
// a GIMP proprietary XML format (no xmlns defined)
|
||||
value.replace('\0', QByteArray());
|
||||
image.setText(QStringLiteral("XML:org.gimp.xml"), QString::fromUtf8(value));
|
||||
continue;
|
||||
}
|
||||
|
||||
#if 0 // Unable to generate it using latest GIMP version
|
||||
// "gimp-metadata" (IMAGE, PERSISTENT)
|
||||
// The metadata associated with the image, serialized as one XMP
|
||||
// packet. This metadata includes the contents of any XMP, EXIF
|
||||
// and IPTC blocks from the original image, as well as
|
||||
// user-specified values such as image comment, copyright,
|
||||
// license, etc.
|
||||
if (key == QStringLiteral("gimp-metadata")) {
|
||||
// NOTE: "XML:com.adobe.xmp" is the meta set by Qt reader when an
|
||||
// XMP packet is found (e.g. when reading a PNG saved by Photoshop).
|
||||
// I reused the same key because some programs could search for it.
|
||||
value.replace('\0', QByteArray());
|
||||
image.setText(QStringLiteral("XML:com.adobe.xmp"), QString::fromUtf8(value));
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
* Copy the bytes from the tile buffer into the image tile QImage, taking into
|
||||
* account all the myriad different modes.
|
||||
@ -3192,6 +3270,52 @@ bool XCFHandler::write(const QImage &)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool XCFHandler::supportsOption(ImageOption option) const
|
||||
{
|
||||
if (option == QImageIOHandler::Size)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
QVariant XCFHandler::option(ImageOption option) const
|
||||
{
|
||||
QVariant v;
|
||||
|
||||
if (option == QImageIOHandler::Size) {
|
||||
/*
|
||||
* The image structure always starts at offset 0 in the XCF file.
|
||||
* byte[9] "gimp xcf " File type identification
|
||||
* byte[4] version XCF version
|
||||
* "file": version 0
|
||||
* "v001": version 1
|
||||
* "v002": version 2
|
||||
* "v003": version 3
|
||||
* byte 0 Zero marks the end of the version tag.
|
||||
* uint32 width Width of canvas
|
||||
* uint32 height Height of canvas
|
||||
*/
|
||||
if (auto d = device()) {
|
||||
// transactions works on both random and sequential devices
|
||||
d->startTransaction();
|
||||
auto ba9 = d->read(9); // "gimp xcf "
|
||||
auto ba5 = d->read(4+1); // version + null terminator
|
||||
auto ba = d->read(8); // width and height
|
||||
d->rollbackTransaction();
|
||||
if (ba9 == QByteArray("gimp xcf ") && ba5.size() == 5) {
|
||||
QDataStream ds(ba);
|
||||
quint32 width;
|
||||
ds >> width;
|
||||
quint32 height;
|
||||
ds >> height;
|
||||
if (ds.status() == QDataStream::Ok)
|
||||
v = QVariant::fromValue(QSize(width, height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
bool XCFHandler::canRead(QIODevice *device)
|
||||
{
|
||||
if (!device) {
|
||||
|
@ -20,6 +20,9 @@ public:
|
||||
bool read(QImage *image) override;
|
||||
bool write(const QImage &image) override;
|
||||
|
||||
bool supportsOption(QImageIOHandler::ImageOption option) const override;
|
||||
QVariant option(QImageIOHandler::ImageOption option) const override;
|
||||
|
||||
static bool canRead(QIODevice *device);
|
||||
};
|
||||
|
||||
|
@ -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()
|
||||
|