Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
bfc73ca260 | |||
f6bb59228e | |||
ee381958b2 | |||
478e49b8a6 | |||
c3daf86079 | |||
a981cefdd2 | |||
723f72930b | |||
99bb24803a | |||
63a9de758f | |||
240e28aac5 | |||
906ecce500 | |||
b2b677b8a5 | |||
bcec942cc9 | |||
66cb8c91d0 | |||
899a2df42d | |||
47920ed63c | |||
274f30e008 | |||
4348a09733 | |||
dd4576a472 | |||
9438540735 | |||
cf78907ff4 | |||
bcb5308545 | |||
c3a91c3bc6 | |||
034b8f331b | |||
ed6a3c520d | |||
bf1c7e8508 | |||
3cb6519dcc | |||
6cbf7529ee | |||
55227815d5 | |||
64d51ed610 | |||
2ca57c9c59 | |||
f7fd14d418 | |||
c9aa1ff629 | |||
91d3bd5227 | |||
bb66367bc8 | |||
14770318a3 | |||
9b1fafe29b | |||
fa673b5df8 | |||
e96b43aef5 | |||
64f3303ef0 | |||
63056c52f9 | |||
2997f7ae8d | |||
0b4741f4b7 | |||
bc52c03981 | |||
c1c57d9a11 |
@ -6,9 +6,5 @@ include:
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-static.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/windows-static.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
|
||||
|
@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16)
|
||||
project(KImageFormats)
|
||||
|
||||
include(FeatureSummary)
|
||||
find_package(ECM 5.102.0 NO_MODULE)
|
||||
find_package(ECM 5.112.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)
|
||||
|
||||
@ -51,7 +51,10 @@ set_package_properties(OpenEXR PROPERTIES
|
||||
PURPOSE "Required for the QImage plugin for OpenEXR images"
|
||||
)
|
||||
|
||||
find_package(libavif 0.8.2 CONFIG)
|
||||
find_package(libavif 0.8.2 CONFIG QUIET)
|
||||
if(NOT libavif_FOUND)
|
||||
find_package(libavif 1 CONFIG)
|
||||
endif()
|
||||
set_package_properties(libavif PROPERTIES
|
||||
TYPE OPTIONAL
|
||||
PURPOSE "Required for the QImage plugin for AVIF images"
|
||||
@ -65,8 +68,8 @@ add_feature_info(LibHeif LibHeif_FOUND "required for the QImage plugin for HEIF/
|
||||
|
||||
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)
|
||||
pkg_check_modules(LibJXL IMPORTED_TARGET libjxl>=0.7.0)
|
||||
pkg_check_modules(LibJXLThreads IMPORTED_TARGET libjxl_threads>=0.7.0)
|
||||
endif()
|
||||
add_feature_info(LibJXL LibJXL_FOUND "required for the QImage plugin for JPEG XL images")
|
||||
|
||||
|
@ -14,11 +14,12 @@ image formats.
|
||||
The following image formats have read-only support:
|
||||
|
||||
- Animated Windows cursors (ani)
|
||||
- Camera RAW images (arw, cr2, cr3, dcs, dng, ...)
|
||||
- Gimp (xcf)
|
||||
- OpenEXR (exr)
|
||||
- Photoshop documents (psd, psb, pdd, psdt)
|
||||
- Radiance HDR (hdr)
|
||||
- Sun Raster (ras)
|
||||
- Camera RAW images (arw, cr2, cr3, dcs, dng, ...)
|
||||
|
||||
The following image formats have read and write support:
|
||||
|
||||
@ -26,6 +27,7 @@ The following image formats have read and write support:
|
||||
- Encapsulated PostScript (eps)
|
||||
- JPEG XL (jxl)
|
||||
- Personal Computer Exchange (pcx)
|
||||
- Quite OK Image format (qoi)
|
||||
- SGI images (rgb, rgba, sgi, bw)
|
||||
- Softimage PIC (pic)
|
||||
- Targa (tga): supports more formats than Qt's version
|
||||
|
@ -1,7 +1,6 @@
|
||||
#find_package(Qt5Test ${REQUIRED_QT_VERSION} NO_MODULE)
|
||||
|
||||
include(ECMMarkAsTest)
|
||||
include(CMakeParseArguments)
|
||||
|
||||
add_definitions(-DPLUGIN_DIR="${CMAKE_CURRENT_BINARY_DIR}/../bin")
|
||||
remove_definitions(-DQT_NO_CAST_FROM_ASCII)
|
||||
@ -70,6 +69,7 @@ kimageformats_read_tests(
|
||||
hdr
|
||||
pcx
|
||||
psd
|
||||
qoi
|
||||
ras
|
||||
rgb
|
||||
tga
|
||||
@ -125,6 +125,7 @@ kimageformats_read_tests(FUZZ 1
|
||||
kimageformats_write_tests(
|
||||
pcx-lossless
|
||||
pic-lossless
|
||||
qoi-lossless
|
||||
rgb-lossless
|
||||
tga # fixme: the alpha images appear not to be written properly
|
||||
)
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.7 KiB |
BIN
autotests/read/pcx/ccbug_463951.pcx
Normal file
BIN
autotests/read/pcx/ccbug_463951.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
autotests/read/pcx/mono.pcx
Normal file
BIN
autotests/read/pcx/mono.png
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 983 B After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 94 KiB |
BIN
autotests/read/psd/birthday.tif
Normal file
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 189 KiB |
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 114 KiB |
BIN
autotests/read/psd/mch3-16bits.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
autotests/read/psd/mch3-16bits.psb
Normal file
BIN
autotests/read/psd/mch3-8bits.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
autotests/read/psd/mch3-8bits.psb
Normal file
BIN
autotests/read/qoi/1px.png
Normal file
After Width: | Height: | Size: 551 B |
BIN
autotests/read/qoi/1px.qoi
Normal file
BIN
autotests/read/qoi/2px.png
Normal file
After Width: | Height: | Size: 561 B |
BIN
autotests/read/qoi/2px.qoi
Normal file
BIN
autotests/read/qoi/bnm_rgb.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
autotests/read/qoi/bnm_rgb.qoi
Normal file
BIN
autotests/read/qoi/bnm_rgba.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
autotests/read/qoi/bnm_rgba.qoi
Normal file
BIN
autotests/read/qoi/testcard.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
autotests/read/qoi/testcard.qoi
Normal file
BIN
autotests/read/qoi/testcard_rgba.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
autotests/read/qoi/testcard_rgba.qoi
Normal file
BIN
autotests/read/xcf/birthday.png
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
autotests/read/xcf/birthday.xcf
Normal file
BIN
autotests/read/xcf/birthday16.png
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
autotests/read/xcf/birthday16.xcf
Normal file
BIN
autotests/read/xcf/birthday16_gray.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
autotests/read/xcf/birthday16_gray.xcf
Normal file
BIN
autotests/read/xcf/birthday16_grayA.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
autotests/read/xcf/birthday16_grayA.xcf
Normal file
BIN
autotests/read/xcf/birthday16fp.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
autotests/read/xcf/birthday16fp.xcf
Normal file
BIN
autotests/read/xcf/birthday16fp_gray.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
autotests/read/xcf/birthday16fp_gray.xcf
Normal file
BIN
autotests/read/xcf/birthday16fp_grayA.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
autotests/read/xcf/birthday16fp_grayA.xcf
Normal file
BIN
autotests/read/xcf/birthday32.png
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
autotests/read/xcf/birthday32.xcf
Normal file
BIN
autotests/read/xcf/birthday32_gray.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
autotests/read/xcf/birthday32_gray.xcf
Normal file
BIN
autotests/read/xcf/birthday32_grayA.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
autotests/read/xcf/birthday32_grayA.xcf
Normal file
BIN
autotests/read/xcf/birthday32fp.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
autotests/read/xcf/birthday32fp.xcf
Normal file
BIN
autotests/read/xcf/birthday32fp_gray.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
autotests/read/xcf/birthday32fp_gray.xcf
Normal file
BIN
autotests/read/xcf/birthday32fp_grayA.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
autotests/read/xcf/birthday32fp_grayA.xcf
Normal file
BIN
autotests/read/xcf/birthday_grayA.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
autotests/read/xcf/birthday_grayA.xcf
Normal file
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 115 KiB |
BIN
autotests/read/xcf/fruktpilot16_icc.png
Normal file
After Width: | Height: | Size: 115 KiB |
BIN
autotests/read/xcf/fruktpilot16_icc.xcf
Normal file
BIN
autotests/read/xcf/fruktpilot16fplin_icc.png
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
autotests/read/xcf/fruktpilot16fplin_icc.xcf
Normal file
BIN
autotests/read/xcf/fruktpilot32_icc.png
Normal file
After Width: | Height: | Size: 115 KiB |
BIN
autotests/read/xcf/fruktpilot32_icc.xcf
Normal file
BIN
autotests/read/xcf/fruktpilot32fplin_icc.png
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
autotests/read/xcf/fruktpilot32fplin_icc.xcf
Normal file
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
@ -159,17 +159,22 @@ int main(int argc, char **argv)
|
||||
QTextStream(stdout) << "* Run on RANDOM ACCESS device\n";
|
||||
}
|
||||
for (const QFileInfo &fi : lstImgDir) {
|
||||
if (!fi.suffix().compare("png", Qt::CaseInsensitive)) {
|
||||
if (!fi.suffix().compare("png", Qt::CaseInsensitive) || !fi.suffix().compare("tif", Qt::CaseInsensitive)) {
|
||||
continue;
|
||||
}
|
||||
int suffixPos = fi.filePath().count() - suffix.count();
|
||||
QString inputfile = fi.filePath();
|
||||
QString expfile = fi.filePath().replace(suffixPos, suffix.count(), QStringLiteral("png"));
|
||||
QString fmt = QStringLiteral("png");
|
||||
QString expfile = fi.filePath().replace(suffixPos, suffix.count(), fmt);
|
||||
if (!QFile::exists(expfile)) { // try with tiff
|
||||
fmt = QStringLiteral("tif");
|
||||
expfile = fi.filePath().replace(suffixPos, suffix.count(), fmt);
|
||||
}
|
||||
QString expfilename = QFileInfo(expfile).fileName();
|
||||
|
||||
std::unique_ptr<QIODevice> inputDevice(seq ? new SequentialFile(inputfile) : new QFile(inputfile));
|
||||
QImageReader inputReader(inputDevice.get(), format);
|
||||
QImageReader expReader(expfile, "png");
|
||||
QImageReader expReader(expfile, fmt.toLatin1());
|
||||
|
||||
QImage inputImage;
|
||||
QImage expImage;
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
BIN
autotests/write/rgb.qoi
Normal file
BIN
autotests/write/rgba.qoi
Normal file
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@ -12,3 +12,4 @@ Ignacio Castaño <castano@ludicon.com> -- DDS and PDS format reader.
|
||||
Christoph Hormann <chris_hormann@gmx.de> -- HDR format read support.
|
||||
Michael Ritzert <kde@ritzert.de> -- JPEG 2000 format read/write support
|
||||
Troy Unrau <troy@kde.org> -- Sun RASter read support
|
||||
Ernest Gupik <ernestgupik@wp.pl> -- QOI format read support
|
||||
|
@ -104,9 +104,6 @@ endif()
|
||||
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
|
||||
kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp)
|
||||
target_link_libraries(kimg_jxl PkgConfig::LibJXL PkgConfig::LibJXLThreads)
|
||||
if (LibJXL_VERSION VERSION_GREATER_EQUAL "0.7.0")
|
||||
target_compile_definitions(kimg_jxl PRIVATE KIMG_JXL_API_VERSION=70)
|
||||
endif()
|
||||
|
||||
if (QT_MAJOR_VERSION STREQUAL "5")
|
||||
install(FILES jxl.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
@ -136,6 +133,13 @@ endif()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_qoi SOURCES qoi.cpp scanlineconverter.cpp)
|
||||
if (QT_MAJOR_VERSION STREQUAL "5")
|
||||
install(FILES qoi.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
endif()
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_ras SOURCES ras.cpp)
|
||||
if (QT_MAJOR_VERSION STREQUAL "5")
|
||||
install(FILES ras.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
|
@ -570,3 +570,5 @@ QImageIOHandler *ANIPlugin::create(QIODevice *device, const QByteArray &format)
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_ani_p.cpp"
|
||||
|
@ -330,6 +330,10 @@ bool QAVIFHandler::decode_one_frame()
|
||||
avifRGBImage rgb;
|
||||
avifRGBImageSetDefaults(&rgb, m_decoder->image);
|
||||
|
||||
#if AVIF_VERSION >= 1000000
|
||||
rgb.maxThreads = m_decoder->maxThreads;
|
||||
#endif
|
||||
|
||||
if (m_decoder->image->depth > 8) {
|
||||
rgb.depth = 16;
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
@ -424,7 +428,7 @@ bool QAVIFHandler::decode_one_frame()
|
||||
}
|
||||
|
||||
if (m_decoder->image->transformFlags & AVIF_TRANSFORM_IMIR) {
|
||||
#if AVIF_VERSION > 90100
|
||||
#if AVIF_VERSION > 90100 && AVIF_VERSION < 1000000
|
||||
switch (m_decoder->image->imir.mode) {
|
||||
#else
|
||||
switch (m_decoder->image->imir.axis) {
|
||||
@ -714,9 +718,9 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
if (save_depth == 8) {
|
||||
save_depth = 10;
|
||||
if (tmpcolorimage.hasAlphaChannel()) {
|
||||
tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBA64);
|
||||
tmpcolorimage.convertTo(QImage::Format_RGBA64);
|
||||
} else {
|
||||
tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBX64);
|
||||
tmpcolorimage.convertTo(QImage::Format_RGBX64);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1039,6 +1043,11 @@ int QAVIFHandler::loopCount() const
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if AVIF_VERSION >= 1000000
|
||||
if (m_decoder->repetitionCount >= 0) {
|
||||
return m_decoder->repetitionCount;
|
||||
}
|
||||
#endif
|
||||
// Endless loop to work around https://github.com/AOMediaCodec/libavif/issues/347
|
||||
return -1;
|
||||
}
|
||||
@ -1103,3 +1112,5 @@ QImageIOHandler *QAVIFPlugin::create(QIODevice *device, const QByteArray &format
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_avif_p.cpp"
|
||||
|
@ -365,3 +365,5 @@ QImageIOHandler *EPSPlugin::create(QIODevice *device, const QByteArray &format)
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_eps_p.cpp"
|
||||
|
@ -3,10 +3,27 @@
|
||||
in the high dynamic range EXR format.
|
||||
|
||||
SPDX-FileCopyrightText: 2003 Brad Hards <bradh@frogmouth.net>
|
||||
SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
/* *** EXR_USE_LEGACY_CONVERSIONS ***
|
||||
* If defined, the result image is an 8-bit RGB(A) converted
|
||||
* without icc profiles. Otherwise, a 16-bit images is generated.
|
||||
* NOTE: The use of legacy conversions are discouraged due to
|
||||
* imprecise image result.
|
||||
*/
|
||||
//#define EXR_USE_LEGACY_CONVERSIONS // default commented -> you should define it in your cmake file
|
||||
|
||||
/* *** EXR_ALLOW_LINEAR_COLORSPACE ***
|
||||
* If defined, the linear data is kept and it is the display program that
|
||||
* must convert to the monitor profile. Otherwise the data is converted to sRGB
|
||||
* to accommodate programs that do not support color profiles.
|
||||
* NOTE: If EXR_USE_LEGACY_CONVERSIONS is active, this is ignored.
|
||||
*/
|
||||
//#define EXR_ALLOW_LINEAR_COLORSPACE // default: commented -> you should define it in your cmake file
|
||||
|
||||
#include "exr_p.h"
|
||||
#include "util_p.h"
|
||||
|
||||
@ -30,11 +47,24 @@
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <QColorSpace>
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
#include <QFloat16>
|
||||
#include <QImage>
|
||||
#include <QImageIOPlugin>
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
#include <QTimeZone>
|
||||
#endif
|
||||
|
||||
// Allow the code to works on all QT versions supported by KDE
|
||||
// project (Qt 5.15 and Qt 6.x) to easy backports fixes.
|
||||
#if (QT_VERSION_MAJOR >= 6) && !defined(EXR_USE_LEGACY_CONVERSIONS)
|
||||
// If uncommented, the image is rendered in a float16 format, the result is very precise
|
||||
#define EXR_USE_QT6_FLOAT_IMAGE // default uncommented
|
||||
#endif
|
||||
|
||||
class K_IStream : public Imf::IStream
|
||||
{
|
||||
public:
|
||||
@ -94,77 +124,21 @@ void K_IStream::clear()
|
||||
// TODO
|
||||
}
|
||||
|
||||
/* this does a conversion from the ILM Half (equal to Nvidia Half)
|
||||
* format into the normal 32 bit pixel format. Process is from the
|
||||
* ILM code.
|
||||
*/
|
||||
QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
|
||||
#ifdef EXR_USE_LEGACY_CONVERSIONS
|
||||
// source: https://openexr.com/en/latest/ReadingAndWritingImageFiles.html
|
||||
inline unsigned char gamma(float x)
|
||||
{
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float a;
|
||||
|
||||
// 1) Compensate for fogging by subtracting defog
|
||||
// from the raw pixel values.
|
||||
// Response: We work with defog of 0.0, so this is a no-op
|
||||
|
||||
// 2) Multiply the defogged pixel values by
|
||||
// 2^(exposure + 2.47393).
|
||||
// Response: We work with exposure of 0.0.
|
||||
// (2^2.47393) is 5.55555
|
||||
r = imagePixel.r * 5.55555;
|
||||
g = imagePixel.g * 5.55555;
|
||||
b = imagePixel.b * 5.55555;
|
||||
a = imagePixel.a * 5.55555;
|
||||
|
||||
// 3) Values, which are now 1.0, are called "middle gray".
|
||||
// If defog and exposure are both set to 0.0, then
|
||||
// middle gray corresponds to a raw pixel value of 0.18.
|
||||
// In step 6, middle gray values will be mapped to an
|
||||
// intensity 3.5 f-stops below the display's maximum
|
||||
// intensity.
|
||||
// Response: no apparent content.
|
||||
|
||||
// 4) Apply a knee function. The knee function has two
|
||||
// parameters, kneeLow and kneeHigh. Pixel values
|
||||
// below 2^kneeLow are not changed by the knee
|
||||
// function. Pixel values above kneeLow are lowered
|
||||
// according to a logarithmic curve, such that the
|
||||
// value 2^kneeHigh is mapped to 2^3.5 (in step 6,
|
||||
// this value will be mapped to the display's
|
||||
// maximum intensity).
|
||||
// Response: kneeLow = 0.0 (2^0.0 => 1); kneeHigh = 5.0 (2^5 =>32)
|
||||
if (r > 1.0) {
|
||||
r = 1.0 + std::log((r - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
}
|
||||
if (g > 1.0) {
|
||||
g = 1.0 + std::log((g - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
}
|
||||
if (b > 1.0) {
|
||||
b = 1.0 + std::log((b - 1.0) * 0.184874 + 1) / 0.184874;
|
||||
}
|
||||
if (a > 1.0) {
|
||||
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 = 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].
|
||||
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)),
|
||||
(unsigned char)(Imath::clamp(a * 84.66f, 0.f, 255.f)));
|
||||
x = std::pow(5.5555f * std::max(0.f, x), 0.4545f) * 84.66f;
|
||||
return (unsigned char)qBound(0.f, x, 255.f);
|
||||
}
|
||||
inline QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
|
||||
{
|
||||
return qRgba(gamma(float(imagePixel.r)),
|
||||
gamma(float(imagePixel.g)),
|
||||
gamma(float(imagePixel.b)),
|
||||
(unsigned char)(qBound(0.f, imagePixel.a * 255.f, 255.f) + 0.5f));
|
||||
}
|
||||
#endif
|
||||
|
||||
EXRHandler::EXRHandler()
|
||||
{
|
||||
@ -188,29 +162,98 @@ bool EXRHandler::read(QImage *outImage)
|
||||
K_IStream istr(device(), QByteArray());
|
||||
Imf::RgbaInputFile file(istr);
|
||||
Imath::Box2i dw = file.dataWindow();
|
||||
bool isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
|
||||
|
||||
width = dw.max.x - dw.min.x + 1;
|
||||
height = dw.max.y - dw.min.y + 1;
|
||||
|
||||
QImage image = imageAlloc(width, height, QImage::Format_RGB32);
|
||||
#if defined(EXR_USE_LEGACY_CONVERSIONS)
|
||||
QImage image = imageAlloc(width, height, isRgba ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||
#elif defined(EXR_USE_QT6_FLOAT_IMAGE)
|
||||
QImage image = imageAlloc(width, height, isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4);
|
||||
#else
|
||||
QImage image = imageAlloc(width, height, isRgba ? QImage::Format_RGBA64 : QImage::Format_RGBX64);
|
||||
#endif
|
||||
if (image.isNull()) {
|
||||
qWarning() << "Failed to allocate image, invalid size?" << QSize(width, height);
|
||||
return false;
|
||||
}
|
||||
|
||||
Imf::Array2D<Imf::Rgba> pixels;
|
||||
pixels.resizeErase(height, width);
|
||||
|
||||
file.setFrameBuffer(&pixels[0][0] - dw.min.x - dw.min.y * width, 1, width);
|
||||
file.readPixels(dw.min.y, dw.max.y);
|
||||
|
||||
// somehow copy pixels into image
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
// copy pixels(x,y) into image(x,y)
|
||||
image.setPixel(x, y, RgbaToQrgba(pixels[y][x]));
|
||||
// set some useful metadata
|
||||
auto &&h = file.header();
|
||||
if (auto comments = h.findTypedAttribute<Imf::StringAttribute>("comments")) {
|
||||
image.setText(QStringLiteral("Comment"), QString::fromStdString(comments->value()));
|
||||
}
|
||||
if (auto owner = h.findTypedAttribute<Imf::StringAttribute>("owner")) {
|
||||
image.setText(QStringLiteral("Owner"), QString::fromStdString(owner->value()));
|
||||
}
|
||||
if (auto capDate = h.findTypedAttribute<Imf::StringAttribute>("capDate")) {
|
||||
float off = 0;
|
||||
if (auto utcOffset = h.findTypedAttribute<Imf::FloatAttribute>("utcOffset")) {
|
||||
off = utcOffset->value();
|
||||
}
|
||||
auto dateTime = QDateTime::fromString(QString::fromStdString(capDate->value()), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
|
||||
if (dateTime.isValid()) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
dateTime.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(off));
|
||||
#else
|
||||
dateTime.setOffsetFromUtc(off);
|
||||
#endif
|
||||
image.setText(QStringLiteral("Date"), dateTime.toString(Qt::ISODate));
|
||||
}
|
||||
}
|
||||
if (auto xDensity = h.findTypedAttribute<Imf::FloatAttribute>("xDensity")) {
|
||||
float par = 1;
|
||||
if (auto pixelAspectRatio = h.findTypedAttribute<Imf::FloatAttribute>("pixelAspectRatio")) {
|
||||
par = pixelAspectRatio->value();
|
||||
}
|
||||
image.setDotsPerMeterX(qRound(xDensity->value() * 100.0 / 2.54));
|
||||
image.setDotsPerMeterY(qRound(xDensity->value() * par * 100.0 / 2.54));
|
||||
}
|
||||
|
||||
Imf::Array<Imf::Rgba> pixels;
|
||||
pixels.resizeErase(width);
|
||||
|
||||
// somehow copy pixels into image
|
||||
for (int y = 0; y < height; ++y) {
|
||||
auto my = dw.min.y + y;
|
||||
if (my <= dw.max.y) { // paranoia check
|
||||
file.setFrameBuffer(&pixels[0] - dw.min.x - qint64(my) * width, 1, width);
|
||||
file.readPixels(my, my);
|
||||
|
||||
#if defined(EXR_USE_LEGACY_CONVERSIONS)
|
||||
auto scanLine = reinterpret_cast<QRgb *>(image.scanLine(y));
|
||||
for (int x = 0; x < width; ++x) {
|
||||
*(scanLine + x) = RgbaToQrgba(pixels[x]);
|
||||
}
|
||||
#elif defined(EXR_USE_QT6_FLOAT_IMAGE)
|
||||
auto scanLine = reinterpret_cast<qfloat16 *>(image.scanLine(y));
|
||||
for (int x = 0; x < width; ++x) {
|
||||
auto xcs = x * 4;
|
||||
*(scanLine + xcs) = qfloat16(qBound(0.f, float(pixels[x].r), 1.f));
|
||||
*(scanLine + xcs + 1) = qfloat16(qBound(0.f, float(pixels[x].g), 1.f));
|
||||
*(scanLine + xcs + 2) = qfloat16(qBound(0.f, float(pixels[x].b), 1.f));
|
||||
*(scanLine + xcs + 3) = qfloat16(isRgba ? qBound(0.f, float(pixels[x].a), 1.f) : 1.f);
|
||||
}
|
||||
#else
|
||||
auto scanLine = reinterpret_cast<QRgba64 *>(image.scanLine(y));
|
||||
for (int x = 0; x < width; ++x) {
|
||||
*(scanLine + x) = QRgba64::fromRgba64(quint16(qBound(0.f, float(pixels[x].r) * 65535.f + 0.5f, 65535.f)),
|
||||
quint16(qBound(0.f, float(pixels[x].g) * 65535.f + 0.5f, 65535.f)),
|
||||
quint16(qBound(0.f, float(pixels[x].b) * 65535.f + 0.5f, 65535.f)),
|
||||
isRgba ? quint16(qBound(0.f, float(pixels[x].a) * 65535.f + 0.5f, 65535.f)) : quint16(65535));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// final color operations
|
||||
#ifndef EXR_USE_LEGACY_CONVERSIONS
|
||||
image.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
|
||||
#ifndef EXR_ALLOW_LINEAR_COLORSPACE
|
||||
image.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
|
||||
#endif // !EXR_ALLOW_LINEAR_COLORSPACE
|
||||
#endif // !EXR_USE_LEGACY_CONVERSIONS
|
||||
|
||||
*outImage = image;
|
||||
|
||||
@ -259,3 +302,5 @@ QImageIOHandler *EXRPlugin::create(QIODevice *device, const QByteArray &format)
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_exr_p.cpp"
|
||||
|
35
src/imageformats/fastmath_p.h
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
Approximated math functions used into conversions.
|
||||
|
||||
SPDX-FileCopyrightText: Edward Kmett
|
||||
SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
|
||||
|
||||
SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
#ifndef FASTMATH_P_H
|
||||
#define FASTMATH_P_H
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
/*!
|
||||
* \brief fastPow
|
||||
* Based on Edward Kmett code released into the public domain.
|
||||
* See also: https://github.com/ekmett/approximate
|
||||
*/
|
||||
inline double fastPow(double x, double y)
|
||||
{
|
||||
union {
|
||||
double d;
|
||||
qint32 i[2];
|
||||
} u = {x};
|
||||
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||||
u.i[1] = qint32(y * (u.i[1] - 1072632447) + 1072632447);
|
||||
u.i[0] = 0;
|
||||
#else // never tested
|
||||
u.i[0] = qint32(y * (u.i[0] - 1072632447) + 1072632447);
|
||||
u.i[1] = 0;
|
||||
#endif
|
||||
return u.d;
|
||||
}
|
||||
|
||||
#endif // FASTMATH_P_H
|
@ -46,33 +46,6 @@ typedef enum {
|
||||
|
||||
// From GIMP "libgimp/gimpenums.h" v2.4
|
||||
|
||||
//! Effect to apply when layers are merged together.
|
||||
|
||||
typedef enum {
|
||||
NORMAL_MODE,
|
||||
DISSOLVE_MODE,
|
||||
BEHIND_MODE,
|
||||
MULTIPLY_MODE,
|
||||
SCREEN_MODE,
|
||||
OVERLAY_MODE,
|
||||
DIFFERENCE_MODE,
|
||||
ADDITION_MODE,
|
||||
SUBTRACT_MODE,
|
||||
DARKEN_ONLY_MODE,
|
||||
LIGHTEN_ONLY_MODE,
|
||||
HUE_MODE,
|
||||
SATURATION_MODE,
|
||||
COLOR_MODE,
|
||||
VALUE_MODE,
|
||||
DIVIDE_MODE,
|
||||
DODGE_MODE,
|
||||
BURN_MODE,
|
||||
HARDLIGHT_MODE,
|
||||
SOFTLIGHT_MODE,
|
||||
GRAIN_EXTRACT_MODE,
|
||||
GRAIN_MERGE_MODE
|
||||
} LayerModeEffects;
|
||||
|
||||
// From GIMP "paint_funcs.c" v1.2
|
||||
|
||||
/*!
|
||||
|
@ -9,6 +9,7 @@
|
||||
#include "hdr_p.h"
|
||||
#include "util_p.h"
|
||||
|
||||
#include <QColorSpace>
|
||||
#include <QDataStream>
|
||||
#include <QImage>
|
||||
#include <QLoggingCategory>
|
||||
@ -28,11 +29,8 @@ namespace // Private.
|
||||
|
||||
static inline uchar ClipToByte(float value)
|
||||
{
|
||||
if (value > 255.0f) {
|
||||
return 255;
|
||||
}
|
||||
// else if (value < 0.0f) return 0; // we know value is positive.
|
||||
return uchar(value);
|
||||
// we know value is positive.
|
||||
return uchar(std::min(value + 0.5f, 255.0f));
|
||||
}
|
||||
|
||||
// read an old style line from the hdr image file
|
||||
@ -42,6 +40,7 @@ static bool Read_Old_Line(uchar *image, int width, QDataStream &s)
|
||||
int rshift = 0;
|
||||
int i;
|
||||
|
||||
uchar *start = image;
|
||||
while (width > 0) {
|
||||
s >> image[0];
|
||||
s >> image[1];
|
||||
@ -53,7 +52,14 @@ 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--) {
|
||||
// NOTE: we don't have an image sample that cover this code
|
||||
if (rshift > 31) {
|
||||
return false;
|
||||
}
|
||||
for (i = image[3] << rshift; i > 0 && width > 0; i--) {
|
||||
if (image == start) {
|
||||
return false; // you cannot be here at the first run
|
||||
}
|
||||
// memcpy(image, image-4, 4);
|
||||
(uint &)image[0] = (uint &)image[0 - 4];
|
||||
image += 4;
|
||||
@ -74,7 +80,7 @@ static void RGBE_To_QRgbLine(uchar *image, QRgb *scanline, int width)
|
||||
for (int j = 0; j < width; j++) {
|
||||
// v = ldexp(1.0, int(image[3]) - 128);
|
||||
float v;
|
||||
int e = int(image[3]) - 128;
|
||||
int e = qBound(-31, int(image[3]) - 128, 31);
|
||||
if (e > 0) {
|
||||
v = float(1 << e);
|
||||
} else {
|
||||
@ -148,7 +154,7 @@ static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &i
|
||||
}
|
||||
|
||||
// read each component
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int i = 0, len = int(lineArray.size()); i < 4; i++) {
|
||||
for (int j = 0; j < width;) {
|
||||
s >> code;
|
||||
if (s.atEnd()) {
|
||||
@ -160,14 +166,20 @@ static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &i
|
||||
code &= 127;
|
||||
s >> val;
|
||||
while (code != 0) {
|
||||
image[i + j * 4] = val;
|
||||
auto idx = i + j * 4;
|
||||
if (idx < len) {
|
||||
image[idx] = val;
|
||||
}
|
||||
j++;
|
||||
code--;
|
||||
}
|
||||
} else {
|
||||
// non-run
|
||||
while (code != 0) {
|
||||
s >> image[i + j * 4];
|
||||
auto idx = i + j * 4;
|
||||
if (idx < len) {
|
||||
s >> image[idx];
|
||||
}
|
||||
j++;
|
||||
code--;
|
||||
}
|
||||
@ -242,6 +254,9 @@ bool HDRHandler::read(QImage *outImage)
|
||||
// qDebug() << "Error loading HDR file.";
|
||||
return false;
|
||||
}
|
||||
// The images read by Gimp and Photoshop (including those of the tests) are interpreted with linear color space.
|
||||
// By setting the linear color space, programs that support profiles display HDR files as in GIMP and Photoshop.
|
||||
img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
|
||||
|
||||
*outImage = img;
|
||||
return true;
|
||||
@ -296,3 +311,5 @@ QImageIOHandler *HDRPlugin::create(QIODevice *device, const QByteArray &format)
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_hdr_p.cpp"
|
||||
|
@ -449,6 +449,14 @@ bool HEIFHandler::ensureDecoder()
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((heif_image_handle_get_width(handle) == 0) || (heif_image_handle_get_height(handle) == 0)) {
|
||||
m_parseState = ParseHeicError;
|
||||
heif_image_handle_release(handle);
|
||||
heif_context_free(ctx);
|
||||
qWarning() << "HEIC image has zero dimension";
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool hasAlphaChannel = heif_image_handle_has_alpha_channel(handle);
|
||||
const int bit_depth = heif_image_handle_get_luma_bits_per_pixel(handle);
|
||||
heif_chroma chroma;
|
||||
@ -918,3 +926,5 @@ QImageIOHandler *HEIFPlugin::create(QIODevice *device, const QByteArray &format)
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_heif_p.cpp"
|
||||
|
@ -150,9 +150,7 @@ bool QJpegXLHandler::ensureDecoder()
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef KIMG_JXL_API_VERSION
|
||||
JxlDecoderCloseInput(m_decoder);
|
||||
#endif
|
||||
|
||||
JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME);
|
||||
if (status == JXL_DEC_ERROR) {
|
||||
@ -269,18 +267,31 @@ bool QJpegXLHandler::countALLFrames()
|
||||
}
|
||||
}
|
||||
|
||||
status = JxlDecoderGetColorAsEncodedProfile(m_decoder, &m_input_pixel_format, JXL_COLOR_PROFILE_TARGET_DATA, &color_encoding);
|
||||
status = JxlDecoderGetColorAsEncodedProfile(m_decoder,
|
||||
#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
|
||||
&m_input_pixel_format,
|
||||
#endif
|
||||
JXL_COLOR_PROFILE_TARGET_DATA,
|
||||
&color_encoding);
|
||||
|
||||
if (status == JXL_DEC_SUCCESS && color_encoding.color_space == JXL_COLOR_SPACE_RGB && color_encoding.white_point == JXL_WHITE_POINT_D65
|
||||
&& color_encoding.primaries == JXL_PRIMARIES_SRGB && color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_SRGB) {
|
||||
m_colorspace = QColorSpace(QColorSpace::SRgb);
|
||||
} else {
|
||||
size_t icc_size = 0;
|
||||
if (JxlDecoderGetICCProfileSize(m_decoder, &m_input_pixel_format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size) == JXL_DEC_SUCCESS) {
|
||||
if (JxlDecoderGetICCProfileSize(m_decoder,
|
||||
#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
|
||||
&m_input_pixel_format,
|
||||
#endif
|
||||
JXL_COLOR_PROFILE_TARGET_DATA,
|
||||
&icc_size)
|
||||
== JXL_DEC_SUCCESS) {
|
||||
if (icc_size > 0) {
|
||||
QByteArray icc_data(icc_size, 0);
|
||||
if (JxlDecoderGetColorAsICCProfile(m_decoder,
|
||||
#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
|
||||
&m_input_pixel_format,
|
||||
#endif
|
||||
JXL_COLOR_PROFILE_TARGET_DATA,
|
||||
reinterpret_cast<uint8_t *>(icc_data.data()),
|
||||
icc_data.size())
|
||||
@ -534,9 +545,7 @@ bool QJpegXLHandler::write(const QImage &image)
|
||||
if (save_depth == 16 && (image.hasAlphaChannel() || output_info.uses_original_profile)) {
|
||||
output_info.have_container = JXL_TRUE;
|
||||
JxlEncoderUseContainer(encoder, JXL_TRUE);
|
||||
#ifdef KIMG_JXL_API_VERSION
|
||||
JxlEncoderSetCodestreamLevel(encoder, 10);
|
||||
#endif
|
||||
}
|
||||
|
||||
void *runner = nullptr;
|
||||
@ -650,19 +659,11 @@ bool QJpegXLHandler::write(const QImage &image)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef KIMG_JXL_API_VERSION
|
||||
JxlEncoderFrameSettings *encoder_options = JxlEncoderFrameSettingsCreate(encoder, nullptr);
|
||||
|
||||
JxlEncoderSetFrameDistance(encoder_options, (100.0f - m_quality) / 10.0f);
|
||||
|
||||
JxlEncoderSetFrameLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE);
|
||||
#else
|
||||
JxlEncoderOptions *encoder_options = JxlEncoderOptionsCreate(encoder, nullptr);
|
||||
|
||||
JxlEncoderOptionsSetDistance(encoder_options, (100.0f - m_quality) / 10.0f);
|
||||
|
||||
JxlEncoderOptionsSetLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE);
|
||||
#endif
|
||||
|
||||
if (image.hasAlphaChannel() || ((save_depth == 8) && (xsize % 4 == 0))) {
|
||||
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmpimage.constBits()), buffer_size);
|
||||
@ -957,9 +958,7 @@ bool QJpegXLHandler::rewind()
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef KIMG_JXL_API_VERSION
|
||||
JxlDecoderCloseInput(m_decoder);
|
||||
#endif
|
||||
|
||||
if (m_basicinfo.uses_original_profile) {
|
||||
if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
|
||||
@ -1021,3 +1020,5 @@ QImageIOHandler *QJpegXLPlugin::create(QIODevice *device, const QByteArray &form
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_jxl_p.cpp"
|
||||
|
@ -95,3 +95,5 @@ QImageIOHandler *KraPlugin::create(QIODevice *device, const QByteArray &format)
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_kra.cpp"
|
||||
|
@ -94,3 +94,5 @@ QImageIOHandler *OraPlugin::create(QIODevice *device, const QByteArray &format)
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_ora.cpp"
|
||||
|
@ -230,7 +230,7 @@ PCXHEADER::PCXHEADER()
|
||||
s >> *this;
|
||||
}
|
||||
|
||||
static void readLine(QDataStream &s, QByteArray &buf, const PCXHEADER &header)
|
||||
static bool readLine(QDataStream &s, QByteArray &buf, const PCXHEADER &header)
|
||||
{
|
||||
quint32 i = 0;
|
||||
quint32 size = buf.size();
|
||||
@ -257,9 +257,11 @@ static void readLine(QDataStream &s, QByteArray &buf, const PCXHEADER &header)
|
||||
buf[i++] = byte;
|
||||
}
|
||||
}
|
||||
|
||||
return (s.status() == QDataStream::Ok);
|
||||
}
|
||||
|
||||
static void readImage1(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
static bool readImage1(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
{
|
||||
QByteArray buf(header.BytesPerLine, 0);
|
||||
|
||||
@ -268,16 +270,18 @@ static void readImage1(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
|
||||
if (img.isNull()) {
|
||||
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int y = 0; y < header.height(); ++y) {
|
||||
if (s.atEnd()) {
|
||||
img = QImage();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!readLine(s, buf, header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
readLine(s, buf, header);
|
||||
uchar *p = img.scanLine(y);
|
||||
unsigned int bpl = qMin((quint16)((header.width() + 7) / 8), header.BytesPerLine);
|
||||
for (unsigned int x = 0; x < bpl; ++x) {
|
||||
@ -288,9 +292,11 @@ static void readImage1(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
// Set the color palette
|
||||
img.setColor(0, qRgb(0, 0, 0));
|
||||
img.setColor(1, qRgb(255, 255, 255));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
static bool readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
{
|
||||
QByteArray buf(header.BytesPerLine * 4, 0);
|
||||
QByteArray pixbuf(header.width(), 0);
|
||||
@ -299,17 +305,18 @@ static void readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
img.setColorCount(16);
|
||||
if (img.isNull()) {
|
||||
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int y = 0; y < header.height(); ++y) {
|
||||
if (s.atEnd()) {
|
||||
img = QImage();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
pixbuf.fill(0);
|
||||
readLine(s, buf, header);
|
||||
if (!readLine(s, buf, header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
quint32 offset = i * header.BytesPerLine;
|
||||
@ -333,9 +340,11 @@ static void readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
img.setColor(i, header.ColorMap.color(i));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void readImage8(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
static bool readImage8(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
{
|
||||
QByteArray buf(header.BytesPerLine, 0);
|
||||
|
||||
@ -344,21 +353,21 @@ static void readImage8(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
|
||||
if (img.isNull()) {
|
||||
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int y = 0; y < header.height(); ++y) {
|
||||
if (s.atEnd()) {
|
||||
img = QImage();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
readLine(s, buf, header);
|
||||
if (!readLine(s, buf, header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uchar *p = img.scanLine(y);
|
||||
|
||||
if (!p) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned int bpl = qMin(header.BytesPerLine, (quint16)header.width());
|
||||
@ -367,10 +376,21 @@ static void readImage8(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
}
|
||||
}
|
||||
|
||||
quint8 flag;
|
||||
s >> flag;
|
||||
// qDebug() << "Palette Flag: " << flag;
|
||||
// by specification, the extended palette starts at file.size() - 769
|
||||
quint8 flag = 0;
|
||||
if (auto device = s.device()) {
|
||||
if (device->isSequential()) {
|
||||
while (flag != 12 && s.status() == QDataStream::Ok) {
|
||||
s >> flag;
|
||||
}
|
||||
}
|
||||
else {
|
||||
device->seek(device->size() - 769);
|
||||
s >> flag;
|
||||
}
|
||||
}
|
||||
|
||||
// qDebug() << "Palette Flag: " << flag;
|
||||
if (flag == 12 && (header.Version == 5 || header.Version == 2)) {
|
||||
// Read the palette
|
||||
quint8 r;
|
||||
@ -381,9 +401,11 @@ static void readImage8(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
img.setColor(i, qRgb(r, g, b));
|
||||
}
|
||||
}
|
||||
|
||||
return (s.status() == QDataStream::Ok);
|
||||
}
|
||||
|
||||
static void readImage24(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
static bool readImage24(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
{
|
||||
QByteArray r_buf(header.BytesPerLine, 0);
|
||||
QByteArray g_buf(header.BytesPerLine, 0);
|
||||
@ -393,27 +415,34 @@ static void readImage24(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
|
||||
if (img.isNull()) {
|
||||
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int y = 0; y < header.height(); ++y) {
|
||||
if (s.atEnd()) {
|
||||
img = QImage();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
readLine(s, r_buf, header);
|
||||
readLine(s, g_buf, header);
|
||||
readLine(s, b_buf, header);
|
||||
if (!readLine(s, r_buf, header)) {
|
||||
return false;
|
||||
}
|
||||
if (!readLine(s, g_buf, header)) {
|
||||
return false;
|
||||
}
|
||||
if (!readLine(s, b_buf, header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint *p = (uint *)img.scanLine(y);
|
||||
for (int x = 0; x < header.width(); ++x) {
|
||||
p[x] = qRgb(r_buf[x], g_buf[x], b_buf[x]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void writeLine(QDataStream &s, QByteArray &buf)
|
||||
static bool writeLine(QDataStream &s, QByteArray &buf)
|
||||
{
|
||||
quint32 i = 0;
|
||||
quint32 size = buf.size();
|
||||
@ -439,15 +468,26 @@ static void writeLine(QDataStream &s, QByteArray &buf)
|
||||
|
||||
s << data;
|
||||
}
|
||||
return (s.status() == QDataStream::Ok);
|
||||
}
|
||||
|
||||
static void writeImage1(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
static bool writeImage1(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
{
|
||||
img = img.convertToFormat(QImage::Format_Mono);
|
||||
if (img.format() != QImage::Format_Mono) {
|
||||
img = img.convertToFormat(QImage::Format_Mono);
|
||||
}
|
||||
if (img.isNull() || img.colorCount() < 1) {
|
||||
return false;
|
||||
}
|
||||
auto rgb = img.color(0);
|
||||
auto minIsBlack = (qRed(rgb) + qGreen(rgb) + qBlue(rgb)) / 3 < 127;
|
||||
|
||||
header.Bpp = 1;
|
||||
header.NPlanes = 1;
|
||||
header.BytesPerLine = img.bytesPerLine();
|
||||
if (header.BytesPerLine == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
s << header;
|
||||
|
||||
@ -458,18 +498,24 @@ static void writeImage1(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
|
||||
// Invert as QImage uses reverse palette for monochrome images?
|
||||
for (int i = 0; i < header.BytesPerLine; ++i) {
|
||||
buf[i] = ~p[i];
|
||||
buf[i] = minIsBlack ? p[i] : ~p[i];
|
||||
}
|
||||
|
||||
writeLine(s, buf);
|
||||
if (!writeLine(s, buf)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void writeImage4(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
static bool writeImage4(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
{
|
||||
header.Bpp = 1;
|
||||
header.NPlanes = 4;
|
||||
header.BytesPerLine = header.width() / 8;
|
||||
if (header.BytesPerLine == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
header.ColorMap.setColor(i, img.color(i));
|
||||
@ -499,16 +545,22 @@ static void writeImage4(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
writeLine(s, buf[i]);
|
||||
if (!writeLine(s, buf[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void writeImage8(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
static bool writeImage8(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
{
|
||||
header.Bpp = 8;
|
||||
header.NPlanes = 1;
|
||||
header.BytesPerLine = img.bytesPerLine();
|
||||
if (header.BytesPerLine == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
s << header;
|
||||
|
||||
@ -521,7 +573,9 @@ static void writeImage8(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
buf[i] = p[i];
|
||||
}
|
||||
|
||||
writeLine(s, buf);
|
||||
if (!writeLine(s, buf)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Write palette flag
|
||||
@ -532,13 +586,25 @@ static void writeImage8(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
s << RGB::from(img.color(i));
|
||||
}
|
||||
|
||||
return (s.status() == QDataStream::Ok);
|
||||
}
|
||||
|
||||
static void writeImage24(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
static bool writeImage24(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
{
|
||||
header.Bpp = 8;
|
||||
header.NPlanes = 3;
|
||||
header.BytesPerLine = header.width();
|
||||
if (header.BytesPerLine == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (img.format() != QImage::Format_ARGB32 && img.format() != QImage::Format_RGB32) {
|
||||
img = img.convertToFormat(QImage::Format_RGB32);
|
||||
}
|
||||
if (img.isNull()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
s << header;
|
||||
|
||||
@ -547,7 +613,7 @@ static void writeImage24(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
QByteArray b_buf(header.width(), 0);
|
||||
|
||||
for (int y = 0; y < header.height(); ++y) {
|
||||
uint *p = (uint *)img.scanLine(y);
|
||||
auto p = (QRgb*)img.scanLine(y);
|
||||
|
||||
for (int x = 0; x < header.width(); ++x) {
|
||||
QRgb rgb = *p++;
|
||||
@ -556,10 +622,18 @@ static void writeImage24(QImage &img, QDataStream &s, PCXHEADER &header)
|
||||
b_buf[x] = qBlue(rgb);
|
||||
}
|
||||
|
||||
writeLine(s, r_buf);
|
||||
writeLine(s, g_buf);
|
||||
writeLine(s, b_buf);
|
||||
if (!writeLine(s, r_buf)) {
|
||||
return false;
|
||||
}
|
||||
if (!writeLine(s, g_buf)) {
|
||||
return false;
|
||||
}
|
||||
if (!writeLine(s, b_buf)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
PCXHandler::PCXHandler()
|
||||
@ -588,46 +662,30 @@ bool PCXHandler::read(QImage *outImage)
|
||||
|
||||
s >> header;
|
||||
|
||||
if (header.Manufacturer != 10 || s.atEnd()) {
|
||||
if (header.Manufacturer != 10 || header.BytesPerLine == 0 || s.atEnd()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// int w = header.width();
|
||||
// int h = header.height();
|
||||
|
||||
// qDebug() << "Manufacturer: " << header.Manufacturer;
|
||||
// qDebug() << "Version: " << header.Version;
|
||||
// qDebug() << "Encoding: " << header.Encoding;
|
||||
// qDebug() << "Bpp: " << header.Bpp;
|
||||
// qDebug() << "Width: " << w;
|
||||
// qDebug() << "Height: " << h;
|
||||
// qDebug() << "Window: " << header.XMin << "," << header.XMax << ","
|
||||
// << header.YMin << "," << header.YMax << endl;
|
||||
// qDebug() << "BytesPerLine: " << header.BytesPerLine;
|
||||
// qDebug() << "NPlanes: " << header.NPlanes;
|
||||
|
||||
auto ok = false;
|
||||
QImage img;
|
||||
|
||||
if (header.Bpp == 1 && header.NPlanes == 1) {
|
||||
readImage1(img, s, header);
|
||||
ok = readImage1(img, s, header);
|
||||
} else if (header.Bpp == 1 && header.NPlanes == 4) {
|
||||
readImage4(img, s, header);
|
||||
ok = readImage4(img, s, header);
|
||||
} else if (header.Bpp == 8 && header.NPlanes == 1) {
|
||||
readImage8(img, s, header);
|
||||
ok = readImage8(img, s, header);
|
||||
} else if (header.Bpp == 8 && header.NPlanes == 3) {
|
||||
readImage24(img, s, header);
|
||||
ok = readImage24(img, s, header);
|
||||
}
|
||||
|
||||
// qDebug() << "Image Bytes: " << img.numBytes();
|
||||
// qDebug() << "Image Bytes Per Line: " << img.bytesPerLine();
|
||||
// qDebug() << "Image Depth: " << img.depth();
|
||||
|
||||
if (!img.isNull()) {
|
||||
*outImage = img;
|
||||
return true;
|
||||
} else {
|
||||
if (img.isNull() || !ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
img.setDotsPerMeterX(qRound(header.HDpi / 25.4 * 1000));
|
||||
img.setDotsPerMeterY(qRound(header.YDpi / 25.4 * 1000));
|
||||
*outImage = img;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCXHandler::write(const QImage &image)
|
||||
@ -644,12 +702,6 @@ bool PCXHandler::write(const QImage &image)
|
||||
return false;
|
||||
}
|
||||
|
||||
// qDebug() << "Width: " << w;
|
||||
// qDebug() << "Height: " << h;
|
||||
// qDebug() << "Depth: " << img.depth();
|
||||
// qDebug() << "BytesPerLine: " << img.bytesPerLine();
|
||||
// qDebug() << "Color Count: " << img.colorCount();
|
||||
|
||||
PCXHEADER header;
|
||||
|
||||
header.Manufacturer = 10;
|
||||
@ -659,22 +711,23 @@ bool PCXHandler::write(const QImage &image)
|
||||
header.YMin = 0;
|
||||
header.XMax = w - 1;
|
||||
header.YMax = h - 1;
|
||||
header.HDpi = 300;
|
||||
header.YDpi = 300;
|
||||
header.HDpi = qRound(image.dotsPerMeterX() * 25.4 / 1000);
|
||||
header.YDpi = qRound(image.dotsPerMeterY() * 25.4 / 1000);
|
||||
header.Reserved = 0;
|
||||
header.PaletteInfo = 1;
|
||||
|
||||
auto ok = false;
|
||||
if (img.depth() == 1) {
|
||||
writeImage1(img, s, header);
|
||||
ok = writeImage1(img, s, header);
|
||||
} else if (img.depth() == 8 && img.colorCount() <= 16) {
|
||||
writeImage4(img, s, header);
|
||||
ok = writeImage4(img, s, header);
|
||||
} else if (img.depth() == 8) {
|
||||
writeImage8(img, s, header);
|
||||
} else if (img.depth() == 32) {
|
||||
writeImage24(img, s, header);
|
||||
ok = writeImage8(img, s, header);
|
||||
} else if (img.depth() >= 24) {
|
||||
ok = writeImage24(img, s, header);
|
||||
}
|
||||
|
||||
return true;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool PCXHandler::canRead(QIODevice *device)
|
||||
@ -739,3 +792,5 @@ QImageIOHandler *PCXPlugin::create(QIODevice *device, const QByteArray &format)
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_pcx_p.cpp"
|
||||
|
@ -452,3 +452,5 @@ QImageIOHandler *SoftimagePICPlugin::create(QIODevice *device, const QByteArray
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_pic_p.cpp"
|
||||
|
@ -3,14 +3,14 @@
|
||||
|
||||
SPDX-FileCopyrightText: 2003 Ignacio Castaño <castano@ludicon.com>
|
||||
SPDX-FileCopyrightText: 2015 Alex Merry <alex.merry@kde.org>
|
||||
SPDX-FileCopyrightText: 2022 Mirco Miranda <mircomir@outlook.com>
|
||||
SPDX-FileCopyrightText: 2022-2023 Mirco Miranda <mircomir@outlook.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
/*
|
||||
* This code is based on Thacher Ulrich PSD loading code released
|
||||
* into the public domain. See: http://tulrich.com/geekstuff/
|
||||
* The early version of this code was based on Thacher Ulrich PSD loading code
|
||||
* released into the public domain. See: http://tulrich.com/geekstuff/
|
||||
*/
|
||||
|
||||
/*
|
||||
@ -21,7 +21,6 @@
|
||||
/*
|
||||
* Limitations of the current code:
|
||||
* - 32-bit float image are converted to 16-bit integer image.
|
||||
* NOTE: Qt 6.2 allow 32-bit float images (RGB only)
|
||||
* - Other color spaces cannot directly be read due to lack of QImage support for
|
||||
* color spaces other than RGB (and Grayscale). Where possible, a conversion
|
||||
* to RGB is done:
|
||||
@ -33,6 +32,7 @@
|
||||
* color management engine (e.g. LittleCMS).
|
||||
*/
|
||||
|
||||
#include "fastmath_p.h"
|
||||
#include "psd_p.h"
|
||||
#include "util_p.h"
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
#include <QColorSpace>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
typedef quint32 uint;
|
||||
typedef quint16 ushort;
|
||||
@ -51,7 +52,7 @@ typedef quint8 uchar;
|
||||
* This should not be a problem because the Qt's QColorSpace supports the linear
|
||||
* sRgb colorspace.
|
||||
*
|
||||
* Using linear conversion, the loading speed is improved by 4x. Anyway, if you are using
|
||||
* Using linear conversion, the loading speed is slightly improved. Anyway, if you are using
|
||||
* an software that discard color info, you should comment it.
|
||||
*
|
||||
* At the time I'm writing (07/2022), Gwenview and Krita supports linear sRgb but KDE
|
||||
@ -669,7 +670,7 @@ static bool IsSupported(const PSDHeader &header)
|
||||
return false;
|
||||
}
|
||||
if (header.color_mode == CM_MULTICHANNEL &&
|
||||
header.channel_count < 4) {
|
||||
header.channel_count < 3) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -733,9 +734,9 @@ static QImage::Format imageFormat(const PSDHeader &header, bool alpha)
|
||||
switch(header.color_mode) {
|
||||
case CM_RGB:
|
||||
if (header.depth == 16 || header.depth == 32)
|
||||
format = header.channel_count < 4 || !alpha ? QImage::Format_RGBX64 : QImage::Format_RGBA64;
|
||||
format = header.channel_count < 4 || !alpha ? QImage::Format_RGBX64 : QImage::Format_RGBA64_Premultiplied;
|
||||
else
|
||||
format = header.channel_count < 4 || !alpha ? QImage::Format_RGB888 : QImage::Format_RGBA8888;
|
||||
format = header.channel_count < 4 || !alpha ? QImage::Format_RGB888 : QImage::Format_RGBA8888_Premultiplied;
|
||||
break;
|
||||
case CM_MULTICHANNEL: // Treat MCH as CMYK (number of channel check is done in IsSupported())
|
||||
case CM_CMYK: // Photoshop supports CMYK/MCH 8-bits and 16-bits only
|
||||
@ -808,25 +809,80 @@ inline quint32 xchg(quint32 v) {
|
||||
#endif
|
||||
}
|
||||
|
||||
inline float xchg(float v)
|
||||
{
|
||||
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||||
# ifdef Q_CC_MSVC
|
||||
float *pf = &v;
|
||||
quint32 f = xchg(*reinterpret_cast<quint32*>(pf));
|
||||
quint32 *pi = &f;
|
||||
return *reinterpret_cast<float*>(pi);
|
||||
# else
|
||||
quint32 t;
|
||||
std::memcpy(&t, &v, sizeof(quint32));
|
||||
t = xchg(t);
|
||||
std::memcpy(&v, &t, sizeof(quint32));
|
||||
return v;
|
||||
# endif
|
||||
#else
|
||||
return v; // never tested
|
||||
#endif
|
||||
}
|
||||
|
||||
template<class T>
|
||||
inline void planarToChunchy(uchar *target, const char *source, qint32 width, qint32 c, qint32 cn)
|
||||
{
|
||||
auto s = reinterpret_cast<const T*>(source);
|
||||
auto t = reinterpret_cast<T*>(target);
|
||||
for (qint32 x = 0; x < width; ++x) {
|
||||
t[x*cn+c] = xchg(s[x]);
|
||||
t[x * cn + c] = xchg(s[x]);
|
||||
}
|
||||
}
|
||||
|
||||
template<class T, T min = 0, T max = 1>
|
||||
inline void planarToChunchyFloat(uchar *target, const char *source, qint32 width, qint32 c, qint32 cn)
|
||||
template<class T>
|
||||
inline void planarToChunchyFloatToUInt16(uchar *target, const char *source, qint32 width, qint32 c, qint32 cn)
|
||||
{
|
||||
auto s = reinterpret_cast<const T*>(source);
|
||||
auto t = reinterpret_cast<quint16*>(target);
|
||||
for (qint32 x = 0; x < width; ++x) {
|
||||
auto tmp = xchg(s[x]);
|
||||
auto ftmp = (*reinterpret_cast<float*>(&tmp) - double(min)) / (double(max) - double(min));
|
||||
t[x*cn+c] = quint16(std::min(ftmp * std::numeric_limits<quint16>::max() + 0.5, double(std::numeric_limits<quint16>::max())));
|
||||
t[x * cn + c] = quint16(std::min(xchg(s[x]) * std::numeric_limits<quint16>::max() + 0.5, double(std::numeric_limits<quint16>::max())));
|
||||
}
|
||||
}
|
||||
|
||||
enum class PremulConversion {
|
||||
PS2P, // Photoshop premul to qimage premul (required by RGB)
|
||||
PS2A, // Photoshop premul to unassociated alpha (required by RGB, CMYK and L* components of LAB)
|
||||
PSLab2A // Photoshop premul to unassociated alpha (required by a* and b* components of LAB)
|
||||
};
|
||||
|
||||
template<class T>
|
||||
inline void premulConversion(char *stride, qint32 width, qint32 ac, qint32 cn, const PremulConversion &conv)
|
||||
{
|
||||
auto s = reinterpret_cast<T *>(stride);
|
||||
auto max = qint64(std::numeric_limits<T>::max());
|
||||
|
||||
for (qint32 c = 0; c < ac; ++c) {
|
||||
if (conv == PremulConversion::PS2P) {
|
||||
for (qint32 x = 0; x < width; ++x) {
|
||||
auto xcn = x * cn;
|
||||
auto alpha = *(s + xcn + ac);
|
||||
*(s + xcn + c) = *(s + xcn + c) + alpha - max;
|
||||
}
|
||||
} else if (conv == PremulConversion::PS2A || (conv == PremulConversion::PSLab2A && c == 0)) {
|
||||
for (qint32 x = 0; x < width; ++x) {
|
||||
auto xcn = x * cn;
|
||||
auto alpha = *(s + xcn + ac);
|
||||
if (alpha > 0)
|
||||
*(s + xcn + c) = ((*(s + xcn + c) + alpha - max) * max + alpha / 2) / alpha;
|
||||
}
|
||||
} else if (conv == PremulConversion::PSLab2A) {
|
||||
for (qint32 x = 0; x < width; ++x) {
|
||||
auto xcn = x * cn;
|
||||
auto alpha = *(s + xcn + ac);
|
||||
if (alpha > 0)
|
||||
*(s + xcn + c) = ((*(s + xcn + c) + (alpha - max + 1) / 2) * max + alpha / 2) / alpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -839,24 +895,37 @@ inline void monoInvert(uchar *target, const char* source, qint32 bytes)
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
inline void rawChannelsCopy(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width)
|
||||
{
|
||||
auto s = reinterpret_cast<const T *>(source);
|
||||
auto t = reinterpret_cast<T *>(target);
|
||||
for (qint32 c = 0, cs = std::min(targetChannels, sourceChannels); c < cs; ++c) {
|
||||
for (qint32 x = 0; x < width; ++x) {
|
||||
t[x * targetChannels + c] = s[x * sourceChannels + c];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
inline void cmykToRgb(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width, bool alpha = false)
|
||||
{
|
||||
auto s = reinterpret_cast<const T*>(source);
|
||||
auto t = reinterpret_cast<T*>(target);
|
||||
auto max = double(std::numeric_limits<T>::max());
|
||||
auto invmax = 1.0 / max; // speed improvements by ~10%
|
||||
|
||||
if (sourceChannels < 4) {
|
||||
qDebug() << "cmykToRgb: image is not a valid CMYK!";
|
||||
if (sourceChannels < 3) {
|
||||
qDebug() << "cmykToRgb: image is not a valid CMY/CMYK!";
|
||||
return;
|
||||
}
|
||||
|
||||
for (qint32 w = 0; w < width; ++w) {
|
||||
auto ps = s + sourceChannels * w;
|
||||
auto C = 1 - *(ps + 0) / max;
|
||||
auto M = 1 - *(ps + 1) / max;
|
||||
auto Y = 1 - *(ps + 2) / max;
|
||||
auto K = 1 - *(ps + 3) / max;
|
||||
auto C = 1 - *(ps + 0) * invmax;
|
||||
auto M = 1 - *(ps + 1) * invmax;
|
||||
auto Y = 1 - *(ps + 2) * invmax;
|
||||
auto K = sourceChannels > 3 ? 1 - *(ps + 3) * invmax : 0.0;
|
||||
|
||||
auto pt = t + targetChannels * w;
|
||||
*(pt + 0) = T(std::min(max - (C * (1 - K) + K) * max + 0.5, max));
|
||||
@ -881,8 +950,9 @@ inline double gammaCorrection(double linear)
|
||||
#ifdef PSD_FAST_LAB_CONVERSION
|
||||
return linear;
|
||||
#else
|
||||
// NOTE: pow() slow down the performance by a 4 factor :(
|
||||
return (linear > 0.0031308 ? 1.055 * std::pow(linear, 1.0 / 2.4) - 0.055 : 12.92 * linear);
|
||||
// Replacing fastPow with std::pow the conversion time is 2/3 times longer: using fastPow
|
||||
// there are minimal differences in the conversion that are not visually noticeable.
|
||||
return (linear > 0.0031308 ? 1.055 * fastPow(linear, 1.0 / 2.4) - 0.055 : 12.92 * linear);
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -892,6 +962,7 @@ inline void labToRgb(uchar *target, qint32 targetChannels, const char *source, q
|
||||
auto s = reinterpret_cast<const T*>(source);
|
||||
auto t = reinterpret_cast<T*>(target);
|
||||
auto max = double(std::numeric_limits<T>::max());
|
||||
auto invmax = 1.0 / max;
|
||||
|
||||
if (sourceChannels < 3) {
|
||||
qDebug() << "labToRgb: image is not a valid LAB!";
|
||||
@ -900,14 +971,14 @@ inline void labToRgb(uchar *target, qint32 targetChannels, const char *source, q
|
||||
|
||||
for (qint32 w = 0; w < width; ++w) {
|
||||
auto ps = s + sourceChannels * w;
|
||||
auto L = (*(ps + 0) / max) * 100.0;
|
||||
auto A = (*(ps + 1) / max) * 255.0 - 128.0;
|
||||
auto B = (*(ps + 2) / max) * 255.0 - 128.0;
|
||||
auto L = (*(ps + 0) * invmax) * 100.0;
|
||||
auto A = (*(ps + 1) * invmax) * 255.0 - 128.0;
|
||||
auto B = (*(ps + 2) * invmax) * 255.0 - 128.0;
|
||||
|
||||
// converting LAB to XYZ (D65 illuminant)
|
||||
auto Y = (L + 16.0) / 116.0;
|
||||
auto X = A / 500.0 + Y;
|
||||
auto Z = Y - B / 200.0;
|
||||
auto Y = (L + 16.0) * (1.0 / 116.0);
|
||||
auto X = A * (1.0 / 500.0) + Y;
|
||||
auto Z = Y - B * (1.0 / 200.0);
|
||||
|
||||
// NOTE: use the constants of the illuminant of the target RGB color space
|
||||
X = finv(X) * 0.9504; // D50: * 0.9642
|
||||
@ -1057,7 +1128,15 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
QByteArray rawStride;
|
||||
rawStride.resize(raw_count);
|
||||
|
||||
if (header.color_mode == CM_CMYK || header.color_mode == CM_LABCOLOR || header.color_mode == CM_MULTICHANNEL) {
|
||||
// clang-format off
|
||||
// checks the need of color conversion (that requires random access to the image)
|
||||
auto randomAccess = (header.color_mode == CM_CMYK) ||
|
||||
(header.color_mode == CM_LABCOLOR) ||
|
||||
(header.color_mode == CM_MULTICHANNEL) ||
|
||||
(header.color_mode != CM_INDEXED && img.hasAlphaChannel());
|
||||
// clang-format on
|
||||
|
||||
if (randomAccess) {
|
||||
// In order to make a colorspace transformation, we need all channels of a scanline
|
||||
QByteArray psdScanline;
|
||||
psdScanline.resize(qsizetype(header.width * std::min(header.depth, quint16(16)) * header.channel_count + 7) / 8);
|
||||
@ -1077,12 +1156,32 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
auto scanLine = reinterpret_cast<unsigned char*>(psdScanline.data());
|
||||
if (header.depth == 8) {
|
||||
planarToChunchy<quint8>(scanLine, rawStride.data(), header.width, c, header.channel_count);
|
||||
}
|
||||
else if (header.depth == 16) {
|
||||
} else if (header.depth == 16) {
|
||||
planarToChunchy<quint16>(scanLine, rawStride.data(), header.width, c, header.channel_count);
|
||||
} else if (header.depth == 32) {
|
||||
planarToChunchyFloatToUInt16<float>(scanLine, rawStride.data(), header.width, c, header.channel_count);
|
||||
}
|
||||
else if (header.depth == 32) { // Not currently used
|
||||
planarToChunchyFloat<quint32>(scanLine, rawStride.data(), header.width, c, header.channel_count);
|
||||
}
|
||||
|
||||
// Convert premultiplied data to unassociated data
|
||||
if (img.hasAlphaChannel()) {
|
||||
if (header.color_mode == CM_CMYK) {
|
||||
if (header.depth == 8)
|
||||
premulConversion<quint8>(psdScanline.data(), header.width, 4, header.channel_count, PremulConversion::PS2A);
|
||||
else if (header.depth == 16)
|
||||
premulConversion<quint16>(psdScanline.data(), header.width, 4, header.channel_count, PremulConversion::PS2A);
|
||||
}
|
||||
if (header.color_mode == CM_LABCOLOR) {
|
||||
if (header.depth == 8)
|
||||
premulConversion<quint8>(psdScanline.data(), header.width, 3, header.channel_count, PremulConversion::PSLab2A);
|
||||
else if (header.depth == 16)
|
||||
premulConversion<quint16>(psdScanline.data(), header.width, 3, header.channel_count, PremulConversion::PSLab2A);
|
||||
}
|
||||
if (header.color_mode == CM_RGB) {
|
||||
if (header.depth == 8)
|
||||
premulConversion<quint8>(psdScanline.data(), header.width, 3, header.channel_count, PremulConversion::PS2P);
|
||||
else if (header.depth == 16 || header.depth == 32)
|
||||
premulConversion<quint16>(psdScanline.data(), header.width, 3, header.channel_count, PremulConversion::PS2P);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1090,18 +1189,23 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
if (header.color_mode == CM_CMYK || header.color_mode == CM_MULTICHANNEL) {
|
||||
if (header.depth == 8)
|
||||
cmykToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
|
||||
else
|
||||
else if (header.depth == 16)
|
||||
cmykToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
|
||||
}
|
||||
if (header.color_mode == CM_LABCOLOR) {
|
||||
if (header.depth == 8)
|
||||
labToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
|
||||
else
|
||||
else if (header.depth == 16)
|
||||
labToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
|
||||
}
|
||||
if (header.color_mode == CM_RGB) {
|
||||
if (header.depth == 8)
|
||||
rawChannelsCopy<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width);
|
||||
else if (header.depth == 16 || header.depth == 32)
|
||||
rawChannelsCopy<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// Linear read (no position jumps): optimized code usable only for the colorspaces supported by QImage
|
||||
for (qint32 c = 0; c < channel_num; ++c) {
|
||||
for (qint32 y = 0, h = header.height; y < h; ++y) {
|
||||
@ -1112,17 +1216,14 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
}
|
||||
|
||||
auto scanLine = img.scanLine(y);
|
||||
if (header.depth == 1) { // Bitmap
|
||||
if (header.depth == 1) { // Bitmap
|
||||
monoInvert(scanLine, rawStride.data(), std::min(rawStride.size(), img.bytesPerLine()));
|
||||
}
|
||||
else if (header.depth == 8) { // 8-bits images: Indexed, Grayscale, RGB/RGBA
|
||||
} else if (header.depth == 8) { // 8-bits images: Indexed, Grayscale, RGB/RGBA
|
||||
planarToChunchy<quint8>(scanLine, rawStride.data(), header.width, c, imgChannels);
|
||||
}
|
||||
else if (header.depth == 16) { // 16-bits integer images: Grayscale, RGB/RGBA
|
||||
} else if (header.depth == 16) { // 16-bits integer images: Grayscale, RGB/RGBA
|
||||
planarToChunchy<quint16>(scanLine, rawStride.data(), header.width, c, imgChannels);
|
||||
}
|
||||
else if (header.depth == 32) { // 32-bits float images: Grayscale, RGB/RGBA (coverted to equivalent integer 16-bits)
|
||||
planarToChunchyFloat<quint32>(scanLine, rawStride.data(), header.width, c, imgChannels);
|
||||
} else if (header.depth == 32) { // 32-bits float images: Grayscale, RGB/RGBA (coverted to equivalent integer 16-bits)
|
||||
planarToChunchyFloatToUInt16<float>(scanLine, rawStride.data(), header.width, c, imgChannels);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1264,6 +1365,9 @@ bool PSDHandler::canRead(QIODevice *device)
|
||||
if (header.color_mode == CM_CMYK || header.color_mode == CM_LABCOLOR || header.color_mode == CM_MULTICHANNEL) {
|
||||
return false;
|
||||
}
|
||||
if (header.color_mode == CM_RGB && header.channel_count > 3) {
|
||||
return false; // supposing extra channel as alpha
|
||||
}
|
||||
}
|
||||
|
||||
return IsSupported(header);
|
||||
@ -1295,3 +1399,5 @@ QImageIOHandler *PSDPlugin::create(QIODevice *device, const QByteArray &format)
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_psd_p.cpp"
|
||||
|
475
src/imageformats/qoi.cpp
Normal file
@ -0,0 +1,475 @@
|
||||
/*
|
||||
This file is part of the KDE project
|
||||
SPDX-FileCopyrightText: 2023 Ernest Gupik <ernestgupik@wp.pl>
|
||||
SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "qoi_p.h"
|
||||
#include "scanlineconverter_p.h"
|
||||
#include "util_p.h"
|
||||
|
||||
#include <QColorSpace>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QImage>
|
||||
|
||||
namespace // Private
|
||||
{
|
||||
|
||||
#define QOI_OP_INDEX 0x00 /* 00xxxxxx */
|
||||
#define QOI_OP_DIFF 0x40 /* 01xxxxxx */
|
||||
#define QOI_OP_LUMA 0x80 /* 10xxxxxx */
|
||||
#define QOI_OP_RUN 0xc0 /* 11xxxxxx */
|
||||
#define QOI_OP_RGB 0xfe /* 11111110 */
|
||||
#define QOI_OP_RGBA 0xff /* 11111111 */
|
||||
#define QOI_MASK_2 0xc0 /* 11000000 */
|
||||
|
||||
#define QOI_MAGIC (((unsigned int)'q') << 24 | ((unsigned int)'o') << 16 | ((unsigned int)'i') << 8 | ((unsigned int)'f'))
|
||||
#define QOI_HEADER_SIZE 14
|
||||
#define QOI_END_STREAM_PAD 8
|
||||
|
||||
struct QoiHeader {
|
||||
quint32 MagicNumber;
|
||||
quint32 Width;
|
||||
quint32 Height;
|
||||
quint8 Channels;
|
||||
quint8 Colorspace;
|
||||
};
|
||||
|
||||
struct Px {
|
||||
bool operator==(const Px &other) const
|
||||
{
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
quint8 r;
|
||||
quint8 g;
|
||||
quint8 b;
|
||||
quint8 a;
|
||||
};
|
||||
|
||||
static QDataStream &operator>>(QDataStream &s, QoiHeader &head)
|
||||
{
|
||||
s >> head.MagicNumber;
|
||||
s >> head.Width;
|
||||
s >> head.Height;
|
||||
s >> head.Channels;
|
||||
s >> head.Colorspace;
|
||||
return s;
|
||||
}
|
||||
|
||||
static QDataStream &operator<<(QDataStream &s, const QoiHeader &head)
|
||||
{
|
||||
s << head.MagicNumber;
|
||||
s << head.Width;
|
||||
s << head.Height;
|
||||
s << head.Channels;
|
||||
s << head.Colorspace;
|
||||
return s;
|
||||
}
|
||||
|
||||
static bool IsSupported(const QoiHeader &head)
|
||||
{
|
||||
// Check magic number
|
||||
if (head.MagicNumber != QOI_MAGIC) {
|
||||
return false;
|
||||
}
|
||||
// Check if the header is a valid QOI header
|
||||
if (head.Width == 0 || head.Height == 0 || head.Channels < 3 || head.Colorspace > 1) {
|
||||
return false;
|
||||
}
|
||||
// Set a reasonable upper limit
|
||||
if (head.Width > 300000 || head.Height > 300000) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int QoiHash(const Px &px)
|
||||
{
|
||||
return px.r * 3 + px.g * 5 + px.b * 7 + px.a * 11;
|
||||
}
|
||||
|
||||
static QImage::Format imageFormat(const QoiHeader &head)
|
||||
{
|
||||
if (IsSupported(head)) {
|
||||
return (head.Channels == 3 ? QImage::Format_RGB32 : QImage::Format_ARGB32);
|
||||
}
|
||||
return QImage::Format_Invalid;
|
||||
}
|
||||
|
||||
static bool LoadQOI(QIODevice *device, const QoiHeader &qoi, QImage &img)
|
||||
{
|
||||
Px index[64] = {Px{0, 0, 0, 0}};
|
||||
Px px = Px{0, 0, 0, 255};
|
||||
|
||||
// The px_len should be enough to read a complete "compressed" row: an uncompressible row can become
|
||||
// larger than the row itself. It should never be more than 1/3 (RGB) or 1/4 (RGBA) the length of the
|
||||
// row itself (see test bnm_rgb*.qoi) so I set the extra data to 1/2.
|
||||
// The minimum value is to ensure that enough bytes are read when the image is very small (e.g. 1x1px):
|
||||
// it can be set as large as you like.
|
||||
quint64 px_len = std::max(quint64(1024), quint64(qoi.Width) * qoi.Channels * 3 / 2);
|
||||
if (px_len > kMaxQVectorSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate image
|
||||
img = imageAlloc(qoi.Width, qoi.Height, imageFormat(qoi));
|
||||
if (img.isNull()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set the image colorspace based on the qoi.Colorspace value
|
||||
// As per specification: 0 = sRGB with linear alpha, 1 = all channels linear
|
||||
if (qoi.Colorspace) {
|
||||
img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
|
||||
} else {
|
||||
img.setColorSpace(QColorSpace(QColorSpace::SRgb));
|
||||
}
|
||||
|
||||
// Handle the byte stream
|
||||
QByteArray ba;
|
||||
for (quint32 y = 0, run = 0; y < qoi.Height; ++y) {
|
||||
if (quint64(ba.size()) < px_len) {
|
||||
ba.append(device->read(px_len));
|
||||
}
|
||||
|
||||
if (ba.size() < QOI_END_STREAM_PAD) {
|
||||
return false;
|
||||
}
|
||||
|
||||
quint64 chunks_len = ba.size() - QOI_END_STREAM_PAD;
|
||||
quint64 p = 0;
|
||||
QRgb *scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
|
||||
const quint8 *input = reinterpret_cast<const quint8 *>(ba.constData());
|
||||
for (quint32 x = 0; x < qoi.Width; ++x) {
|
||||
if (run > 0) {
|
||||
run--;
|
||||
} else if (p < chunks_len) {
|
||||
quint32 b1 = input[p++];
|
||||
|
||||
if (b1 == QOI_OP_RGB) {
|
||||
px.r = input[p++];
|
||||
px.g = input[p++];
|
||||
px.b = input[p++];
|
||||
} else if (b1 == QOI_OP_RGBA) {
|
||||
px.r = input[p++];
|
||||
px.g = input[p++];
|
||||
px.b = input[p++];
|
||||
px.a = input[p++];
|
||||
} else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX) {
|
||||
px = index[b1];
|
||||
} else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF) {
|
||||
px.r += ((b1 >> 4) & 0x03) - 2;
|
||||
px.g += ((b1 >> 2) & 0x03) - 2;
|
||||
px.b += (b1 & 0x03) - 2;
|
||||
} else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA) {
|
||||
quint32 b2 = input[p++];
|
||||
quint32 vg = (b1 & 0x3f) - 32;
|
||||
px.r += vg - 8 + ((b2 >> 4) & 0x0f);
|
||||
px.g += vg;
|
||||
px.b += vg - 8 + (b2 & 0x0f);
|
||||
} else if ((b1 & QOI_MASK_2) == QOI_OP_RUN) {
|
||||
run = (b1 & 0x3f);
|
||||
}
|
||||
index[QoiHash(px) & 0x3F] = px;
|
||||
}
|
||||
// Set the values for the pixel at (x, y)
|
||||
scanline[x] = qRgba(px.r, px.g, px.b, px.a);
|
||||
}
|
||||
|
||||
if (p) {
|
||||
ba.remove(0, p);
|
||||
}
|
||||
}
|
||||
|
||||
// From specs the byte stream's end is marked with 7 0x00 bytes followed by a single 0x01 byte.
|
||||
// NOTE: Instead of using "ba == QByteArray::fromRawData("\x00\x00\x00\x00\x00\x00\x00\x01", 8)"
|
||||
// we preferred a generic check that allows data to exist after the end of the file.
|
||||
return (ba.startsWith(QByteArray::fromRawData("\x00\x00\x00\x00\x00\x00\x00\x01", 8)));
|
||||
}
|
||||
|
||||
static bool SaveQOI(QIODevice *device, const QoiHeader &qoi, const QImage &img)
|
||||
{
|
||||
Px index[64] = {Px{0, 0, 0, 0}};
|
||||
Px px = Px{0, 0, 0, 255};
|
||||
Px px_prev = px;
|
||||
|
||||
auto run = 0;
|
||||
auto channels = qoi.Channels;
|
||||
|
||||
QByteArray ba;
|
||||
ba.reserve(img.width() * channels * 3 / 2);
|
||||
|
||||
ScanLineConverter converter(channels == 3 ? QImage::Format_RGB888 : QImage::Format_RGBA8888);
|
||||
converter.setTargetColorSpace(QColorSpace(qoi.Colorspace == 1 ? QColorSpace::SRgbLinear : QColorSpace::SRgb));
|
||||
|
||||
for (auto h = img.height(), y = 0; y < h; ++y) {
|
||||
auto pixels = converter.convertedScanLine(img, y);
|
||||
if (pixels == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto w = img.width() * channels, px_pos = 0; px_pos < w; px_pos += channels) {
|
||||
px.r = pixels[px_pos + 0];
|
||||
px.g = pixels[px_pos + 1];
|
||||
px.b = pixels[px_pos + 2];
|
||||
|
||||
if (channels == 4) {
|
||||
px.a = pixels[px_pos + 3];
|
||||
}
|
||||
|
||||
if (px == px_prev) {
|
||||
run++;
|
||||
if (run == 62 || (px_pos == w - channels && y == h - 1)) {
|
||||
ba.append(QOI_OP_RUN | (run - 1));
|
||||
run = 0;
|
||||
}
|
||||
} else {
|
||||
int index_pos;
|
||||
|
||||
if (run > 0) {
|
||||
ba.append(QOI_OP_RUN | (run - 1));
|
||||
run = 0;
|
||||
}
|
||||
|
||||
index_pos = QoiHash(px) & 0x3F;
|
||||
|
||||
if (index[index_pos] == px) {
|
||||
ba.append(QOI_OP_INDEX | index_pos);
|
||||
} else {
|
||||
index[index_pos] = px;
|
||||
|
||||
if (px.a == px_prev.a) {
|
||||
signed char vr = px.r - px_prev.r;
|
||||
signed char vg = px.g - px_prev.g;
|
||||
signed char vb = px.b - px_prev.b;
|
||||
|
||||
signed char vg_r = vr - vg;
|
||||
signed char vg_b = vb - vg;
|
||||
|
||||
if (vr > -3 && vr < 2 && vg > -3 && vg < 2 && vb > -3 && vb < 2) {
|
||||
ba.append(QOI_OP_DIFF | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2));
|
||||
} else if (vg_r > -9 && vg_r < 8 && vg > -33 && vg < 32 && vg_b > -9 && vg_b < 8) {
|
||||
ba.append(QOI_OP_LUMA | (vg + 32));
|
||||
ba.append((vg_r + 8) << 4 | (vg_b + 8));
|
||||
} else {
|
||||
ba.append(char(QOI_OP_RGB));
|
||||
ba.append(px.r);
|
||||
ba.append(px.g);
|
||||
ba.append(px.b);
|
||||
}
|
||||
} else {
|
||||
ba.append(char(QOI_OP_RGBA));
|
||||
ba.append(px.r);
|
||||
ba.append(px.g);
|
||||
ba.append(px.b);
|
||||
ba.append(px.a);
|
||||
}
|
||||
}
|
||||
}
|
||||
px_prev = px;
|
||||
}
|
||||
|
||||
auto written = device->write(ba);
|
||||
if (written < 0) {
|
||||
return false;
|
||||
}
|
||||
if (written) {
|
||||
ba.remove(0, written);
|
||||
}
|
||||
}
|
||||
|
||||
// QOI end of stream
|
||||
ba.append(QByteArray::fromRawData("\x00\x00\x00\x00\x00\x00\x00\x01", 8));
|
||||
|
||||
// write remaining data
|
||||
for (qint64 w = 0, write = 0, size = ba.size(); write < size; write += w) {
|
||||
w = device->write(ba.constData() + write, size - write);
|
||||
if (w < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QOIHandler::QOIHandler()
|
||||
{
|
||||
}
|
||||
|
||||
bool QOIHandler::canRead() const
|
||||
{
|
||||
if (canRead(device())) {
|
||||
setFormat("qoi");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool QOIHandler::canRead(QIODevice *device)
|
||||
{
|
||||
if (!device) {
|
||||
qWarning("QOIHandler::canRead() called with no device");
|
||||
return false;
|
||||
}
|
||||
|
||||
device->startTransaction();
|
||||
QByteArray head = device->read(QOI_HEADER_SIZE);
|
||||
qsizetype readBytes = head.size();
|
||||
device->rollbackTransaction();
|
||||
|
||||
if (readBytes < QOI_HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QDataStream stream(head);
|
||||
stream.setByteOrder(QDataStream::BigEndian);
|
||||
QoiHeader qoi = {0, 0, 0, 0, 2};
|
||||
stream >> qoi;
|
||||
|
||||
return IsSupported(qoi);
|
||||
}
|
||||
|
||||
bool QOIHandler::read(QImage *image)
|
||||
{
|
||||
QDataStream s(device());
|
||||
s.setByteOrder(QDataStream::BigEndian);
|
||||
|
||||
// Read image header
|
||||
QoiHeader qoi = {0, 0, 0, 0, 2};
|
||||
s >> qoi;
|
||||
|
||||
// Check if file is supported
|
||||
if (!IsSupported(qoi)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QImage img;
|
||||
bool result = LoadQOI(s.device(), qoi, img);
|
||||
|
||||
if (result == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*image = img;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QOIHandler::write(const QImage &image)
|
||||
{
|
||||
if (image.isNull()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QoiHeader qoi;
|
||||
qoi.MagicNumber = QOI_MAGIC;
|
||||
qoi.Width = image.width();
|
||||
qoi.Height = image.height();
|
||||
qoi.Channels = image.hasAlphaChannel() ? 4 : 3;
|
||||
qoi.Colorspace = image.colorSpace().transferFunction() == QColorSpace::TransferFunction::Linear ? 1 : 0;
|
||||
|
||||
if (!IsSupported(qoi)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QDataStream s(device());
|
||||
s.setByteOrder(QDataStream::BigEndian);
|
||||
s << qoi;
|
||||
if (s.status() != QDataStream::Ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SaveQOI(s.device(), qoi, image);
|
||||
}
|
||||
|
||||
bool QOIHandler::supportsOption(ImageOption option) const
|
||||
{
|
||||
if (option == QImageIOHandler::Size) {
|
||||
return true;
|
||||
}
|
||||
if (option == QImageIOHandler::ImageFormat) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QVariant QOIHandler::option(ImageOption option) const
|
||||
{
|
||||
QVariant v;
|
||||
|
||||
if (option == QImageIOHandler::Size) {
|
||||
if (auto d = device()) {
|
||||
// transactions works on both random and sequential devices
|
||||
d->startTransaction();
|
||||
auto ba = d->read(sizeof(QoiHeader));
|
||||
d->rollbackTransaction();
|
||||
|
||||
QDataStream s(ba);
|
||||
s.setByteOrder(QDataStream::BigEndian);
|
||||
|
||||
QoiHeader header = {0, 0, 0, 0, 2};
|
||||
s >> header;
|
||||
|
||||
if (s.status() == QDataStream::Ok && IsSupported(header)) {
|
||||
v = QVariant::fromValue(QSize(header.Width, header.Height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (option == QImageIOHandler::ImageFormat) {
|
||||
if (auto d = device()) {
|
||||
// transactions works on both random and sequential devices
|
||||
d->startTransaction();
|
||||
auto ba = d->read(sizeof(QoiHeader));
|
||||
d->rollbackTransaction();
|
||||
|
||||
QDataStream s(ba);
|
||||
s.setByteOrder(QDataStream::BigEndian);
|
||||
|
||||
QoiHeader header = {0, 0, 0, 0, 2};
|
||||
s >> header;
|
||||
|
||||
if (s.status() == QDataStream::Ok && IsSupported(header)) {
|
||||
v = QVariant::fromValue(imageFormat(header));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
QImageIOPlugin::Capabilities QOIPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
if (format == "qoi" || format == "QOI") {
|
||||
return Capabilities(CanRead | CanWrite);
|
||||
}
|
||||
if (!format.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
if (!device->isOpen()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Capabilities cap;
|
||||
if (device->isReadable() && QOIHandler::canRead(device)) {
|
||||
cap |= CanRead;
|
||||
}
|
||||
if (device->isWritable()) {
|
||||
cap |= CanWrite;
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
|
||||
QImageIOHandler *QOIPlugin::create(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
QImageIOHandler *handler = new QOIHandler;
|
||||
handler->setDevice(device);
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_qoi_p.cpp"
|
7
src/imageformats/qoi.desktop
Normal file
@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Service
|
||||
X-KDE-ServiceTypes=QImageIOPlugins
|
||||
X-KDE-ImageFormat=qoi
|
||||
X-KDE-MimeType=image/qoi
|
||||
X-KDE-Read=true
|
||||
X-KDE-Write=true
|
4
src/imageformats/qoi.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Keys": [ "qoi" ],
|
||||
"MimeTypes": [ "image/qoi" ]
|
||||
}
|
38
src/imageformats/qoi_p.h
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
This file is part of the KDE project
|
||||
SPDX-FileCopyrightText: 2023 Ernest Gupik <ernestgupik@wp.pl>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KIMG_QOI_P_H
|
||||
#define KIMG_QOI_P_H
|
||||
|
||||
#include <QImageIOPlugin>
|
||||
|
||||
class QOIHandler : public QImageIOHandler
|
||||
{
|
||||
public:
|
||||
QOIHandler();
|
||||
|
||||
bool canRead() const override;
|
||||
bool read(QImage *image) override;
|
||||
bool write(const QImage &image) override;
|
||||
|
||||
bool supportsOption(QImageIOHandler::ImageOption option) const override;
|
||||
QVariant option(QImageIOHandler::ImageOption option) const override;
|
||||
|
||||
static bool canRead(QIODevice *device);
|
||||
};
|
||||
|
||||
class QOIPlugin : public QImageIOPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "qoi.json")
|
||||
|
||||
public:
|
||||
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
|
||||
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||
};
|
||||
|
||||
#endif // KIMG_QOI_P_H
|
@ -340,3 +340,5 @@ QImageIOHandler *RASPlugin::create(QIODevice *device, const QByteArray &format)
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_ras_p.cpp"
|
||||
|