diff --git a/libs/ui/kis_png_converter.cpp b/libs/ui/kis_png_converter.cpp index 9a0e02c73e..b1b090967c 100644 --- a/libs/ui/kis_png_converter.cpp +++ b/libs/ui/kis_png_converter.cpp @@ -1,1383 +1,1386 @@ /* * Copyright (c) 2005-2007 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kis_png_converter.h" // A big thank to Glenn Randers-Pehrson for his wonderful // documentation of libpng available at // http://www.libpng.org/pub/png/libpng-1.2.5-manual.html #ifndef PNG_MAX_UINT // Removed in libpng 1.4 #define PNG_MAX_UINT PNG_UINT_31_MAX #endif #include // WORDS_BIGENDIAN #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dialogs/kis_dlg_png_import.h" #include "kis_clipboard.h" #include #include "kis_undo_stores.h" #include "half.h" namespace { int getColorTypeforColorSpace(const KoColorSpace * cs , bool alpha) { QString id = cs->id(); if (id == "GRAYA" || id == "GRAYAU16" || id == "GRAYA16") { return alpha ? PNG_COLOR_TYPE_GRAY_ALPHA : PNG_COLOR_TYPE_GRAY; } if (id == "RGBA" || id == "RGBA16") { return alpha ? PNG_COLOR_TYPE_RGB_ALPHA : PNG_COLOR_TYPE_RGB; } return -1; } bool colorSpaceIdSupported(const QString &id) { return id == "RGBA" || id == "RGBA16" || id == "GRAYA" || id == "GRAYAU16" || id == "GRAYA16"; } QPair getColorSpaceForColorType(int color_type, int color_nb_bits) { QPair r; if (color_type == PNG_COLOR_TYPE_PALETTE) { r.first = RGBAColorModelID.id(); r.second = Integer8BitsColorDepthID.id(); } else { if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { r.first = GrayAColorModelID.id(); } else if (color_type == PNG_COLOR_TYPE_RGB_ALPHA || color_type == PNG_COLOR_TYPE_RGB) { r.first = RGBAColorModelID.id(); } if (color_nb_bits == 16) { r.second = Integer16BitsColorDepthID.id(); } else if (color_nb_bits <= 8) { r.second = Integer8BitsColorDepthID.id(); } } return r; } void fillText(png_text* p_text, const char* key, QString& text) { p_text->compression = PNG_TEXT_COMPRESSION_zTXt; p_text->key = const_cast(key); char* textc = new char[text.length()+1]; strcpy(textc, text.toLatin1()); p_text->text = textc; p_text->text_length = text.length() + 1; } long formatStringList(char *string, const size_t length, const char *format, va_list operands) { int n = vsnprintf(string, length, format, operands); if (n < 0) string[length-1] = '\0'; return((long) n); } long formatString(char *string, const size_t length, const char *format, ...) { long n; va_list operands; va_start(operands, format); n = (long) formatStringList(string, length, format, operands); va_end(operands); return(n); } void writeRawProfile(png_struct *ping, png_info *ping_info, QString profile_type, QByteArray profile_data) { png_textp text; png_uint_32 allocated_length, description_length; const uchar hex[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; dbgFile << "Writing Raw profile: type=" << profile_type << ", length=" << profile_data.length() << endl; text = (png_textp) png_malloc(ping, (png_uint_32) sizeof(png_text)); description_length = profile_type.length(); allocated_length = (png_uint_32)(profile_data.length() * 2 + (profile_data.length() >> 5) + 20 + description_length); text[0].text = (png_charp) png_malloc(ping, allocated_length); QString key = QLatin1Literal("Raw profile type ") + profile_type.toLatin1(); QByteArray keyData = key.toLatin1(); text[0].key = keyData.data(); uchar* sp = (uchar*)profile_data.data(); png_charp dp = text[0].text; *dp++ = '\n'; memcpy(dp, profile_type.toLatin1().constData(), profile_type.length()); dp += description_length; *dp++ = '\n'; formatString(dp, allocated_length - strlen(text[0].text), "%8lu ", profile_data.length()); dp += 8; for (long i = 0; i < (long) profile_data.length(); i++) { if (i % 36 == 0) *dp++ = '\n'; *(dp++) = (char) hex[((*sp >> 4) & 0x0f)]; *(dp++) = (char) hex[((*sp++) & 0x0f)]; } *dp++ = '\n'; *dp = '\0'; text[0].text_length = (png_size_t)(dp - text[0].text); text[0].compression = -1; if (text[0].text_length <= allocated_length) png_set_text(ping, ping_info, text, 1); png_free(ping, text[0].text); png_free(ping, text); } QByteArray png_read_raw_profile(png_textp text) { QByteArray profile; static const unsigned char unhex[103] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, 14, 15 }; png_charp sp = text[0].text + 1; /* look for newline */ while (*sp != '\n') sp++; /* look for length */ while (*sp == '\0' || *sp == ' ' || *sp == '\n') sp++; png_uint_32 length = (png_uint_32) atol(sp); while (*sp != ' ' && *sp != '\n') sp++; if (length == 0) { return profile; } profile.resize(length); /* copy profile, skipping white space and column 1 "=" signs */ unsigned char *dp = (unsigned char*)profile.data(); png_uint_32 nibbles = length * 2; for (png_uint_32 i = 0; i < nibbles; i++) { while (*sp < '0' || (*sp > '9' && *sp < 'a') || *sp > 'f') { if (*sp == '\0') { return QByteArray(); } sp++; } if (i % 2 == 0) *dp = (unsigned char)(16 * unhex[(int) *sp++]); else (*dp++) += unhex[(int) *sp++]; } return profile; } void decode_meta_data(png_textp text, KisMetaData::Store* store, QString type, int headerSize) { dbgFile << "Decoding " << type << " " << text[0].key; KisMetaData::IOBackend* exifIO = KisMetaData::IOBackendRegistry::instance()->value(type); Q_ASSERT(exifIO); QByteArray rawProfile = png_read_raw_profile(text); if (headerSize > 0) { rawProfile.remove(0, headerSize); } if (rawProfile.size() > 0) { QBuffer buffer; buffer.setData(rawProfile); exifIO->loadFrom(store, &buffer); } else { dbgFile << "Decoding failed"; } } } KisPNGConverter::KisPNGConverter(KisDocument *doc, bool batchMode) { // Q_ASSERT(doc); // Q_ASSERT(adapter); m_doc = doc; m_stop = false; m_max_row = 0; m_image = 0; m_batchMode = batchMode; } KisPNGConverter::~KisPNGConverter() { } class KisPNGReadStream { public: KisPNGReadStream(quint8* buf, quint32 depth) : m_posinc(8), m_depth(depth), m_buf(buf) { } int nextValue() { if (m_posinc == 0) { m_posinc = 8; m_buf++; } m_posinc -= m_depth; return (((*m_buf) >> (m_posinc)) & ((1 << m_depth) - 1)); } private: quint32 m_posinc, m_depth; quint8* m_buf; }; class KisPNGWriteStream { public: KisPNGWriteStream(quint8* buf, quint32 depth) : m_posinc(8), m_depth(depth), m_buf(buf) { *m_buf = 0; } void setNextValue(int v) { if (m_posinc == 0) { m_posinc = 8; m_buf++; *m_buf = 0; } m_posinc -= m_depth; *m_buf = (v << m_posinc) | *m_buf; } private: quint32 m_posinc, m_depth; quint8* m_buf; }; class KisPNGReaderAbstract { public: KisPNGReaderAbstract(png_structp _png_ptr, int _width, int _height) : png_ptr(_png_ptr), width(_width), height(_height) {} virtual ~KisPNGReaderAbstract() {} virtual png_bytep readLine() = 0; protected: png_structp png_ptr; int width, height; }; class KisPNGReaderLineByLine : public KisPNGReaderAbstract { public: KisPNGReaderLineByLine(png_structp _png_ptr, png_infop info_ptr, int _width, int _height) : KisPNGReaderAbstract(_png_ptr, _width, _height) { png_uint_32 rowbytes = png_get_rowbytes(png_ptr, info_ptr); row_pointer = new png_byte[rowbytes]; } ~KisPNGReaderLineByLine() override { delete[] row_pointer; } png_bytep readLine() override { png_read_row(png_ptr, row_pointer, 0); return row_pointer; } private: png_bytep row_pointer; }; class KisPNGReaderFullImage : public KisPNGReaderAbstract { public: KisPNGReaderFullImage(png_structp _png_ptr, png_infop info_ptr, int _width, int _height) : KisPNGReaderAbstract(_png_ptr, _width, _height), y(0) { row_pointers = new png_bytep[height]; png_uint_32 rowbytes = png_get_rowbytes(png_ptr, info_ptr); for (int i = 0; i < height; i++) { row_pointers[i] = new png_byte[rowbytes]; } png_read_image(png_ptr, row_pointers); } ~KisPNGReaderFullImage() override { for (int i = 0; i < height; i++) { delete[] row_pointers[i]; } delete[] row_pointers; } png_bytep readLine() override { return row_pointers[y++]; } private: png_bytepp row_pointers; int y; }; static void _read_fn(png_structp png_ptr, png_bytep data, png_size_t length) { QIODevice *in = (QIODevice *)png_get_io_ptr(png_ptr); while (length) { int nr = in->read((char*)data, length); if (nr <= 0) { png_error(png_ptr, "Read Error"); return; } length -= nr; } } static void _write_fn(png_structp png_ptr, png_bytep data, png_size_t length) { QIODevice* out = (QIODevice*)png_get_io_ptr(png_ptr); uint nr = out->write((char*)data, length); if (nr != length) { png_error(png_ptr, "Write Error"); return; } } static void _flush_fn(png_structp png_ptr) { Q_UNUSED(png_ptr); } KisImageBuilder_Result KisPNGConverter::buildImage(QIODevice* iod) { dbgFile << "Start decoding PNG File"; png_byte signature[8]; iod->peek((char*)signature, 8); #if PNG_LIBPNG_VER < 10400 if (!png_check_sig(signature, 8)) { #else if (png_sig_cmp(signature, 0, 8) != 0) { #endif iod->close(); return (KisImageBuilder_RESULT_BAD_FETCH); } // Initialize the internal structures png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); if (!png_ptr) { iod->close(); } png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_read_struct(&png_ptr, (png_infopp)0, (png_infopp)0); iod->close(); return (KisImageBuilder_RESULT_FAILURE); } png_infop end_info = png_create_info_struct(png_ptr); if (!end_info) { png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)0); iod->close(); return (KisImageBuilder_RESULT_FAILURE); } // Catch errors if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); iod->close(); return (KisImageBuilder_RESULT_FAILURE); } // Initialize the special png_set_read_fn(png_ptr, iod, _read_fn); #if defined(PNG_SKIP_sRGB_CHECK_PROFILE) && defined(PNG_SET_OPTION_SUPPORTED) png_set_option(png_ptr, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON); #endif // read all PNG info up to image data png_read_info(png_ptr, info_ptr); if (png_get_color_type(png_ptr, info_ptr) == PNG_COLOR_TYPE_GRAY && png_get_bit_depth(png_ptr, info_ptr) < 8) { png_set_expand(png_ptr); } if (png_get_color_type(png_ptr, info_ptr) == PNG_COLOR_TYPE_PALETTE && png_get_bit_depth(png_ptr, info_ptr) < 8) { png_set_packing(png_ptr); } if (png_get_color_type(png_ptr, info_ptr) != PNG_COLOR_TYPE_PALETTE && (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))) { png_set_expand(png_ptr); } png_read_update_info(png_ptr, info_ptr); // Read information about the png png_uint_32 width, height; int color_nb_bits, color_type, interlace_type; png_get_IHDR(png_ptr, info_ptr, &width, &height, &color_nb_bits, &color_type, &interlace_type, 0, 0); dbgFile << "width = " << width << " height = " << height << " color_nb_bits = " << color_nb_bits << " color_type = " << color_type << " interlace_type = " << interlace_type << endl; // swap byteorder on little endian machines. #ifndef WORDS_BIGENDIAN if (color_nb_bits > 8) png_set_swap(png_ptr); #endif // Determine the colorspace QPair csName = getColorSpaceForColorType(color_type, color_nb_bits); if (csName.first.isEmpty()) { png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); iod->close(); return KisImageBuilder_RESULT_UNSUPPORTED_COLORSPACE; } bool hasalpha = (color_type == PNG_COLOR_TYPE_RGB_ALPHA || color_type == PNG_COLOR_TYPE_GRAY_ALPHA); // Read image profile png_charp profile_name; #if PNG_LIBPNG_VER_MAJOR >= 1 && PNG_LIBPNG_VER_MINOR >= 5 png_bytep profile_data; #else png_charp profile_data; #endif int compression_type; png_uint_32 proflen; // Get the various optional chunks // https://www.w3.org/TR/PNG/#11cHRM #if defined(PNG_cHRM_SUPPORTED) double whitePointX, whitePointY; double redX, redY; double greenX, greenY; double blueX, blueY; png_get_cHRM(png_ptr,info_ptr, &whitePointX, &whitePointY, &redX, &redY, &greenX, &greenY, &blueX, &blueY); dbgFile << "cHRM:" << whitePointX << whitePointY << redX << redY << greenX << greenY << blueX << blueY; #endif // https://www.w3.org/TR/PNG/#11gAMA #if defined(PNG_GAMMA_SUPPORTED) double gamma; png_get_gAMA(png_ptr, info_ptr, &gamma); dbgFile << "gAMA" << gamma; #endif // https://www.w3.org/TR/PNG/#11sRGB #if defined(PNG_sRGB_SUPPORTED) int sRGBIntent; png_get_sRGB(png_ptr, info_ptr, &sRGBIntent); dbgFile << "sRGB" << sRGBIntent; #endif bool fromBlender = false; png_text* text_ptr; int num_comments; png_get_text(png_ptr, info_ptr, &text_ptr, &num_comments); for (int i = 0; i < num_comments; i++) { QString key = QString(text_ptr[i].key).toLower(); if (key == "file") { QString relatedFile = text_ptr[i].text; if (relatedFile.contains(".blend", Qt::CaseInsensitive)){ fromBlender=true; } } } bool loadedImageIsHDR = false; const KoColorProfile* profile = 0; if (png_get_iCCP(png_ptr, info_ptr, &profile_name, &compression_type, &profile_data, &proflen)) { QByteArray profile_rawdata; // XXX: Hardcoded for icc type -- is that correct for us? profile_rawdata.resize(proflen); memcpy(profile_rawdata.data(), profile_data, proflen); profile = KoColorSpaceRegistry::instance()->createColorProfile(csName.first, csName.second, profile_rawdata); Q_CHECK_PTR(profile); if (profile) { // dbgFile << "profile name: " << profile->productName() << " profile description: " << profile->productDescription() << " information sur le produit: " << profile->productInfo(); if (!profile->isSuitableForOutput()) { dbgFile << "the profile is not suitable for output and therefore cannot be used in krita, we need to convert the image to a standard profile"; // TODO: in ko2 popup a selection menu to inform the user } } loadedImageIsHDR = strcmp(profile_name, "ITUR_2100_PQ_FULL") == 0; } else { dbgFile << "no embedded profile, will use the default profile"; if (color_nb_bits == 16 && !fromBlender && !qAppName().toLower().contains("test") && !m_batchMode) { KisConfig cfg(true); quint32 behaviour = cfg.pasteBehaviour(); if (behaviour == PASTE_ASK) { KisDlgPngImport dlg(m_path, csName.first, csName.second); KisCursorOverrideHijacker hijacker; Q_UNUSED(hijacker); dlg.exec(); if (!dlg.profile().isEmpty()) { profile = KoColorSpaceRegistry::instance()->profileByName(dlg.profile()); } } } dbgFile << "no embedded profile, will use the default profile"; } const QString colorSpaceId = KoColorSpaceRegistry::instance()->colorSpaceId(csName.first, csName.second); // Check that the profile is used by the color space if (profile && !KoColorSpaceRegistry::instance()->profileIsCompatible(profile, colorSpaceId)) { warnFile << "The profile " << profile->name() << " is not compatible with the color space model " << csName.first << " " << csName.second; profile = 0; } // Retrieve a pointer to the colorspace QScopedPointer locallyStoredTransform; KoColorConversionTransformation* transform = 0; const KoColorSpace* cs = 0; if (loadedImageIsHDR && csName.first == RGBAColorModelID.id() && csName.second == Integer16BitsColorDepthID.id()) { const KoColorSpace *p2020G10CS = KoColorSpaceRegistry::instance()->colorSpace( RGBAColorModelID.id(), Float16BitsColorDepthID.id(), KoColorSpaceRegistry::instance()->p2020G10Profile()); const KoColorSpace *p2020PQCS = KoColorSpaceRegistry::instance()->colorSpace( RGBAColorModelID.id(), Integer16BitsColorDepthID.id(), - KoColorSpaceRegistry::instance()->p2020G10Profile()); + KoColorSpaceRegistry::instance()->p2020PQProfile()); locallyStoredTransform.reset( p2020PQCS->createColorConverter( p2020G10CS, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags())); transform = locallyStoredTransform.data(); cs = p2020G10CS; } else if (profile && profile->isSuitableForOutput()) { dbgFile << "image has embedded profile: " << profile->name() << "\n"; cs = KoColorSpaceRegistry::instance()->colorSpace(csName.first, csName.second, profile); } else { if (csName.first == RGBAColorModelID.id()) { cs = KoColorSpaceRegistry::instance()->colorSpace(csName.first, csName.second, "sRGB-elle-V2-srgbtrc.icc"); } else if (csName.first == GrayAColorModelID.id()) { cs = KoColorSpaceRegistry::instance()->colorSpace(csName.first, csName.second, "Gray-D50-elle-V2-srgbtrc.icc"); } else { cs = KoColorSpaceRegistry::instance()->colorSpace(csName.first, csName.second, 0); } //TODO: two fixes : one tell the user about the problem and ask for a solution, and two once the kocolorspace include KoColorTransformation, use that instead of hacking a lcms transformation // Create the cmsTransform if needed if (profile) { transform = KoColorSpaceRegistry::instance()->colorSpace(csName.first, csName.second, profile)->createColorConverter(cs, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); } } if (cs == 0) { png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); return KisImageBuilder_RESULT_UNSUPPORTED_COLORSPACE; } // Creating the KisImageSP if (m_image == 0) { KisUndoStore *store = m_doc ? m_doc->createUndoStore() : new KisSurrogateUndoStore(); m_image = new KisImage(store, width, height, cs, "built image"); } // Read resolution int unit_type; png_uint_32 x_resolution, y_resolution; png_get_pHYs(png_ptr, info_ptr, &x_resolution, &y_resolution, &unit_type); if (x_resolution > 0 && y_resolution > 0 && unit_type == PNG_RESOLUTION_METER) { m_image->setResolution((double) POINT_TO_CM(x_resolution) / 100.0, (double) POINT_TO_CM(y_resolution) / 100.0); // It is the "invert" macro because we convert from pointer-per-inchs to points } double coeff = quint8_MAX / (double)(pow((double)2, color_nb_bits) - 1); KisPaintLayerSP layer = new KisPaintLayer(m_image.data(), m_image -> nextLayerName(), UCHAR_MAX); // Read comments/texts... png_get_text(png_ptr, info_ptr, &text_ptr, &num_comments); if (m_doc) { KoDocumentInfo * info = m_doc->documentInfo(); dbgFile << "There are " << num_comments << " comments in the text"; for (int i = 0; i < num_comments; i++) { QString key = QString(text_ptr[i].key).toLower(); dbgFile << "key: " << text_ptr[i].key << ", containing: " << text_ptr[i].text << ": " << (key == "raw profile type exif " ? "isExif" : "something else"); if (key == "title") { info->setAboutInfo("title", text_ptr[i].text); } else if (key == "description") { info->setAboutInfo("comment", text_ptr[i].text); } else if (key == "author") { info->setAuthorInfo("creator", text_ptr[i].text); } else if (key.contains("raw profile type exif")) { decode_meta_data(text_ptr + i, layer->metaData(), "exif", 6); } else if (key.contains("raw profile type iptc")) { decode_meta_data(text_ptr + i, layer->metaData(), "iptc", 14); } else if (key.contains("raw profile type xmp")) { decode_meta_data(text_ptr + i, layer->metaData(), "xmp", 0); } else if (key == "version") { m_image->addAnnotation(new KisAnnotation("kpp_version", "version", QByteArray(text_ptr[i].text))); } else if (key == "preset") { m_image->addAnnotation(new KisAnnotation("kpp_preset", "preset", QByteArray(text_ptr[i].text))); } } } // Read image data QScopedPointer reader; try { if (interlace_type == PNG_INTERLACE_ADAM7) { reader.reset(new KisPNGReaderFullImage(png_ptr, info_ptr, width, height)); } else { reader.reset(new KisPNGReaderLineByLine(png_ptr, info_ptr, width, height)); } } catch (const std::bad_alloc& e) { // new png_byte[] may raise such an exception if the image // is invalid / to large. dbgFile << "bad alloc: " << e.what(); // Free only the already allocated png_byte instances. png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); return (KisImageBuilder_RESULT_FAILURE); } // Read the palette if the file is indexed png_colorp palette ; int num_palette; if (color_type == PNG_COLOR_TYPE_PALETTE) { png_get_PLTE(png_ptr, info_ptr, &palette, &num_palette); } // Read the transparency palette quint8 palette_alpha[256]; memset(palette_alpha, 255, 256); if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { if (color_type == PNG_COLOR_TYPE_PALETTE) { png_bytep alpha_ptr; int num_alpha; png_get_tRNS(png_ptr, info_ptr, &alpha_ptr, &num_alpha, 0); for (int i = 0; i < num_alpha; ++i) { palette_alpha[i] = alpha_ptr[i]; } } } for (png_uint_32 y = 0; y < height; y++) { KisHLineIteratorSP it = layer -> paintDevice() -> createHLineIteratorNG(0, y, width); png_bytep row_pointer = reader->readLine(); switch (color_type) { case PNG_COLOR_TYPE_GRAY: case PNG_COLOR_TYPE_GRAY_ALPHA: if (color_nb_bits == 16) { quint16 *src = reinterpret_cast(row_pointer); do { quint16 *d = reinterpret_cast(it->rawData()); d[0] = *(src++); if (hasalpha) { d[1] = *(src++); } else { d[1] = quint16_MAX; } if (transform) transform->transformInPlace(reinterpret_cast(d), reinterpret_cast(d), 1); } while (it->nextPixel()); } else { KisPNGReadStream stream(row_pointer, color_nb_bits); do { quint8 *d = it->rawData(); d[0] = (quint8)(stream.nextValue() * coeff); if (hasalpha) { d[1] = (quint8)(stream.nextValue() * coeff); } else { d[1] = UCHAR_MAX; } if (transform) transform->transformInPlace(d, d, 1); } while (it->nextPixel()); } // FIXME:should be able to read 1 and 4 bits depth and scale them to 8 bits" break; case PNG_COLOR_TYPE_RGB: case PNG_COLOR_TYPE_RGB_ALPHA: if (color_nb_bits == 16) { quint16 *src = reinterpret_cast(row_pointer); do { quint16 *d = reinterpret_cast(it->rawData()); d[2] = *(src++); d[1] = *(src++); d[0] = *(src++); if (hasalpha) d[3] = *(src++); else d[3] = quint16_MAX; if (transform) transform->transformInPlace(reinterpret_cast(d), reinterpret_cast(d), 1); } while (it->nextPixel()); } else { KisPNGReadStream stream(row_pointer, color_nb_bits); do { quint8 *d = it->rawData(); d[2] = (quint8)(stream.nextValue() * coeff); d[1] = (quint8)(stream.nextValue() * coeff); d[0] = (quint8)(stream.nextValue() * coeff); if (hasalpha) d[3] = (quint8)(stream.nextValue() * coeff); else d[3] = UCHAR_MAX; if (transform) transform->transformInPlace(d, d, 1); } while (it->nextPixel()); } break; case PNG_COLOR_TYPE_PALETTE: { KisPNGReadStream stream(row_pointer, color_nb_bits); do { quint8 *d = it->rawData(); quint8 index = stream.nextValue(); quint8 alpha = palette_alpha[ index ]; if (alpha == 0) { memset(d, 0, 4); } else { png_color c = palette[ index ]; d[2] = c.red; d[1] = c.green; d[0] = c.blue; d[3] = alpha; } } while (it->nextPixel()); } break; default: return KisImageBuilder_RESULT_UNSUPPORTED; } } m_image->addNode(layer.data(), m_image->rootLayer().data()); png_read_end(png_ptr, end_info); iod->close(); // Freeing memory png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); return KisImageBuilder_RESULT_OK; } KisImageBuilder_Result KisPNGConverter::buildImage(const QString &filename) { m_path = filename; QFile fp(filename); if (fp.exists()) { if (!fp.open(QIODevice::ReadOnly)) { dbgFile << "Failed to open PNG File"; return (KisImageBuilder_RESULT_FAILURE); } return buildImage(&fp); } return (KisImageBuilder_RESULT_NOT_EXIST); } KisImageSP KisPNGConverter::image() { return m_image; } bool KisPNGConverter::saveDeviceToStore(const QString &filename, const QRect &imageRect, const qreal xRes, const qreal yRes, KisPaintDeviceSP dev, KoStore *store, KisMetaData::Store* metaData) { if (store->open(filename)) { KoStoreDevice io(store); if (!io.open(QIODevice::WriteOnly)) { dbgFile << "Could not open for writing:" << filename; return false; } KisPNGConverter pngconv(0); vKisAnnotationSP_it annotIt = 0; KisMetaData::Store* metaDataStore = 0; if (metaData) { metaDataStore = new KisMetaData::Store(*metaData); } KisPNGOptions options; options.compression = 0; options.interlace = false; options.tryToSaveAsIndexed = false; options.alpha = true; options.saveSRGBProfile = false; if (dev->colorSpace()->id() != "RGBA") { dev = new KisPaintDevice(*dev.data()); KUndo2Command *cmd = dev->convertTo(KoColorSpaceRegistry::instance()->rgb8()); delete cmd; } bool success = pngconv.buildFile(&io, imageRect, xRes, yRes, dev, annotIt, annotIt, options, metaDataStore); if (success != KisImageBuilder_RESULT_OK) { dbgFile << "Saving PNG failed:" << filename; delete metaDataStore; return false; } delete metaDataStore; io.close(); if (!store->close()) { return false; } } else { dbgFile << "Opening of data file failed :" << filename; return false; } return true; } KisImageBuilder_Result KisPNGConverter::buildFile(const QString &filename, const QRect &imageRect, const qreal xRes, const qreal yRes, KisPaintDeviceSP device, vKisAnnotationSP_it annotationsStart, vKisAnnotationSP_it annotationsEnd, KisPNGOptions options, KisMetaData::Store* metaData) { dbgFile << "Start writing PNG File " << filename; // Open a QIODevice for writing QFile fp (filename); if (!fp.open(QIODevice::WriteOnly)) { dbgFile << "Failed to open PNG File for writing"; return (KisImageBuilder_RESULT_FAILURE); } KisImageBuilder_Result result = buildFile(&fp, imageRect, xRes, yRes, device, annotationsStart, annotationsEnd, options, metaData); return result; } KisImageBuilder_Result KisPNGConverter::buildFile(QIODevice* iodevice, const QRect &imageRect, const qreal xRes, const qreal yRes, KisPaintDeviceSP device, vKisAnnotationSP_it annotationsStart, vKisAnnotationSP_it annotationsEnd, KisPNGOptions options, KisMetaData::Store* metaData) { if (!device) return KisImageBuilder_RESULT_INVALID_ARG; if (!options.alpha) { KisPaintDeviceSP tmp = new KisPaintDevice(device->colorSpace()); KoColor c(options.transparencyFillColor, device->colorSpace()); tmp->fill(imageRect, c); KisPainter gc(tmp); gc.bitBlt(imageRect.topLeft(), device, imageRect); gc.end(); device = tmp; } if (device->colorSpace()->colorDepthId() == Float16BitsColorDepthID || device->colorSpace()->colorDepthId() == Float32BitsColorDepthID || device->colorSpace()->colorDepthId() == Float64BitsColorDepthID) { const KoColorSpace *dstCS = KoColorSpaceRegistry::instance()->colorSpace( device->colorSpace()->colorModelId().id(), Integer16BitsColorDepthID.id(), device->colorSpace()->profile()); if (options.saveAsHDR) { dstCS = KoColorSpaceRegistry::instance()->colorSpace( RGBAColorModelID.id(), Integer16BitsColorDepthID.id(), KoColorSpaceRegistry::instance()->p2020PQProfile()); } KisPaintDeviceSP tmp = new KisPaintDevice(device->colorSpace()); tmp->makeCloneFromRough(device, imageRect); delete tmp->convertTo(dstCS); device = tmp; } else { - KIS_SAFE_ASSERT_RECOVER(!options.saveAsHDR) { + KIS_SAFE_ASSERT_RECOVER(!options.saveAsHDR || + (device->colorSpace()->profile() && + device->colorSpace()->profile()->uniqueId() == + KoColorSpaceRegistry::instance()->p2020PQProfile()->uniqueId())) { options.saveAsHDR = false; } } KIS_SAFE_ASSERT_RECOVER(!options.saveAsHDR || !options.forceSRGB) { options.forceSRGB = false; } KIS_SAFE_ASSERT_RECOVER(!options.saveAsHDR || !options.tryToSaveAsIndexed) { options.tryToSaveAsIndexed = false; } QStringList colormodels = QStringList() << RGBAColorModelID.id() << GrayAColorModelID.id(); if (options.forceSRGB || !colormodels.contains(device->colorSpace()->colorModelId().id())) { const KoColorSpace* cs = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), device->colorSpace()->colorDepthId().id(), "sRGB built-in - (lcms internal)"); device = new KisPaintDevice(*device); KUndo2Command *cmd = device->convertTo(cs); delete cmd; } // Initialize structures png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); if (!png_ptr) { return (KisImageBuilder_RESULT_FAILURE); } #if defined(PNG_SKIP_sRGB_CHECK_PROFILE) && defined(PNG_SET_OPTION_SUPPORTED) png_set_option(png_ptr, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON); #endif #ifdef PNG_READ_CHECK_FOR_INVALID_INDEX_SUPPORTED png_set_check_for_invalid_index(png_ptr, 0); #endif png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_write_struct(&png_ptr, (png_infopp)0); return (KisImageBuilder_RESULT_FAILURE); } // If an error occurs during writing, libpng will jump here if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_write_struct(&png_ptr, &info_ptr); return (KisImageBuilder_RESULT_FAILURE); } // Initialize the writing // png_init_io(png_ptr, fp); // Setup the progress function // XXX: Implement progress updating -- png_set_write_status_fn(png_ptr, progress);" // setProgressTotalSteps(100/*height*/); /* set the zlib compression level */ png_set_compression_level(png_ptr, options.compression); png_set_write_fn(png_ptr, (void*)iodevice, _write_fn, _flush_fn); /* set other zlib parameters */ png_set_compression_mem_level(png_ptr, 8); png_set_compression_strategy(png_ptr, Z_DEFAULT_STRATEGY); png_set_compression_window_bits(png_ptr, 15); png_set_compression_method(png_ptr, 8); png_set_compression_buffer_size(png_ptr, 8192); int color_nb_bits = 8 * device->pixelSize() / device->channelCount(); int color_type = getColorTypeforColorSpace(device->colorSpace(), options.alpha); Q_ASSERT(color_type > -1); // Try to compute a table of color if the colorspace is RGB8f QScopedArrayPointer palette; int num_palette = 0; if (!options.alpha && options.tryToSaveAsIndexed && KoID(device->colorSpace()->id()) == KoID("RGBA")) { // png doesn't handle indexed images and alpha, and only have indexed for RGB8 palette.reset(new png_color[255]); KisSequentialIterator it(device, imageRect); bool toomuchcolor = false; while (it.nextPixel()) { const quint8* c = it.oldRawData(); bool findit = false; for (int i = 0; i < num_palette; i++) { if (palette[i].red == c[2] && palette[i].green == c[1] && palette[i].blue == c[0]) { findit = true; break; } } if (!findit) { if (num_palette == 255) { toomuchcolor = true; break; } palette[num_palette].red = c[2]; palette[num_palette].green = c[1]; palette[num_palette].blue = c[0]; num_palette++; } } if (!toomuchcolor) { dbgFile << "Found a palette of " << num_palette << " colors"; color_type = PNG_COLOR_TYPE_PALETTE; if (num_palette <= 2) { color_nb_bits = 1; } else if (num_palette <= 4) { color_nb_bits = 2; } else if (num_palette <= 16) { color_nb_bits = 4; } else { color_nb_bits = 8; } } else { palette.reset(); } } int interlacetype = options.interlace ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(color_type >= 0, KisImageBuilder_RESULT_FAILURE); png_set_IHDR(png_ptr, info_ptr, imageRect.width(), imageRect.height(), color_nb_bits, color_type, interlacetype, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); // set sRGB only if the profile is sRGB -- http://www.w3.org/TR/PNG/#11sRGB says sRGB and iCCP should not both be present bool sRGB = device->colorSpace()->profile()->name().contains(QLatin1String("srgb"), Qt::CaseInsensitive); /* * This automatically writes the correct gamma and chroma chunks along with the sRGB chunk, but firefox's * color management is bugged, so once you give it any incentive to start color managing an sRGB image it * will turn, for example, a nice desaturated rusty red into bright poppy red. So this is disabled for now. */ /*if (!options.saveSRGBProfile && sRGB) { png_set_sRGB_gAMA_and_cHRM(png_ptr, info_ptr, PNG_sRGB_INTENT_PERCEPTUAL); }*/ // we should ensure we don't access non-existing palette object KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(palette || color_type != PNG_COLOR_TYPE_PALETTE, KisImageBuilder_RESULT_FAILURE); // set the palette if (color_type == PNG_COLOR_TYPE_PALETTE) { png_set_PLTE(png_ptr, info_ptr, palette.data(), num_palette); } // Save annotation vKisAnnotationSP_it it = annotationsStart; while (it != annotationsEnd) { if (!(*it) || (*it)->type().isEmpty()) { dbgFile << "Warning: empty annotation"; it++; continue; } dbgFile << "Trying to store annotation of type " << (*it) -> type() << " of size " << (*it) -> annotation() . size(); if ((*it) -> type().startsWith(QString("krita_attribute:"))) { // // Attribute // XXX: it should be possible to save krita_attributes in the \"CHUNKs\"" dbgFile << "cannot save this annotation : " << (*it) -> type(); } else if ((*it)->type() == "kpp_version" || (*it)->type() == "kpp_preset" ) { dbgFile << "Saving preset information " << (*it)->description(); png_textp text = (png_textp) png_malloc(png_ptr, (png_uint_32) sizeof(png_text)); QByteArray keyData = (*it)->description().toLatin1(); text[0].key = keyData.data(); text[0].text = (char*)(*it)->annotation().data(); text[0].text_length = (*it)->annotation().size(); text[0].compression = -1; png_set_text(png_ptr, info_ptr, text, 1); png_free(png_ptr, text); } it++; } // Save the color profile const KoColorProfile* colorProfile = device->colorSpace()->profile(); QByteArray colorProfileData = colorProfile->rawData(); if (!sRGB || options.saveSRGBProfile) { #if PNG_LIBPNG_VER_MAJOR >= 1 && PNG_LIBPNG_VER_MINOR >= 5 const char *typeString = !options.saveAsHDR ? "icc" : "ITUR_2100_PQ_FULL"; png_set_iCCP(png_ptr, info_ptr, (png_const_charp)typeString, PNG_COMPRESSION_TYPE_BASE, (png_const_bytep)colorProfileData.constData(), colorProfileData . size()); #else // older version of libpng has a problem with constness on the parameters char typeStringICC[] = "icc"; char typeStringHDR[] = "ITUR_2100_PQ_FULL"; char *typeString = !options.saveAsHDR ? typeStringICC : typeStringHDR; png_set_iCCP(png_ptr, info_ptr, typeString, PNG_COMPRESSION_TYPE_BASE, colorProfileData.data(), colorProfileData . size()); #endif } // save comments from the document information // warning: according to the official png spec, the keys need to be capitalized! if (m_doc) { png_text texts[4]; int nbtexts = 0; KoDocumentInfo * info = m_doc->documentInfo(); QString title = info->aboutInfo("title"); if (!title.isEmpty() && options.storeMetaData) { fillText(texts + nbtexts, "Title", title); nbtexts++; } QString abstract = info->aboutInfo("subject"); if (abstract.isEmpty()) { abstract = info->aboutInfo("abstract"); } if (!abstract.isEmpty() && options.storeMetaData) { QString keywords = info->aboutInfo("keyword"); if (!keywords.isEmpty()) { abstract = abstract + " keywords: " + keywords; } fillText(texts + nbtexts, "Description", abstract); nbtexts++; } QString license = info->aboutInfo("license"); if (!license.isEmpty() && options.storeMetaData) { fillText(texts + nbtexts, "Copyright", license); nbtexts++; } QString author = info->authorInfo("creator"); if (!author.isEmpty() && options.storeAuthor) { if (!info->authorContactInfo().isEmpty()) { QString contact = info->authorContactInfo().at(0); if (!contact.isEmpty()) { author = author+"("+contact+")"; } } fillText(texts + nbtexts, "Author", author); nbtexts++; } png_set_text(png_ptr, info_ptr, texts, nbtexts); } // Save metadata following imagemagick way // Save exif if (metaData && !metaData->empty()) { if (options.exif) { dbgFile << "Trying to save exif information"; KisMetaData::IOBackend* exifIO = KisMetaData::IOBackendRegistry::instance()->value("exif"); Q_ASSERT(exifIO); QBuffer buffer; exifIO->saveTo(metaData, &buffer, KisMetaData::IOBackend::JpegHeader); writeRawProfile(png_ptr, info_ptr, "exif", buffer.data()); } // Save IPTC if (options.iptc) { dbgFile << "Trying to save exif information"; KisMetaData::IOBackend* iptcIO = KisMetaData::IOBackendRegistry::instance()->value("iptc"); Q_ASSERT(iptcIO); QBuffer buffer; iptcIO->saveTo(metaData, &buffer, KisMetaData::IOBackend::JpegHeader); dbgFile << "IPTC information size is" << buffer.data().size(); writeRawProfile(png_ptr, info_ptr, "iptc", buffer.data()); } // Save XMP if (options.xmp) { dbgFile << "Trying to save XMP information"; KisMetaData::IOBackend* xmpIO = KisMetaData::IOBackendRegistry::instance()->value("xmp"); Q_ASSERT(xmpIO); QBuffer buffer; xmpIO->saveTo(metaData, &buffer, KisMetaData::IOBackend::NoHeader); dbgFile << "XMP information size is" << buffer.data().size(); writeRawProfile(png_ptr, info_ptr, "xmp", buffer.data()); } } #if 0 // Unimplemented? // Save resolution int unit_type; png_uint_32 x_resolution, y_resolution; #endif png_set_pHYs(png_ptr, info_ptr, CM_TO_POINT(xRes) * 100.0, CM_TO_POINT(yRes) * 100.0, PNG_RESOLUTION_METER); // It is the "invert" macro because we convert from pointer-per-inchs to points // Save the information to the file png_write_info(png_ptr, info_ptr); png_write_flush(png_ptr); // swap byteorder on little endian machines. #ifndef WORDS_BIGENDIAN if (color_nb_bits > 8) png_set_swap(png_ptr); #endif // Write the PNG // png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, 0); struct RowPointersStruct { RowPointersStruct(const QSize &size, int pixelSize) : numRows(size.height()) { rows = new png_byte*[numRows]; for (int i = 0; i < numRows; i++) { rows[i] = new png_byte[size.width() * pixelSize]; } } ~RowPointersStruct() { for (int i = 0; i < numRows; i++) { delete[] rows[i]; } delete[] rows; } const int numRows = 0; png_byte** rows = 0; }; // Fill the data structure RowPointersStruct rowPointers(imageRect.size(), device->pixelSize()); int row = 0; for (int y = imageRect.y(); y < imageRect.y() + imageRect.height(); y++, row++) { KisHLineConstIteratorSP it = device->createHLineConstIteratorNG(imageRect.x(), y, imageRect.width()); switch (color_type) { case PNG_COLOR_TYPE_GRAY: case PNG_COLOR_TYPE_GRAY_ALPHA: if (color_nb_bits == 16) { quint16 *dst = reinterpret_cast(rowPointers.rows[row]); do { const quint16 *d = reinterpret_cast(it->oldRawData()); *(dst++) = d[0]; if (options.alpha) *(dst++) = d[1]; } while (it->nextPixel()); } else { quint8 *dst = rowPointers.rows[row]; do { const quint8 *d = it->oldRawData(); *(dst++) = d[0]; if (options.alpha) *(dst++) = d[1]; } while (it->nextPixel()); } break; case PNG_COLOR_TYPE_RGB: case PNG_COLOR_TYPE_RGB_ALPHA: if (color_nb_bits == 16) { quint16 *dst = reinterpret_cast(rowPointers.rows[row]); do { const quint16 *d = reinterpret_cast(it->oldRawData()); *(dst++) = d[2]; *(dst++) = d[1]; *(dst++) = d[0]; if (options.alpha) *(dst++) = d[3]; } while (it->nextPixel()); } else { quint8 *dst = rowPointers.rows[row]; do { const quint8 *d = it->oldRawData(); *(dst++) = d[2]; *(dst++) = d[1]; *(dst++) = d[0]; if (options.alpha) *(dst++) = d[3]; } while (it->nextPixel()); } break; case PNG_COLOR_TYPE_PALETTE: { quint8 *dst = rowPointers.rows[row]; KisPNGWriteStream writestream(dst, color_nb_bits); do { const quint8 *d = it->oldRawData(); int i; for (i = 0; i < num_palette; i++) { if (palette[i].red == d[2] && palette[i].green == d[1] && palette[i].blue == d[0]) { break; } } writestream.setNextValue(i); } while (it->nextPixel()); } break; default: return KisImageBuilder_RESULT_UNSUPPORTED; } } png_write_image(png_ptr, rowPointers.rows); // Writing is over png_write_end(png_ptr, info_ptr); // Free memory png_destroy_write_struct(&png_ptr, &info_ptr); return KisImageBuilder_RESULT_OK; } void KisPNGConverter::cancel() { m_stop = true; } void KisPNGConverter::progress(png_structp png_ptr, png_uint_32 row_number, int pass) { if (png_ptr == 0 || row_number > PNG_MAX_UINT || pass > 7) return; // setProgress(row_number); } bool KisPNGConverter::isColorSpaceSupported(const KoColorSpace *cs) { return colorSpaceIdSupported(cs->id()); } diff --git a/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpace.h b/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpace.h index eb8ccea047..e3e6fdd021 100644 --- a/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpace.h +++ b/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpace.h @@ -1,105 +1,109 @@ #ifndef LCMSRGBP2020PQCOLORSPACE_H #define LCMSRGBP2020PQCOLORSPACE_H #include #include #include #include #include "KoColorConversionTransformationFactory.h" #include template struct ColorSpaceFromFactory { }; template<> struct ColorSpaceFromFactory { typedef RgbU8ColorSpace type; }; template<> struct ColorSpaceFromFactory { typedef RgbU16ColorSpace type; }; template<> struct ColorSpaceFromFactory { typedef RgbF16ColorSpace type; }; template<> struct ColorSpaceFromFactory { typedef RgbF32ColorSpace type; }; /** * Define a singly linked list of supported bit depth traits */ template struct NextTrait { using type = void; }; template<> struct NextTrait { using type = KoBgrU16Traits; }; template<> struct NextTrait { using type = KoRgbF16Traits; }; template<> struct NextTrait { using type = KoRgbF32Traits; }; /** * Recursively add bit-depths conversions to the color space. We add only * **outgoing** conversions for every RGB color space. That is, every color * space has exactly three outgoing edges for color conversion. */ template void addInternalConversion(QList &list, CurrentTraits*) { // general case: add a converter and recurse for the next traits list << new LcmsScaleRGBP2020PQTransformationFactory(); using NextTraits = typename NextTrait::type; addInternalConversion(list, static_cast(0)); } template void addInternalConversion(QList &list, typename ParentColorSpace::ColorSpaceTraits*) { // exception: skip adding an edge to the same bit depth using CurrentTraits = typename ParentColorSpace::ColorSpaceTraits; using NextTraits = typename NextTrait::type; addInternalConversion(list, static_cast(0)); } template void addInternalConversion(QList &, void*) { // stop recursion } template class LcmsRGBP2020PQColorSpaceFactoryWrapper : public BaseColorSpaceFactory { typedef typename ColorSpaceFromFactory::type RelatedColorSpaceType; KoColorSpace *createColorSpace(const KoColorProfile *p) const override { return new RelatedColorSpaceType(this->name(), p->clone()); } QList colorConversionLinks() const override { QList list; - // we skip direct conversions to RGB U8, because it cannot fit linear color space - list << new LcmsFromRGBP2020PQTransformationFactory(); + /** + * We explicitly disable direct conversions to/from integer color spaces, because + * they may cause the the conversion system to choose them as an intermediate + * color space for the conversion chain, e.g. + * p709-g10 F32 -> p2020-g10 U16 -> Rec2020-pq U16, which is incorrect and loses + * all the HDR data + */ list << new LcmsFromRGBP2020PQTransformationFactory(); list << new LcmsFromRGBP2020PQTransformationFactory(); - list << new LcmsToRGBP2020PQTransformationFactory(); list << new LcmsToRGBP2020PQTransformationFactory(); list << new LcmsToRGBP2020PQTransformationFactory(); // internally, we can convert to RGB U8 if needed addInternalConversion(list, static_cast(0)); return list; } }; #endif // LCMSRGBP2020PQCOLORSPACE_H diff --git a/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpaceTransformation.h b/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpaceTransformation.h index a96517e5eb..e361715837 100644 --- a/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpaceTransformation.h +++ b/plugins/color/lcms2engine/LcmsRGBP2020PQColorSpaceTransformation.h @@ -1,248 +1,251 @@ #ifndef LCMSRGBP2020PQCOLORSPACETRANSFORMATION_H #define LCMSRGBP2020PQCOLORSPACETRANSFORMATION_H #include "KoAlwaysInline.h" #include "KoColorModelStandardIds.h" #include "KoColorSpaceMaths.h" #include "KoColorModelStandardIdsUtils.h" #include "KoColorConversionTransformationFactory.h" #include #include #include #include namespace { ALWAYS_INLINE float applySmpte2048Curve(float x) { const float m1 = 2610.0 / 4096.0 / 4.0; const float m2 = 2523.0 / 4096.0 * 128.0; const float a1 = 3424.0 / 4096.0; const float c2 = 2413.0 / 4096.0 * 32.0; const float c3 = 2392.0 / 4096.0 * 32.0; const float a4 = 1.0; const float x_p = powf(0.008 * std::max(0.0f, x), m1); const float res = powf((a1 + c2 * x_p) / (a4 + c3 * x_p), m2); return res; } ALWAYS_INLINE float removeSmpte2048Curve(float x) { const float m1_r = 4096.0 * 4.0 / 2610.0; const float m2_r = 4096.0 / 2523.0 / 128.0; const float a1 = 3424.0 / 4096.0; const float c2 = 2413.0 / 4096.0 * 32.0; const float c3 = 2392.0 / 4096.0 * 32.0; const float x_p = powf(x, m2_r); const float res = powf(qMax(0.0f, x_p - a1) / (c2 - c3 * x_p), m1_r); return res * 125.0f; } template struct DstTraitsForSource { typedef KoRgbF32Traits result; }; template <> struct DstTraitsForSource { typedef KoRgbF16Traits result; }; template <> struct DstTraitsForSource { typedef KoRgbF16Traits result; }; template struct RemoveSmpte2048Policy { static ALWAYS_INLINE dst_channel_type process(src_channel_type value) { return KoColorSpaceMaths::scaleToA( removeSmpte2048Curve( KoColorSpaceMaths::scaleToA( value))); } }; template struct ApplySmpte2048Policy { static ALWAYS_INLINE dst_channel_type process(src_channel_type value) { return KoColorSpaceMaths::scaleToA( applySmpte2048Curve( KoColorSpaceMaths::scaleToA( value))); } }; template struct NoopPolicy { static ALWAYS_INLINE dst_channel_type process(src_channel_type value) { return KoColorSpaceMaths::scaleToA(value); } }; } template class Policy> struct ApplyRgbShaper : public KoColorConversionTransformation { ApplyRgbShaper(const KoColorSpace* srcCs, const KoColorSpace* dstCs, Intent renderingIntent, ConversionFlags conversionFlags) : KoColorConversionTransformation(srcCs, dstCs, renderingIntent, conversionFlags) { } void transform(const quint8 *src, quint8 *dst, qint32 nPixels) const override { KIS_ASSERT(src != dst); const typename SrcCSTraits::Pixel *srcPixel = reinterpret_cast(src); typename DstCSTraits::Pixel *dstPixel = reinterpret_cast(dst); typedef typename SrcCSTraits::channels_type src_channel_type; typedef typename DstCSTraits::channels_type dst_channel_type; typedef Policy ConcretePolicy; for (int i = 0; i < nPixels; i++) { dstPixel->red = ConcretePolicy::process(srcPixel->red); dstPixel->green = ConcretePolicy::process(srcPixel->green); dstPixel->blue = ConcretePolicy::process(srcPixel->blue); dstPixel->alpha = KoColorSpaceMaths::scaleToA( srcPixel->alpha); srcPixel++; dstPixel++; } } }; template::result> class LcmsFromRGBP2020PQTransformationFactory : public KoColorConversionTransformationFactory { public: LcmsFromRGBP2020PQTransformationFactory() : KoColorConversionTransformationFactory(RGBAColorModelID.id(), colorDepthIdForChannelType().id(), "High Dynamic Range UHDTV Wide Color Gamut Display (Rec. 2020) - SMPTE ST 2084 PQ EOTF", RGBAColorModelID.id(), colorDepthIdForChannelType().id(), "Rec2020-elle-V4-g10.icc") { } bool conserveColorInformation() const override { return true; } bool conserveDynamicRange() const override { return dstColorDepthId() == Float16BitsColorDepthID.id() || dstColorDepthId() == Float32BitsColorDepthID.id() || dstColorDepthId() == Float64BitsColorDepthID.id(); } KoColorConversionTransformation* createColorTransformation(const KoColorSpace* srcColorSpace, const KoColorSpace* dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const override { return new ApplyRgbShaper< typename ParentColorSpace::ColorSpaceTraits, DstColorSpaceTraits, RemoveSmpte2048Policy>(srcColorSpace, dstColorSpace, renderingIntent, conversionFlags); } }; template::result> class LcmsToRGBP2020PQTransformationFactory : public KoColorConversionTransformationFactory { public: LcmsToRGBP2020PQTransformationFactory() : KoColorConversionTransformationFactory(RGBAColorModelID.id(), colorDepthIdForChannelType().id(), "Rec2020-elle-V4-g10.icc", RGBAColorModelID.id(), colorDepthIdForChannelType().id(), "High Dynamic Range UHDTV Wide Color Gamut Display (Rec. 2020) - SMPTE ST 2084 PQ EOTF") { } bool conserveColorInformation() const override { return true; } bool conserveDynamicRange() const override { return true; } KoColorConversionTransformation* createColorTransformation(const KoColorSpace* srcColorSpace, const KoColorSpace* dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const override { return new ApplyRgbShaper< DstColorSpaceTraits, typename ParentColorSpace::ColorSpaceTraits, ApplySmpte2048Policy>(srcColorSpace, dstColorSpace, renderingIntent, conversionFlags); } }; template class LcmsScaleRGBP2020PQTransformationFactory : public KoColorConversionTransformationFactory { public: LcmsScaleRGBP2020PQTransformationFactory() : KoColorConversionTransformationFactory(RGBAColorModelID.id(), colorDepthIdForChannelType().id(), "High Dynamic Range UHDTV Wide Color Gamut Display (Rec. 2020) - SMPTE ST 2084 PQ EOTF", RGBAColorModelID.id(), colorDepthIdForChannelType().id(), "High Dynamic Range UHDTV Wide Color Gamut Display (Rec. 2020) - SMPTE ST 2084 PQ EOTF") { KIS_SAFE_ASSERT_RECOVER_NOOP(srcColorDepthId() != dstColorDepthId()); } bool conserveColorInformation() const override { return true; } bool conserveDynamicRange() const override { - return true; + return + srcColorDepthId() == Float16BitsColorDepthID.id() || + srcColorDepthId() == Float32BitsColorDepthID.id() || + srcColorDepthId() == Float64BitsColorDepthID.id(); } KoColorConversionTransformation* createColorTransformation(const KoColorSpace* srcColorSpace, const KoColorSpace* dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const override { return new ApplyRgbShaper< typename ParentColorSpace::ColorSpaceTraits, DstColorSpaceTraits, NoopPolicy>(srcColorSpace, dstColorSpace, renderingIntent, conversionFlags); } }; #endif // LCMSRGBP2020PQCOLORSPACETRANSFORMATION_H diff --git a/plugins/impex/png/tests/kis_png_test.cpp b/plugins/impex/png/tests/kis_png_test.cpp index 874b6c8387..583ba20488 100644 --- a/plugins/impex/png/tests/kis_png_test.cpp +++ b/plugins/impex/png/tests/kis_png_test.cpp @@ -1,40 +1,155 @@ /* * Copyright (C) 2007 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_png_test.h" #include #include #include "filestest.h" #include #ifndef FILES_DATA_DIR #error "FILES_DATA_DIR not set. A directory with the data used for testing the importing of files in krita" #endif void KisPngTest::testFiles() { TestUtil::testFiles(QString(FILES_DATA_DIR) + "/sources", QStringList(), QString(), 1); } +void roudTripHdrImage(const KoColorSpace *savingColorSpace) +{ + qDebug() << "Test saving" << savingColorSpace->id() << savingColorSpace->profile()->name(); + + const KoColorSpace * scRGBF32 = + KoColorSpaceRegistry::instance()->colorSpace( + RGBAColorModelID.id(), + Float32BitsColorDepthID.id(), + KoColorSpaceRegistry::instance()->p709G10Profile()); + + KoColor fillColor(scRGBF32); + float *pixelPtr = reinterpret_cast(fillColor.data()); + + pixelPtr[0] = 2.7; + pixelPtr[1] = 1.6; + pixelPtr[2] = 0.8; + pixelPtr[3] = 0.9; + + { + QScopedPointer doc(KisPart::instance()->createDocument()); + + KisImageSP image = new KisImage(0, 3, 3, scRGBF32, "png test"); + KisPaintLayerSP paintLayer0 = new KisPaintLayer(image, "paint0", OPACITY_OPAQUE_U8); + paintLayer0->paintDevice()->fill(image->bounds(), fillColor); + image->addNode(paintLayer0, image->root()); + + // convert image color space before saving + image->convertImageColorSpace(savingColorSpace, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); + image->waitForDone(); + + KisImportExportManager manager(doc.data()); + doc->setFileBatchMode(true); + doc->setCurrentImage(image); + + KisPropertiesConfigurationSP exportConfiguration = new KisPropertiesConfiguration(); + exportConfiguration->setProperty("saveAsHDR", true); + exportConfiguration->setProperty("saveSRGBProfile", false); + exportConfiguration->setProperty("forceSRGB", false); + doc->exportDocumentSync(QUrl::fromLocalFile("test.png"), "image/png", exportConfiguration); + } + + { + QScopedPointer doc(KisPart::instance()->createDocument()); + KisImportExportManager manager(doc.data()); + doc->setFileBatchMode(true); + + KisImportExportFilter::ConversionStatus loadingStatus = + manager.importDocument("test.png", QString()); + + QCOMPARE(loadingStatus, KisImportExportFilter::OK); + + KisImageSP image = doc->image(); + image->initialRefreshGraph(); + + KoColor resultColor; + +// qDebug() << ppVar(image->colorSpace()) << image->colorSpace()->profile()->name(); +// image->projection()->pixel(1, 1, &resultColor); +// qDebug() << ppVar(resultColor); + + image->convertImageColorSpace(scRGBF32, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); + image->waitForDone(); + + image->projection()->pixel(1, 1, &resultColor); +// qDebug() << ppVar(resultColor); + + const float tolerance = savingColorSpace->colorDepthId() == Integer8BitsColorDepthID ? 0.02 : 0.01; + bool resultIsValid = true; + float *resultPtr = reinterpret_cast(resultColor.data()); + for (int i = 0; i < 4; i++) { + resultIsValid &= qAbs(resultPtr[i] - pixelPtr[i]) < tolerance; + } + + if (!resultIsValid) { + qDebug() << ppVar(fillColor) << ppVar(resultColor); + } + QVERIFY(resultIsValid); + } +} + +void KisPngTest::testSaveHDR() +{ + QVector colorDepthIds; + colorDepthIds << Float16BitsColorDepthID; + colorDepthIds << Float32BitsColorDepthID; + + QVector profiles; + profiles << KoColorSpaceRegistry::instance()->p709G10Profile(); + profiles << KoColorSpaceRegistry::instance()->p2020G10Profile(); + profiles << KoColorSpaceRegistry::instance()->p2020PQProfile(); + + + Q_FOREACH(const KoID &depth, colorDepthIds) { + Q_FOREACH(const KoColorProfile *profile, profiles) { + roudTripHdrImage( + KoColorSpaceRegistry::instance()->colorSpace( + RGBAColorModelID.id(), + depth.id(), + profile)); + } + } + + roudTripHdrImage( + KoColorSpaceRegistry::instance()->colorSpace( + RGBAColorModelID.id(), + Integer16BitsColorDepthID.id(), + KoColorSpaceRegistry::instance()->p2020PQProfile())); + + roudTripHdrImage( + KoColorSpaceRegistry::instance()->colorSpace( + RGBAColorModelID.id(), + Integer8BitsColorDepthID.id(), + KoColorSpaceRegistry::instance()->p2020PQProfile())); +} + KISTEST_MAIN(KisPngTest) diff --git a/plugins/impex/png/tests/kis_png_test.h b/plugins/impex/png/tests/kis_png_test.h index 3bb00c83f1..e312f85756 100644 --- a/plugins/impex/png/tests/kis_png_test.h +++ b/plugins/impex/png/tests/kis_png_test.h @@ -1,31 +1,32 @@ /* * Copyright (C) 2007 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_PNG_TEST_H_ #define _KIS_PNG_TEST_H_ #include class KisPngTest : public QObject { Q_OBJECT private Q_SLOTS: void testFiles(); + void testSaveHDR(); }; #endif