diff --git a/CMakeLists.txt b/CMakeLists.txt index da5571b3a..f152c682b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,11 +188,11 @@ set_package_properties(Threads PROPERTIES TYPE REQUIRED ) -find_package(PNG) -set_package_properties(PNG PROPERTIES +find_package(SPNG) +set_package_properties(SPNG PROPERTIES PURPOSE "PNG format handler" - DESCRIPTION "A collection of routines used to create PNG format graphics files" - URL "http://www.libpng.org/pub/png/libpng.html" + DESCRIPTION "Simple, modern libpng alternative" + URL "https://libspng.org/" TYPE REQUIRED ) diff --git a/cmake/modules/FindSPNG.cmake b/cmake/modules/FindSPNG.cmake new file mode 100644 index 000000000..8235c626f --- /dev/null +++ b/cmake/modules/FindSPNG.cmake @@ -0,0 +1,31 @@ +# Try to find SPNG, once done this will define: +# +# SPNG_FOUND - system has SPNG +# SPNG_INCLUDES - the SPNG include directory +# SPNG_LIBRARIES - the libraries needed to use SPNG +# +# Copyright (C) 2023 Ivailo Monev +# +# Redistribution and use is allowed according to the terms of the BSD license. + +find_package(PkgConfig REQUIRED) +include(FindPackageHandleStandardArgs) + +pkg_check_modules(PC_SPNG QUIET spng) + +find_path(SPNG_INCLUDES + NAMES spng.h + HINTS $ENV{SPNGDIR}/include ${PC_SPNG_INCLUDEDIR} +) + +find_library(SPNG_LIBRARIES + NAMES spng + HINTS $ENV{SPNGDIR}/lib ${PC_SPNG_LIBDIR} +) + +find_package_handle_standard_args(SPNG + VERSION_VAR PC_SPNG_VERSION + REQUIRED_VARS SPNG_LIBRARIES SPNG_INCLUDES +) + +mark_as_advanced(SPNG_INCLUDES SPNG_LIBRARIES) diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 372f721cb..acf31e570 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1,9 +1,9 @@ -add_definitions(${PNG_DEFINITIONS}) +# add_definitions() set(EXTRA_GUI_LIBS KtCore ${FREETYPE_LIBRARIES} ${X11_X11_LIB} - ${PNG_LIBRARIES} + ${SPNG_LIBRARIES} ${DEFLATE_LIBRARIES} ) @@ -200,7 +200,7 @@ include_directories( ${CMAKE_BINARY_DIR}/include/QtGui ${FREETYPE_INCLUDE_DIRS} ${X11_INCLUDE_DIR} - ${PNG_INCLUDE_DIRS} + ${SPNG_INCLUDES} ${DEFLATE_INCLUDES} ) diff --git a/src/gui/image/qpnghandler.cpp b/src/gui/image/qpnghandler.cpp index c5b3a4636..c62141d5a 100644 --- a/src/gui/image/qpnghandler.cpp +++ b/src/gui/image/qpnghandler.cpp @@ -1,7 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2015 The Qt Company Ltd. -** Copyright (C) 2016 Ivailo Monev +** Copyright (C) 2023 Ivailo Monev ** ** This file is part of the QtGui module of the Katie Toolkit. ** @@ -23,64 +22,45 @@ #include "qiodevice.h" #include "qvariant.h" #include "qimage.h" -#include "qimage_p.h" -#include "qdrawhelper_p.h" #include "qcorecommon_p.h" -#include "qguicommon_p.h" +#include "qdebug.h" -#include -#include +#include + +// for reference: +// https://www.w3.org/TR/PNG-Chunks.html QT_BEGIN_NAMESPACE -#if Q_BYTE_ORDER == Q_BIG_ENDIAN -# define QFILLER_ORDER PNG_FILLER_BEFORE -#else -# define QFILLER_ORDER PNG_FILLER_AFTER -#endif - -/* - All PNG files load to the minimal QImage equivalent. - - All QImage formats output to reasonably efficient PNG equivalents. - Never to grayscale. -*/ - -extern "C" { - -static void qt_png_warning(png_structp /*png_ptr*/, png_const_charp message) +int spng_read_proc(spng_ctx *ctx, void *user, void *dst_src, size_t length) { - qWarning("libpng warning: %s", message); -} - -static void qt_png_read(png_structp png_ptr, png_bytep data, png_size_t length) -{ - QPngHandler *handler = (QPngHandler *)png_get_io_ptr(png_ptr); - - png_size_t nr = handler->device()->read((char*)data, length); - if (nr != length) { - png_error(png_ptr, "Read error"); + // qDebug() << Q_FUNC_INFO << length; + Q_UNUSED(ctx); + QIODevice* iodevice = static_cast(user); + char* dst = static_cast(dst_src); + const qint64 result = iodevice->read(dst, length); + if (result <= 0) { + return SPNG_IO_ERROR; } + return SPNG_OK; } -static void qt_png_write(png_structp png_ptr, png_bytep data, png_size_t length) +int spng_write_proc(spng_ctx *ctx, void *user, void *dst_src, size_t length) { - QPngHandler *handler = (QPngHandler *)png_get_io_ptr(png_ptr); - - png_size_t nr = handler->device()->write((char*)data, length); - if (nr != length) { - png_error(png_ptr, "Write error"); + // qDebug() << Q_FUNC_INFO << length; + Q_UNUSED(ctx); + QIODevice* iodevice = static_cast(user); + const char* src = static_cast(dst_src); + const qint64 result = iodevice->write(src, length); + if (result <= 0) { + return SPNG_IO_ERROR; } + return SPNG_OK; } -static void qt_png_flush(png_structp /* png_ptr */) -{ -} - -} QPngHandler::QPngHandler() - : m_compression(1) + : m_complevel(1) { } @@ -100,185 +80,174 @@ bool QPngHandler::canRead() const bool QPngHandler::read(QImage *image) { - if (!canRead()) { + spng_ctx *spngctx = spng_ctx_new(static_cast(0)); + if (Q_UNLIKELY(!spngctx)) { + qWarning("QPngHandler::read() Could not create context"); return false; } - png_struct *png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); - if (!png_ptr) { + int spngresult = spng_set_png_stream(spngctx, spng_read_proc, device()); + if (Q_UNLIKELY(spngresult != SPNG_OK)) { + qWarning("QPngHandler::read() Could not set stream: %s", spng_strerror(spngresult)); + spng_ctx_free(spngctx); return false; } - png_set_error_fn(png_ptr, 0, 0, qt_png_warning); - - png_info *info_ptr = png_create_info_struct(png_ptr); - if (!info_ptr) { - png_destroy_read_struct(&png_ptr, 0, 0); + struct spng_ihdr spngihdr; + ::memset(&spngihdr, 0, sizeof(struct spng_ihdr)); + spngresult = spng_get_ihdr(spngctx, &spngihdr); + if (Q_UNLIKELY(spngresult != SPNG_OK)) { + qWarning("QPngHandler::read() Could not get IHDR: %s", spng_strerror(spngresult)); + spng_ctx_free(spngctx); return false; } - png_info *end_info = png_create_info_struct(png_ptr); - if (!end_info) { - png_destroy_read_struct(&png_ptr, &info_ptr, 0); + *image = QImage(spngihdr.width, spngihdr.height, QImage::Format_ARGB32); + if (Q_UNLIKELY(image->isNull())) { + qWarning("QPngHandler::read() Could not create image"); + spng_ctx_free(spngctx); return false; } - if (setjmp(png_jmpbuf(png_ptr))) { - png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); + spngresult = spng_decode_image( + spngctx, + image->bits(), image->byteCount(), + SPNG_FMT_RGBA8, + SPNG_DECODE_TRNS + ); + if (Q_UNLIKELY(spngresult != SPNG_OK)) { + qWarning("QPngHandler::read() Could not decode image: %s", spng_strerror(spngresult)); + *image = QImage(); + spng_ctx_free(spngctx); return false; } - png_set_read_fn(png_ptr, this, qt_png_read); - png_read_info(png_ptr, info_ptr); - - png_uint_32 width = 0; - png_uint_32 height = 0; - int bit_depth; - int color_type; - png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, 0, 0, 0); - const int passes = png_set_interlace_handling(png_ptr); - - if (bit_depth == 16) { - png_set_strip_16(png_ptr); - } - - png_set_expand(png_ptr); - - if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { - png_set_gray_to_rgb(png_ptr); - } else if (color_type == PNG_COLOR_TYPE_PALETTE) { - png_set_palette_to_rgb(png_ptr); - } - - QImage::Format format = QImage::Format_ARGB32; - // Only add filler if no alpha, or we can get 5 channel data. - if (!(color_type & PNG_COLOR_MASK_ALPHA) - && !png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { - png_set_filler(png_ptr, 0xff, QFILLER_ORDER); - // We want 4 bytes, but it isn't an alpha channel - format = QImage::Format_RGB32; - } - *image = QImage(width, height, format); - if (image->isNull()) { - png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); - return false; - } - -#if Q_BYTE_ORDER == Q_BIG_ENDIAN - png_set_swap_alpha(png_ptr); -#endif - - png_read_update_info(png_ptr, info_ptr); - - // Qt==ARGB==Big(ARGB)==Little(BGRA) -#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN - png_set_bgr(png_ptr); -#endif - - uchar *data = image->d->data; - const int bpl = image->bytesPerLine(); - for (int p = 0; p < passes; p++) { - for (uint y = 0; y < height; y++) { - png_read_row(png_ptr, QFAST_SCAN_LINE(data, bpl, y), NULL); - } - } - - png_read_end(png_ptr, end_info); - - png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); - + *image = image->rgbSwapped(); + spng_ctx_free(spngctx); return true; } bool QPngHandler::write(const QImage &image) { - QImage copy = image.convertToFormat(image.hasAlphaChannel() ? QImage::Format_ARGB32 : QImage::Format_RGB32); - const int height = copy.height(); + const QImage copy = image.convertToFormat(QImage::Format_ARGB32).rgbSwapped(); - png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); - if (!png_ptr) { + spng_ctx *spngctx = spng_ctx_new(SPNG_CTX_ENCODER); + if (Q_UNLIKELY(!spngctx)) { + qWarning("QPngHandler::write() Could not create context"); return false; } - png_set_error_fn(png_ptr, 0, 0, qt_png_warning); - - png_infop info_ptr = png_create_info_struct(png_ptr); - if (!info_ptr) { - png_destroy_write_struct(&png_ptr, 0); + int spngresult = spng_set_png_stream(spngctx, spng_write_proc, device()); + if (Q_UNLIKELY(spngresult != SPNG_OK)) { + qWarning("QPngHandler::write() Could not set stream: %s", spng_strerror(spngresult)); + spng_ctx_free(spngctx); return false; } - if (setjmp(png_jmpbuf(png_ptr))) { - png_destroy_write_struct(&png_ptr, &info_ptr); + struct spng_ihdr spngihdr; + ::memset(&spngihdr, 0, sizeof(struct spng_ihdr)); + spngihdr.width = copy.width(); + spngihdr.height = copy.height(); + spngihdr.bit_depth = 8; + spngihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; + spngihdr.compression_method = 0; // no enum for it + spngihdr.filter_method = SPNG_FILTER_NONE; + spngihdr.interlace_method = SPNG_INTERLACE_NONE; + spngresult = spng_set_ihdr(spngctx, &spngihdr); + if (Q_UNLIKELY(spngresult != SPNG_OK)) { + qWarning("QPngHandler::write() Could not set IHDR: %s", spng_strerror(spngresult)); + spng_ctx_free(spngctx); return false; } - png_set_write_fn(png_ptr, (void*)this, qt_png_write, qt_png_flush); - - int color_type = 0; - if (copy.hasAlphaChannel()) { - color_type = PNG_COLOR_TYPE_RGB_ALPHA; - } else { - color_type = PNG_COLOR_TYPE_RGB; + spngresult = spng_set_option(spngctx, SPNG_IMG_COMPRESSION_LEVEL, m_complevel); + if (Q_UNLIKELY(spngresult != SPNG_OK)) { + qWarning("QPngHandler::write() Could not set image compression level: %s", spng_strerror(spngresult)); + spng_ctx_free(spngctx); + return false; } - png_set_IHDR(png_ptr, info_ptr, copy.width(), height, - 8, // per channel - color_type, 0, 0, 0); // sets #channels - - png_color_8 sig_bit; - sig_bit.red = 8; - sig_bit.green = 8; - sig_bit.blue = 8; - sig_bit.alpha = copy.hasAlphaChannel() ? 8 : 0; - png_set_sBIT(png_ptr, info_ptr, &sig_bit); - png_set_compression_level(png_ptr, m_compression); - -#if Q_BYTE_ORDER == Q_BIG_ENDIAN - // Swap ARGB to RGBA (normal PNG format) before saving on - // BigEndian machines - png_set_swap_alpha(png_ptr); -#elif Q_BYTE_ORDER == Q_LITTLE_ENDIAN - // Qt==ARGB==Big(ARGB)==Little(BGRA). But RGB888 is RGB regardless - png_set_bgr(png_ptr); -#endif - - png_write_info(png_ptr, info_ptr); - - png_set_packing(png_ptr); - - if (color_type == PNG_COLOR_TYPE_RGB) { - png_set_filler(png_ptr, 0, QFILLER_ORDER); + spngresult = spng_encode_image( + spngctx, + copy.constBits(), copy.byteCount(), + SPNG_FMT_PNG, + SPNG_ENCODE_FINALIZE + ); + if (Q_UNLIKELY(spngresult != SPNG_OK)) { + qWarning("QPngHandler::write() Could not encode image: %s", spng_strerror(spngresult)); + spng_ctx_free(spngctx); + return false; } - for (int y = 0; y < height; y++) { - png_write_row(png_ptr, (png_const_bytep)copy.constScanLine(y)); - } - png_write_end(png_ptr, info_ptr); - - png_destroy_write_struct(&png_ptr, &info_ptr); - + spng_ctx_free(spngctx); return true; } -bool QPngHandler::supportsOption(QImageIOHandler::ImageOption option) const +QVariant QPngHandler::option(QImageIOHandler::ImageOption option) const { - return (option == QImageIOHandler::CompressionLevel); + if (option == QImageIOHandler::Size) { + const qint64 devicepos = device()->pos(); + + spng_ctx *spngctx = spng_ctx_new(static_cast(0)); + if (Q_UNLIKELY(!spngctx)) { + qWarning("QPngHandler::option() Could not create context"); + return false; + } + + int spngresult = spng_set_png_stream(spngctx, spng_read_proc, device()); + if (Q_UNLIKELY(spngresult != SPNG_OK)) { + qWarning("QPngHandler::option() Could not set stream: %s", spng_strerror(spngresult)); + spng_ctx_free(spngctx); + return false; + } + + struct spng_ihdr spngihdr; + ::memset(&spngihdr, 0, sizeof(struct spng_ihdr)); + spngresult = spng_get_ihdr(spngctx, &spngihdr); + if (Q_UNLIKELY(spngresult != SPNG_OK)) { + qWarning("QPngHandler::option() Could not get IHDR: %s", spng_strerror(spngresult)); + spng_ctx_free(spngctx); + device()->seek(devicepos); + return false; + } + + spng_ctx_free(spngctx); + device()->seek(devicepos); + return QSize(spngihdr.width, spngihdr.height); + } else if (option == QImageIOHandler::CompressionLevel) { + return QVariant(m_complevel); + } + return QVariant(); } void QPngHandler::setOption(QImageIOHandler::ImageOption option, const QVariant &value) { if (option == QImageIOHandler::CompressionLevel) { - const int newlevel = value.toInt(); - if (Q_UNLIKELY(newlevel < 0 || newlevel > 9)) { - qWarning("QPngHandler::setOption() invalid compression level value"); - m_compression = 1; + const int newcomplevel = value.toInt(); + if (Q_UNLIKELY(newcomplevel < 0 || newcomplevel > 9)) { + qWarning("QPngHandler::setOption() Invalid compression level (%d)", newcomplevel); + m_complevel = 1; } else { - m_compression = newlevel; + m_complevel = newcomplevel; } } } +bool QPngHandler::supportsOption(QImageIOHandler::ImageOption option) const +{ + switch (option) { + case QImageIOHandler::Size: + case QImageIOHandler::CompressionLevel: { + return true; + } + default: { + return false; + } + } + Q_UNREACHABLE(); + return false; +} + QByteArray QPngHandler::name() const { return "png"; @@ -287,16 +256,16 @@ QByteArray QPngHandler::name() const bool QPngHandler::canRead(QIODevice *device) { if (Q_UNLIKELY(!device)) { - qWarning("QPngHandler::canRead() called with no device"); + qWarning("QPngHandler::canRead() Called with no device"); return false; } QSTACKARRAY(char, head, 8); - if (device->peek(head, sizeof(head)) != sizeof(head)) + if (device->peek(head, sizeof(head)) != sizeof(head)) { return false; + } - static const uchar pngheader[] - = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a }; + static const uchar pngheader[] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a }; return (::memcmp(head, pngheader, 8) == 0); } diff --git a/src/gui/image/qpnghandler_p.h b/src/gui/image/qpnghandler_p.h index da5654b2e..a93c36fb7 100644 --- a/src/gui/image/qpnghandler_p.h +++ b/src/gui/image/qpnghandler_p.h @@ -1,7 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2015 The Qt Company Ltd. -** Copyright (C) 2016 Ivailo Monev +** Copyright (C) 2023 Ivailo Monev ** ** This file is part of the QtGui module of the Katie Toolkit. ** @@ -47,15 +46,16 @@ public: bool read(QImage *image) final; bool write(const QImage &image) final; - bool supportsOption(QImageIOHandler::ImageOption option) const final; + QVariant option(QImageIOHandler::ImageOption option) const final; void setOption(QImageIOHandler::ImageOption option, const QVariant &value) final; + bool supportsOption(QImageIOHandler::ImageOption option) const final; QByteArray name() const final; static bool canRead(QIODevice *device); private: - int m_compression; + int m_complevel; }; QT_END_NAMESPACE