Compare commits

...

27 Commits

Author SHA1 Message Date
2bf3a859fc GIT_SILENT Upgrade ECM and KF version requirements for 5.115.0 release. 2024-02-03 14:24:22 +00:00
3ce3ab2364 GIT_SILENT Upgrade ECM and KF version requirements for 5.114.0 release. 2024-01-06 08:53:06 +00:00
c09b88b7c6 avif: new quality settings
Backported from master
2023-12-14 15:57:50 +01:00
8cc25c39b4 Update CI template 2023-12-07 13:48:53 +01:00
43c80793ac HEIF plug-in extended to support HEJ2 format
HEJ2 is JPEG 2000 codec encapsulated in HEIF container.
Only HEJ2 reading is implemented.
2023-12-07 12:58:14 +01:00
e90bca4924 GIT_SILENT Upgrade ECM and KF version requirements for 5.113.0 release. 2023-12-02 09:26:53 +00:00
bfc73ca260 GIT_SILENT Upgrade ECM and KF version requirements for 5.112.0 release. 2023-11-04 10:03:03 +00:00
f6bb59228e GIT_SILENT Upgrade ECM and KF version requirements for 5.111.0 release. 2023-10-07 10:20:46 +00:00
ee381958b2 avif: support repetition count
and minor performance optimizations.
2023-10-06 12:52:48 +02:00
478e49b8a6 raw: fix multi image load
Fixes not loading a second image in the file. This patch allow code like the following.

QImageReader r(file);
do {
    auto qi = r.read();
    if (!qi.isNull()) {
        qi.save(QString("/tmp/%1_%2.tif")
                .arg(QFileInfo(file).baseName())
                .arg(r.currentImageNumber()));
    }
}
while (r.jumpToNextImage());

m_startPos is used to reposition the device if you decide to do a subsequent read: libraw wants it to be at the beginning of the RAW stream

(cherry picked from commit 18ea0492bc)
2023-09-25 22:56:39 +02:00
c3daf86079 hdr: fix oss-fuzz issue 62197
Fixes the following error:

| /src/kimageformats/src/imageformats/hdr.cpp:56:31: runtime error: shift exponent 32 is too large for 32-bit type 'int' |
|------------------------------------------------------------------------------------------------------------------------|
2023-09-11 09:07:53 +00:00
a981cefdd2 hdr: fix crash (oss-fuzz)
This patch Fixes crash when RLE data is corrupted (test cases attached).

Should also fixes (no test cases available):
- Issue 62044 in oss-fuzz: kimageformats:kimgio_hdr_fuzzer: Undefined-shift in RGBE_To_QRgbLine
- Issue 62057 in oss-fuzz: kimageformats:kimgio_hdr_fuzzer: Heap-buffer-overflow in Read_Old_Line


[crash-646a4364479f54278ff8c30c69b0c9665e5869af.hdr](/uploads/44760f0286cde4236feab8e352493556/crash-646a4364479f54278ff8c30c69b0c9665e5869af.hdr)

[crash-88c33e2b49e57e6d1d4ec6945476f605f00e714a.hdr](/uploads/e35bf53ee717134a796c4b17cfacca42/crash-88c33e2b49e57e6d1d4ec6945476f605f00e714a.hdr)
2023-09-07 16:22:33 +00:00
723f72930b xcf: fix crash (oss-fuzz issue 62075) 2023-09-07 16:13:18 +00:00
99bb24803a xcf: fix oss-fuzz issue
May fix possible crash on QPainter.
2023-09-05 10:17:22 +00:00
63a9de758f GIT_SILENT Upgrade ECM and KF version requirements for 5.110.0 release. 2023-09-03 07:35:19 +00:00
240e28aac5 QOI: Advertise write support in the desktop file 2023-08-30 14:56:55 +02:00
906ecce500 qoi: write support backported from master 2023-08-30 06:36:28 +00:00
b2b677b8a5 xcf: format v12 support (kf5)
The code is the same of MR !166 (master) but test cases are slightly different due to rounding.
2023-08-29 22:03:34 +00:00
bcec942cc9 Support libavif 1.0
Make cmake find libavif 1.0 and adapt to API changes (which is reverting to pre 0.9.2 API)

