diff --git a/YACReader/YACReader.pro b/YACReader/YACReader.pro index d50940f8f..743b448cf 100644 --- a/YACReader/YACReader.pro +++ b/YACReader/YACReader.pro @@ -67,6 +67,8 @@ macx { lessThan(QT_MAJOR_VERSION, 6): QT += macextras } +LIBS += -lavif -ljxl -ljxl_threads + QT += network widgets core multimedia svg greaterThan(QT_MAJOR_VERSION, 5): QT += openglwidgets core5compat @@ -109,6 +111,7 @@ HEADERS += ../common/comic.h \ ../common/opengl_checker.h \ ../common/pdf_comic.h \ ../common/global_info_provider.h \ + image_decoders.h \ !CONFIG(no_opengl) { HEADERS += ../common/gl/yacreader_flow_gl.h \ @@ -117,6 +120,7 @@ HEADERS += ../common/comic.h \ SOURCES += ../common/comic.cpp \ configuration.cpp \ + image_decoders.cpp \ goto_dialog.cpp \ magnifying_glass.cpp \ main_window_viewer.cpp \ diff --git a/YACReader/image_decoders.cpp b/YACReader/image_decoders.cpp new file mode 100644 index 000000000..cf68fee6b --- /dev/null +++ b/YACReader/image_decoders.cpp @@ -0,0 +1,124 @@ +#include "image_decoders.h" + +#include +#include +#include +#include +#include +#include + +bool isAvif(const QByteArray &data) +{ + if (data.size() < 12) + return false; + return (data.at(4) == 'f' && data.at(5) == 't' && data.at(6) == 'y' && data.at(7) == 'p' && + data.at(8) == 'a' && data.at(9) == 'v' && data.at(10) == 'i' && data.at(11) == 'f'); +} + +bool isJxl(const QByteArray &data) +{ + if (data.size() < 2) + return false; + return (static_cast(data.at(0)) == 0xFF && static_cast(data.at(1)) == 0x0A); +} + +QImage decodeAvif(const QByteArray &data) +{ + avifDecoder *decoder = avifDecoderCreate(); + avifResult result = avifDecoderSetIOMemory(decoder, (const uint8_t *)data.constData(), data.size()); + if (result != AVIF_RESULT_OK) { + avifDecoderDestroy(decoder); + return QImage(); + } + + result = avifDecoderParse(decoder); + if (result != AVIF_RESULT_OK) { + avifDecoderDestroy(decoder); + return QImage(); + } + + QImage image; + if (avifDecoderNextImage(decoder) == AVIF_RESULT_OK) { + avifRGBImage rgb; + avifRGBImageSetDefaults(&rgb, decoder->image); + rgb.format = AVIF_RGB_FORMAT_RGBA; + rgb.depth = 8; + + avifRGBImageAllocatePixels(&rgb); + avifImageYUVToRGB(decoder->image, &rgb); + image = QImage(rgb.pixels, decoder->image->width, decoder->image->height, QImage::Format_RGBA8888).copy(); + avifRGBImageFreePixels(&rgb); + } + + avifDecoderDestroy(decoder); + return image.convertToFormat(QImage::Format_ARGB32); +} + +QImage decodeJxl(const QByteArray &data) +{ + auto dec = JxlDecoderMake(nullptr); + if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE)) { + return QImage(); + } + + void* runner = JxlResizableParallelRunnerCreate(nullptr); + if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), JxlResizableParallelRunner, runner)) { + JxlResizableParallelRunnerDestroy(runner); + return QImage(); + } + + JxlBasicInfo info; + JxlPixelFormat format = {4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + std::vector pixels; + + JxlDecoderSetInput(dec.get(), (const uint8_t *)data.constData(), data.size()); + JxlDecoderCloseInput(dec.get()); + + for (;;) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); + if (status == JXL_DEC_ERROR) { + JxlResizableParallelRunnerDestroy(runner); + return QImage(); + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + JxlResizableParallelRunnerDestroy(runner); + return QImage(); + } else if (status == JXL_DEC_BASIC_INFO) { + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &info)) { + JxlResizableParallelRunnerDestroy(runner); + return QImage(); + } + JxlResizableParallelRunnerSetThreads(runner, + JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize)); + } else if (status == JXL_DEC_SUCCESS) { + break; + } else if (status == JXL_DEC_FULL_IMAGE) { + // Nothing to do. + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + size_t buffer_size; + if (JXL_DEC_SUCCESS != + JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)) { + JxlResizableParallelRunnerDestroy(runner); + return QImage(); + } + if (buffer_size != info.xsize * info.ysize * 4) { + JxlResizableParallelRunnerDestroy(runner); + return QImage(); + } + pixels.resize(buffer_size); + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec.get(), &format, pixels.data(), pixels.size())) { + JxlResizableParallelRunnerDestroy(runner); + return QImage(); + } + } else { + JxlResizableParallelRunnerDestroy(runner); + return QImage(); + } + } + + JxlResizableParallelRunnerDestroy(runner); + + if(pixels.empty()) + return QImage(); + + return QImage(pixels.data(), info.xsize, info.ysize, QImage::Format_RGBA8888).copy().convertToFormat(QImage::Format_ARGB32); +} diff --git a/YACReader/image_decoders.h b/YACReader/image_decoders.h new file mode 100644 index 000000000..bcf107358 --- /dev/null +++ b/YACReader/image_decoders.h @@ -0,0 +1,12 @@ +#ifndef IMAGE_DECODERS_H +#define IMAGE_DECODERS_H + +#include +#include + +bool isAvif(const QByteArray &data); +bool isJxl(const QByteArray &data); +QImage decodeAvif(const QByteArray &data); +QImage decodeJxl(const QByteArray &data); + +#endif // IMAGE_DECODERS_H diff --git a/YACReader/render.cpp b/YACReader/render.cpp index fee12b11d..9853d1728 100644 --- a/YACReader/render.cpp +++ b/YACReader/render.cpp @@ -12,6 +12,7 @@ #include "comic_db.h" #include "yacreader_global_gui.h" #include "configuration.h" +#include "image_decoders.h" template inline const T &kClamp(const T &x, const T &low, const T &high) @@ -346,7 +347,14 @@ void PageRender::run() QMutexLocker locker(&(render->mutex)); QImage img; - img.loadFromData(data); + if (isAvif(data)) { + img = decodeAvif(data); + } else if (isJxl(data)) { + img = decodeJxl(data); + } else { + img.loadFromData(data); + } + if (degrees > 0) { QTransform m; m.rotate(degrees); diff --git a/docker/Dockerfile b/docker/Dockerfile index 12c5ed4b5..6e49b1502 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -51,7 +51,9 @@ RUN \ 7zip \ 7zip-rar \ libpoppler-qt6-dev \ - zlib1g-dev && \ + zlib1g-dev \ + libavif-dev \ + libjxl-dev && \ ldconfig diff --git a/docker/Dockerfile.test b/docker/Dockerfile.test new file mode 100644 index 000000000..7d21627bf --- /dev/null +++ b/docker/Dockerfile.test @@ -0,0 +1,28 @@ +FROM ghcr.io/linuxserver/baseimage-ubuntu:noble AS builder + +# env variables +ARG DEBIAN_FRONTEND="noninteractive" + +# install build packages +RUN \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + git \ + qt6-tools-dev \ + qt6-base-dev-tools \ + qmake6 \ + libqt6core5compat6-dev \ + libavif-dev \ + libjxl-dev \ + qt6-declarative-dev && \ + ldconfig + +# copy the local repository to the image +COPY . /yacreader + +# build and run the test +RUN cd /yacreader/tests/image_format_test && \ + qmake6 image_format_test.pro && \ + make && \ + ./image_format_test diff --git a/tests/image_format_test/image_format_test.pro b/tests/image_format_test/image_format_test.pro new file mode 100644 index 000000000..450869399 --- /dev/null +++ b/tests/image_format_test/image_format_test.pro @@ -0,0 +1,16 @@ +TEMPLATE = app +CONFIG += console + +SOURCES += \ + main.cpp \ + ../../YACReader/image_decoders.cpp + +HEADERS += \ + ../../YACReader/image_decoders.h + +QT += core + +LIBS += -lavif -ljxl -ljxl_threads + +RESOURCES += \ + test_images.qrc diff --git a/tests/image_format_test/main.cpp b/tests/image_format_test/main.cpp new file mode 100644 index 000000000..15e1f7405 --- /dev/null +++ b/tests/image_format_test/main.cpp @@ -0,0 +1,37 @@ +#include +#include +#include +#include +#include "../../YACReader/image_decoders.h" + +int main(int argc, char *argv[]) +{ + QCoreApplication a(argc, argv); + + QFile avifFile(":/sample.avif"); + if (!avifFile.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open sample.avif"; + return 1; + } + QByteArray avifData = avifFile.readAll(); + QImage avifImage = decodeAvif(avifData); + if (avifImage.isNull()) { + qCritical() << "Failed to decode sample.avif"; + return 1; + } + + QFile jxlFile(":/sample.jxl"); + if (!jxlFile.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open sample.jxl"; + return 1; + } + QByteArray jxlData = jxlFile.readAll(); + QImage jxlImage = decodeJxl(jxlData); + if (jxlImage.isNull()) { + qCritical() << "Failed to decode sample.jxl"; + return 1; + } + + qDebug() << "Successfully decoded sample.avif and sample.jxl"; + return 0; +} diff --git a/tests/image_format_test/original sample.jpg b/tests/image_format_test/original sample.jpg new file mode 100644 index 000000000..bf5f95edc Binary files /dev/null and b/tests/image_format_test/original sample.jpg differ diff --git a/tests/image_format_test/readme.md b/tests/image_format_test/readme.md new file mode 100644 index 000000000..8bc76e333 --- /dev/null +++ b/tests/image_format_test/readme.md @@ -0,0 +1,3 @@ +[Public domain comic image from the Digital Comic Museum](https://digitalcomicmuseum.com/index.php?dlid=2100) + +`original sample.jpg` was converted to several image formats at around 50% quality. These new sample images will be used in tests. \ No newline at end of file diff --git a/tests/image_format_test/sample.avif b/tests/image_format_test/sample.avif new file mode 100644 index 000000000..00e1d8804 Binary files /dev/null and b/tests/image_format_test/sample.avif differ diff --git a/tests/image_format_test/sample.heic b/tests/image_format_test/sample.heic new file mode 100644 index 000000000..cf325b588 Binary files /dev/null and b/tests/image_format_test/sample.heic differ diff --git a/tests/image_format_test/sample.jxl b/tests/image_format_test/sample.jxl new file mode 100644 index 000000000..ada096b01 Binary files /dev/null and b/tests/image_format_test/sample.jxl differ diff --git a/tests/image_format_test/sample.webp b/tests/image_format_test/sample.webp new file mode 100644 index 000000000..f4a8bbf3c Binary files /dev/null and b/tests/image_format_test/sample.webp differ diff --git a/tests/image_format_test/test_images.qrc b/tests/image_format_test/test_images.qrc new file mode 100644 index 000000000..7d667cb73 --- /dev/null +++ b/tests/image_format_test/test_images.qrc @@ -0,0 +1,6 @@ + + + sample.avif + sample.jxl + +