mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-07-15 11:14:18 -04:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
e6955e1f03 | |||
6074c4d6fd | |||
6f44c5c52a | |||
d030c75925 | |||
9b3133ac92 | |||
b0a0bb1294 | |||
3d5090593c | |||
d4966d169b | |||
bf52896347 | |||
c52ffa2227 | |||
e4e386babf | |||
b47a9d7022 | |||
2cbf815d1f | |||
6cd0056f3b | |||
83374f390e | |||
5e59d950bd | |||
de320447f6 | |||
cf375a207f |
@ -6,3 +6,4 @@ Dependencies:
|
||||
|
||||
Options:
|
||||
test-before-installing: True
|
||||
require-passing-tests-on: [ 'Linux', 'FreeBSD', 'Windows' ]
|
||||
|
@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16)
|
||||
project(KImageFormats)
|
||||
|
||||
include(FeatureSummary)
|
||||
find_package(ECM 5.95.0 NO_MODULE)
|
||||
find_package(ECM 5.97.0 NO_MODULE)
|
||||
set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules")
|
||||
feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||
|
||||
@ -13,9 +13,9 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||
include(KDEInstallDirs)
|
||||
include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE)
|
||||
include(KDECMakeSettings)
|
||||
|
||||
include(KDEGitCommitHooks)
|
||||
|
||||
|
||||
include(ECMDeprecationSettings)
|
||||
include(CheckIncludeFiles)
|
||||
include(FindPkgConfig)
|
||||
|
||||
@ -70,8 +70,11 @@ if(KIMAGEFORMATS_JXL)
|
||||
endif()
|
||||
add_feature_info(LibJXL LibJXL_FOUND "required for the QImage plugin for JPEG XL images")
|
||||
|
||||
add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f02)
|
||||
add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x055900)
|
||||
ecm_set_disabled_deprecation_versions(
|
||||
QT 5.15.2
|
||||
KF 5.95
|
||||
)
|
||||
|
||||
add_subdirectory(src)
|
||||
if (BUILD_TESTING)
|
||||
add_subdirectory(autotests)
|
||||
|
BIN
autotests/read/psd/ccbug182496.png
Normal file
BIN
autotests/read/psd/ccbug182496.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 114 KiB |
BIN
autotests/read/psd/ccbug182496.psd
Normal file
BIN
autotests/read/psd/ccbug182496.psd
Normal file
Binary file not shown.
BIN
autotests/read/psd/cmyka-16bits.png
Normal file
BIN
autotests/read/psd/cmyka-16bits.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 112 KiB |
BIN
autotests/read/psd/cmyka-16bits.psd
Normal file
BIN
autotests/read/psd/cmyka-16bits.psd
Normal file
Binary file not shown.
BIN
autotests/read/psd/cmyka-8bits.png
Normal file
BIN
autotests/read/psd/cmyka-8bits.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
BIN
autotests/read/psd/cmyka-8bits.psd
Normal file
BIN
autotests/read/psd/cmyka-8bits.psd
Normal file
Binary file not shown.
BIN
autotests/read/psd/laba_16bit.png
Normal file
BIN
autotests/read/psd/laba_16bit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 189 KiB |
BIN
autotests/read/psd/laba_16bit.psd
Normal file
BIN
autotests/read/psd/laba_16bit.psd
Normal file
Binary file not shown.
BIN
autotests/read/psd/laba_8bit.png
Normal file
BIN
autotests/read/psd/laba_8bit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 117 KiB |
BIN
autotests/read/psd/laba_8bit.psd
Normal file
BIN
autotests/read/psd/laba_8bit.psd
Normal file
Binary file not shown.
@ -4,6 +4,7 @@
|
||||
|
||||
function(kimageformats_add_plugin plugin)
|
||||
set(options)
|
||||
set(oneValueArgs)
|
||||
set(multiValueArgs SOURCES)
|
||||
cmake_parse_arguments(KIF_ADD_PLUGIN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
||||
if(NOT KIF_ADD_PLUGIN_SOURCES)
|
||||
@ -86,6 +87,9 @@ endif()
|
||||
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
|
||||
kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp)
|
||||
target_link_libraries(kimg_jxl PkgConfig::LibJXL PkgConfig::LibJXLThreads)
|
||||
if (LibJXL_VERSION VERSION_GREATER_EQUAL "0.7.0")
|
||||
target_compile_definitions(kimg_jxl PRIVATE KIMG_JXL_API_VERSION=70)
|
||||
endif()
|
||||
install(FILES jxl.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
|
||||
endif()
|
||||
|
||||
|
@ -67,7 +67,7 @@ bool QAVIFHandler::canRead(QIODevice *device)
|
||||
|
||||
bool QAVIFHandler::ensureParsed() const
|
||||
{
|
||||
if (m_parseState == ParseAvifSuccess) {
|
||||
if (m_parseState == ParseAvifSuccess || m_parseState == ParseAvifMetadata) {
|
||||
return true;
|
||||
}
|
||||
if (m_parseState == ParseAvifError) {
|
||||
@ -79,6 +79,28 @@ bool QAVIFHandler::ensureParsed() const
|
||||
return that->ensureDecoder();
|
||||
}
|
||||
|
||||
bool QAVIFHandler::ensureOpened() const
|
||||
{
|
||||
if (m_parseState == ParseAvifSuccess) {
|
||||
return true;
|
||||
}
|
||||
if (m_parseState == ParseAvifError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QAVIFHandler *that = const_cast<QAVIFHandler *>(this);
|
||||
if (ensureParsed()) {
|
||||
if (m_parseState == ParseAvifMetadata) {
|
||||
bool success = that->jumpToNextImage();
|
||||
that->m_parseState = success ? ParseAvifSuccess : ParseAvifError;
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
that->m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::ensureDecoder()
|
||||
{
|
||||
if (m_decoder) {
|
||||
@ -97,6 +119,9 @@ bool QAVIFHandler::ensureDecoder()
|
||||
|
||||
m_decoder = avifDecoderCreate();
|
||||
|
||||
m_decoder->ignoreExif = AVIF_TRUE;
|
||||
m_decoder->ignoreXMP = AVIF_TRUE;
|
||||
|
||||
#if AVIF_VERSION >= 80400
|
||||
m_decoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
|
||||
#endif
|
||||
@ -127,45 +152,58 @@ bool QAVIFHandler::ensureDecoder()
|
||||
return false;
|
||||
}
|
||||
|
||||
decodeResult = avifDecoderNextImage(m_decoder);
|
||||
m_container_width = m_decoder->image->width;
|
||||
m_container_height = m_decoder->image->height;
|
||||
|
||||
if (decodeResult == AVIF_RESULT_OK) {
|
||||
m_container_width = m_decoder->image->width;
|
||||
m_container_height = m_decoder->image->height;
|
||||
|
||||
if ((m_container_width > 65535) || (m_container_height > 65535)) {
|
||||
qWarning("AVIF image (%dx%d) is too large!", m_container_width, m_container_height);
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((m_container_width == 0) || (m_container_height == 0)) {
|
||||
qWarning("Empty image, nothing to decode");
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_container_width > ((16384 * 16384) / m_container_height)) {
|
||||
qWarning("AVIF image (%dx%d) has more than 256 megapixels!", m_container_width, m_container_height);
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_parseState = ParseAvifSuccess;
|
||||
if (decode_one_frame()) {
|
||||
return true;
|
||||
} else {
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
qWarning("ERROR: Failed to decode image: %s", avifResultToString(decodeResult));
|
||||
if ((m_container_width > 65535) || (m_container_height > 65535)) {
|
||||
qWarning("AVIF image (%dx%d) is too large!", m_container_width, m_container_height);
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
avifDecoderDestroy(m_decoder);
|
||||
m_decoder = nullptr;
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
if ((m_container_width == 0) || (m_container_height == 0)) {
|
||||
qWarning("Empty image, nothing to decode");
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_container_width > ((16384 * 16384) / m_container_height)) {
|
||||
qWarning("AVIF image (%dx%d) has more than 256 megapixels!", m_container_width, m_container_height);
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// calculate final dimensions with crop and rotate operations applied
|
||||
int new_width = m_container_width;
|
||||
int new_height = m_container_height;
|
||||
|
||||
if (m_decoder->image->transformFlags & AVIF_TRANSFORM_CLAP) {
|
||||
if ((m_decoder->image->clap.widthD > 0) && (m_decoder->image->clap.heightD > 0) && (m_decoder->image->clap.horizOffD > 0)
|
||||
&& (m_decoder->image->clap.vertOffD > 0)) {
|
||||
int crop_width = (int)((double)(m_decoder->image->clap.widthN) / (m_decoder->image->clap.widthD) + 0.5);
|
||||
if (crop_width < new_width && crop_width > 0) {
|
||||
new_width = crop_width;
|
||||
}
|
||||
int crop_height = (int)((double)(m_decoder->image->clap.heightN) / (m_decoder->image->clap.heightD) + 0.5);
|
||||
if (crop_height < new_height && crop_height > 0) {
|
||||
new_height = crop_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_decoder->image->transformFlags & AVIF_TRANSFORM_IROT) {
|
||||
if (m_decoder->image->irot.angle == 1 || m_decoder->image->irot.angle == 3) {
|
||||
int tmp = new_width;
|
||||
new_width = new_height;
|
||||
new_height = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
m_estimated_dimensions.setWidth(new_width);
|
||||
m_estimated_dimensions.setHeight(new_height);
|
||||
|
||||
m_parseState = ParseAvifMetadata;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::decode_one_frame()
|
||||
@ -192,9 +230,9 @@ bool QAVIFHandler::decode_one_frame()
|
||||
}
|
||||
} else {
|
||||
if (loadalpha) {
|
||||
resultformat = QImage::Format_RGBA8888;
|
||||
resultformat = QImage::Format_ARGB32;
|
||||
} else {
|
||||
resultformat = QImage::Format_RGBX8888;
|
||||
resultformat = QImage::Format_RGB32;
|
||||
}
|
||||
}
|
||||
QImage result(m_decoder->image->width, m_decoder->image->height, resultformat);
|
||||
@ -285,14 +323,16 @@ bool QAVIFHandler::decode_one_frame()
|
||||
rgb.depth = 16;
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
|
||||
if (!loadalpha) {
|
||||
if (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
|
||||
resultformat = QImage::Format_Grayscale16;
|
||||
}
|
||||
if (!loadalpha && (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) {
|
||||
resultformat = QImage::Format_Grayscale16;
|
||||
}
|
||||
} else {
|
||||
rgb.depth = 8;
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||||
rgb.format = AVIF_RGB_FORMAT_BGRA;
|
||||
#else
|
||||
rgb.format = AVIF_RGB_FORMAT_ARGB;
|
||||
#endif
|
||||
|
||||
#if AVIF_VERSION >= 80400
|
||||
if (m_decoder->imageCount > 1) {
|
||||
@ -301,14 +341,8 @@ bool QAVIFHandler::decode_one_frame()
|
||||
}
|
||||
#endif
|
||||
|
||||
if (loadalpha) {
|
||||
resultformat = QImage::Format_ARGB32;
|
||||
} else {
|
||||
if (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
|
||||
resultformat = QImage::Format_Grayscale8;
|
||||
} else {
|
||||
resultformat = QImage::Format_RGB32;
|
||||
}
|
||||
if (!loadalpha && (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) {
|
||||
resultformat = QImage::Format_Grayscale8;
|
||||
}
|
||||
}
|
||||
|
||||
@ -399,13 +433,15 @@ bool QAVIFHandler::decode_one_frame()
|
||||
m_current_image = result.convertToFormat(resultformat);
|
||||
}
|
||||
|
||||
m_estimated_dimensions = m_current_image.size();
|
||||
|
||||
m_must_jump_to_next_image = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QAVIFHandler::read(QImage *image)
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
if (!ensureOpened()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -792,7 +828,7 @@ QVariant QAVIFHandler::option(ImageOption option) const
|
||||
|
||||
switch (option) {
|
||||
case Size:
|
||||
return m_current_image.size();
|
||||
return m_estimated_dimensions;
|
||||
case Animation:
|
||||
if (imageCount() >= 2) {
|
||||
return true;
|
||||
@ -848,6 +884,14 @@ int QAVIFHandler::currentImageNumber() const
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (m_parseState == ParseAvifMetadata) {
|
||||
if (m_decoder->imageCount >= 2) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return m_decoder->imageIndex;
|
||||
}
|
||||
|
||||
@ -857,12 +901,14 @@ bool QAVIFHandler::jumpToNextImage()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_decoder->imageCount < 2) {
|
||||
return true;
|
||||
}
|
||||
if (m_decoder->imageIndex >= 0) {
|
||||
if (m_decoder->imageCount < 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning
|
||||
avifDecoderReset(m_decoder);
|
||||
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning
|
||||
avifDecoderReset(m_decoder);
|
||||
}
|
||||
}
|
||||
|
||||
avifResult decodeResult = avifDecoderNextImage(m_decoder);
|
||||
@ -885,6 +931,7 @@ bool QAVIFHandler::jumpToNextImage()
|
||||
}
|
||||
|
||||
if (decode_one_frame()) {
|
||||
m_parseState = ParseAvifSuccess;
|
||||
return true;
|
||||
} else {
|
||||
m_parseState = ParseAvifError;
|
||||
@ -900,7 +947,7 @@ bool QAVIFHandler::jumpToImage(int imageNumber)
|
||||
|
||||
if (m_decoder->imageCount < 2) { // not an animation
|
||||
if (imageNumber == 0) {
|
||||
return true;
|
||||
return ensureOpened();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@ -935,6 +982,7 @@ bool QAVIFHandler::jumpToImage(int imageNumber)
|
||||
}
|
||||
|
||||
if (decode_one_frame()) {
|
||||
m_parseState = ParseAvifSuccess;
|
||||
return true;
|
||||
} else {
|
||||
m_parseState = ParseAvifError;
|
||||
@ -944,7 +992,7 @@ bool QAVIFHandler::jumpToImage(int imageNumber)
|
||||
|
||||
int QAVIFHandler::nextImageDelay() const
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
if (!ensureOpened()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include <QImage>
|
||||
#include <QImageIOPlugin>
|
||||
#include <QPointF>
|
||||
#include <QSize>
|
||||
#include <QVariant>
|
||||
#include <avif/avif.h>
|
||||
#include <qimageiohandler.h>
|
||||
@ -45,6 +46,7 @@ public:
|
||||
private:
|
||||
static QPointF CompatibleChromacity(qreal chrX, qreal chrY);
|
||||
bool ensureParsed() const;
|
||||
bool ensureOpened() const;
|
||||
bool ensureDecoder();
|
||||
bool decode_one_frame();
|
||||
|
||||
@ -52,6 +54,7 @@ private:
|
||||
ParseAvifError = -1,
|
||||
ParseAvifNotParsed = 0,
|
||||
ParseAvifSuccess = 1,
|
||||
ParseAvifMetadata = 2,
|
||||
};
|
||||
|
||||
ParseAvifState m_parseState;
|
||||
@ -59,6 +62,7 @@ private:
|
||||
|
||||
uint32_t m_container_width;
|
||||
uint32_t m_container_height;
|
||||
QSize m_estimated_dimensions;
|
||||
|
||||
QByteArray m_rawData;
|
||||
avifROData m_rawAvifData;
|
||||
|
@ -143,6 +143,10 @@ 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) {
|
||||
qWarning("ERROR: JxlDecoderSubscribeEvents failed");
|
||||
@ -482,37 +486,15 @@ bool QJpegXLHandler::write(const QImage &image)
|
||||
return false;
|
||||
}
|
||||
|
||||
void *runner = nullptr;
|
||||
int num_worker_threads = qBound(1, QThread::idealThreadCount(), 64);
|
||||
|
||||
if (num_worker_threads > 1) {
|
||||
runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
|
||||
if (JxlEncoderSetParallelRunner(encoder, JxlThreadParallelRunner, runner) != JXL_ENC_SUCCESS) {
|
||||
qWarning("JxlEncoderSetParallelRunner failed");
|
||||
JxlThreadParallelRunnerDestroy(runner);
|
||||
JxlEncoderDestroy(encoder);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
JxlEncoderOptions *encoder_options = JxlEncoderOptionsCreate(encoder, nullptr);
|
||||
|
||||
if (m_quality > 100) {
|
||||
m_quality = 100;
|
||||
} else if (m_quality < 0) {
|
||||
m_quality = 90;
|
||||
}
|
||||
|
||||
JxlEncoderOptionsSetDistance(encoder_options, (100.0f - m_quality) / 10.0f);
|
||||
|
||||
JxlEncoderOptionsSetLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE);
|
||||
|
||||
JxlBasicInfo output_info;
|
||||
JxlEncoderInitBasicInfo(&output_info);
|
||||
|
||||
JxlColorEncoding color_profile;
|
||||
JxlColorEncodingSetToSRGB(&color_profile, JXL_FALSE);
|
||||
|
||||
bool convert_color_profile;
|
||||
QByteArray iccprofile;
|
||||
|
||||
@ -526,7 +508,28 @@ bool QJpegXLHandler::write(const QImage &image)
|
||||
convert_color_profile = false;
|
||||
iccprofile = image.colorSpace().iccProfile();
|
||||
if (iccprofile.size() > 0 || m_quality == 100) {
|
||||
output_info.uses_original_profile = 1;
|
||||
output_info.uses_original_profile = JXL_TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
int num_worker_threads = qBound(1, QThread::idealThreadCount(), 64);
|
||||
|
||||
if (num_worker_threads > 1) {
|
||||
runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
|
||||
if (JxlEncoderSetParallelRunner(encoder, JxlThreadParallelRunner, runner) != JXL_ENC_SUCCESS) {
|
||||
qWarning("JxlEncoderSetParallelRunner failed");
|
||||
JxlThreadParallelRunnerDestroy(runner);
|
||||
JxlEncoderDestroy(encoder);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -537,7 +540,6 @@ bool QJpegXLHandler::write(const QImage &image)
|
||||
pixel_format.endianness = JXL_NATIVE_ENDIAN;
|
||||
pixel_format.align = 0;
|
||||
|
||||
output_info.intensity_target = 255.0f;
|
||||
output_info.orientation = JXL_ORIENT_IDENTITY;
|
||||
output_info.num_color_channels = 3;
|
||||
output_info.animation.tps_numerator = 10;
|
||||
@ -615,6 +617,9 @@ bool QJpegXLHandler::write(const QImage &image)
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
JxlColorEncoding color_profile;
|
||||
JxlColorEncodingSetToSRGB(&color_profile, JXL_FALSE);
|
||||
|
||||
status = JxlEncoderSetColorEncoding(encoder, &color_profile);
|
||||
if (status != JXL_ENC_SUCCESS) {
|
||||
qWarning("JxlEncoderSetColorEncoding failed!");
|
||||
@ -626,6 +631,20 @@ 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, (void *)tmpimage.constBits(), buffer_size);
|
||||
} else {
|
||||
@ -915,6 +934,10 @@ 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) {
|
||||
qWarning("ERROR: JxlDecoderSubscribeEvents failed");
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
SPDX-FileCopyrightText: 2003 Ignacio Castaño <castano@ludicon.com>
|
||||
SPDX-FileCopyrightText: 2015 Alex Merry <alex.merry@kde.org>
|
||||
SPDX-FileCopyrightText: 2022 Mirco Miranda <mirco.miranda@systemceramics.com>
|
||||
SPDX-FileCopyrightText: 2022 Mirco Miranda <mircomir@outlook.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
@ -22,13 +22,15 @@
|
||||
* 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 be read due to lack of QImage support for
|
||||
* color spaces other than RGB (and Grayscale): a conversion to
|
||||
* RGB must be done.
|
||||
* - The best way to convert between different color spaces is to use a
|
||||
* color management engine (e.g. LittleCMS).
|
||||
* - An approximate way is to ignore the color information and use
|
||||
* literature formulas (possible but not recommended).
|
||||
* - 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:
|
||||
* - CMYK images are converted using an approximated way that ignores the color
|
||||
* information (ICC profile).
|
||||
* - LAB images are converted to sRGB using literature formulas.
|
||||
*
|
||||
* NOTE: The best way to convert between different color spaces is to use a
|
||||
* color management engine (e.g. LittleCMS).
|
||||
*/
|
||||
|
||||
#include "psd_p.h"
|
||||
@ -40,13 +42,35 @@
|
||||
#include <QImage>
|
||||
#include <QColorSpace>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
typedef quint32 uint;
|
||||
typedef quint16 ushort;
|
||||
typedef quint8 uchar;
|
||||
|
||||
/* The fast LAB conversion converts the image to linear sRgb instead to sRgb.
|
||||
* 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
|
||||
* 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
|
||||
* preview creator does not. This is the why, for now, it is disabled.
|
||||
*/
|
||||
//#define PSD_FAST_LAB_CONVERSION
|
||||
|
||||
namespace // Private.
|
||||
{
|
||||
enum ColorMode {
|
||||
|
||||
enum Signature : quint32 {
|
||||
S_8BIM = 0x3842494D, // '8BIM'
|
||||
S_8B64 = 0x38423634, // '8B64'
|
||||
|
||||
S_MeSa = 0x4D655361 // 'MeSa'
|
||||
};
|
||||
|
||||
enum ColorMode : quint16 {
|
||||
CM_BITMAP = 0,
|
||||
CM_GRAYSCALE = 1,
|
||||
CM_INDEXED = 2,
|
||||
@ -65,6 +89,12 @@ enum ImageResourceId : quint16 {
|
||||
IRI_XMPMETADATA = 0x0424
|
||||
};
|
||||
|
||||
enum LayerId : quint32 {
|
||||
LI_MT16 = 0x4D743136, // 'Mt16',
|
||||
LI_MT32 = 0x4D743332, // 'Mt32',
|
||||
LI_MTRN = 0x4D74726E // 'Mtrn'
|
||||
};
|
||||
|
||||
struct PSDHeader {
|
||||
uint signature;
|
||||
ushort version;
|
||||
@ -101,6 +131,57 @@ struct PSDColorModeDataSection {
|
||||
|
||||
using PSDImageResourceSection = QHash<quint16, PSDImageResourceBlock>;
|
||||
|
||||
struct PSDLayerInfo {
|
||||
qint64 size = -1;
|
||||
qint16 layerCount = 0;
|
||||
};
|
||||
|
||||
struct PSDGlobalLayerMaskInfo {
|
||||
qint64 size = -1;
|
||||
};
|
||||
|
||||
struct PSDAdditionalLayerInfo {
|
||||
Signature signature = Signature();
|
||||
LayerId id = LayerId();
|
||||
qint64 size = -1;
|
||||
};
|
||||
|
||||
struct PSDLayerAndMaskSection {
|
||||
qint64 size = -1;
|
||||
PSDLayerInfo layerInfo;
|
||||
PSDGlobalLayerMaskInfo globalLayerMaskInfo;
|
||||
QHash<LayerId, PSDAdditionalLayerInfo> additionalLayerInfo;
|
||||
|
||||
bool isNull() const {
|
||||
return (size <= 0);
|
||||
}
|
||||
|
||||
bool hasAlpha() const {
|
||||
return layerInfo.layerCount < 0 ||
|
||||
additionalLayerInfo.contains(LI_MT16) ||
|
||||
additionalLayerInfo.contains(LI_MT32) ||
|
||||
additionalLayerInfo.contains(LI_MTRN);
|
||||
}
|
||||
|
||||
bool atEnd(bool isPsb) const {
|
||||
qint64 currentSize = 0;
|
||||
if (layerInfo.size > -1) {
|
||||
currentSize += layerInfo.size + 4;
|
||||
if (isPsb)
|
||||
currentSize += 4;
|
||||
}
|
||||
if (globalLayerMaskInfo.size > -1) {
|
||||
currentSize += globalLayerMaskInfo.size + 4;
|
||||
}
|
||||
for (auto && v : additionalLayerInfo.values()) {
|
||||
currentSize += (12 + v.size);
|
||||
if (v.signature == S_8B64)
|
||||
currentSize += 4;
|
||||
}
|
||||
return (size <= currentSize);
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief fixedPointToDouble
|
||||
* Converts a fixed point number to floating point one.
|
||||
@ -112,6 +193,43 @@ static double fixedPointToDouble(qint32 fixedPoint)
|
||||
return (i+d);
|
||||
}
|
||||
|
||||
static qint64 readSize(QDataStream &s, bool psb = false)
|
||||
{
|
||||
qint64 size = 0;
|
||||
if (!psb) {
|
||||
quint32 tmp;
|
||||
s >> tmp;
|
||||
size = tmp;
|
||||
}
|
||||
else {
|
||||
s >> size;
|
||||
}
|
||||
if (s.status() != QDataStream::Ok) {
|
||||
size = -1;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
static bool skip_data(QDataStream &s, qint64 size)
|
||||
{
|
||||
// Skip mode data.
|
||||
for (qint32 i32 = 0; size; size -= i32) {
|
||||
i32 = std::min(size, qint64(std::numeric_limits<qint32>::max()));
|
||||
i32 = s.skipRawData(i32);
|
||||
if (i32 < 1)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool skip_section(QDataStream &s, bool psb = false)
|
||||
{
|
||||
auto section_length = readSize(s, psb);
|
||||
if (section_length < 0)
|
||||
return false;
|
||||
return skip_data(s, section_length);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief readPascalString
|
||||
* Reads the Pascal string as defined in the PSD specification.
|
||||
@ -193,7 +311,7 @@ static PSDImageResourceSection readImageResourceSection(QDataStream &s, bool *ok
|
||||
s >> signature;
|
||||
size -= sizeof(signature);
|
||||
// NOTE: MeSa signature is not documented but found in some old PSD take from Photoshop 7.0 CD.
|
||||
if (signature != 0x3842494D && signature != 0x4D655361) { // 8BIM and MeSa
|
||||
if (signature != S_8BIM && signature != S_MeSa) { // 8BIM and MeSa
|
||||
qDebug() << "Invalid Image Resource Block Signature!";
|
||||
*ok = false;
|
||||
break;
|
||||
@ -218,12 +336,12 @@ static PSDImageResourceSection readImageResourceSection(QDataStream &s, bool *ok
|
||||
size -= sizeof(dataSize);
|
||||
// NOTE: Qt device::read() and QDataStream::readRawData() could read less data than specified.
|
||||
// The read code should be improved.
|
||||
if(auto dev = s.device())
|
||||
if (auto dev = s.device())
|
||||
irb.data = dev->read(dataSize);
|
||||
auto read = irb.data.size();
|
||||
if (read > 0)
|
||||
size -= read;
|
||||
if (read != dataSize) {
|
||||
if (quint32(read) != dataSize) {
|
||||
qDebug() << "Image Resource Block Read Error!";
|
||||
*ok = false;
|
||||
break;
|
||||
@ -250,6 +368,79 @@ static PSDImageResourceSection readImageResourceSection(QDataStream &s, bool *ok
|
||||
return irs;
|
||||
}
|
||||
|
||||
PSDAdditionalLayerInfo readAdditionalLayer(QDataStream &s, bool *ok = nullptr)
|
||||
{
|
||||
PSDAdditionalLayerInfo li;
|
||||
|
||||
bool tmp = true;
|
||||
if (ok == nullptr)
|
||||
ok = &tmp;
|
||||
|
||||
s >> li.signature;
|
||||
*ok = li.signature == S_8BIM || li.signature == S_8B64;
|
||||
if (!*ok)
|
||||
return li;
|
||||
|
||||
s >> li.id;
|
||||
*ok = s.status() == QDataStream::Ok;
|
||||
if (!*ok)
|
||||
return li;
|
||||
|
||||
li.size = readSize(s, li.signature == S_8B64);
|
||||
*ok = li.size >= 0;
|
||||
if (!*ok)
|
||||
return li;
|
||||
|
||||
*ok = skip_data(s, li.size);
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
PSDLayerAndMaskSection readLayerAndMaskSection(QDataStream &s, bool isPsb, bool *ok = nullptr)
|
||||
{
|
||||
PSDLayerAndMaskSection lms;
|
||||
|
||||
bool tmp = true;
|
||||
if (ok == nullptr)
|
||||
ok = &tmp;
|
||||
*ok = true;
|
||||
|
||||
auto device = s.device();
|
||||
device->startTransaction();
|
||||
|
||||
lms.size = readSize(s, isPsb);
|
||||
|
||||
// read layer info
|
||||
if (s.status() == QDataStream::Ok && !lms.atEnd(isPsb)) {
|
||||
lms.layerInfo.size = readSize(s, isPsb);
|
||||
if (lms.layerInfo.size > 0) {
|
||||
s >> lms.layerInfo.layerCount;
|
||||
skip_data(s, lms.layerInfo.size - sizeof(lms.layerInfo.layerCount));
|
||||
}
|
||||
}
|
||||
|
||||
// read global layer mask info
|
||||
if (s.status() == QDataStream::Ok && !lms.atEnd(isPsb)) {
|
||||
lms.globalLayerMaskInfo.size = readSize(s, false); // always 32-bits
|
||||
if (lms.globalLayerMaskInfo.size > 0) {
|
||||
skip_data(s, lms.globalLayerMaskInfo.size);
|
||||
}
|
||||
}
|
||||
|
||||
// read additional layer info
|
||||
if (s.status() == QDataStream::Ok) {
|
||||
for (bool ok = true; ok && !lms.atEnd(isPsb);) {
|
||||
auto al = readAdditionalLayer(s, &ok);
|
||||
if (ok)
|
||||
lms.additionalLayerInfo.insert(al.id, al);
|
||||
}
|
||||
}
|
||||
|
||||
device->rollbackTransaction();
|
||||
*ok = skip_section(s, isPsb);
|
||||
return lms;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief readColorModeDataSection
|
||||
* Read the color mode section
|
||||
@ -424,16 +615,47 @@ static QDataStream &operator>>(QDataStream &s, PSDHeader &header)
|
||||
return s;
|
||||
}
|
||||
|
||||
// Check that the header is a valid PSD.
|
||||
// Check that the header is a valid PSD (as written in the PSD specification).
|
||||
static bool IsValid(const PSDHeader &header)
|
||||
{
|
||||
if (header.signature != 0x38425053) { // '8BPS'
|
||||
//qDebug() << "PSD header: invalid signature" << header.signature;
|
||||
return false;
|
||||
}
|
||||
if (header.version != 1 && header.version != 2) {
|
||||
qDebug() << "PSD header: invalid version" << header.version;
|
||||
return false;
|
||||
}
|
||||
if (header.depth != 8 &&
|
||||
header.depth != 16 &&
|
||||
header.depth != 32 &&
|
||||
header.depth != 1) {
|
||||
qDebug() << "PSD header: invalid depth" << header.depth;
|
||||
return false;
|
||||
}
|
||||
if (header.color_mode != CM_RGB &&
|
||||
header.color_mode != CM_GRAYSCALE &&
|
||||
header.color_mode != CM_INDEXED &&
|
||||
header.color_mode != CM_DUOTONE &&
|
||||
header.color_mode != CM_CMYK &&
|
||||
header.color_mode != CM_LABCOLOR &&
|
||||
header.color_mode != CM_MULTICHANNEL &&
|
||||
header.color_mode != CM_BITMAP) {
|
||||
qDebug() << "PSD header: invalid color mode" << header.color_mode;
|
||||
return false;
|
||||
}
|
||||
if (header.channel_count < 1 || header.channel_count > 56) {
|
||||
qDebug() << "PSD header: invalid number of channels" << header.channel_count;
|
||||
return false;
|
||||
}
|
||||
if (header.width > 300000 || header.height > 300000) {
|
||||
qDebug() << "PSD header: invalid image size" << header.width << "x" << header.height;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check that the header is supported.
|
||||
// Check that the header is supported by this plugin.
|
||||
static bool IsSupported(const PSDHeader &header)
|
||||
{
|
||||
if (header.version != 1 && header.version != 2) {
|
||||
@ -449,34 +671,14 @@ static bool IsSupported(const PSDHeader &header)
|
||||
header.color_mode != CM_GRAYSCALE &&
|
||||
header.color_mode != CM_INDEXED &&
|
||||
header.color_mode != CM_DUOTONE &&
|
||||
header.color_mode != CM_CMYK &&
|
||||
header.color_mode != CM_LABCOLOR &&
|
||||
header.color_mode != CM_BITMAP) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool skip_section(QDataStream &s, bool psb = false)
|
||||
{
|
||||
qint64 section_length;
|
||||
if (!psb) {
|
||||
quint32 tmp;
|
||||
s >> tmp;
|
||||
section_length = tmp;
|
||||
}
|
||||
else {
|
||||
s >> section_length;
|
||||
}
|
||||
|
||||
// Skip mode data.
|
||||
for (qint32 i32 = 0; section_length; section_length -= i32) {
|
||||
i32 = std::min(section_length, qint64(std::numeric_limits<qint32>::max()));
|
||||
i32 = s.skipRawData(i32);
|
||||
if (i32 < 1)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief decompress
|
||||
* Fast PackBits decompression.
|
||||
@ -497,7 +699,7 @@ qint64 decompress(const char *input, qint64 ilen, char *output, qint64 olen)
|
||||
if (n >= 0) {
|
||||
rr = qint64(n) + 1;
|
||||
if (available < rr) {
|
||||
ip--;
|
||||
--ip;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -509,7 +711,7 @@ qint64 decompress(const char *input, qint64 ilen, char *output, qint64 olen)
|
||||
else if (ip < ilen) {
|
||||
rr = qint64(1-n);
|
||||
if (available < rr) {
|
||||
ip--;
|
||||
--ip;
|
||||
break;
|
||||
}
|
||||
memset(output + j, input[ip++], size_t(rr));
|
||||
@ -525,7 +727,7 @@ qint64 decompress(const char *input, qint64 ilen, char *output, qint64 olen)
|
||||
* \param header The PSD header.
|
||||
* \return The Qt image format.
|
||||
*/
|
||||
static QImage::Format imageFormat(const PSDHeader &header)
|
||||
static QImage::Format imageFormat(const PSDHeader &header, bool alpha)
|
||||
{
|
||||
if (header.channel_count == 0) {
|
||||
return QImage::Format_Invalid;
|
||||
@ -535,9 +737,21 @@ static QImage::Format imageFormat(const PSDHeader &header)
|
||||
switch(header.color_mode) {
|
||||
case CM_RGB:
|
||||
if (header.depth == 16 || header.depth == 32)
|
||||
format = header.channel_count < 4 ? QImage::Format_RGBX64 : QImage::Format_RGBA64;
|
||||
format = header.channel_count < 4 || !alpha ? QImage::Format_RGBX64 : QImage::Format_RGBA64;
|
||||
else
|
||||
format = header.channel_count < 4 ? QImage::Format_RGB888 : QImage::Format_RGBA8888;
|
||||
format = header.channel_count < 4 || !alpha ? QImage::Format_RGB888 : QImage::Format_RGBA8888;
|
||||
break;
|
||||
case CM_CMYK: // Photoshop supports CMYK 8-bits and 16-bits only
|
||||
if (header.depth == 16)
|
||||
format = header.channel_count < 5 || !alpha ? QImage::Format_RGBX64 : QImage::Format_RGBA64;
|
||||
else if (header.depth == 8)
|
||||
format = header.channel_count < 5 || !alpha ? QImage::Format_RGB888 : QImage::Format_RGBA8888;
|
||||
break;
|
||||
case CM_LABCOLOR: // Photoshop supports LAB 8-bits and 16-bits only
|
||||
if (header.depth == 16)
|
||||
format = header.channel_count < 4 || !alpha ? QImage::Format_RGBX64 : QImage::Format_RGBA64;
|
||||
else if (header.depth == 8)
|
||||
format = header.channel_count < 4 || !alpha ? QImage::Format_RGB888 : QImage::Format_RGBA8888;
|
||||
break;
|
||||
case CM_GRAYSCALE:
|
||||
case CM_DUOTONE:
|
||||
@ -598,23 +812,24 @@ inline quint32 xchg(quint32 v) {
|
||||
}
|
||||
|
||||
template<class T>
|
||||
inline void planarToChunchy(uchar *target, const char* source, qint32 width, qint32 c, qint32 cn)
|
||||
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)
|
||||
for (qint32 x = 0; x < width; ++x) {
|
||||
t[x*cn+c] = xchg(s[x]);
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
inline void planarToChunchyFloat(uchar *target, const char* source, qint32 width, qint32 c, qint32 cn)
|
||||
template<class T, T min = 0, T max = 1>
|
||||
inline void planarToChunchyFloat(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]);
|
||||
t[x*cn+c] = std::min(quint16(*reinterpret_cast<float*>(&tmp) * std::numeric_limits<quint16>::max() + 0.5),
|
||||
std::numeric_limits<quint16>::max());
|
||||
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())));
|
||||
}
|
||||
}
|
||||
|
||||
@ -622,8 +837,121 @@ inline void monoInvert(uchar *target, const char* source, qint32 bytes)
|
||||
{
|
||||
auto s = reinterpret_cast<const quint8*>(source);
|
||||
auto t = reinterpret_cast<quint8*>(target);
|
||||
for (qint32 x = 0; x < bytes; ++x)
|
||||
for (qint32 x = 0; x < bytes; ++x) {
|
||||
t[x] = ~s[x];
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
if (sourceChannels < 4) {
|
||||
qDebug() << "cmykToRgb: image is not a valid 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 pt = t + targetChannels * w;
|
||||
*(pt + 0) = T(std::min(max - (C * (1 - K) + K) * max + 0.5, max));
|
||||
*(pt + 1) = T(std::min(max - (M * (1 - K) + K) * max + 0.5, max));
|
||||
*(pt + 2) = T(std::min(max - (Y * (1 - K) + K) * max + 0.5, max));
|
||||
if (targetChannels == 4) {
|
||||
if (sourceChannels >= 5 && alpha)
|
||||
*(pt + 3) = *(ps + 4);
|
||||
else
|
||||
*(pt + 3) = std::numeric_limits<T>::max();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline double finv(double v)
|
||||
{
|
||||
return (v > 6.0 / 29.0 ? v * v * v : (v - 16.0 / 116.0) / 7.787);
|
||||
}
|
||||
|
||||
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);
|
||||
#endif
|
||||
}
|
||||
|
||||
template<class T>
|
||||
inline void labToRgb(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());
|
||||
|
||||
if (sourceChannels < 3) {
|
||||
qDebug() << "labToRgb: image is not a valid LAB!";
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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;
|
||||
|
||||
// NOTE: use the constants of the illuminant of the target RGB color space
|
||||
X = finv(X) * 0.9504; // D50: * 0.9642
|
||||
Y = finv(Y) * 1.0000; // D50: * 1.0000
|
||||
Z = finv(Z) * 1.0888; // D50: * 0.8251
|
||||
|
||||
// converting XYZ to sRGB (sRGB illuminant is D65)
|
||||
auto r = gammaCorrection( 3.24071 * X - 1.53726 * Y - 0.498571 * Z);
|
||||
auto g = gammaCorrection(- 0.969258 * X + 1.87599 * Y + 0.0415557 * Z);
|
||||
auto b = gammaCorrection( 0.0556352 * X - 0.203996 * Y + 1.05707 * Z);
|
||||
|
||||
auto pt = t + targetChannels * w;
|
||||
*(pt + 0) = T(std::max(std::min(r * max + 0.5, max), 0.0));
|
||||
*(pt + 1) = T(std::max(std::min(g * max + 0.5, max), 0.0));
|
||||
*(pt + 2) = T(std::max(std::min(b * max + 0.5, max), 0.0));
|
||||
if (targetChannels == 4) {
|
||||
if (sourceChannels >= 4 && alpha)
|
||||
*(pt + 3) = *(ps + 3);
|
||||
else
|
||||
*(pt + 3) = std::numeric_limits<T>::max();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool readChannel(QByteArray& target, QDataStream &stream, quint32 compressedSize, quint16 compression)
|
||||
{
|
||||
if (compression) {
|
||||
QByteArray tmp;
|
||||
tmp.resize(compressedSize);
|
||||
if (stream.readRawData(tmp.data(), tmp.size()) != tmp.size()) {
|
||||
return false;
|
||||
}
|
||||
if (decompress(tmp.data(), tmp.size(), target.data(), target.size()) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (stream.readRawData(target.data(), target.size()) != target.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return stream.status() == QDataStream::Ok;
|
||||
}
|
||||
|
||||
// Load the PSD image.
|
||||
@ -653,7 +981,8 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
}
|
||||
|
||||
// Layer and Mask section
|
||||
if (!skip_section(stream, isPsb)) {
|
||||
auto lms = readLayerAndMaskSection(stream, isPsb, &ok);
|
||||
if (!ok) {
|
||||
qDebug() << "Error while skipping Layer and Mask section";
|
||||
return false;
|
||||
}
|
||||
@ -669,7 +998,13 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
return false;
|
||||
}
|
||||
|
||||
const QImage::Format format = imageFormat(header);
|
||||
// Try to identify the nature of spots: note that this is just one of many ways to identify the presence
|
||||
// of alpha channels: should work in most cases where colorspaces != RGB/Gray
|
||||
auto alpha = header.color_mode == CM_RGB;
|
||||
if (!lms.isNull())
|
||||
alpha = lms.hasAlpha();
|
||||
|
||||
const QImage::Format format = imageFormat(header, alpha);
|
||||
if (format == QImage::Format_Invalid) {
|
||||
qWarning() << "Unsupported image format. color_mode:" << header.color_mode << "depth:" << header.depth << "channel_count:" << header.channel_count;
|
||||
return false;
|
||||
@ -697,7 +1032,7 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
|
||||
QVector<quint32> strides(header.height * header.channel_count, raw_count);
|
||||
// Read the compressed stride sizes
|
||||
if (compression)
|
||||
if (compression) {
|
||||
for (auto&& v : strides) {
|
||||
if (isPsb) {
|
||||
stream >> v;
|
||||
@ -707,48 +1042,100 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
stream >> tmp;
|
||||
v = tmp;
|
||||
}
|
||||
}
|
||||
// calculate the absolute file positions of each stride (required when a colorspace conversion should be done)
|
||||
auto device = stream.device();
|
||||
QVector<quint64> stridePositions(strides.size());
|
||||
if (!stridePositions.isEmpty()) {
|
||||
stridePositions[0] = device->pos();
|
||||
}
|
||||
for (qsizetype i = 1, n = stridePositions.size(); i < n; ++i) {
|
||||
stridePositions[i] = stridePositions[i-1] + strides.at(i-1);
|
||||
}
|
||||
|
||||
// Read the image
|
||||
QByteArray rawStride;
|
||||
rawStride.resize(raw_count);
|
||||
for (qint32 c = 0; c < channel_num; ++c) {
|
||||
for(qint32 y = 0, h = header.height; y < h; ++y) {
|
||||
auto&& strideSize = strides.at(c*qsizetype(h)+y);
|
||||
if (compression) {
|
||||
QByteArray tmp;
|
||||
tmp.resize(strideSize);
|
||||
if (stream.readRawData(tmp.data(), tmp.size()) != tmp.size()) {
|
||||
|
||||
if (header.color_mode == CM_CMYK || header.color_mode == CM_LABCOLOR || header.color_mode == CM_MULTICHANNEL) {
|
||||
// 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);
|
||||
for (qint32 y = 0, h = header.height; y < h; ++y) {
|
||||
for (qint32 c = 0; c < header.channel_count; ++c) {
|
||||
auto strideNumber = c * qsizetype(h) + y;
|
||||
if (!device->seek(stridePositions.at(strideNumber))) {
|
||||
qDebug() << "Error while seeking the stream of channel" << c << "line" << y;
|
||||
return false;
|
||||
}
|
||||
auto&& strideSize = strides.at(strideNumber);
|
||||
if (!readChannel(rawStride, stream, strideSize, compression)) {
|
||||
qDebug() << "Error while reading the stream of channel" << c << "line" << y;
|
||||
return false;
|
||||
}
|
||||
if (decompress(tmp.data(), tmp.size(), rawStride.data(), rawStride.size()) < 0) {
|
||||
qDebug() << "Error while decompressing the channel" << c << "line" << y;
|
||||
return false;
|
||||
|
||||
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 (stream.readRawData(rawStride.data(), rawStride.size()) != rawStride.size()) {
|
||||
qDebug() << "Error while reading the stream of channel" << c << "line" << y;
|
||||
return false;
|
||||
else if (header.depth == 16) {
|
||||
planarToChunchy<quint16>(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);
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.status() != QDataStream::Ok) {
|
||||
qDebug() << "Stream read error" << stream.status();
|
||||
return false;
|
||||
// Conversion to RGB
|
||||
if (header.color_mode == CM_CMYK) {
|
||||
if (header.depth == 8)
|
||||
cmykToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
|
||||
else
|
||||
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
|
||||
labToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
|
||||
}
|
||||
|
||||
auto scanLine = img.scanLine(y);
|
||||
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
|
||||
planarToChunchy<quint8>(scanLine, rawStride.data(), header.width, c, imgChannels);
|
||||
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 {
|
||||
// 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) {
|
||||
auto&& strideSize = strides.at(c * qsizetype(h) + y);
|
||||
if (!readChannel(rawStride, stream, strideSize, compression)) {
|
||||
qDebug() << "Error while reading the stream of channel" << c << "line" << y;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto scanLine = img.scanLine(y);
|
||||
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
|
||||
planarToChunchy<quint8>(scanLine, rawStride.data(), header.width, c, imgChannels);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LAB conversion generates a sRGB image
|
||||
if (header.color_mode == CM_LABCOLOR) {
|
||||
#ifdef PSD_FAST_LAB_CONVERSION
|
||||
img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
|
||||
#else
|
||||
img.setColorSpace(QColorSpace(QColorSpace::SRgb));
|
||||
#endif
|
||||
}
|
||||
|
||||
// Resolution info
|
||||
if (!setResolution(img, irs)) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Service
|
||||
X-KDE-ServiceTypes=QImageIOPlugins
|
||||
X-KDE-ImageFormat=psd
|
||||
X-KDE-ImageFormat=psd,psb,pdd,psdt
|
||||
X-KDE-MimeType=image/vnd.adobe.photoshop
|
||||
X-KDE-Read=true
|
||||
X-KDE-Write=false
|
||||
|
@ -135,7 +135,7 @@ public:
|
||||
PROP_SAMPLE_POINTS = 39,
|
||||
MAX_SUPPORTED_PROPTYPE, // should always be at the end so its value is last + 1
|
||||
};
|
||||
Q_ENUM(PropType);
|
||||
Q_ENUM(PropType)
|
||||
|
||||
//! Compression type used in layer tiles.
|
||||
enum XcfCompressionType {
|
||||
@ -145,7 +145,7 @@ public:
|
||||
COMPRESS_ZLIB = 2, /* unused */
|
||||
COMPRESS_FRACTAL = 3, /* unused */
|
||||
};
|
||||
Q_ENUM(XcfCompressionType);
|
||||
Q_ENUM(XcfCompressionType)
|
||||
|
||||
enum LayerModeType {
|
||||
GIMP_LAYER_MODE_NORMAL_LEGACY,
|
||||
@ -212,7 +212,7 @@ public:
|
||||
GIMP_LAYER_MODE_PASS_THROUGH,
|
||||
GIMP_LAYER_MODE_COUNT,
|
||||
};
|
||||
Q_ENUM(LayerModeType);
|
||||
Q_ENUM(LayerModeType)
|
||||
|
||||
//! Type of individual layers in an XCF file.
|
||||
enum GimpImageType {
|
||||
@ -223,7 +223,7 @@ public:
|
||||
INDEXED_GIMAGE,
|
||||
INDEXEDA_GIMAGE,
|
||||
};
|
||||
Q_ENUM(GimpImageType);
|
||||
Q_ENUM(GimpImageType)
|
||||
|
||||
//! Type of individual layers in an XCF file.
|
||||
enum GimpColorSpace {
|
||||
|
Reference in New Issue
Block a user