(cherry picked from commit 4badb3088e)
2023-08-29 11:05:53 +02:00
66cb8c91d0 No longer needed to explicitly include CMakeParseArguments
NO_CHANGELOG

(cherry picked from commit 4bd9d5baec)
2023-08-28 22:55:25 +02:00
899a2df42d exr: multiple fixes (kf5)
The code is the same of MR !170 (master) but test cases are slightly different due to rounding.
* Support for images with transparency
* Precise colorspace conversion using QT color spaces
* Set the correct resolution
* Set useful metadata
* Creates 16-bits images
* Speed improvements
2023-08-28 17:29:34 +00:00
47920ed63c Fix missing qoi.desktop 2023-08-19 08:53:00 +02:00
274f30e008 qoi: fix buffer overflow kf5
Backport of MR !167:
- fix buffer overflow with corrupted images without image data
- fix unable to read very small images (e.g. 1x1 px)
- new test cases added
- detect incomplete files by checking the end of streams as written in the specs
2023-08-18 15:11:41 +00:00
4348a09733 Renamed qoi.h to qoi_p.h
(cherry picked from commit 7a0d95af92)
2023-08-14 00:15:55 +02:00
dd4576a472 Minor improvements
(cherry picked from commit 4c3ade04dd)
2023-08-14 00:15:52 +02:00
9438540735 Add support for the QOI image format
This MR adds read-only QOI (https://qoiformat.org/) image format support for KImageFormats.

This format has received it's MIME type inclusion in shared-mime-info 4 months ago: ff51a0a9e3

The code is based on the reference QOI implementation at https://github.com/phoboslab/qoi/blob/master/qoi.h

Official test images: https://qoiformat.org/qoi_test_images.zip

![pngvsqoi](/uploads/e386aa5057641057106e21940c770d97/pngvsqoi.png)

Also: This is my first MR to KDE ;)
(cherry picked from commit b209e54b6f)
2023-08-11 22:47:48 +02:00
cf78907ff4 Set linear color space and round fix 2023-08-08 19:37:46 +00:00
82 changed files with 2260 additions and 454 deletions

View File

@ -2,9 +2,11 @@
# 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/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/windows.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-static.yml
- project: sysadmin/ci-utilities
file:
- /gitlab-templates/linux.yml
- /gitlab-templates/linux-static.yml
- /gitlab-templates/android.yml
- /gitlab-templates/freebsd.yml
- /gitlab-templates/windows.yml
- /gitlab-templates/windows-static.yml

View File

@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16)
project(KImageFormats)
include(FeatureSummary)
find_package(ECM 5.109.0 NO_MODULE)
find_package(ECM 5.115.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"

View File

@ -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

View File

@ -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
@ -99,6 +99,12 @@ if (LibHeif_FOUND)
kimageformats_write_tests(FUZZ 1
heif-nodatacheck-lossless
)
if (LibHeif_VERSION VERSION_GREATER_EQUAL "1.17.0")
kimageformats_read_tests(FUZZ 1
hej2
)
endif()
endif()
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
@ -125,6 +131,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
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
autotests/read/qoi/1px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

BIN
autotests/read/qoi/1px.qoi Normal file

Binary file not shown.

BIN
autotests/read/qoi/2px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

BIN
autotests/read/qoi/2px.qoi Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

BIN
autotests/write/rgb.qoi Normal file

Binary file not shown.

BIN
autotests/write/rgba.qoi Normal file

Binary file not shown.

View File

@ -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

View File

@ -133,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/)

View File

