reimplemnt png handler

based on code I wrote some time ago

Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
This commit is contained in:
Ivailo Monev 2024-03-31 22:38:37 +03:00
parent 7b50a8c73b
commit 4a4cac8308
5 changed files with 191 additions and 191 deletions

View file

@ -188,11 +188,11 @@ set_package_properties(Threads PROPERTIES
TYPE REQUIRED TYPE REQUIRED
) )
find_package(PNG) find_package(SPNG)
set_package_properties(PNG PROPERTIES set_package_properties(SPNG PROPERTIES
PURPOSE "PNG format handler" PURPOSE "PNG format handler"
DESCRIPTION "A collection of routines used to create PNG format graphics files" DESCRIPTION "Simple, modern libpng alternative"
URL "http://www.libpng.org/pub/png/libpng.html" URL "https://libspng.org/"
TYPE REQUIRED TYPE REQUIRED
) )

View file

@ -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 <xakepa10@gmail.com>
#
# 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)

View file

@ -1,9 +1,9 @@
add_definitions(${PNG_DEFINITIONS}) # add_definitions()
set(EXTRA_GUI_LIBS set(EXTRA_GUI_LIBS
KtCore KtCore
${FREETYPE_LIBRARIES} ${FREETYPE_LIBRARIES}
${X11_X11_LIB} ${X11_X11_LIB}
${PNG_LIBRARIES} ${SPNG_LIBRARIES}
${DEFLATE_LIBRARIES} ${DEFLATE_LIBRARIES}
) )
@ -200,7 +200,7 @@ include_directories(
${CMAKE_BINARY_DIR}/include/QtGui ${CMAKE_BINARY_DIR}/include/QtGui
${FREETYPE_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS}
${X11_INCLUDE_DIR} ${X11_INCLUDE_DIR}
${PNG_INCLUDE_DIRS} ${SPNG_INCLUDES}
${DEFLATE_INCLUDES} ${DEFLATE_INCLUDES}
) )

View file

