Compare commits
141 Commits
Author | SHA1 | Date | |
---|---|---|---|
dfbc6e0f8c | |||
43543f96bc | |||
62e477a6f2 | |||
e6955e1f03 | |||
6074c4d6fd | |||
6f44c5c52a | |||
d030c75925 | |||
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 | |||
e89d21f12d | |||
1d2b51ddf1 | |||
1080976abe | |||
abd550c60c | |||
aade392da3 | |||
7642633551 | |||
9f2c5061c8 | |||
28099eed71 | |||
f5d574b3ad | |||
a8f92e5525 | |||
fbeef559b7 | |||
7f56d835f0 | |||
8f87ce4cb2 | |||
5aa03c12ad | |||
3266a9c466 | |||
1b2bf6e931 | |||
894391b000 | |||
ef6be2c077 | |||
96b40da089 | |||
bf3f94da76 | |||
318dacda75 | |||
e358bb0feb | |||
dca6e87c89 | |||
62f9af9a35 | |||
ff53d3d7e9 | |||
780f342825 | |||
297ed9a2fe | |||
55b4077f2c | |||
2429c95336 | |||
224f892b09 | |||
64fa129ed6 | |||
3cb4021afc | |||
95a19a15c3 | |||
1ba23a1e8e | |||
bc3c04c7ce | |||
2755f74fbb | |||
3b0c767f82 | |||
e80fcd7c30 | |||
a73e3d44dd | |||
1169859b07 | |||
04e276dcb3 | |||
e3ab850712 | |||
503b3eee2b | |||
511a22f0b4 | |||
c532227d43 | |||
1462c3abd6 | |||
ca52d4ddf5 | |||
7ba4d6adda | |||
0aaab103b1 | |||
8b9125c913 | |||
8845dd9818 | |||
a4b8295625 | |||
134c96fa61 | |||
3673874a63 | |||
8ad43638ad | |||
c72c9f577b | |||
bf3f99abf5 | |||
b79d1f222d | |||
bf1de9f8f0 | |||
82d5e0f8a4 | |||
bbf945137a | |||
54ed1dda27 | |||
34a9ec1b06 | |||
a6ec69d276 | |||
02cbf3889f | |||
6cf05cf305 | |||
938b8126b5 | |||
d36c191351 | |||
1acb5a6177 | |||
f2ccbf1724 | |||
5825c83235 | |||
b742cb7cc7 | |||
2e6eeebdfc | |||
db0b5d571a |
3
.git-blame-ignore-revs
Normal file
@ -0,0 +1,3 @@
|
||||
#clang-format/tidy
|
||||
1169859b07f25c865ee0bfc2a7dc97a431651776
|
||||
eaca33bec8b19498c3621fc3a20a8c55a2812462
|
6
.gitignore
vendored
@ -20,3 +20,9 @@ random_seed
|
||||
CMakeLists.txt.user*
|
||||
*.unc-backup*
|
||||
.cmake/
|
||||
/.clang-format
|
||||
/compile_commands.json
|
||||
.clangd
|
||||
.idea
|
||||
/cmake-build*
|
||||
.cache
|
||||
|
11
.gitlab-ci.yml
Normal file
@ -0,0 +1,11 @@
|
||||
# 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
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.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' ]
|
@ -1,11 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(KImageFormats)
|
||||
|
||||
set (CMAKE_CXX_STANDARD 14)
|
||||
|
||||
include(FeatureSummary)
|
||||
find_package(ECM 5.75.0 NO_MODULE)
|
||||
find_package(ECM 5.98.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)
|
||||
|
||||
@ -16,11 +14,13 @@ include(KDEInstallDirs)
|
||||
include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE)
|
||||
include(KDECMakeSettings)
|
||||
|
||||
|
||||
include(KDEGitCommitHooks)
|
||||
include(ECMDeprecationSettings)
|
||||
include(CheckIncludeFiles)
|
||||
include(FindPkgConfig)
|
||||
|
||||
set(REQUIRED_QT_VERSION 5.12.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
|
||||
@ -32,26 +32,49 @@ 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()
|
||||
|
||||
find_package(OpenEXR)
|
||||
find_package(OpenEXR 3.0 CONFIG QUIET)
|
||||
if(NOT OpenEXR_FOUND)
|
||||
find_package(OpenEXR)
|
||||
endif()
|
||||
set_package_properties(OpenEXR PROPERTIES
|
||||
TYPE OPTIONAL
|
||||
PURPOSE "Required for the QImage plugin for OpenEXR images"
|
||||
)
|
||||
add_definitions(-DQT_NO_FOREACH)
|
||||
# 050d00 (5.13) triggers a BIC in qimageiohandler.h, in Qt 5.13, so do not enable that until we can require 5.14
|
||||
# https://codereview.qt-project.org/c/qt/qtbase/+/279215
|
||||
add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050c00)
|
||||
add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x054900)
|
||||
|
||||
find_package(libavif 0.8.2 CONFIG)
|
||||
set_package_properties(libavif PROPERTIES
|
||||
TYPE OPTIONAL
|
||||
PURPOSE "Required for the QImage plugin for AVIF images"
|
||||
)
|
||||
|
||||
option(KIMAGEFORMATS_HEIF "Enable plugin for HEIF format" OFF)
|
||||
if(KIMAGEFORMATS_HEIF)
|
||||
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")
|
||||
|
||||
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)
|
||||
@ -59,3 +82,5 @@ if (BUILD_TESTING)
|
||||
endif()
|
||||
|
||||
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||
|
||||
kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT)
|
||||
|
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.
|
@ -13,14 +13,17 @@ image formats.
|
||||
|
||||
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()
|
||||
@ -70,6 +82,34 @@ if (KF5Archive_FOUND)
|
||||
)
|
||||
endif()
|
||||
|
||||
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
|
||||
# rounding errors (eg: in alpha blending).
|
||||
kimageformats_read_tests(FUZZ 1
|
||||
@ -99,14 +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 Qt${QT_MAJOR_VERSION}::Gui Qt${QT_MAJOR_VERSION}::Test)
|
||||
ecm_mark_as_test(anitest)
|
||||
add_test(NAME kimageformats-ani COMMAND anitest)
|
||||
|
BIN
autotests/ani/test.ani
Normal file
BIN
autotests/ani/test_1.png
Normal file
After Width: | Height: | Size: 813 B |
BIN
autotests/ani/test_2.png
Normal file
After Width: | Height: | Size: 697 B |
BIN
autotests/ani/test_3.png
Normal file
After Width: | Height: | Size: 810 B |
119
autotests/anitest.cpp
Normal file
@ -0,0 +1,119 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <kde@broulik.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
*/
|
||||
|
||||
#include <QImage>
|
||||
#include <QImageReader>
|
||||
#include <QTest>
|
||||
|
||||
static bool imgEquals(const QImage &im1, const QImage &im2)
|
||||
{
|
||||
const int height = im1.height();
|
||||
const int width = im1.width();
|
||||
for (int i = 0; i < height; ++i) {
|
||||
const auto *line1 = reinterpret_cast<const quint8 *>(im1.scanLine(i));
|
||||
const auto *line2 = reinterpret_cast<const quint8 *>(im2.scanLine(i));
|
||||
for (int j = 0; j < width; ++j) {
|
||||
if (line1[j] - line2[j] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class AniTests : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase()
|
||||
{
|
||||
QCoreApplication::addLibraryPath(QStringLiteral(PLUGIN_DIR));
|
||||
}
|
||||
|
||||
void testReadMetadata()
|
||||
{
|
||||
QImageReader reader(QFINDTESTDATA("ani/test.ani"));
|
||||
|
||||
QVERIFY(reader.canRead());
|
||||
|
||||
QCOMPARE(reader.imageCount(), 4);
|
||||
|
||||
QCOMPARE(reader.size(), QSize(32, 32));
|
||||
|
||||
QCOMPARE(reader.text(QStringLiteral("Title")), QStringLiteral("ANI Test"));
|
||||
QCOMPARE(reader.text(QStringLiteral("Author")), QStringLiteral("KDE Community"));
|
||||
}
|
||||
|
||||
void textRead()
|
||||
{
|
||||
QImageReader reader(QFINDTESTDATA("ani/test.ani"));
|
||||
QVERIFY(reader.canRead());
|
||||
QCOMPARE(reader.currentImageNumber(), 0);
|
||||
|
||||
QImage aniFrame;
|
||||
QVERIFY(reader.read(&aniFrame));
|
||||
|
||||
QImage img1(QFINDTESTDATA("ani/test_1.png"));
|
||||
img1.convertTo(aniFrame.format());
|
||||
|
||||
QVERIFY(imgEquals(aniFrame, img1));
|
||||
|
||||
QCOMPARE(reader.nextImageDelay(), 166); // 10 "jiffies"
|
||||
|
||||
QVERIFY(reader.canRead());
|
||||
// that read() above should have advanced us to the next frame
|
||||
QCOMPARE(reader.currentImageNumber(), 1);
|
||||
|
||||
QVERIFY(reader.read(&aniFrame));
|
||||
QImage img2(QFINDTESTDATA("ani/test_2.png"));
|
||||
img2.convertTo(aniFrame.format());
|
||||
|
||||
QVERIFY(imgEquals(aniFrame, img2));
|
||||
|
||||
// The "middle" frame has a longer delay than the others
|
||||
QCOMPARE(reader.nextImageDelay(), 333); // 20 "jiffies"
|
||||
|
||||
QVERIFY(reader.canRead());
|
||||
QCOMPARE(reader.currentImageNumber(), 2);
|
||||
|
||||
QVERIFY(reader.read(&aniFrame));
|
||||
QImage img3(QFINDTESTDATA("ani/test_3.png"));
|
||||
img3.convertTo(aniFrame.format());
|
||||
|
||||
QVERIFY(imgEquals(aniFrame, img3));
|
||||
|
||||
QCOMPARE(reader.nextImageDelay(), 166);
|
||||
|
||||
QVERIFY(reader.canRead());
|
||||
QCOMPARE(reader.currentImageNumber(), 3);
|
||||
|
||||
QVERIFY(reader.read(&aniFrame));
|
||||
// custom sequence in the ANI file should get us back to img2
|
||||
QVERIFY(imgEquals(aniFrame, img2));
|
||||
|
||||
QCOMPARE(reader.nextImageDelay(), 166);
|
||||
|
||||
// We should have reached the end now
|
||||
QVERIFY(!reader.canRead());
|
||||
QVERIFY(!reader.read(&aniFrame));
|
||||
|
||||
// Jump back to the start
|
||||
QVERIFY(reader.jumpToImage(0));
|
||||
|
||||
QVERIFY(reader.canRead());
|
||||
QCOMPARE(reader.currentImageNumber(), 0);
|
||||
|
||||
QCOMPARE(reader.nextImageDelay(), 166);
|
||||
|
||||
QVERIFY(reader.read(&aniFrame));
|
||||
QVERIFY(imgEquals(aniFrame, img1));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(AniTests)
|
||||
|
||||
#include "anitest.moc"
|
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);
|
||||
}
|
@ -35,61 +35,27 @@ private:
|
||||
QTest::addColumn<QImage::Format>("pngformat");
|
||||
QTest::addColumn<bool>("compress");
|
||||
|
||||
QTest::newRow("4x4 no alpha RLE")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QString()
|
||||
<< false
|
||||
<< QImage::Format_RGB32
|
||||
<< true;
|
||||
QTest::newRow("4x4 no alpha RLE") << QFINDTESTDATA("pic/4x4-simple-color.pic") << QFINDTESTDATA("pic/4x4-simple-color.png") << QString() << false
|
||||
<< QImage::Format_RGB32 << true;
|
||||
|
||||
QTest::newRow("4x4 no alpha raw")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color-uncompressed.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QString()
|
||||
<< false
|
||||
<< QImage::Format_RGB32
|
||||
<< false;
|
||||
QTest::newRow("4x4 no alpha raw") << QFINDTESTDATA("pic/4x4-simple-color-uncompressed.pic") << QFINDTESTDATA("pic/4x4-simple-color.png") << QString()
|
||||
<< false << QImage::Format_RGB32 << false;
|
||||
|
||||
QTest::newRow("Short comment")
|
||||
<< QFINDTESTDATA("pic/short-comment.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QStringLiteral("Test comment value")
|
||||
<< false
|
||||
<< QImage::Format_RGB32
|
||||
<< true;
|
||||
QTest::newRow("Short comment") << QFINDTESTDATA("pic/short-comment.pic") << QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QStringLiteral("Test comment value") << false << QImage::Format_RGB32 << true;
|
||||
|
||||
QTest::newRow("Long comment")
|
||||
<< QFINDTESTDATA("pic/long-comment.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QStringLiteral("Test comment value that goes right up to the end of the comment field and has no")
|
||||
<< false
|
||||
<< QImage::Format_RGB32
|
||||
<< true;
|
||||
QTest::newRow("Long comment") << QFINDTESTDATA("pic/long-comment.pic") << QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QStringLiteral("Test comment value that goes right up to the end of the comment field and has no") << false
|
||||
<< QImage::Format_RGB32 << true;
|
||||
|
||||
QTest::newRow("Long run-lengths")
|
||||
<< QFINDTESTDATA("pic/long-runs.pic")
|
||||
<< QFINDTESTDATA("pic/long-runs.png")
|
||||
<< QString()
|
||||
<< false
|
||||
<< QImage::Format_RGB32
|
||||
<< true;
|
||||
QTest::newRow("Long run-lengths") << QFINDTESTDATA("pic/long-runs.pic") << QFINDTESTDATA("pic/long-runs.png") << QString() << false
|
||||
<< QImage::Format_RGB32 << true;
|
||||
|
||||
QTest::newRow("4x4 with alpha RLE")
|
||||
<< QFINDTESTDATA("pic/4x4-alpha.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-alpha.png")
|
||||
<< QString()
|
||||
<< true
|
||||
<< QImage::Format_ARGB32
|
||||
<< true;
|
||||
QTest::newRow("4x4 with alpha RLE") << QFINDTESTDATA("pic/4x4-alpha.pic") << QFINDTESTDATA("pic/4x4-alpha.png") << QString() << true
|
||||
<< QImage::Format_ARGB32 << true;
|
||||
|
||||
QTest::newRow("4x4 with alpha raw")
|
||||
<< QFINDTESTDATA("pic/4x4-alpha-uncompressed.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-alpha.png")
|
||||
<< QString()
|
||||
<< true
|
||||
<< QImage::Format_ARGB32
|
||||
<< false;
|
||||
QTest::newRow("4x4 with alpha raw") << QFINDTESTDATA("pic/4x4-alpha-uncompressed.pic") << QFINDTESTDATA("pic/4x4-alpha.png") << QString() << true
|
||||
<< QImage::Format_ARGB32 << false;
|
||||
}
|
||||
|
||||
private Q_SLOTS:
|
||||
@ -106,13 +72,8 @@ private Q_SLOTS:
|
||||
// so there is no actual data loss in converting to RGB16.
|
||||
// This just tests that the pic plugin can deal with different
|
||||
// input formats.
|
||||
QTest::newRow("altered format")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.pic")
|
||||
<< QFINDTESTDATA("pic/4x4-simple-color.png")
|
||||
<< QString()
|
||||
<< false
|
||||
<< QImage::Format_RGB16
|
||||
<< true;
|
||||
QTest::newRow("altered format") << QFINDTESTDATA("pic/4x4-simple-color.pic") << QFINDTESTDATA("pic/4x4-simple-color.png") << QString() << false
|
||||
<< QImage::Format_RGB16 << true;
|
||||
}
|
||||
|
||||
void testRead_data()
|
||||
@ -148,17 +109,12 @@ private Q_SLOTS:
|
||||
imgWriter.write(pngImage);
|
||||
|
||||
if (expData != picData) {
|
||||
QString fileNameBase = QUuid::createUuid().toString()
|
||||
.remove(QLatin1Char('{'))
|
||||
.remove(QLatin1Char('}'));
|
||||
QString fileNameBase = QUuid::createUuid().toString().remove(QLatin1Char('{')).remove(QLatin1Char('}'));
|
||||
QFile dumpFile(fileNameBase + QStringLiteral(".pic"));
|
||||
QVERIFY2(dumpFile.open(QIODevice::WriteOnly), qPrintable(dumpFile.errorString()));
|
||||
dumpFile.write(picData);
|
||||
QString msg = QStringLiteral("Written data (")
|
||||
+ dumpFile.fileName()
|
||||
+ QStringLiteral(") differed from expected data (")
|
||||
+ picfile
|
||||
+ QLatin1Char(')');
|
||||
QString msg =
|
||||
QStringLiteral("Written data (") + dumpFile.fileName() + QStringLiteral(") differed from expected data (") + picfile + QLatin1Char(')');
|
||||
QFAIL(qPrintable(msg));
|
||||
}
|
||||
}
|
||||
@ -182,29 +138,20 @@ private Q_SLOTS:
|
||||
QCOMPARE(inputImage.width(), expImage.width());
|
||||
QCOMPARE(inputImage.height(), expImage.height());
|
||||
QCOMPARE(inputImage.hasAlphaChannel(), alpha);
|
||||
QCOMPARE(inputImage.format(), alpha ? QImage::Format_ARGB32
|
||||
: QImage::Format_RGB32);
|
||||
QCOMPARE(inputImage.format(), alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||
|
||||
expImage = expImage.convertToFormat(pngformat);
|
||||
expImage = expImage.convertToFormat(alpha ? QImage::Format_ARGB32
|
||||
: QImage::Format_RGB32);
|
||||
expImage = expImage.convertToFormat(alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||
if (inputImage != expImage) {
|
||||
QString fileNameBase = QUuid::createUuid().toString()
|
||||
.remove(QLatin1Char('{'))
|
||||
.remove(QLatin1Char('}'));
|
||||
QString fileNameBase = QUuid::createUuid().toString().remove(QLatin1Char('{')).remove(QLatin1Char('}'));
|
||||
QFile picDumpFile(fileNameBase + QStringLiteral("-expected.data"));
|
||||
QVERIFY2(picDumpFile.open(QIODevice::WriteOnly), qPrintable(picDumpFile.errorString()));
|
||||
picDumpFile.write(reinterpret_cast<const char *>(inputImage.bits()),
|
||||
inputImage.sizeInBytes());
|
||||
picDumpFile.write(reinterpret_cast<const char *>(inputImage.bits()), inputImage.sizeInBytes());
|
||||
QFile pngDumpFile(fileNameBase + QStringLiteral("-actual.data"));
|
||||
QVERIFY2(pngDumpFile.open(QIODevice::WriteOnly), qPrintable(pngDumpFile.errorString()));
|
||||
pngDumpFile.write(reinterpret_cast<const char *>(expImage.bits()),
|
||||
expImage.sizeInBytes());
|
||||
QString msg = QStringLiteral("Read image (")
|
||||
+ picDumpFile.fileName()
|
||||
+ QStringLiteral(") differed from expected image (")
|
||||
+ pngDumpFile.fileName()
|
||||
+ QLatin1Char(')');
|
||||
pngDumpFile.write(reinterpret_cast<const char *>(expImage.bits()), expImage.sizeInBytes());
|
||||
QString msg = QStringLiteral("Read image (") + picDumpFile.fileName() + QStringLiteral(") differed from expected image (") + pngDumpFile.fileName()
|
||||
+ QLatin1Char(')');
|
||||
QFAIL(qPrintable(msg));
|
||||
}
|
||||
}
|
||||
@ -252,8 +199,7 @@ private Q_SLOTS:
|
||||
|
||||
QImageReader inputReader(picfile, "pic");
|
||||
|
||||
QCOMPARE(inputReader.imageFormat(),
|
||||
alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||
QCOMPARE(inputReader.imageFormat(), alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||
}
|
||||
};
|
||||
|
||||
|
BIN
autotests/read/avif/bw.avif
Normal file
After Width: | Height: | Size: 629 B |
BIN
autotests/read/avif/bw.png
Normal file
After Width: | Height: | Size: 743 B |
BIN
autotests/read/avif/bwa.avif
Normal file
After Width: | Height: | Size: 823 B |
BIN
autotests/read/avif/bwa.png
Normal file
After Width: | Height: | Size: 574 B |
BIN
autotests/read/avif/rgb.avif
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
autotests/read/avif/rgb.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
autotests/read/avif/rgba.avif
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
autotests/read/avif/rgba.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
autotests/read/hdr/rgb-landscape.hdr
Executable file
BIN
autotests/read/hdr/rgb-landscape.png
Executable file
After Width: | Height: | Size: 1.2 KiB |
BIN
autotests/read/hdr/rgb-portrait.hdr
Executable file
BIN
autotests/read/hdr/rgb-portrait.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
autotests/read/heif/rgb.heif
Normal file
BIN
autotests/read/heif/rgb.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
autotests/read/heif/rgba.heif
Normal file
BIN
autotests/read/heif/rgba.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
autotests/read/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-rle.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
autotests/read/psd/16bit-rle.psd
Normal file
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/argb16-raw-affinityphoto-1.8.5.png
Normal file
After Width: | Height: | Size: 983 B |
BIN
autotests/read/psd/argb16-raw-affinityphoto-1.8.5.psd
Normal file
BIN
autotests/read/psd/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/ccbug182496.png
Normal file
After Width: | Height: | Size: 114 KiB |
BIN
autotests/read/psd/ccbug182496.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/psd/laba_16bit.png
Normal file
After Width: | Height: | Size: 189 KiB |
BIN
autotests/read/psd/laba_16bit.psd
Normal file
BIN
autotests/read/psd/laba_8bit.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
autotests/read/psd/laba_8bit.psd
Normal file
BIN
autotests/read/psd/rgb16-raw-affinityphoto-1.8.5.png
Normal file
After Width: | Height: | Size: 827 B |
BIN
autotests/read/psd/rgb16-raw-affinityphoto-1.8.5.psd
Normal file
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,48 +16,41 @@
|
||||
|
||||
#include "../tests/format-enum.h"
|
||||
|
||||
#include "fuzzyeq.cpp"
|
||||
|
||||
static void writeImageData(const char *name, const QString &filename, const QImage &image)
|
||||
{
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
qint64 written = file.write(reinterpret_cast<const char *>(image.bits()), image.sizeInBytes());
|
||||
if (written == image.sizeInBytes()) {
|
||||
QTextStream(stdout) << " " << name
|
||||
<< " written to " << filename << "\n";
|
||||
QTextStream(stdout) << " " << name << " written to " << filename << "\n";
|
||||
} else {
|
||||
QTextStream(stdout) << " could not write " << name
|
||||
<< " to " << filename << ":"
|
||||
<< file.errorString() << "\n";
|
||||
QTextStream(stdout) << " could not write " << name << " to " << filename << ":" << file.errorString() << "\n";
|
||||
}
|
||||
} else {
|
||||
QTextStream(stdout) << " could not open "
|
||||
<< filename << ":"
|
||||
<< file.errorString() << "\n";
|
||||
QTextStream(stdout) << " could not open " << filename << ":" << file.errorString() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Returns the original format if we support, or returns
|
||||
// format which we preferred to use for `fuzzyeq()`.
|
||||
// We do only support formats with 8-bits/16-bits pre pixel.
|
||||
// If that changed, don't forget to update `fuzzyeq()` too
|
||||
static QImage::Format preferredFormat(QImage::Format fmt)
|
||||
{
|
||||
const int height = im1.height();
|
||||
const int width = im1.width();
|
||||
for (int i = 0; i < height; ++i) {
|
||||
const uchar *line1 = im1.scanLine(i);
|
||||
const uchar *line2 = im2.scanLine(i);
|
||||
for (int j = 0; j < width; ++j) {
|
||||
if (line1[j] > line2[j]) {
|
||||
if (line1[j] - line2[j] > fuzziness)
|
||||
return false;
|
||||
} else {
|
||||
if (line2[j] - line1[j] > fuzziness)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
switch (fmt) {
|
||||
case QImage::Format_RGB32:
|
||||
case QImage::Format_ARGB32:
|
||||
case QImage::Format_RGBX64:
|
||||
case QImage::Format_RGBA64:
|
||||
return fmt;
|
||||
default:
|
||||
return QImage::Format_ARGB32;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
QCoreApplication::removeLibraryPath(QStringLiteral(PLUGIN_DIR));
|
||||
@ -70,10 +63,9 @@ int main(int argc, char ** argv)
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
parser.addPositionalArgument(QStringLiteral("format"), QStringLiteral("format to test"));
|
||||
QCommandLineOption fuzz(
|
||||
QStringList() << QStringLiteral("f") << QStringLiteral("fuzz"),
|
||||
QStringLiteral("Allow for some deviation in ARGB data."),
|
||||
QStringLiteral("max"));
|
||||
QCommandLineOption fuzz(QStringList() << QStringLiteral("f") << QStringLiteral("fuzz"),
|
||||
QStringLiteral("Allow for some deviation in ARGB data."),
|
||||
QStringLiteral("max"));
|
||||
parser.addOption(fuzz);
|
||||
|
||||
parser.process(app);
|
||||
@ -109,13 +101,14 @@ int main(int argc, char ** argv)
|
||||
int failed = 0;
|
||||
|
||||
QTextStream(stdout) << "********* "
|
||||
<< "Starting basic read tests for "
|
||||
<< suffix << " images *********\n";
|
||||
<< "Starting basic read tests for " << suffix << " images *********\n";
|
||||
|
||||
const QList<QByteArray> formats = QImageReader::supportedImageFormats();
|
||||
QStringList formatStrings;
|
||||
formatStrings.reserve(formats.size());
|
||||
std::transform(formats.begin(), formats.end(), std::back_inserter(formatStrings), [](const QByteArray &format) { return QString(format); });
|
||||
std::transform(formats.begin(), formats.end(), std::back_inserter(formatStrings), [](const QByteArray &format) {
|
||||
return QString(format);
|
||||
});
|
||||
QTextStream(stdout) << "QImageReader::supportedImageFormats: " << formatStrings.join(", ") << "\n";
|
||||
|
||||
const QFileInfoList lstImgDir = imgdir.entryInfoList();
|
||||
@ -132,79 +125,58 @@ int main(int argc, char ** argv)
|
||||
QImage expImage;
|
||||
|
||||
if (!expReader.read(&expImage)) {
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName()
|
||||
<< ": could not load " << expfilename
|
||||
<< ": " << expReader.errorString()
|
||||
<< "\n";
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not load " << expfilename << ": " << expReader.errorString() << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
if (!inputReader.canRead()) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": failed can read: "
|
||||
<< inputReader.errorString()
|
||||
<< "\n";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed can read: " << inputReader.errorString() << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
if (!inputReader.read(&inputImage)) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": failed to load: "
|
||||
<< inputReader.errorString()
|
||||
<< "\n";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed to load: " << inputReader.errorString() << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
if (expImage.width() != inputImage.width()) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": width was " << inputImage.width()
|
||||
<< " but " << expfilename << " width was "
|
||||
<< expImage.width() << "\n";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": width was " << inputImage.width() << " but " << expfilename << " width was "
|
||||
<< expImage.width() << "\n";
|
||||
++failed;
|
||||
} else if (expImage.height() != inputImage.height()) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": height was " << inputImage.height()
|
||||
<< " but " << expfilename << " height was "
|
||||
<< expImage.height() << "\n";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": height was " << inputImage.height() << " but " << expfilename << " height was "
|
||||
<< expImage.height() << "\n";
|
||||
++failed;
|
||||
} else {
|
||||
if (inputImage.format() != QImage::Format_ARGB32) {
|
||||
QTextStream(stdout) << "INFO : " << fi.fileName()
|
||||
<< ": converting " << fi.fileName()
|
||||
<< " from " << formatToString(inputImage.format())
|
||||
<< " to ARGB32\n";
|
||||
inputImage = inputImage.convertToFormat(QImage::Format_ARGB32);
|
||||
QImage::Format inputFormat = preferredFormat(inputImage.format());
|
||||
QImage::Format expFormat = preferredFormat(expImage.format());
|
||||
QImage::Format cmpFormat = inputFormat == expFormat ? inputFormat : QImage::Format_ARGB32;
|
||||
|
||||
if (inputImage.format() != cmpFormat) {
|
||||
QTextStream(stdout) << "INFO : " << fi.fileName() << ": converting " << fi.fileName() << " from " << formatToString(inputImage.format())
|
||||
<< " to " << formatToString(cmpFormat) << '\n';
|
||||
inputImage = inputImage.convertToFormat(cmpFormat);
|
||||
}
|
||||
if (expImage.format() != QImage::Format_ARGB32) {
|
||||
QTextStream(stdout) << "INFO : " << fi.fileName()
|
||||
<< ": converting " << expfilename
|
||||
<< " from " << formatToString(expImage.format())
|
||||
<< " to ARGB32\n";
|
||||
expImage = expImage.convertToFormat(QImage::Format_ARGB32);
|
||||
if (expImage.format() != cmpFormat) {
|
||||
QTextStream(stdout) << "INFO : " << fi.fileName() << ": converting " << expfilename << " from " << formatToString(expImage.format()) << " to "
|
||||
<< formatToString(cmpFormat) << '\n';
|
||||
expImage = expImage.convertToFormat(cmpFormat);
|
||||
}
|
||||
if (fuzzyeq(inputImage, expImage, fuzziness)) {
|
||||
QTextStream(stdout) << "PASS : " << fi.fileName() << "\n";
|
||||
++passed;
|
||||
} else {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": differs from " << expfilename << "\n";
|
||||
writeImageData("expected data",
|
||||
fi.fileName() + QLatin1String("-expected.data"),
|
||||
expImage);
|
||||
writeImageData("actual data",
|
||||
fi.fileName() + QLatin1String("-actual.data"),
|
||||
inputImage);
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": differs from " << expfilename << "\n";
|
||||
writeImageData("expected data", fi.fileName() + QLatin1String("-expected.data"), expImage);
|
||||
writeImageData("actual data", fi.fileName() + QLatin1String("-actual.data"), inputImage);
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QTextStream(stdout) << "Totals: "
|
||||
<< passed << " passed, "
|
||||
<< failed << " failed\n";
|
||||
QTextStream(stdout) << "Totals: " << passed << " passed, " << failed << " failed\n";
|
||||
QTextStream(stdout) << "********* "
|
||||
<< "Finished basic read tests for "
|
||||
<< suffix << " images *********\n";
|
||||
<< "Finished basic read tests for " << suffix << " images *********\n";
|
||||
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
|
@ -16,7 +16,9 @@
|
||||
#include <QImageWriter>
|
||||
#include <QTextStream>
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
#include "fuzzyeq.cpp"
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
QCoreApplication::removeLibraryPath(QStringLiteral(PLUGIN_DIR));
|
||||
@ -29,10 +31,15 @@ int main(int argc, char ** argv)
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
parser.addPositionalArgument(QStringLiteral("format"), QStringLiteral("format to test."));
|
||||
QCommandLineOption lossless(
|
||||
QStringList() << QStringLiteral("l") << QStringLiteral("lossless"),
|
||||
QStringLiteral("Check that reading back the data gives the same image."));
|
||||
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);
|
||||
|
||||
@ -45,78 +52,91 @@ 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;
|
||||
int failed = 0;
|
||||
|
||||
QTextStream(stdout) << "********* "
|
||||
<< "Starting basic write tests for "
|
||||
<< suffix << " images *********\n";
|
||||
<< "Starting basic write tests for " << suffix << " images *********\n";
|
||||
const QFileInfoList lstImgDir = imgdir.entryInfoList();
|
||||
for (const QFileInfo &fi : lstImgDir) {
|
||||
int suffixPos = fi.filePath().count() - suffix.count();
|
||||
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");
|
||||
QImage pngImage;
|
||||
if (!pngReader.read(&pngImage)) {
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName()
|
||||
<< ": could not load " << pngfilename
|
||||
<< ": " << pngReader.errorString()
|
||||
<< "\n";
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not load " << pngfilename << ": " << pngReader.errorString() << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
|
||||
QFile expFile(fi.filePath());
|
||||
if (!expFile.open(QIODevice::ReadOnly)) {
|
||||
QTextStream(stdout) << "ERROR: " << fi.fileName()
|
||||
<< ": could not open " << fi.fileName()
|
||||
<< ": " << expFile.errorString()
|
||||
<< "\n";
|
||||
++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";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed to write image data\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (expData != writtenData) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": written data differs from " << fi.fileName() << "\n";
|
||||
++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;
|
||||
@ -124,8 +144,7 @@ int main(int argc, char ** argv)
|
||||
QBuffer buffer(&writtenData);
|
||||
QImageReader imgReader(&buffer, format.constData());
|
||||
if (!imgReader.read(&reReadImage)) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": could not read back the written data\n";
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": could not read back the written data\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
@ -133,9 +152,18 @@ int main(int argc, char ** argv)
|
||||
}
|
||||
|
||||
if (parser.isSet(lossless)) {
|
||||
if (pngImage != reReadImage) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName()
|
||||
<< ": re-reading the data resulted in a different image\n";
|
||||
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;
|
||||
}
|
||||
@ -145,12 +173,9 @@ int main(int argc, char ** argv)
|
||||
++passed;
|
||||
}
|
||||
|
||||
QTextStream(stdout) << "Totals: "
|
||||
<< passed << " passed, "
|
||||
<< failed << " failed\n";
|
||||
QTextStream(stdout) << "Totals: " << passed << " passed, " << failed << " failed\n";
|
||||
QTextStream(stdout) << "********* "
|
||||
<< "Finished basic write tests for "
|
||||
<< suffix << " images *********\n";
|
||||
<< "Finished basic write tests for " << suffix << " images *********\n";
|
||||
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ type: functional
|
||||
platforms:
|
||||
- name: Linux
|
||||
- name: FreeBSD
|
||||
- name: MacOSX
|
||||
- name: macOS
|
||||
- name: Windows
|
||||
note: No EPS support on Windows
|
||||
- name: Android
|
||||
|
@ -4,103 +4,140 @@
|
||||
|
||||
function(kimageformats_add_plugin plugin)
|
||||
set(options)
|
||||
set(oneValueArgs JSON)
|
||||
set(oneValueArgs)
|
||||
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()
|
||||
|
||||
##################################
|
||||
|
||||
install(FILES dds-qt.desktop RENAME dds.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 SOURCES "avif.cpp")
|
||||
target_link_libraries(kimg_avif "avif")
|
||||
install(FILES avif.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
endif()
|
||||
|
||||
##################################
|
||||
|
||||
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)
|
||||
target_link_libraries(kimg_exr OpenEXR::IlmImf)
|
||||
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/)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_pcx JSON "pcx.json" SOURCES pcx.cpp)
|
||||
install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
|
||||
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_KSERVICESDIR}/qimageioplugins/)
|
||||
endif()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_pic JSON "pic.json" SOURCES pic.cpp)
|
||||
install(FILES pic.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)
|
||||
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_psd JSON "psd.json" SOURCES psd.cpp)
|
||||
install(FILES psd.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_ras JSON "ras.json" SOURCES ras.cpp)
|
||||
install(FILES ras.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_rgb JSON "rgb.json" SOURCES rgb.cpp)
|
||||
install(FILES rgb.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_tga JSON "tga.json" SOURCES tga.cpp)
|
||||
install(FILES tga.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_xcf JSON "xcf.json" SOURCES xcf.cpp)
|
||||
install(FILES xcf.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_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()
|
||||
|
569
src/imageformats/ani.cpp
Normal file
@ -0,0 +1,569 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <kde@broulik.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "ani_p.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QImage>
|
||||
#include <QScopeGuard>
|
||||
#include <QVariant>
|
||||
#include <QtEndian>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct ChunkHeader {
|
||||
char magic[4];
|
||||
quint32_le size;
|
||||
};
|
||||
|
||||
struct AniHeader {
|
||||
quint32_le cbSize;
|
||||
quint32_le nFrames; // number of actual frames in the file
|
||||
quint32_le nSteps; // number of logical images
|
||||
quint32_le iWidth;
|
||||
quint32_le iHeight;
|
||||
quint32_le iBitCount;
|
||||
quint32_le nPlanes;
|
||||
quint32_le iDispRate;
|
||||
quint32_le bfAttributes; // attributes (0 = bitmap images, 1 = ico/cur, 3 = "seq" block available)
|
||||
};
|
||||
|
||||
struct CurHeader {
|
||||
quint16_le wReserved; // always 0
|
||||
quint16_le wResID; // always 2
|
||||
quint16_le wNumImages;
|
||||
};
|
||||
|
||||
struct CursorDirEntry {
|
||||
quint8 bWidth;
|
||||
quint8 bHeight;
|
||||
quint8 bColorCount;
|
||||
quint8 bReserved; // always 0
|
||||
quint16_le wHotspotX;
|
||||
quint16_le wHotspotY;
|
||||
quint32_le dwBytesInImage;
|
||||
quint32_le dwImageOffset;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ANIHandler::ANIHandler() = default;
|
||||
|
||||
bool ANIHandler::canRead() const
|
||||
{
|
||||
if (canRead(device())) {
|
||||
setFormat("ani");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if there's another frame coming
|
||||
const QByteArray nextFrame = device()->peek(sizeof(ChunkHeader));
|
||||
if (nextFrame.size() == sizeof(ChunkHeader)) {
|
||||
const auto *header = reinterpret_cast<const ChunkHeader *>(nextFrame.data());
|
||||
if (qstrncmp(header->magic, "icon", sizeof(header->magic)) == 0 && header->size > 0) {
|
||||
setFormat("ani");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ANIHandler::read(QImage *outImage)
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (device()->pos() < m_firstFrameOffset) {
|
||||
device()->seek(m_firstFrameOffset);
|
||||
}
|
||||
|
||||
const QByteArray frameType = device()->read(4);
|
||||
if (frameType != "icon") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray frameSizeData = device()->read(sizeof(quint32_le));
|
||||
if (frameSizeData.count() != sizeof(quint32_le)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto frameSize = *(reinterpret_cast<const quint32_le *>(frameSizeData.data()));
|
||||
if (!frameSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray frameData = device()->read(frameSize);
|
||||
|
||||
const bool ok = outImage->loadFromData(frameData, "cur");
|
||||
|
||||
++m_currentImageNumber;
|
||||
|
||||
// When we have a custom image sequence, seek to before the frame that would follow
|
||||
if (!m_imageSequence.isEmpty()) {
|
||||
if (m_currentImageNumber < m_imageSequence.count()) {
|
||||
const int nextFrame = m_imageSequence.at(m_currentImageNumber);
|
||||
if (nextFrame < 0 || nextFrame >= m_frameOffsets.count()) {
|
||||
return false;
|
||||
}
|
||||
const auto nextOffset = m_frameOffsets.at(nextFrame);
|
||||
device()->seek(nextOffset);
|
||||
} else if (m_currentImageNumber == m_imageSequence.count()) {
|
||||
const auto endOffset = m_frameOffsets.last();
|
||||
if (device()->pos() != endOffset) {
|
||||
device()->seek(endOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
int ANIHandler::currentImageNumber() const
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return 0;
|
||||
}
|
||||
return m_currentImageNumber;
|
||||
}
|
||||
|
||||
int ANIHandler::imageCount() const
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return 0;
|
||||
}
|
||||
return m_imageCount;
|
||||
}
|
||||
|
||||
bool ANIHandler::jumpToImage(int imageNumber)
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (imageNumber < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (imageNumber == m_currentImageNumber) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we have a custom image sequence we have a index of frames we can jump to
|
||||
if (!m_imageSequence.isEmpty()) {
|
||||
if (imageNumber >= m_imageSequence.count()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int targetFrame = m_imageSequence.at(imageNumber);
|
||||
|
||||
const auto targetOffset = m_frameOffsets.value(targetFrame, -1);
|
||||
|
||||
if (device()->seek(targetOffset)) {
|
||||
m_currentImageNumber = imageNumber;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (imageNumber >= m_frameCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// otherwise we need to jump from frame to frame
|
||||
const auto oldPos = device()->pos();
|
||||
|
||||
if (imageNumber < m_currentImageNumber) {
|
||||
// start from the beginning
|
||||
if (!device()->seek(m_firstFrameOffset)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
while (m_currentImageNumber < imageNumber) {
|
||||
if (!jumpToNextImage()) {
|
||||
device()->seek(oldPos);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_currentImageNumber = imageNumber;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ANIHandler::jumpToNextImage()
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have a custom image sequence we have a index of frames we can jump to
|
||||
// Delegate to jumpToImage
|
||||
if (!m_imageSequence.isEmpty()) {
|
||||
return jumpToImage(m_currentImageNumber + 1);
|
||||
}
|
||||
|
||||
if (device()->pos() < m_firstFrameOffset) {
|
||||
if (!device()->seek(m_firstFrameOffset)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const QByteArray nextFrame = device()->peek(sizeof(ChunkHeader));
|
||||
if (nextFrame.size() != sizeof(ChunkHeader)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto *header = reinterpret_cast<const ChunkHeader *>(nextFrame.data());
|
||||
if (qstrncmp(header->magic, "icon", sizeof(header->magic)) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const qint64 seekBy = sizeof(ChunkHeader) + header->size;
|
||||
|
||||
if (!device()->seek(device()->pos() + seekBy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
++m_currentImageNumber;
|
||||
return true;
|
||||
}
|
||||
|
||||
int ANIHandler::loopCount() const
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ANIHandler::nextImageDelay() const
|
||||
{
|
||||
if (!ensureScanned()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int rate = m_displayRate;
|
||||
|
||||
if (!m_displayRates.isEmpty()) {
|
||||
int previousImage = m_currentImageNumber - 1;
|
||||
if (previousImage < 0) {
|
||||
previousImage = m_displayRates.count() - 1;
|
||||
}
|
||||
rate = m_displayRates.at(previousImage);
|
||||
}
|
||||
|
||||
return rate * 1000 / 60;
|
||||
}
|
||||
|
||||
bool ANIHandler::supportsOption(ImageOption option) const
|
||||
{
|
||||
return option == Size || option == Name || option == Description || option == Animation;
|
||||
}
|
||||
|
||||
QVariant ANIHandler::option(ImageOption option) const
|
||||
{
|
||||
if (!supportsOption(option) || !ensureScanned()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (option) {
|
||||
case QImageIOHandler::Size:
|
||||
return m_size;
|
||||
// TODO QImageIOHandler::Format
|
||||
// but both iBitCount in AniHeader and bColorCount are just zero most of the time
|
||||
// so one would probably need to traverse even further down into IcoHeader and IconDirEntry...
|
||||
// but Qt's ICO/CUR handler always seems to give us a ARB
|
||||
case QImageIOHandler::Name:
|
||||
return m_name;
|
||||
case QImageIOHandler::Description: {
|
||||
QString description;
|
||||
if (!m_name.isEmpty()) {
|
||||
description += QStringLiteral("Title: %1\n\n").arg(m_name);
|
||||
}
|
||||
if (!m_artist.isEmpty()) {
|
||||
description += QStringLiteral("Author: %1\n\n").arg(m_artist);
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
case QImageIOHandler::Animation:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
bool ANIHandler::ensureScanned() const
|
||||
{
|
||||
if (m_scanned) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (device()->isSequential()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *mutableThis = const_cast<ANIHandler *>(this);
|
||||
|
||||
const auto oldPos = device()->pos();
|
||||
auto cleanup = qScopeGuard([this, oldPos] {
|
||||
device()->seek(oldPos);
|
||||
});
|
||||
|
||||
device()->seek(0);
|
||||
|
||||
const QByteArray riffIntro = device()->read(4);
|
||||
if (riffIntro != "RIFF") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto riffSizeData = device()->read(sizeof(quint32_le));
|
||||
if (riffSizeData.size() != sizeof(quint32_le)) {
|
||||
return false;
|
||||
}
|
||||
const auto riffSize = *(reinterpret_cast<const quint32_le *>(riffSizeData.data()));
|
||||
// TODO do a basic sanity check if the size is enough to hold some metadata and a frame?
|
||||
if (riffSize == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mutableThis->m_displayRates.clear();
|
||||
mutableThis->m_imageSequence.clear();
|
||||
|
||||
while (device()->pos() < riffSize) {
|
||||
const QByteArray chunkId = device()->read(4);
|
||||
if (chunkId.length() != 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (chunkId == "ACON") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QByteArray chunkSizeData = device()->read(sizeof(quint32_le));
|
||||
if (chunkSizeData.length() != sizeof(quint32_le)) {
|
||||
return false;
|
||||
}
|
||||
auto chunkSize = *(reinterpret_cast<const quint32_le *>(chunkSizeData.data()));
|
||||
|
||||
if (chunkId == "anih") {
|
||||
if (chunkSize != sizeof(AniHeader)) {
|
||||
qWarning() << "anih chunk size does not match ANIHEADER size";
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray anihData = device()->read(sizeof(AniHeader));
|
||||
if (anihData.size() != sizeof(AniHeader)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *aniHeader = reinterpret_cast<const AniHeader *>(anihData.data());
|
||||
|
||||
// The size in the ani header is usually 0 unfortunately,
|
||||
// so we'll also check the first frame for its size further below
|
||||
mutableThis->m_size = QSize(aniHeader->iWidth, aniHeader->iHeight);
|
||||
mutableThis->m_frameCount = aniHeader->nFrames;
|
||||
mutableThis->m_imageCount = aniHeader->nSteps;
|
||||
mutableThis->m_displayRate = aniHeader->iDispRate;
|
||||
} else if (chunkId == "rate" || chunkId == "seq ") {
|
||||
const QByteArray data = device()->read(chunkSize);
|
||||
if (static_cast<quint32_le>(data.size()) != chunkSize || data.size() % sizeof(quint32_le) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO should we check that the number of rate entries matches nSteps?
|
||||
auto *dataPtr = data.data();
|
||||
QVector<int> list;
|
||||
for (int i = 0; i < data.count(); i += sizeof(quint32_le)) {
|
||||
const auto entry = *(reinterpret_cast<const quint32_le *>(dataPtr + i));
|
||||
list.append(entry);
|
||||
}
|
||||
|
||||
if (chunkId == "rate") {
|
||||
// should we check that the number of rate entries matches nSteps?
|
||||
mutableThis->m_displayRates = list;
|
||||
} else if (chunkId == "seq ") {
|
||||
// Check if it's just an ascending sequence, don't bother with it then
|
||||
bool isAscending = true;
|
||||
for (int i = 0; i < list.count(); ++i) {
|
||||
if (list.at(i) != i) {
|
||||
isAscending = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAscending) {
|
||||
mutableThis->m_imageSequence = list;
|
||||
}
|
||||
}
|
||||
// IART and INAM are technically inside LIST->INFO but "INFO" is supposedly optional
|
||||
// so just handle those two attributes wherever we encounter them
|
||||
} else if (chunkId == "INAM" || chunkId == "IART") {
|
||||
const QByteArray value = device()->read(chunkSize);
|
||||
|
||||
if (static_cast<quint32_le>(value.size()) != chunkSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// DWORDs are aligned to even sizes
|
||||
if (chunkSize % 2 != 0) {
|
||||
device()->read(1);
|
||||
}
|
||||
|
||||
// FIXME encoding
|
||||
const QString stringValue = QString::fromLocal8Bit(value.constData(), std::strlen(value.constData()));
|
||||
if (chunkId == "INAM") {
|
||||
mutableThis->m_name = stringValue;
|
||||
} else if (chunkId == "IART") {
|
||||
mutableThis->m_artist = stringValue;
|
||||
}
|
||||
} else if (chunkId == "LIST") {
|
||||
const QByteArray listType = device()->read(4);
|
||||
|
||||
if (listType == "INFO") {
|
||||
// Technically would contain INAM and IART but we handle them anywhere above
|
||||
} else if (listType == "fram") {
|
||||
quint64 read = 0;
|
||||
while (read < chunkSize) {
|
||||
const QByteArray chunkType = device()->read(4);
|
||||
read += 4;
|
||||
if (chunkType != "icon") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!m_firstFrameOffset) {
|
||||
mutableThis->m_firstFrameOffset = device()->pos() - 4;
|
||||
mutableThis->m_currentImageNumber = 0;
|
||||
|
||||
// If size in header isn't valid, use the first frame's size instead
|
||||
if (!m_size.isValid() || m_size.isEmpty()) {
|
||||
const auto oldPos = device()->pos();
|
||||
|
||||
device()->read(sizeof(quint32_le));
|
||||
|
||||
const QByteArray curHeaderData = device()->read(sizeof(CurHeader));
|
||||
const QByteArray cursorDirEntryData = device()->read(sizeof(CursorDirEntry));
|
||||
|
||||
if (curHeaderData.length() == sizeof(CurHeader) && cursorDirEntryData.length() == sizeof(CursorDirEntry)) {
|
||||
auto *cursorDirEntry = reinterpret_cast<const CursorDirEntry *>(cursorDirEntryData.data());
|
||||
mutableThis->m_size = QSize(cursorDirEntry->bWidth, cursorDirEntry->bHeight);
|
||||
}
|
||||
|
||||
device()->seek(oldPos);
|
||||
}
|
||||
|
||||
// If we don't have a custom image sequence we can stop scanning right here
|
||||
if (m_imageSequence.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mutableThis->m_frameOffsets.append(device()->pos() - 4);
|
||||
|
||||
const QByteArray frameSizeData = device()->read(sizeof(quint32_le));
|
||||
if (frameSizeData.size() != sizeof(quint32_le)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto frameSize = *(reinterpret_cast<const quint32_le *>(frameSizeData.data()));
|
||||
device()->seek(device()->pos() + frameSize);
|
||||
|
||||
read += frameSize;
|
||||
|
||||
if (m_frameOffsets.count() == m_frameCount) {
|
||||
// Also record the end of frame data
|
||||
mutableThis->m_frameOffsets.append(device()->pos() - 4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_imageCount != m_frameCount && m_imageSequence.isEmpty()) {
|
||||
qWarning("ANIHandler: 'nSteps' is not equal to 'nFrames' but no 'seq' entries were provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_imageSequence.isEmpty() && m_imageSequence.count() != m_imageCount) {
|
||||
qWarning("ANIHandler: count of entries in 'seq' does not match 'nSteps' in anih");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_displayRates.isEmpty() && m_displayRates.count() != m_imageCount) {
|
||||
qWarning("ANIHandler: count of entries in 'rate' does not match 'nSteps' in anih");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_frameOffsets.isEmpty() && m_frameOffsets.count() - 1 != m_frameCount) {
|
||||
qWarning("ANIHandler: number of actual frames does not match 'nFrames' in anih");
|
||||
return false;
|
||||
}
|
||||
|
||||
mutableThis->m_scanned = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ANIHandler::canRead(QIODevice *device)
|
||||
{
|
||||
if (!device) {
|
||||
qWarning("ANIHandler::canRead() called with no device");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray riffIntro = device->peek(12);
|
||||
|
||||
if (riffIntro.length() != 12) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!riffIntro.startsWith("RIFF")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO sanity check chunk size?
|
||||
|
||||
if (riffIntro.mid(4 + 4, 4) != "ACON") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QImageIOPlugin::Capabilities ANIPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
if (format == "ani") {
|
||||
return Capabilities(CanRead);
|
||||
}
|
||||
if (!format.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
if (!device->isOpen()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Capabilities cap;
|
||||
if (device->isReadable() && ANIHandler::canRead(device)) {
|
||||
cap |= CanRead;
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
|
||||
QImageIOHandler *ANIPlugin::create(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
QImageIOHandler *handler = new ANIHandler;
|
||||
handler->setDevice(device);
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
7
src/imageformats/ani.desktop
Normal file
@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Service
|
||||
X-KDE-ServiceTypes=QImageIOPlugins
|
||||
X-KDE-ImageFormat=ani
|
||||
X-KDE-MimeType=application/x-navi-animation
|
||||
X-KDE-Read=true
|
||||
X-KDE-Write=false
|
4
src/imageformats/ani.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Keys": [ "ani" ],
|
||||
"MimeTypes": [ "application/x-navi-animation" ]
|
||||
}
|
68
src/imageformats/ani_p.h
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <kde@broulik.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KIMG_ANI_P_H
|
||||
#define KIMG_ANI_P_H
|
||||
|
||||
#include <QImageIOPlugin>
|
||||
#include <QSize>
|
||||
|
||||
class ANIHandler : public QImageIOHandler
|
||||
{
|
||||
public:
|
||||
ANIHandler();
|
||||
|
||||
bool canRead() const override;
|
||||
bool read(QImage *image) override;
|
||||
|
||||
int currentImageNumber() const override;
|
||||
int imageCount() const override;
|
||||
bool jumpToImage(int imageNumber) override;
|
||||
bool jumpToNextImage() override;
|
||||
|
||||
int loopCount() const override;
|
||||
int nextImageDelay() const override;
|
||||
|
||||
bool supportsOption(ImageOption option) const override;
|
||||
QVariant option(ImageOption option) const override;
|
||||
|
||||
static bool canRead(QIODevice *device);
|
||||
|
||||
private:
|
||||
bool ensureScanned() const;
|
||||
|
||||
bool m_scanned = false;
|
||||
|
||||
int m_currentImageNumber = 0;
|
||||
|
||||
int m_frameCount = 0; // "physical" frames
|
||||
int m_imageCount = 0; // logical images
|
||||
// Stores a custom sequence of images
|
||||
QVector<int> m_imageSequence;
|
||||
// and the corresponding offsets where they are
|
||||
// since we can't read the image data sequentally in this case then
|
||||
QVector<qint64> m_frameOffsets;
|
||||
qint64 m_firstFrameOffset = 0;
|
||||
|
||||
int m_displayRate = 0;
|
||||
QVector<int> m_displayRates;
|
||||
|
||||
QString m_name;
|
||||
QString m_artist;
|
||||
QSize m_size;
|
||||
};
|
||||
|
||||
class ANIPlugin : public QImageIOPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "ani.json")
|
||||
|
||||
public:
|
||||
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
|
||||
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||
};
|
||||
|
||||
#endif // KIMG_ANI_P_H
|
1068
src/imageformats/avif.cpp
Normal file
7
src/imageformats/avif.desktop
Normal file
@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Service
|
||||
X-KDE-ServiceTypes=QImageIOPlugins
|
||||
X-KDE-ImageFormat=avif
|
||||
X-KDE-MimeType=image/avif
|
||||
X-KDE-Read=true
|
||||
X-KDE-Write=true
|
4
src/imageformats/avif.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Keys": [ "avif", "avifs" ],
|
||||
"MimeTypes": [ "image/avif", "image/avif" ]
|
||||
}
|
86
src/imageformats/avif_p.h
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
AV1 Image File Format (AVIF) support for QImage.
|
||||
|
||||
SPDX-FileCopyrightText: 2020 Daniel Novomesky <dnovomesky@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#ifndef KIMG_AVIF_P_H
|
||||
#define KIMG_AVIF_P_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QImage>
|
||||
#include <QImageIOPlugin>
|
||||
#include <QPointF>
|
||||
#include <QSize>
|
||||
#include <QVariant>
|
||||
#include <avif/avif.h>
|
||||
#include <qimageiohandler.h>
|
||||
|
||||
class QAVIFHandler : public QImageIOHandler
|
||||
{
|
||||
public:
|
||||
QAVIFHandler();
|
||||
~QAVIFHandler();
|
||||
|
||||
bool canRead() const override;
|
||||
bool read(QImage *image) override;
|
||||
bool write(const QImage &image) override;
|
||||
|
||||
static bool canRead(QIODevice *device);
|
||||
|
||||
QVariant option(ImageOption option) const override;
|
||||
void setOption(ImageOption option, const QVariant &value) override;
|
||||
bool supportsOption(ImageOption option) const override;
|
||||
|
||||
int imageCount() const override;
|
||||
int currentImageNumber() const override;
|
||||
bool jumpToNextImage() override;
|
||||
bool jumpToImage(int imageNumber) override;
|
||||
|
||||
int nextImageDelay() const override;
|
||||
|
||||
int loopCount() const override;
|
||||
|
||||
private:
|
||||
static QPointF CompatibleChromacity(qreal chrX, qreal chrY);
|
||||
bool ensureParsed() const;
|
||||
bool ensureOpened() const;
|
||||
bool ensureDecoder();
|
||||
bool decode_one_frame();
|
||||
|
||||
enum ParseAvifState {
|
||||
ParseAvifError = -1,
|
||||
ParseAvifNotParsed = 0,
|
||||
ParseAvifSuccess = 1,
|
||||
ParseAvifMetadata = 2,
|
||||
};
|
||||
|
||||
ParseAvifState m_parseState;
|
||||
int m_quality;
|
||||
|
||||
uint32_t m_container_width;
|
||||
uint32_t m_container_height;
|
||||
QSize m_estimated_dimensions;
|
||||
|
||||
QByteArray m_rawData;
|
||||
avifROData m_rawAvifData;
|
||||
|
||||
avifDecoder *m_decoder;
|
||||
QImage m_current_image;
|
||||
|
||||
bool m_must_jump_to_next_image;
|
||||
};
|
||||
|
||||
class QAVIFPlugin : public QImageIOPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "avif.json")
|
||||
|
||||
public:
|
||||
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
|
||||
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||
};
|
||||
|
||||
#endif // KIMG_AVIF_P_H
|
@ -9,13 +9,14 @@
|
||||
*/
|
||||
#include "eps_p.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QImage>
|
||||
#include <QImageReader>
|
||||
#include <QPainter>
|
||||
#include <QPrinter>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryFile>
|
||||
#include <QCoreApplication>
|
||||
|
||||
// logging category for this framework, default: log stuff >= warning
|
||||
Q_LOGGING_CATEGORY(EPSPLUGIN, "kf.imageformats.plugins.eps", QtWarningMsg)
|
||||
@ -51,19 +52,13 @@ static bool seekToCodeStart(QIODevice *io, qint64 &ps_offset, qint64 &ps_size)
|
||||
return false;
|
||||
}
|
||||
ps_offset // Offset is in little endian
|
||||
= qint64(((unsigned char)buf[0])
|
||||
+ ((unsigned char)buf[1] << 8)
|
||||
+ ((unsigned char)buf[2] << 16)
|
||||
+ ((unsigned char)buf[3] << 24));
|
||||
= qint64(((unsigned char)buf[0]) + ((unsigned char)buf[1] << 8) + ((unsigned char)buf[2] << 16) + ((unsigned char)buf[3] << 24));
|
||||
if (io->read(buf, 4) != 4) { // Get size of PostScript code in the MS-DOS EPS file.
|
||||
qCDebug(EPSPLUGIN) << "cannot read size of MS-DOS EPS file";
|
||||
return false;
|
||||
}
|
||||
ps_size // Size is in little endian
|
||||
= qint64(((unsigned char)buf[0])
|
||||
+ ((unsigned char)buf[1] << 8)
|
||||
+ ((unsigned char)buf[2] << 16)
|
||||
+ ((unsigned char)buf[3] << 24));
|
||||
= qint64(((unsigned char)buf[0]) + ((unsigned char)buf[1] << 8) + ((unsigned char)buf[2] << 16) + ((unsigned char)buf[3] << 24));
|
||||
qCDebug(EPSPLUGIN) << "Offset: " << ps_offset << " Size: " << ps_size;
|
||||
if (!io->seek(ps_offset)) { // Get offset of PostScript code in the MS-DOS EPS file.
|
||||
qCDebug(EPSPLUGIN) << "cannot seek in MS-DOS EPS file";
|
||||
@ -100,9 +95,11 @@ static bool bbox(QIODevice *io, int *x1, int *y1, int *x2, int *y2)
|
||||
if (strncmp(buf, BBOX, BBOX_LEN) == 0) {
|
||||
// Some EPS files have non-integer values for the bbox
|
||||
// We don't support that currently, but at least we parse it
|
||||
float _x1, _y1, _x2, _y2;
|
||||
if (sscanf(buf, "%*s %f %f %f %f",
|
||||
&_x1, &_y1, &_x2, &_y2) == 4) {
|
||||
float _x1;
|
||||
float _y1;
|
||||
float _x2;
|
||||
float _y2;
|
||||
if (sscanf(buf, "%*s %f %f %f %f", &_x1, &_y1, &_x2, &_y2) == 4) {
|
||||
qCDebug(EPSPLUGIN) << "BBOX: " << _x1 << " " << _y1 << " " << _x2 << " " << _y2;
|
||||
*x1 = int(_x1);
|
||||
*y1 = int(_y1);
|
||||
@ -134,14 +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)) {
|
||||
@ -176,29 +177,29 @@ 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")
|
||||
gsArgs << QLatin1String("-sOutputFile=") + tmpFile.fileName() << QStringLiteral("-q") << QStringLiteral("-g%1x%2").arg(wantedWidth).arg(wantedHeight)
|
||||
<< QStringLiteral("-dSAFER") << QStringLiteral("-dPARANOIDSAFER") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-sDEVICE=ppm")
|
||||
<< QStringLiteral("-c")
|
||||
<< QStringLiteral("0 0 moveto "
|
||||
"1000 0 lineto "
|
||||
"1000 1000 lineto "
|
||||
"0 1000 lineto "
|
||||
"1 1 254 255 div setrgbcolor fill "
|
||||
"0 0 0 setrgbcolor")
|
||||
<< QStringLiteral("-")
|
||||
<< QStringLiteral("-c")
|
||||
<< QStringLiteral("showpage quit");
|
||||
<< QStringLiteral(
|
||||
"0 0 moveto "
|
||||
"1000 0 lineto "
|
||||
"1000 1000 lineto "
|
||||
"0 1000 lineto "
|
||||
"1 1 254 255 div setrgbcolor fill "
|
||||
"0 0 0 setrgbcolor")
|
||||
<< QStringLiteral("-") << QStringLiteral("-c") << QStringLiteral("showpage quit");
|
||||
qCDebug(EPSPLUGIN) << "Running gs with args" << gsArgs;
|
||||
|
||||
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;
|
||||
@ -264,7 +265,8 @@ bool EPSHandler::write(const QImage &image)
|
||||
psOut.setOutputFileName(tmpFile.fileName());
|
||||
psOut.setOutputFormat(QPrinter::PdfFormat);
|
||||
psOut.setFullPage(true);
|
||||
psOut.setPaperSize(image.size(), QPrinter::DevicePixel);
|
||||
const double multiplier = psOut.resolution() <= 0 ? 1.0 : 72.0 / psOut.resolution();
|
||||
psOut.setPageSize(QPageSize(image.size() * multiplier, QPageSize::Point));
|
||||
|
||||
// painting the pixmap to the "printer" which is a file
|
||||
p.begin(&psOut);
|
||||
@ -277,24 +279,16 @@ bool EPSHandler::write(const QImage &image)
|
||||
|
||||
// pdftops comes with Poppler and produces much smaller EPS files than GhostScript
|
||||
QStringList pdftopsArgs;
|
||||
pdftopsArgs << QStringLiteral("-eps")
|
||||
<< tmpFile.fileName()
|
||||
<< QStringLiteral("-");
|
||||
pdftopsArgs << QStringLiteral("-eps") << tmpFile.fileName() << QStringLiteral("-");
|
||||
qCDebug(EPSPLUGIN) << "Running pdftops with args" << pdftopsArgs;
|
||||
converter.start(QStringLiteral("pdftops"), pdftopsArgs);
|
||||
|
||||
if (!converter.waitForStarted()) {
|
||||
// GhostScript produces huge files, and takes a long time doing so
|
||||
QStringList gsArgs;
|
||||
gsArgs << QStringLiteral("-q") << QStringLiteral("-P-")
|
||||
<< QStringLiteral("-dNOPAUSE") << QStringLiteral("-dBATCH")
|
||||
<< QStringLiteral("-dSAFER")
|
||||
<< QStringLiteral("-sDEVICE=epswrite")
|
||||
<< QStringLiteral("-sOutputFile=-")
|
||||
<< QStringLiteral("-c")
|
||||
<< QStringLiteral("save") << QStringLiteral("pop")
|
||||
<< QStringLiteral("-f")
|
||||
<< tmpFile.fileName();
|
||||
gsArgs << QStringLiteral("-q") << QStringLiteral("-P-") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-dBATCH") << QStringLiteral("-dSAFER")
|
||||
<< QStringLiteral("-sDEVICE=epswrite") << QStringLiteral("-sOutputFile=-") << QStringLiteral("-c") << QStringLiteral("save")
|
||||
<< QStringLiteral("pop") << QStringLiteral("-f") << tmpFile.fileName();
|
||||
qCDebug(EPSPLUGIN) << "Failed to start pdftops; trying gs with args" << gsArgs;
|
||||
converter.start(QStringLiteral("gs"), gsArgs);
|
||||
|
||||
|
@ -35,4 +35,3 @@ public:
|
||||
Q_DECLARE_LOGGING_CATEGORY(EPSPLUGIN)
|
||||
|
||||
#endif // KIMG_EPS_P_H
|
||||
|
||||
|
@ -9,41 +9,48 @@
|
||||
|
||||
#include "exr_p.h"
|
||||
|
||||
#include <ImfRgbaFile.h>
|
||||
#include <ImfStandardAttributes.h>
|
||||
#include <IexThrowErrnoExc.h>
|
||||
#include <ImathBox.h>
|
||||
#include <ImfInputFile.h>
|
||||
#include <ImfArray.h>
|
||||
#include <ImfBoxAttribute.h>
|
||||
#include <ImfChannelListAttribute.h>
|
||||
#include <ImfCompressionAttribute.h>
|
||||
#include <ImfConvert.h>
|
||||
#include <ImfFloatAttribute.h>
|
||||
#include <ImfInputFile.h>
|
||||
#include <ImfInt64.h>
|
||||
#include <ImfIntAttribute.h>
|
||||
#include <ImfLineOrderAttribute.h>
|
||||
#include <ImfRgbaFile.h>
|
||||
#include <ImfStandardAttributes.h>
|
||||
#include <ImfStringAttribute.h>
|
||||
#include <ImfVecAttribute.h>
|
||||
#include <ImfArray.h>
|
||||
#include <ImfConvert.h>
|
||||
#include <ImfVersion.h>
|
||||
#include <IexThrowErrnoExc.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <QImage>
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
#include <QImage>
|
||||
#include <QImageIOPlugin>
|
||||
|
||||
class K_IStream: public Imf::IStream
|
||||
class K_IStream : public Imf::IStream
|
||||
{
|
||||
public:
|
||||
K_IStream(QIODevice *dev, const QByteArray &fileName):
|
||||
IStream(fileName.data()), m_dev(dev)
|
||||
K_IStream(QIODevice *dev, const QByteArray &fileName)
|
||||
: IStream(fileName.data())
|
||||
, m_dev(dev)
|
||||
{
|
||||
}
|
||||
|
||||
bool read(char c[], int n) override;
|
||||
bool read(char c[], int n) override;
|
||||
#if OPENEXR_VERSION_MAJOR > 2
|
||||
uint64_t tellg() override;
|
||||
void seekg(uint64_t pos) override;
|
||||
#else
|
||||
Imf::Int64 tellg() override;
|
||||
void seekg(Imf::Int64 pos) override;
|
||||
#endif
|
||||
void clear() override;
|
||||
|
||||
private:
|
||||
@ -63,12 +70,20 @@ bool K_IStream::read(char c[], int n)
|
||||
return false;
|
||||
}
|
||||
|
||||
#if OPENEXR_VERSION_MAJOR > 2
|
||||
uint64_t K_IStream::tellg()
|
||||
#else
|
||||
Imf::Int64 K_IStream::tellg()
|
||||
#endif
|
||||
{
|
||||
return m_dev->pos();
|
||||
}
|
||||
|
||||
#if OPENEXR_VERSION_MAJOR > 2
|
||||
void K_IStream::seekg(uint64_t pos)
|
||||
#else
|
||||
void K_IStream::seekg(Imf::Int64 pos)
|
||||
#endif
|
||||
{
|
||||
m_dev->seek(pos);
|
||||
}
|
||||
@ -84,7 +99,10 @@ void K_IStream::clear()
|
||||
*/
|
||||
QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
|
||||
{
|
||||
float r, g, b, a;
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float a;
|
||||
|
||||
// 1) Compensate for fogging by subtracting defog
|
||||
// from the raw pixel values.
|
||||
@ -117,30 +135,30 @@ 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);
|
||||
//
|
||||
// 5) Gamma-correct the pixel values, assuming that the
|
||||
// screen's gamma is 0.4545 (or 1/2.2).
|
||||
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
|
||||
// the display's maximum intensity).
|
||||
//
|
||||
// 7) Clamp the values to [0, 255].
|
||||
// 6) Scale the values such that pixels middle gray
|
||||
// pixels are mapped to 84.66 (or 3.5 f-stops below
|
||||
// the display's maximum intensity).
|
||||
//
|
||||
// 7) Clamp the values to [0, 255].
|
||||
return qRgba((unsigned char)(Imath::clamp(r * 84.66f, 0.f, 255.f)),
|
||||
(unsigned char)(Imath::clamp(g * 84.66f, 0.f, 255.f)),
|
||||
(unsigned char)(Imath::clamp(b * 84.66f, 0.f, 255.f)),
|
||||
@ -163,13 +181,14 @@ 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);
|
||||
Imath::Box2i dw = file.dataWindow();
|
||||
|
||||
width = dw.max.x - dw.min.x + 1;
|
||||
width = dw.max.x - dw.min.x + 1;
|
||||
height = dw.max.y - dw.min.y + 1;
|
||||
|
||||
QImage image(width, height, QImage::Format_RGB32);
|
||||
@ -196,7 +215,7 @@ bool EXRHandler::read(QImage *outImage)
|
||||
|
||||
return true;
|
||||
} catch (const std::exception &exc) {
|
||||
// qDebug() << exc.what();
|
||||
// qDebug() << exc.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -20,13 +20,13 @@ typedef unsigned char uchar;
|
||||
// From GIMP "tile.h" v1.2
|
||||
|
||||
const uint TILE_WIDTH = 64; //!< Width of a tile in the XCF file.
|
||||
const uint TILE_HEIGHT = 64; //!< Height of a tile in the XCF file.
|
||||
const uint TILE_HEIGHT = 64; //!< Height of a tile in the XCF file.
|
||||
|
||||
// From GIMP "paint_funcs.c" v1.2
|
||||
|
||||
const int RANDOM_TABLE_SIZE = 4096; //!< Size of dissolve random number table.
|
||||
const int RANDOM_SEED = 314159265; //!< Seed for dissolve random number table.
|
||||
const double EPSILON = 0.0001; //!< Roundup in alpha blending.
|
||||
const double EPSILON = 0.0001; //!< Roundup in alpha blending.
|
||||
|
||||
// From GIMP "paint_funcs.h" v1.2
|
||||
|
||||
@ -41,10 +41,9 @@ const uchar OPAQUE_OPACITY = 255; //!< Opaque value for 8-bit alpha component.
|
||||
typedef enum {
|
||||
RGB,
|
||||
GRAY,
|
||||
INDEXED
|
||||
INDEXED,
|
||||
} GimpImageBaseType;
|
||||
|
||||
|
||||
// From GIMP "libgimp/gimpenums.h" v2.4
|
||||
|
||||
//! Effect to apply when layers are merged together.
|
||||
@ -74,7 +73,6 @@ typedef enum {
|
||||
GRAIN_MERGE_MODE
|
||||
} LayerModeEffects;
|
||||
|
||||
|
||||
// From GIMP "paint_funcs.c" v1.2
|
||||
|
||||
/*!
|
||||
@ -163,9 +161,9 @@ static void RGBTOHSV(uchar &red, uchar &green, uchar &blue)
|
||||
}
|
||||
}
|
||||
|
||||
red = (uchar)h;
|
||||
red = (uchar)h;
|
||||
green = (uchar)s;
|
||||
blue = (uchar)v;
|
||||
blue = (uchar)v;
|
||||
}
|
||||
|
||||
/*!
|
||||
@ -177,9 +175,9 @@ static void RGBTOHSV(uchar &red, uchar &green, uchar &blue)
|
||||
static void HSVTORGB(uchar &hue, uchar &saturation, uchar &value)
|
||||
{
|
||||
if (saturation == 0) {
|
||||
hue = value;
|
||||
hue = value;
|
||||
saturation = value;
|
||||
//value = value;
|
||||
// value = value;
|
||||
} else {
|
||||
double h = hue * 6. / 255.;
|
||||
double s = saturation / 255.;
|
||||
@ -195,34 +193,34 @@ static void HSVTORGB(uchar &hue, uchar &saturation, uchar &value)
|
||||
|
||||
switch ((int)h) {
|
||||
case 0:
|
||||
hue = (uchar)(v * 255);
|
||||
hue = (uchar)(v * 255);
|
||||
saturation = (uchar)(t * 255);
|
||||
value = (uchar)(p * 255);
|
||||
value = (uchar)(p * 255);
|
||||
break;
|
||||
case 1:
|
||||
hue = (uchar)(q * 255);
|
||||
hue = (uchar)(q * 255);
|
||||
saturation = (uchar)(v * 255);
|
||||
value = (uchar)(p * 255);
|
||||
value = (uchar)(p * 255);
|
||||
break;
|
||||
case 2:
|
||||
hue = (uchar)(p * 255);
|
||||
hue = (uchar)(p * 255);
|
||||
saturation = (uchar)(v * 255);
|
||||
value = (uchar)(t * 255);
|
||||
value = (uchar)(t * 255);
|
||||
break;
|
||||
case 3:
|
||||
hue = (uchar)(p * 255);
|
||||
hue = (uchar)(p * 255);
|
||||
saturation = (uchar)(q * 255);
|
||||
value = (uchar)(v * 255);
|
||||
value = (uchar)(v * 255);
|
||||
break;
|
||||
case 4:
|
||||
hue = (uchar)(t * 255);
|
||||
hue = (uchar)(t * 255);
|
||||
saturation = (uchar)(p * 255);
|
||||
value = (uchar)(v * 255);
|
||||
value = (uchar)(v * 255);
|
||||
break;
|
||||
case 5:
|
||||
hue = (uchar)(v * 255);
|
||||
hue = (uchar)(v * 255);
|
||||
saturation = (uchar)(p * 255);
|
||||
value = (uchar)(q * 255);
|
||||
value = (uchar)(q * 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -282,9 +280,9 @@ static void RGBTOHLS(uchar &red, uchar &green, uchar &blue)
|
||||
}
|
||||
}
|
||||
|
||||
red = (uchar)h;
|
||||
red = (uchar)h;
|
||||
green = (uchar)l;
|
||||
blue = (uchar)s;
|
||||
blue = (uchar)s;
|
||||
}
|
||||
|
||||
/*!
|
||||
@ -330,8 +328,8 @@ static void HLSTORGB(uchar &hue, uchar &lightness, uchar &saturation)
|
||||
double s = saturation;
|
||||
|
||||
if (s == 0) {
|
||||
hue = (uchar)l;
|
||||
lightness = (uchar)l;
|
||||
hue = (uchar)l;
|
||||
lightness = (uchar)l;
|
||||
saturation = (uchar)l;
|
||||
} else {
|
||||
double m1, m2;
|
||||
@ -344,8 +342,8 @@ static void HLSTORGB(uchar &hue, uchar &lightness, uchar &saturation)
|
||||
|
||||
m1 = (l / 127.5) - m2;
|
||||
|
||||
hue = HLSVALUE(m1, m2, h + 85);
|
||||
lightness = HLSVALUE(m1, m2, h);
|
||||
hue = HLSVALUE(m1, m2, h + 85);
|
||||
lightness = HLSVALUE(m1, m2, h);
|
||||
saturation = HLSVALUE(m1, m2, h - 85);
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,8 @@
|
||||
|
||||
#include "hdr_p.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <QDataStream>
|
||||
#include <QImage>
|
||||
#include <QLoggingCategory>
|
||||
#include <QRegularExpressionMatch>
|
||||
|
||||
@ -19,19 +19,18 @@ typedef unsigned char uchar;
|
||||
|
||||
Q_LOGGING_CATEGORY(HDRPLUGIN, "kf.imageformats.plugins.hdr", QtWarningMsg)
|
||||
|
||||
namespace // Private.
|
||||
namespace // Private.
|
||||
{
|
||||
|
||||
#define MAXLINE 1024
|
||||
#define MINELEN 8 // minimum scanline length for encoding
|
||||
#define MAXELEN 0x7fff // maximum scanline length for encoding
|
||||
#define MAXLINE 1024
|
||||
#define MINELEN 8 // minimum scanline length for encoding
|
||||
#define MAXELEN 0x7fff // maximum scanline length for encoding
|
||||
|
||||
static inline uchar ClipToByte(float value)
|
||||
{
|
||||
if (value > 255.0f) {
|
||||
return 255;
|
||||
}
|
||||
//else if (value < 0.0f) return 0; // we know value is positive.
|
||||
// else if (value < 0.0f) return 0; // we know value is positive.
|
||||
return uchar(value);
|
||||
}
|
||||
|
||||
@ -39,8 +38,8 @@ static inline uchar ClipToByte(float value)
|
||||
// if 'first' is true the first byte is already read
|
||||
static bool Read_Old_Line(uchar *image, int width, QDataStream &s)
|
||||
{
|
||||
int rshift = 0;
|
||||
int i;
|
||||
int rshift = 0;
|
||||
int i;
|
||||
|
||||
while (width > 0) {
|
||||
s >> image[0];
|
||||
@ -54,7 +53,7 @@ static bool Read_Old_Line(uchar *image, int width, QDataStream &s)
|
||||
|
||||
if ((image[0] == 1) && (image[1] == 1) && (image[2] == 1)) {
|
||||
for (i = image[3] << rshift; i > 0; i--) {
|
||||
//memcpy(image, image-4, 4);
|
||||
// memcpy(image, image-4, 4);
|
||||
(uint &)image[0] = (uint &)image[0 - 4];
|
||||
image += 4;
|
||||
width--;
|
||||
@ -81,9 +80,7 @@ static void RGBE_To_QRgbLine(uchar *image, QRgb *scanline, int width)
|
||||
v = 1.0f / float(1 << -e);
|
||||
}
|
||||
|
||||
scanline[j] = qRgb(ClipToByte(float(image[0]) * v),
|
||||
ClipToByte(float(image[1]) * v),
|
||||
ClipToByte(float(image[2]) * v));
|
||||
scanline[j] = qRgb(ClipToByte(float(image[0]) * v), ClipToByte(float(image[1]) * v), ClipToByte(float(image[2]) * v));
|
||||
|
||||
image += 4;
|
||||
}
|
||||
@ -92,7 +89,8 @@ static void RGBE_To_QRgbLine(uchar *image, QRgb *scanline, int width)
|
||||
// Load the HDR image.
|
||||
static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &img)
|
||||
{
|
||||
uchar val, code;
|
||||
uchar val;
|
||||
uchar code;
|
||||
|
||||
// Create dst image.
|
||||
img = QImage(width, height, QImage::Format_RGB32);
|
||||
@ -103,10 +101,10 @@ static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &i
|
||||
|
||||
QByteArray lineArray;
|
||||
lineArray.resize(4 * width);
|
||||
uchar *image = (uchar *) lineArray.data();
|
||||
uchar *image = (uchar *)lineArray.data();
|
||||
|
||||
for (int cline = 0; cline < height; cline++) {
|
||||
QRgb *scanline = (QRgb *) img.scanLine(cline);
|
||||
QRgb *scanline = (QRgb *)img.scanLine(cline);
|
||||
|
||||
// determine scanline type
|
||||
if ((width < MINELEN) || (MAXELEN < width)) {
|
||||
@ -168,7 +166,7 @@ static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &i
|
||||
} else {
|
||||
// non-run
|
||||
while (code != 0) {
|
||||
s >> image[i + j * 4];
|
||||
s >> image[i + j * 4];
|
||||
j++;
|
||||
code--;
|
||||
}
|
||||
@ -227,8 +225,14 @@ bool HDRHandler::read(QImage *outImage)
|
||||
qCDebug(HDRPLUGIN) << "Invalid HDR file, the first line after the header didn't have the expected format:" << line;
|
||||
return false;
|
||||
}
|
||||
const int width = match.captured(2).toInt();
|
||||
const int height = match.captured(4).toInt();
|
||||
|
||||
if ((match.captured(1).at(1) != u'Y') || (match.captured(3).at(1) != u'X')) {
|
||||
qCDebug(HDRPLUGIN) << "Unsupported image orientation in HDR file.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const int width = match.captured(4).toInt();
|
||||
const int height = match.captured(2).toInt();
|
||||
|
||||
QDataStream s(device());
|
||||
|
||||
|
734
src/imageformats/heif.cpp
Normal file
@ -0,0 +1,734 @@
|
||||
/*
|
||||
High Efficiency Image File Format (HEIF) support for QImage.
|
||||
|
||||
SPDX-FileCopyrightText: 2020 Sirius Bakke <sirius@bakke.co>
|
||||
SPDX-FileCopyrightText: 2021 Daniel Novomesky <dnovomesky@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "heif_p.h"
|
||||
#include "libheif/heif_cxx.h"
|
||||
|
||||
#include <QColorSpace>
|
||||
#include <QDebug>
|
||||
#include <QPointF>
|
||||
#include <QSysInfo>
|
||||
#include <string.h>
|
||||
|
||||
namespace // Private.
|
||||
{
|
||||
struct HeifQIODeviceWriter : public heif::Context::Writer {
|
||||
HeifQIODeviceWriter(QIODevice *device)
|
||||
: m_ioDevice(device)
|
||||
{
|
||||
}
|
||||
|
||||
heif_error write(const void *data, size_t size) override
|
||||
{
|
||||
heif_error error;
|
||||
error.code = heif_error_Ok;
|
||||
error.subcode = heif_suberror_Unspecified;
|
||||
error.message = errorOkMessage;
|
||||
|
||||
qint64 bytesWritten = m_ioDevice->write(static_cast<const char *>(data), size);
|
||||
|
||||
if (bytesWritten < static_cast<qint64>(size)) {
|
||||
error.code = heif_error_Encoding_error;
|
||||
error.message = QIODeviceWriteErrorMessage;
|
||||
error.subcode = heif_suberror_Cannot_write_output_data;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
static constexpr const char *errorOkMessage = "Success";
|
||||
static constexpr const char *QIODeviceWriteErrorMessage = "Bytes written to QIODevice are smaller than input data size";
|
||||
|
||||
private:
|
||||
QIODevice *m_ioDevice;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
HEIFHandler::HEIFHandler()
|
||||
: m_parseState(ParseHeicNotParsed)
|
||||
, m_quality(100)
|
||||
{
|
||||
}
|
||||
|
||||
bool HEIFHandler::canRead() const
|
||||
{
|
||||
if (m_parseState == ParseHeicNotParsed && !canRead(device())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_parseState != ParseHeicError) {
|
||||
setFormat("heif");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HEIFHandler::read(QImage *outImage)
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*outImage = m_current_image;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HEIFHandler::write(const QImage &image)
|
||||
{
|
||||
if (image.format() == QImage::Format_Invalid || image.isNull()) {
|
||||
qWarning("No image data to save");
|
||||
return false;
|
||||
}
|
||||
|
||||
int save_depth; // 8 or 10bit per channel
|
||||
QImage::Format tmpformat; // format for temporary image
|
||||
const bool save_alpha = image.hasAlphaChannel();
|
||||
|
||||
switch (image.format()) {
|
||||
case QImage::Format_BGR30:
|
||||
case QImage::Format_A2BGR30_Premultiplied:
|
||||
case QImage::Format_RGB30:
|
||||
case QImage::Format_A2RGB30_Premultiplied:
|
||||
case QImage::Format_Grayscale16:
|
||||
case QImage::Format_RGBX64:
|
||||
case QImage::Format_RGBA64:
|
||||
case QImage::Format_RGBA64_Premultiplied:
|
||||
save_depth = 10;
|
||||
break;
|
||||
default:
|
||||
if (image.depth() > 32) {
|
||||
save_depth = 10;
|
||||
} else {
|
||||
save_depth = 8;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
heif_chroma chroma;
|
||||
if (save_depth > 8) {
|
||||
if (save_alpha) {
|
||||
tmpformat = QImage::Format_RGBA64;
|
||||
chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBBAA_BE;
|
||||
} else {
|
||||
tmpformat = QImage::Format_RGBX64;
|
||||
chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RRGGBB_BE;
|
||||
}
|
||||
} else {
|
||||
if (save_alpha) {
|
||||
tmpformat = QImage::Format_RGBA8888;
|
||||
chroma = heif_chroma_interleaved_RGBA;
|
||||
} else {
|
||||
tmpformat = QImage::Format_RGB888;
|
||||
chroma = heif_chroma_interleaved_RGB;
|
||||
}
|
||||
}
|
||||
|
||||
const QImage tmpimage = image.convertToFormat(tmpformat);
|
||||
|
||||
try {
|
||||
heif::Context ctx;
|
||||
heif::Image heifImage;
|
||||
heifImage.create(tmpimage.width(), tmpimage.height(), heif_colorspace_RGB, chroma);
|
||||
|
||||
QByteArray iccprofile = tmpimage.colorSpace().iccProfile();
|
||||
if (iccprofile.size() > 0) {
|
||||
std::vector<uint8_t> rawProfile(iccprofile.begin(), iccprofile.end());
|
||||
heifImage.set_raw_color_profile(heif_color_profile_type_prof, rawProfile);
|
||||
}
|
||||
|
||||
heifImage.add_plane(heif_channel_interleaved, image.width(), image.height(), save_depth);
|
||||
int stride = 0;
|
||||
uint8_t *const dst = heifImage.get_plane(heif_channel_interleaved, &stride);
|
||||
size_t rowbytes;
|
||||
|
||||
switch (save_depth) {
|
||||
case 10:
|
||||
if (save_alpha) {
|
||||
for (int y = 0; y < tmpimage.height(); y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
|
||||
uint16_t *dest_word = reinterpret_cast<uint16_t *>(dst + (y * stride));
|
||||
for (int x = 0; x < tmpimage.width(); x++) {
|
||||
int tmp_pixelval;
|
||||
// R
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// G
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// B
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// A
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
}
|
||||
}
|
||||
} else { // no alpha channel
|
||||
for (int y = 0; y < tmpimage.height(); y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
|
||||
uint16_t *dest_word = reinterpret_cast<uint16_t *>(dst + (y * stride));
|
||||
for (int x = 0; x < tmpimage.width(); x++) {
|
||||
int tmp_pixelval;
|
||||
// R
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// G
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// B
|
||||
tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
|
||||
*dest_word = qBound(0, tmp_pixelval, 1023);
|
||||
src_word++;
|
||||
dest_word++;
|
||||
// X
|
||||
src_word++;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
rowbytes = save_alpha ? (tmpimage.width() * 4) : (tmpimage.width() * 3);
|
||||
for (int y = 0; y < tmpimage.height(); y++) {
|
||||
memcpy(dst + (y * stride), tmpimage.constScanLine(y), rowbytes);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
qWarning() << "Unsupported depth:" << save_depth;
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
heif::Encoder encoder(heif_compression_HEVC);
|
||||
|
||||
encoder.set_lossy_quality(m_quality);
|
||||
if (m_quality > 90) {
|
||||
if (m_quality == 100) {
|
||||
encoder.set_lossless(true);
|
||||
}
|
||||
encoder.set_string_parameter("chroma", "444");
|
||||
}
|
||||
|
||||
heif::Context::EncodingOptions encodingOptions;
|
||||
encodingOptions.save_alpha_channel = save_alpha;
|
||||
|
||||
if ((tmpimage.width() % 2 == 1) || (tmpimage.height() % 2 == 1)) {
|
||||
qWarning() << "Image has odd dimension!\nUse even-numbered dimension(s) for better compatibility with other HEIF implementations.";
|
||||
if (save_alpha) {
|
||||
// This helps to save alpha channel when image has odd dimension
|
||||
encodingOptions.macOS_compatibility_workaround = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.encode_image(heifImage, encoder, encodingOptions);
|
||||
|
||||
HeifQIODeviceWriter writer(device());
|
||||
|
||||
ctx.write(writer);
|
||||
|
||||
} catch (const heif::Error &err) {
|
||||
qWarning() << "libheif error:" << err.get_message().c_str();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HEIFHandler::canRead(QIODevice *device)
|
||||
{
|
||||
if (!device) {
|
||||
qWarning("HEIFHandler::canRead() called with no device");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray header = device->peek(28);
|
||||
return HEIFHandler::isSupportedBMFFType(header);
|
||||
}
|
||||
|
||||
bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
|
||||
{
|
||||
if (header.size() < 28) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *buffer = header.constData();
|
||||
if (qstrncmp(buffer + 4, "ftyp", 4) == 0) {
|
||||
if (qstrncmp(buffer + 8, "heic", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (qstrncmp(buffer + 8, "heis", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (qstrncmp(buffer + 8, "heix", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* we want to avoid loading AVIF files via this plugin */
|
||||
if (qstrncmp(buffer + 8, "mif1", 4) == 0) {
|
||||
for (int offset = 16; offset <= 24; offset += 4) {
|
||||
if (qstrncmp(buffer + offset, "avif", 4) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (qstrncmp(buffer + 8, "mif2", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (qstrncmp(buffer + 8, "msf1", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QVariant HEIFHandler::option(ImageOption option) const
|
||||
{
|
||||
if (option == Quality) {
|
||||
return m_quality;
|
||||
}
|
||||
|
||||
if (!supportsOption(option) || !ensureParsed()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (option) {
|
||||
case Size:
|
||||
return m_current_image.size();
|
||||
break;
|
||||
default:
|
||||
return QVariant();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HEIFHandler::setOption(ImageOption option, const QVariant &value)
|
||||
{
|
||||
switch (option) {
|
||||
case Quality:
|
||||
m_quality = value.toInt();
|
||||
if (m_quality > 100) {
|
||||
m_quality = 100;
|
||||
} else if (m_quality < 0) {
|
||||
m_quality = 100;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
QImageIOHandler::setOption(option, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool HEIFHandler::supportsOption(ImageOption option) const
|
||||
{
|
||||
return option == Quality || option == Size;
|
||||
}
|
||||
|
||||
bool HEIFHandler::ensureParsed() const
|
||||
{
|
||||
if (m_parseState == ParseHeicSuccess) {
|
||||
return true;
|
||||
}
|
||||
if (m_parseState == ParseHeicError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HEIFHandler *that = const_cast<HEIFHandler *>(this);
|
||||
|
||||
return that->ensureDecoder();
|
||||
}
|
||||
bool HEIFHandler::ensureDecoder()
|
||||
{
|
||||
if (m_parseState != ParseHeicNotParsed) {
|
||||
if (m_parseState == ParseHeicSuccess) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray buffer = device()->readAll();
|
||||
if (!HEIFHandler::isSupportedBMFFType(buffer)) {
|
||||
m_parseState = ParseHeicError;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
heif::Context ctx;
|
||||
ctx.read_from_memory_without_copy((const void *)(buffer.constData()), buffer.size());
|
||||
|
||||
heif::ImageHandle handle = ctx.get_primary_image_handle();
|
||||
|
||||
const bool hasAlphaChannel = handle.has_alpha_channel();
|
||||
const int bit_depth = handle.get_luma_bits_per_pixel();
|
||||
heif_chroma chroma;
|
||||
|
||||
QImage::Format target_image_format;
|
||||
|
||||
if (bit_depth == 10 || bit_depth == 12) {
|
||||
if (hasAlphaChannel) {
|
||||
chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBBAA_BE;
|
||||
target_image_format = QImage::Format_RGBA64;
|
||||
} else {
|
||||
chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RRGGBB_BE;
|
||||
target_image_format = QImage::Format_RGBX64;
|
||||
}
|
||||
} else if (bit_depth == 8) {
|
||||
if (hasAlphaChannel) {
|
||||
chroma = heif_chroma_interleaved_RGBA;
|
||||
target_image_format = QImage::Format_ARGB32;
|
||||
} else {
|
||||
chroma = heif_chroma_interleaved_RGB;
|
||||
target_image_format = QImage::Format_RGB32;
|
||||
}
|
||||
} else {
|
||||
m_parseState = ParseHeicError;
|
||||
if (bit_depth > 0) {
|
||||
qWarning() << "Unsupported bit depth:" << bit_depth;
|
||||
} else {
|
||||
qWarning() << "Undefined bit depth.";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
heif::Image img = handle.decode_image(heif_colorspace_RGB, chroma);
|
||||
|
||||
const int imageWidth = img.get_width(heif_channel_interleaved);
|
||||
const int imageHeight = img.get_height(heif_channel_interleaved);
|
||||
|
||||
QSize imageSize(imageWidth, imageHeight);
|
||||
|
||||
if (!imageSize.isValid()) {
|
||||
m_parseState = ParseHeicError;
|
||||
qWarning() << "HEIC image size invalid:" << imageSize;
|
||||
return false;
|
||||
}
|
||||
|
||||
int stride = 0;
|
||||
const uint8_t *const src = img.get_plane(heif_channel_interleaved, &stride);
|
||||
|
||||
if (!src || stride <= 0) {
|
||||
m_parseState = ParseHeicError;
|
||||
qWarning() << "HEIC data pixels information not valid!";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_current_image = QImage(imageSize, target_image_format);
|
||||
if (m_current_image.isNull()) {
|
||||
m_parseState = ParseHeicError;
|
||||
qWarning() << "Unable to allocate memory!";
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (bit_depth) {
|
||||
case 12:
|
||||
if (hasAlphaChannel) {
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
|
||||
uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int tmpvalue;
|
||||
// R
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// G
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// B
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// A
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
}
|
||||
}
|
||||
} else { // no alpha channel
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
|
||||
uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int tmpvalue;
|
||||
// R
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// G
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// B
|
||||
tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// X = 0xffff
|
||||
*dest_data = 0xffff;
|
||||
dest_data++;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 10:
|
||||
if (hasAlphaChannel) {
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
|
||||
uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int tmpvalue;
|
||||
// R
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// G
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// B
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// A
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
}
|
||||
}
|
||||
} else { // no alpha channel
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
|
||||
uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int tmpvalue;
|
||||
// R
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// G
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// B
|
||||
tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
|
||||
tmpvalue = qBound(0, tmpvalue, 65535);
|
||||
*dest_data = (uint16_t)tmpvalue;
|
||||
src_word++;
|
||||
dest_data++;
|
||||
// X = 0xffff
|
||||
*dest_data = 0xffff;
|
||||
dest_data++;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
if (hasAlphaChannel) {
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint8_t *src_byte = src + (y * stride);
|
||||
uint32_t *dest_pixel = reinterpret_cast<uint32_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int red = *src_byte++;
|
||||
int green = *src_byte++;
|
||||
int blue = *src_byte++;
|
||||
int alpha = *src_byte++;
|
||||
*dest_pixel = qRgba(red, green, blue, alpha);
|
||||
dest_pixel++;
|
||||
}
|
||||
}
|
||||
} else { // no alpha channel
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
const uint8_t *src_byte = src + (y * stride);
|
||||
uint32_t *dest_pixel = reinterpret_cast<uint32_t *>(m_current_image.scanLine(y));
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
int red = *src_byte++;
|
||||
int green = *src_byte++;
|
||||
int blue = *src_byte++;
|
||||
*dest_pixel = qRgb(red, green, blue);
|
||||
dest_pixel++;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
m_parseState = ParseHeicError;
|
||||
qWarning() << "Unsupported bit depth:" << bit_depth;
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
heif_color_profile_type profileType = heif_image_handle_get_color_profile_type(handle.get_raw_image_handle());
|
||||
struct heif_error err;
|
||||
if (profileType == heif_color_profile_type_prof || profileType == heif_color_profile_type_rICC) {
|
||||
int rawProfileSize = (int)heif_image_handle_get_raw_color_profile_size(handle.get_raw_image_handle());
|
||||
if (rawProfileSize > 0) {
|
||||
QByteArray ba(rawProfileSize, 0);
|
||||
err = heif_image_handle_get_raw_color_profile(handle.get_raw_image_handle(), ba.data());
|
||||
if (err.code) {
|
||||
qWarning() << "icc profile loading failed";
|
||||
} else {
|
||||
m_current_image.setColorSpace(QColorSpace::fromIccProfile(ba));
|
||||
if (!m_current_image.colorSpace().isValid()) {
|
||||
qWarning() << "HEIC image has Qt-unsupported or invalid ICC profile!";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qWarning() << "icc profile is empty";
|
||||
}
|
||||
|
||||
} else if (profileType == heif_color_profile_type_nclx) {
|
||||
struct heif_color_profile_nclx *nclx = nullptr;
|
||||
err = heif_image_handle_get_nclx_color_profile(handle.get_raw_image_handle(), &nclx);
|
||||
if (err.code || !nclx) {
|
||||
qWarning() << "nclx profile loading failed";
|
||||
} else {
|
||||
const QPointF redPoint(nclx->color_primary_red_x, nclx->color_primary_red_y);
|
||||
const QPointF greenPoint(nclx->color_primary_green_x, nclx->color_primary_green_y);
|
||||
const QPointF bluePoint(nclx->color_primary_blue_x, nclx->color_primary_blue_y);
|
||||
const QPointF whitePoint(nclx->color_primary_white_x, nclx->color_primary_white_y);
|
||||
|
||||
QColorSpace::TransferFunction q_trc = QColorSpace::TransferFunction::Custom;
|
||||
float q_trc_gamma = 0.0f;
|
||||
|
||||
switch (nclx->transfer_characteristics) {
|
||||
case 4:
|
||||
q_trc = QColorSpace::TransferFunction::Gamma;
|
||||
q_trc_gamma = 2.2f;
|
||||
break;
|
||||
case 5:
|
||||
q_trc = QColorSpace::TransferFunction::Gamma;
|
||||
q_trc_gamma = 2.8f;
|
||||
break;
|
||||
case 8:
|
||||
q_trc = QColorSpace::TransferFunction::Linear;
|
||||
break;
|
||||
case 2:
|
||||
case 13:
|
||||
q_trc = QColorSpace::TransferFunction::SRgb;
|
||||
break;
|
||||
default:
|
||||
qWarning("CICP color_primaries: %d, transfer_characteristics: %d\nThe colorspace is unsupported by this plug-in yet.",
|
||||
nclx->color_primaries,
|
||||
nclx->transfer_characteristics);
|
||||
q_trc = QColorSpace::TransferFunction::SRgb;
|
||||
break;
|
||||
}
|
||||
|
||||
if (q_trc != QColorSpace::TransferFunction::Custom) { // we create new colorspace using Qt
|
||||
switch (nclx->color_primaries) {
|
||||
case 1:
|
||||
case 2:
|
||||
m_current_image.setColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, q_trc, q_trc_gamma));
|
||||
break;
|
||||
case 12:
|
||||
m_current_image.setColorSpace(QColorSpace(QColorSpace::Primaries::DciP3D65, q_trc, q_trc_gamma));
|
||||
break;
|
||||
default:
|
||||
m_current_image.setColorSpace(QColorSpace(whitePoint, redPoint, greenPoint, bluePoint, q_trc, q_trc_gamma));
|
||||
break;
|
||||
}
|
||||
}
|
||||
heif_nclx_color_profile_free(nclx);
|
||||
|
||||
if (!m_current_image.colorSpace().isValid()) {
|
||||
qWarning() << "HEIC plugin created invalid QColorSpace from NCLX!";
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
m_current_image.setColorSpace(QColorSpace(QColorSpace::SRgb));
|
||||
}
|
||||
|
||||
} catch (const heif::Error &err) {
|
||||
m_parseState = ParseHeicError;
|
||||
qWarning() << "libheif error:" << err.get_message().c_str();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_parseState = ParseHeicSuccess;
|
||||
return true;
|
||||
}
|
||||
|
||||
QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
if (format == "heif" || format == "heic") {
|
||||
Capabilities format_cap;
|
||||
if (heif_have_decoder_for_format(heif_compression_HEVC)) {
|
||||
format_cap |= CanRead;
|
||||
}
|
||||
if (heif_have_encoder_for_format(heif_compression_HEVC)) {
|
||||
format_cap |= CanWrite;
|
||||
}
|
||||
return format_cap;
|
||||
}
|
||||
if (!format.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
if (!device->isOpen()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Capabilities cap;
|
||||
if (device->isReadable() && HEIFHandler::canRead(device) && heif_have_decoder_for_format(heif_compression_HEVC)) {
|
||||
cap |= CanRead;
|
||||
}
|
||||
|
||||
if (device->isWritable() && heif_have_encoder_for_format(heif_compression_HEVC)) {
|
||||
cap |= CanWrite;
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
|
||||
QImageIOHandler *HEIFPlugin::create(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
QImageIOHandler *handler = new HEIFHandler;
|
||||
handler->setDevice(device);
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
7
src/imageformats/heif.desktop
Normal file
@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Service
|
||||
X-KDE-ServiceTypes=QImageIOPlugins
|
||||
X-KDE-ImageFormat=heif
|
||||
X-KDE-MimeType=image/heif
|
||||
X-KDE-Read=true
|
||||
X-KDE-Write=true
|
4
src/imageformats/heif.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Keys": [ "heif", "heic" ],
|
||||
"MimeTypes": [ "image/heif", "image/heif" ]
|
||||
}
|
58
src/imageformats/heif_p.h
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
High Efficiency Image File Format (HEIF) support for QImage.
|
||||
|
||||
SPDX-FileCopyrightText: 2020 Sirius Bakke <sirius@bakke.co>
|
||||
SPDX-FileCopyrightText: 2021 Daniel Novomesky <dnovomesky@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KIMG_HEIF_P_H
|
||||
#define KIMG_HEIF_P_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QImage>
|
||||
#include <QImageIOPlugin>
|
||||
|
||||
class HEIFHandler : public QImageIOHandler
|
||||
{
|
||||
public:
|
||||
HEIFHandler();
|
||||
|
||||
bool canRead() const override;
|
||||
bool read(QImage *image) override;
|
||||
bool write(const QImage &image) override;
|
||||
|
||||
static bool canRead(QIODevice *device);
|
||||
|
||||
QVariant option(ImageOption option) const override;
|
||||
void setOption(ImageOption option, const QVariant &value) override;
|
||||
bool supportsOption(ImageOption option) const override;
|
||||
|
||||
private:
|
||||
static bool isSupportedBMFFType(const QByteArray &header);
|
||||
bool ensureParsed() const;
|
||||
bool ensureDecoder();
|
||||
|
||||
enum ParseHeicState {
|
||||
ParseHeicError = -1,
|
||||
ParseHeicNotParsed = 0,
|
||||
ParseHeicSuccess = 1,
|
||||
};
|
||||
|
||||
ParseHeicState m_parseState;
|
||||
int m_quality;
|
||||
QImage m_current_image;
|
||||
};
|
||||
|
||||
class HEIFPlugin : public QImageIOPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "heif.json")
|
||||
|
||||
public:
|
||||
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
|
||||
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||
};
|
||||
|
||||
#endif // KIMG_HEIF_P_H
|
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
|