@ -16,9 +16,34 @@
#include <cfloat>
/*
Quality range - compression/subsampling
100 - lossless RGB compression
< KIMG_AVIF_QUALITY_BEST, 100 ) - YUV444 color subsampling
< KIMG_AVIF_QUALITY_HIGH, KIMG_AVIF_QUALITY_BEST ) - YUV422 color subsampling
< 0, KIMG_AVIF_QUALITY_HIGH ) - YUV420 color subsampling
< 0, KIMG_AVIF_QUALITY_LOW ) - lossy compression of alpha channel
*/
#ifndef KIMG_AVIF_DEFAULT_QUALITY
#define KIMG_AVIF_DEFAULT_QUALITY 68
#endif
#ifndef KIMG_AVIF_QUALITY_BEST
#define KIMG_AVIF_QUALITY_BEST 90
#endif
#ifndef KIMG_AVIF_QUALITY_HIGH
#define KIMG_AVIF_QUALITY_HIGH 80
#endif
#ifndef KIMG_AVIF_QUALITY_LOW
#define KIMG_AVIF_QUALITY_LOW 51
#endif
QAVIFHandler::QAVIFHandler()
: m_parseState(ParseAvifNotParsed)
, m_quality(52)
, m_quality(KIMG_AVIF_DEFAULT_QUALITY)
, m_container_width(0)
, m_container_height(0)
, m_rawAvifData(AVIF_DATA_EMPTY)
@ -330,6 +355,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 +453,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) {
@ -515,9 +544,17 @@ bool QAVIFHandler::write(const QImage &image)
}
}
if (m_quality > 100) {
m_quality = 100;
} else if (m_quality < 0) {
m_quality = KIMG_AVIF_DEFAULT_QUALITY;
}
#if AVIF_VERSION < 1000000
int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100;
int minQuantizer = 0;
int maxQuantizerAlpha = 0;
#endif
avifResult res;
bool save_grayscale; // true - monochrome, false - colors
@ -563,13 +600,15 @@ bool QAVIFHandler::write(const QImage &image)
break;
}
// quality settings
#if AVIF_VERSION < 1000000
// deprecated quality settings
if (maxQuantizer > 20) {
minQuantizer = maxQuantizer - 20;
if (maxQuantizer > 40) { // we decrease quality of alpha channel here
maxQuantizerAlpha = maxQuantizer - 40;
}
}
#endif
if (save_grayscale && !image.hasAlphaChannel()) { // we are going to save grayscale image without alpha channel
if (save_depth > 8) {
@ -642,8 +681,8 @@ bool QAVIFHandler::write(const QImage &image)
QImage tmpcolorimage = image.convertToFormat(tmpformat);
avifPixelFormat pixel_format = AVIF_PIXEL_FORMAT_YUV420;
if (maxQuantizer < 20) {
if (maxQuantizer < 10) {
if (m_quality >= KIMG_AVIF_QUALITY_HIGH) {
if (m_quality >= KIMG_AVIF_QUALITY_BEST) {
pixel_format = AVIF_PIXEL_FORMAT_YUV444; // best quality
} else {
pixel_format = AVIF_PIXEL_FORMAT_YUV422; // high quality
@ -714,9 +753,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);
}
}
@ -803,6 +842,8 @@ bool QAVIFHandler::write(const QImage &image)
avifRWData raw = AVIF_DATA_EMPTY;
avifEncoder *encoder = avifEncoderCreate();
encoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
#if AVIF_VERSION < 1000000
encoder->minQuantizer = minQuantizer;
encoder->maxQuantizer = maxQuantizer;
@ -810,6 +851,17 @@ bool QAVIFHandler::write(const QImage &image)
encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
encoder->maxQuantizerAlpha = maxQuantizerAlpha;
}
#else
encoder->quality = m_quality;
if (image.hasAlphaChannel()) {
if (m_quality >= KIMG_AVIF_QUALITY_LOW) {
encoder->qualityAlpha = 100;
} else {
encoder->qualityAlpha = 100 - (KIMG_AVIF_QUALITY_LOW - m_quality) / 2;
}
}
#endif
encoder->speed = 6;
@ -866,7 +918,7 @@ void QAVIFHandler::setOption(ImageOption option, const QVariant &value)
if (m_quality > 100) {
m_quality = 100;
} else if (m_quality < 0) {
m_quality = 52;
m_quality = KIMG_AVIF_DEFAULT_QUALITY;
}
return;
default:
@ -1039,6 +1091,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;
}

View File

@ -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;

View File

@ -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
/*!

View File

@ -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;

View File

@ -22,6 +22,7 @@ size_t HEIFHandler::m_initialized_count = 0;
bool HEIFHandler::m_plugins_queried = false;
bool HEIFHandler::m_heif_decoder_available = false;
bool HEIFHandler::m_heif_encoder_available = false;
bool HEIFHandler::m_hej2_decoder_available = false;
extern "C" {
static struct heif_error heifhandler_write_callback(struct heif_context * /* ctx */, const void *data, size_t size, void *userdata)
@ -59,12 +60,25 @@ HEIFHandler::HEIFHandler()
bool HEIFHandler::canRead() const
{
if (m_parseState == ParseHeicNotParsed && !canRead(device())) {
if (m_parseState == ParseHeicNotParsed) {
QIODevice *dev = device();
if (dev) {
const QByteArray header = dev->peek(28);
if (HEIFHandler::isSupportedBMFFType(header)) {
setFormat("heif");
return true;
}
if (HEIFHandler::isSupportedHEJ2(header)) {
setFormat("hej2");
return true;
}
}
return false;
}
if (m_parseState != ParseHeicError) {
setFormat("heif");
return true;
}
return false;
@ -300,17 +314,6 @@ bool HEIFHandler::write_helper(const QImage &image)
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) {
@ -350,6 +353,22 @@ bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
return false;
}
bool HEIFHandler::isSupportedHEJ2(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, "j2ki", 4) == 0) {
return true;
}
}
return false;
}
QVariant HEIFHandler::option(ImageOption option) const
{
if (option == Quality) {
@ -425,7 +444,7 @@ bool HEIFHandler::ensureDecoder()
}
const QByteArray buffer = device()->readAll();
if (!HEIFHandler::isSupportedBMFFType(buffer)) {
if (!HEIFHandler::isSupportedBMFFType(buffer) && !HEIFHandler::isSupportedHEJ2(buffer)) {
m_parseState = ParseHeicError;
return false;
}
@ -814,6 +833,9 @@ bool HEIFHandler::isHeifDecoderAvailable()
}
#endif
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
#endif
m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
m_plugins_queried = true;
@ -839,6 +861,9 @@ bool HEIFHandler::isHeifEncoderAvailable()
}
#endif
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
#endif
m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
m_plugins_queried = true;
@ -853,6 +878,34 @@ bool HEIFHandler::isHeifEncoderAvailable()
return m_heif_encoder_available;
}
bool HEIFHandler::isHej2DecoderAvailable()
{
QMutexLocker locker(&getHEIFHandlerMutex());
if (!m_plugins_queried) {
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (m_initialized_count == 0) {
heif_init(nullptr);
}
#endif
m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
#endif
m_plugins_queried = true;
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (m_initialized_count == 0) {
heif_deinit();
}
#endif
}
return m_hej2_decoder_available;
}
void HEIFHandler::startHeifLib()
{
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
@ -901,6 +954,15 @@ QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const Q
}
return format_cap;
}
if (format == "hej2") {
Capabilities format_cap;
if (HEIFHandler::isHej2DecoderAvailable()) {
format_cap |= CanRead;
}
return format_cap;
}
if (!format.isEmpty()) {
return {};
}
@ -909,8 +971,16 @@ QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const Q
}
Capabilities cap;
if (device->isReadable() && HEIFHandler::canRead(device) && HEIFHandler::isHeifDecoderAvailable()) {
cap |= CanRead;
if (device->isReadable()) {
const QByteArray header = device->peek(28);
if (HEIFHandler::isSupportedBMFFType(header) && HEIFHandler::isHeifDecoderAvailable()) {
cap |= CanRead;
}
if (HEIFHandler::isSupportedHEJ2(header) && HEIFHandler::isHej2DecoderAvailable()) {
cap |= CanRead;
}
}
if (device->isWritable() && HEIFHandler::isHeifEncoderAvailable()) {

View File

@ -1,4 +1,4 @@
{
"Keys": [ "heif", "heic" ],
"MimeTypes": [ "image/heif", "image/heif" ]
"Keys": [ "heif", "heic", "hej2" ],
"MimeTypes": [ "image/heif", "image/heif", "image/hej2k" ]
}

View File

@ -24,17 +24,18 @@ public:
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;
static bool isHeifDecoderAvailable();
static bool isHeifEncoderAvailable();
static bool isHej2DecoderAvailable();
static bool isSupportedBMFFType(const QByteArray &header);
static bool isSupportedHEJ2(const QByteArray &header);
private:
static bool isSupportedBMFFType(const QByteArray &header);
bool ensureParsed() const;
bool ensureDecoder();
@ -57,6 +58,7 @@ private:
static bool m_plugins_queried;
static bool m_heif_decoder_available;
static bool m_heif_encoder_available;
static bool m_hej2_decoder_available;
static QMutex &getHEIFHandlerMutex();
};

475
src/imageformats/qoi.cpp Normal file
View 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"

View 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

View File

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

38
src/imageformats/qoi_p.h Normal file
View 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

View File

@ -443,7 +443,9 @@ void setParams(QImageIOHandler *handler, LibRaw *rawProcessor)
auto &&rawparams = rawProcessor->imgdata.rawparams;
#endif
// Select one raw image from input file (0 - first, ...)
rawparams.shot_select = handler->currentImageNumber();
if (handler->currentImageNumber() > -1) {
rawparams.shot_select = handler->currentImageNumber();
}
// *** Set processing parameters
@ -723,6 +725,7 @@ RAWHandler::RAWHandler()
: m_imageNumber(0)
, m_imageCount(0)
, m_quality(-1)
, m_startPos(-1)
{
}
@ -739,6 +742,15 @@ bool RAWHandler::read(QImage *image)
{
auto dev = device();
// set the image position after the first run.
if (!dev->isSequential()) {
if (m_startPos < 0) {
m_startPos = dev->pos();
} else {
dev->seek(m_startPos);
}
}
// Check image file format.
if (dev->atEnd()) {
return false;
@ -820,7 +832,7 @@ bool RAWHandler::jumpToNextImage()
bool RAWHandler::jumpToImage(int imageNumber)
{
if (imageNumber >= imageCount()) {
if (imageNumber < 0 || imageNumber >= imageCount()) {
return false;
}
m_imageNumber = imageNumber;

View File

@ -78,6 +78,12 @@ private:
* @note It is safe to set both W and A: W is used if camera white balance is found, otherwise A is used.
*/
qint32 m_quality;
/*!
* \brief m_startPos
* The initial device position to allow multi image load (cache value).
*/
qint64 m_startPos;
};
class RAWPlugin : public QImageIOPlugin

View File

@ -0,0 +1,101 @@
/*
SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "scanlineconverter_p.h"
#include <cstring>
ScanLineConverter::ScanLineConverter(const QImage::Format &targetFormat)
: _targetFormat(targetFormat)
{
}
ScanLineConverter::ScanLineConverter(const ScanLineConverter &other)
: _targetFormat(other._targetFormat)
, _colorSpace(other._colorSpace)
{
}
ScanLineConverter &ScanLineConverter::operator=(const ScanLineConverter &other)
{
this->_targetFormat = other._targetFormat;
this->_colorSpace = other._colorSpace;
return (*this);
}
QImage::Format ScanLineConverter::targetFormat() const
{
return _targetFormat;
}
void ScanLineConverter::setTargetColorSpace(const QColorSpace &colorSpace)
{
_colorSpace = colorSpace;
}
QColorSpace ScanLineConverter::targetColorSpace() const
{
return _colorSpace;
}
const uchar *ScanLineConverter::convertedScanLine(const QImage &image, qint32 y)
{
auto colorSpaceConversion = isColorSpaceConversionNeeded(image, _colorSpace);
if (image.format() == _targetFormat && !colorSpaceConversion) {
return image.constScanLine(y);
}
if (image.width() != _tmpBuffer.width() || image.format() != _tmpBuffer.format()) {
_tmpBuffer = QImage(image.width(), 1, image.format());
}
if (_tmpBuffer.isNull()) {
return nullptr;
}
std::memcpy(_tmpBuffer.bits(), image.constScanLine(y), std::min(_tmpBuffer.bytesPerLine(), image.bytesPerLine()));
if (colorSpaceConversion) {
_tmpBuffer.setColorSpace(image.colorSpace());
_tmpBuffer.convertToColorSpace(_colorSpace);
}
_convBuffer = _tmpBuffer.convertToFormat(_targetFormat);
if (_convBuffer.isNull()) {
return nullptr;
}
return _convBuffer.constBits();
}
qsizetype ScanLineConverter::bytesPerLine() const
{
if (_convBuffer.isNull()) {
return 0;
}
return _convBuffer.bytesPerLine();
}
bool ScanLineConverter::isColorSpaceConversionNeeded(const QImage &image, const QColorSpace &targetColorSpace) const
{
if (image.depth() < 24) { // RGB 8 bit or grater only
return false;
}
auto sourceColorSpace = image.colorSpace();
if (!sourceColorSpace.isValid() || !targetColorSpace.isValid()) {
return false;
}
auto stf = sourceColorSpace.transferFunction();
auto spr = sourceColorSpace.primaries();
auto ttf = targetColorSpace.transferFunction();
auto tpr = targetColorSpace.primaries();
// clang-format off
if (stf == QColorSpace::TransferFunction::Custom ||
ttf == QColorSpace::TransferFunction::Custom ||
spr == QColorSpace::Primaries::Custom ||
tpr == QColorSpace::Primaries::Custom) {
return true;
}
// clang-format on
if (stf == ttf && spr == tpr) {
return false;
}
return true;
}

View File

@ -0,0 +1,79 @@
/*
SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef SCANLINECONVERTER_P_H
#define SCANLINECONVERTER_P_H
#include <QColorSpace>
#include <QImage>
/*!
* \brief The scanlineFormatConversion class
* A class to convert an image scan line. It introduces some overhead on small images
* but performs better on large images. :)
*/
class ScanLineConverter
{
public:
ScanLineConverter(const QImage::Format &targetFormat);
ScanLineConverter(const ScanLineConverter &other);
ScanLineConverter &operator=(const ScanLineConverter &other);
/*!
* \brief targetFormat
* \return The target format set in the constructor.
*/
QImage::Format targetFormat() const;
/*!
* \brief setTargetColorSpace
* Set the colorspace conversion.
*
* In addition to format conversion, it is also possible to convert the color
* space if the source image has a different one set.
* The conversion is done on the source format if and only if the image
* has a color depth greater than 24 bit and the color profile set is different
* from that of the image itself.
* \param colorSpace
*/
void setTargetColorSpace(const QColorSpace &colorSpace);
QColorSpace targetColorSpace() const;
/*!
* \brief convertedScanLine
* Convert the scanline \a y.
* \note If the image format (and color space) is the same of converted format, it returns the image scan line.
* \return The scan line converted.
*/
const uchar *convertedScanLine(const QImage &image, qint32 y);
/*!
* \brief bytesPerLine
* \return The size of the last converted scanline.
*/
qsizetype bytesPerLine() const;
/*!
* \brief isColorSpaceConversionNeeded
* Calculates if a color space conversion is needed.
* \note Only 24 bit or grater images.
* \param image The source image.
* \param targetColorSpace The target color space.
* \return True if the conversion should be done otherwise false.
*/
bool isColorSpaceConversionNeeded(const QImage &image, const QColorSpace &targetColorSpace) const;
private:
// data
QImage::Format _targetFormat;
QColorSpace _colorSpace;
// internal buffers
QImage _tmpBuffer;
QImage _convBuffer;
};
#endif // SCANLINECONVERTER_P_H

File diff suppressed because it is too large Load Diff