Compare commits

...

18 Commits

Author SHA1 Message Date
e6955e1f03 GIT_SILENT Upgrade ECM and KF version requirements for 5.97.0 release. 2022-08-07 12:18:50 +00:00
6074c4d6fd Use right type on enums 2022-07-29 07:46:10 +02:00
6f44c5c52a PSD: Improve alpha detection
BUG: 182496
2022-07-25 19:34:57 +00:00
d030c75925 PSD: LAB support
LAB images support:
- Added LAB 8/16-bits support by converting them to sRGB
- The conversion are done using literature formulas (LAB -> XYZ -> sRGB): [sRGB on Wiki](https://en.wikipedia.org/wiki/SRGB), [LAB on wiki](https://en.wikipedia.org/wiki/CIELAB_color_space)
- Removed unused code
2022-07-06 21:30:23 +00:00
9b3133ac92 GIT_SILENT Upgrade ECM and KF version requirements for 5.96.0 release. 2022-07-02 14:33:58 +00:00
b0a0bb1294 PSD header checks according to specifications 2022-06-30 06:56:21 +00:00
3d5090593c Improved detection of alpha channel on CMYK images 2022-06-30 06:56:21 +00:00
d4966d169b Minor code optimization 2022-06-30 06:56:21 +00:00
bf52896347 Minor code improvements (tested on all my MCYK PSD/PSB files) 2022-06-30 06:56:21 +00:00
c52ffa2227 Fix Alpha + testcase images 2022-06-30 06:56:21 +00:00
e4e386babf Fix regression 2022-06-30 06:56:21 +00:00
b47a9d7022 Basic support to CMYK 8/16 bits (not fully tested) 2022-06-30 06:56:21 +00:00
2cbf815d1f Require passing tests for the CI to pass 2022-06-29 20:09:38 +02:00
6cd0056f3b Use ECMDeprecationSettings, bump hidden deprec. API to KF 5.95
NO_CHANGELOG
2022-06-28 00:35:18 +02:00
83374f390e Fix missing init of oneValueArgs variable
NO_CHANGELOG
2022-06-28 00:17:56 +02:00
5e59d950bd jxl: support both old 0.6.1 and new 0.7.0 libjxl API
New libjxl API changed the way how lossless 16bit depth images
must be encoded: codestream level 10 must be set,
which implies use of container format.
Unfortunately, there isn’t version number inside libjxl header yet,
so we must detect new version on cmake/PkgConfig level.
2022-06-22 22:21:33 +00:00
de320447f6 Remove extra ';' 2022-06-22 19:52:13 +02:00
cf375a207f avif: read performance improvements 2022-06-20 18:50:03 +02:00
19 changed files with 644 additions and 174 deletions

View File

@ -6,3 +6,4 @@ Dependencies:
Options:
test-before-installing: True
require-passing-tests-on: [ 'Linux', 'FreeBSD', 'Windows' ]

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 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: 72 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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