@ -1,7 +1,6 @@
/**************************************************************************** /****************************************************************************
** **
** Copyright (C) 2015 The Qt Company Ltd. ** Copyright (C) 2023 Ivailo Monev
** Copyright (C) 2016 Ivailo Monev
** **
** This file is part of the QtGui module of the Katie Toolkit. ** This file is part of the QtGui module of the Katie Toolkit.
** **
@ -23,64 +22,45 @@
#include "qiodevice.h" #include "qiodevice.h"
#include "qvariant.h" #include "qvariant.h"
#include "qimage.h" #include "qimage.h"
#include "qimage_p.h"
#include "qdrawhelper_p.h"
#include "qcorecommon_p.h" #include "qcorecommon_p.h"
#include "qguicommon_p.h" #include "qdebug.h"
#include <png.h> #include <spng.h>
#include <pngconf.h>
// for reference:
// https://www.w3.org/TR/PNG-Chunks.html
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
#if Q_BYTE_ORDER == Q_BIG_ENDIAN int spng_read_proc(spng_ctx *ctx, void *user, void *dst_src, size_t length)
# 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)
{ {
qWarning("libpng warning: %s", message); // qDebug() << Q_FUNC_INFO << length;
} Q_UNUSED(ctx);
QIODevice* iodevice = static_cast<QIODevice*>(user);
static void qt_png_read(png_structp png_ptr, png_bytep data, png_size_t length) char* dst = static_cast<char*>(dst_src);
{ const qint64 result = iodevice->read(dst, length);
QPngHandler *handler = (QPngHandler *)png_get_io_ptr(png_ptr); if (result <= 0) {
return SPNG_IO_ERROR;
png_size_t nr = handler->device()->read((char*)data, length);
if (nr != length) {
png_error(png_ptr, "Read 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); // qDebug() << Q_FUNC_INFO << length;
Q_UNUSED(ctx);
png_size_t nr = handler->device()->write((char*)data, length); QIODevice* iodevice = static_cast<QIODevice*>(user);
if (nr != length) { const char* src = static_cast<char*>(dst_src);
png_error(png_ptr, "Write error"); 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() QPngHandler::QPngHandler()
: m_compression(1) : m_complevel(1)
{ {
} }
@ -100,185 +80,174 @@ bool QPngHandler::canRead() const
bool QPngHandler::read(QImage *image) bool QPngHandler::read(QImage *image)
{ {
if (!canRead()) { spng_ctx *spngctx = spng_ctx_new(static_cast<spng_ctx_flags>(0));
if (Q_UNLIKELY(!spngctx)) {
qWarning("QPngHandler::read() Could not create context");
return false; return false;
} }
png_struct *png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); int spngresult = spng_set_png_stream(spngctx, spng_read_proc, device());
if (!png_ptr) { if (Q_UNLIKELY(spngresult != SPNG_OK)) {
qWarning("QPngHandler::read() Could not set stream: %s", spng_strerror(spngresult));
spng_ctx_free(spngctx);
return false; return false;
} }
png_set_error_fn(png_ptr, 0, 0, qt_png_warning); struct spng_ihdr spngihdr;
::memset(&spngihdr, 0, sizeof(struct spng_ihdr));
png_info *info_ptr = png_create_info_struct(png_ptr); spngresult = spng_get_ihdr(spngctx, &spngihdr);
if (!info_ptr) { if (Q_UNLIKELY(spngresult != SPNG_OK)) {
png_destroy_read_struct(&png_ptr, 0, 0); qWarning("QPngHandler::read() Could not get IHDR: %s", spng_strerror(spngresult));
spng_ctx_free(spngctx);
return false; return false;
} }
png_info *end_info = png_create_info_struct(png_ptr); *image = QImage(spngihdr.width, spngihdr.height, QImage::Format_ARGB32);
if (!end_info) { if (Q_UNLIKELY(image->isNull())) {
png_destroy_read_struct(&png_ptr, &info_ptr, 0); qWarning("QPngHandler::read() Could not create image");
spng_ctx_free(spngctx);
return false; return false;
} }
if (setjmp(png_jmpbuf(png_ptr))) { spngresult = spng_decode_image(
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); 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; return false;
} }
png_set_read_fn(png_ptr, this, qt_png_read); *image = image->rgbSwapped();
png_read_info(png_ptr, info_ptr); spng_ctx_free(spngctx);
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);
return true; return true;
} }
bool QPngHandler::write(const QImage &image) bool QPngHandler::write(const QImage &image)
{ {
QImage copy = image.convertToFormat(image.hasAlphaChannel() ? QImage::Format_ARGB32 : QImage::Format_RGB32); const QImage copy = image.convertToFormat(QImage::Format_ARGB32).rgbSwapped();
const int height = copy.height();
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); spng_ctx *spngctx = spng_ctx_new(SPNG_CTX_ENCODER);
if (!png_ptr) { if (Q_UNLIKELY(!spngctx)) {
qWarning("QPngHandler::write() Could not create context");
return false; return false;
} }
png_set_error_fn(png_ptr, 0, 0, qt_png_warning); int spngresult = spng_set_png_stream(spngctx, spng_write_proc, device());
if (Q_UNLIKELY(spngresult != SPNG_OK)) {
png_infop info_ptr = png_create_info_struct(png_ptr); qWarning("QPngHandler::write() Could not set stream: %s", spng_strerror(spngresult));
if (!info_ptr) { spng_ctx_free(spngctx);
png_destroy_write_struct(&png_ptr, 0);
return false; return false;
} }
if (setjmp(png_jmpbuf(png_ptr))) { struct spng_ihdr spngihdr;
png_destroy_write_struct(&png_ptr, &info_ptr); ::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; return false;
} }
png_set_write_fn(png_ptr, (void*)this, qt_png_write, qt_png_flush); spngresult = spng_set_option(spngctx, SPNG_IMG_COMPRESSION_LEVEL, m_complevel);
if (Q_UNLIKELY(spngresult != SPNG_OK)) {
int color_type = 0; qWarning("QPngHandler::write() Could not set image compression level: %s", spng_strerror(spngresult));
if (copy.hasAlphaChannel()) { spng_ctx_free(spngctx);
color_type = PNG_COLOR_TYPE_RGB_ALPHA; return false;
} else {
color_type = PNG_COLOR_TYPE_RGB;
} }
png_set_IHDR(png_ptr, info_ptr, copy.width(), height, spngresult = spng_encode_image(
8, // per channel spngctx,
color_type, 0, 0, 0); // sets #channels copy.constBits(), copy.byteCount(),
SPNG_FMT_PNG,
png_color_8 sig_bit; SPNG_ENCODE_FINALIZE
sig_bit.red = 8; );
sig_bit.green = 8; if (Q_UNLIKELY(spngresult != SPNG_OK)) {
sig_bit.blue = 8; qWarning("QPngHandler::write() Could not encode image: %s", spng_strerror(spngresult));
sig_bit.alpha = copy.hasAlphaChannel() ? 8 : 0; spng_ctx_free(spngctx);
png_set_sBIT(png_ptr, info_ptr, &sig_bit); return false;
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);
} }
for (int y = 0; y < height; y++) { spng_ctx_free(spngctx);
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);
return true; 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<spng_ctx_flags>(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) void QPngHandler::setOption(QImageIOHandler::ImageOption option, const QVariant &value)
{ {
if (option == QImageIOHandler::CompressionLevel) { if (option == QImageIOHandler::CompressionLevel) {
const int newlevel = value.toInt(); const int newcomplevel = value.toInt();
if (Q_UNLIKELY(newlevel < 0 || newlevel > 9)) { if (Q_UNLIKELY(newcomplevel < 0 || newcomplevel > 9)) {
qWarning("QPngHandler::setOption() invalid compression level value"); qWarning("QPngHandler::setOption() Invalid compression level (%d)", newcomplevel);
m_compression = 1; m_complevel = 1;
} else { } 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 QByteArray QPngHandler::name() const
{ {
return "png"; return "png";
@ -287,16 +256,16 @@ QByteArray QPngHandler::name() const
bool QPngHandler::canRead(QIODevice *device) bool QPngHandler::canRead(QIODevice *device)
{ {
if (Q_UNLIKELY(!device)) { if (Q_UNLIKELY(!device)) {
qWarning("QPngHandler::canRead() called with no device"); qWarning("QPngHandler::canRead() Called with no device");
return false; return false;
} }
QSTACKARRAY(char, head, 8); QSTACKARRAY(char, head, 8);
if (device->peek(head, sizeof(head)) != sizeof(head)) if (device->peek(head, sizeof(head)) != sizeof(head)) {
return false; return false;
}
static const uchar pngheader[] static const uchar pngheader[] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
= { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
return (::memcmp(head, pngheader, 8) == 0); return (::memcmp(head, pngheader, 8) == 0);
} }

View file

@ -1,7 +1,6 @@
/**************************************************************************** /****************************************************************************
** **
** Copyright (C) 2015 The Qt Company Ltd. ** Copyright (C) 2023 Ivailo Monev
** Copyright (C) 2016 Ivailo Monev
** **
** This file is part of the QtGui module of the Katie Toolkit. ** This file is part of the QtGui module of the Katie Toolkit.
** **
@ -47,15 +46,16 @@ public:
bool read(QImage *image) final; bool read(QImage *image) final;
bool write(const 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; void setOption(QImageIOHandler::ImageOption option, const QVariant &value) final;
bool supportsOption(QImageIOHandler::ImageOption option) const final;
QByteArray name() const final; QByteArray name() const final;
static bool canRead(QIODevice *device); static bool canRead(QIODevice *device);
private: private:
int m_compression; int m_complevel;
}; };
QT_END_NAMESPACE QT_END_NAMESPACE