Skip to content

Commit 4b8bb21

Browse files
committed
Add AVIF and JPEG XL image format support
This commit adds support for AVIF and JPEG XL (JXL) image formats to YACReader: Core Implementation: - Add common/image_decoders.{cpp,h} with isAvif(), isJxl(), decodeAvif(), decodeJxl() - Format detection supports AVIF ftyp headers and JXL codestream/container formats - Integrate libavif (1.0.4) and libjxl (0.7) decoders - Add unified loadImageFromData() helper in cover_utils for all image formats - Update comic.cpp to recognize *.avif and *.jxl file extensions Build System: - Update YACReaderLibrary.pro and YACReaderLibraryServer.pro with decoder linking - Add pkgconfig integration for libavif and libjxl on Unix systems - Docker: Add libavif16 and libjxl0.7 runtime libraries - Docker: Optimize build with sevenzip-builder stage for better caching Testing: - Add comprehensive image_format_test with Qt Test framework - Test format detection, decoding, error handling, and edge cases - Include sample AVIF (470KB) and JXL (180KB) test images with Qt resource file Image Support: - Library scanner now extracts covers from AVIF/JXL comics - Server serves native AVIF/JXL image bytes to clients (no conversion) - Compatible with clients that support these formats natively
1 parent 8685290 commit 4b8bb21

17 files changed

Lines changed: 537 additions & 63 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,9 @@ c_x86_64.pch
7676

7777
compile_commands.json
7878
.ccls-cache
79+
80+
local-build-logs
81+
82+
# Test artifacts (generated during testing, should not be committed)
83+
tests/docker_config_test/
84+
tests/test_library/library/.yacreaderlibrary/

YACReaderLibrary/YACReaderLibrary.pro

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ DEFINES += SERVER_RELEASE YACREADER_LIBRARY
1818
include (../config.pri)
1919
include (../dependencies/pdf_backend.pri)
2020

21+
# AVIF and JXL image format support
22+
unix {
23+
CONFIG += link_pkgconfig
24+
PKGCONFIG += libavif libjxl libjxl_threads
25+
}
26+
win32 {
27+
# For Windows, manually specify paths if needed
28+
LIBS += -lavif -ljxl -ljxl_threads
29+
}
30+
macx {
31+
# For macOS, specify library paths if using Homebrew or MacPorts
32+
LIBS += -lavif -ljxl -ljxl_threads
33+
}
34+
2135
INCLUDEPATH += ../common/gl
2236

2337
# there are two builds for Windows, Desktop OpenGL based and ANGLE OpenGL ES based
@@ -75,6 +89,7 @@ greaterThan(QT_MAJOR_VERSION, 5): QT += openglwidgets core5compat
7589
HEADERS += comic_flow.h \
7690
../common/concurrent_queue.h \
7791
../common/cover_utils.h \
92+
../common/image_decoders.h \
7893
create_library_dialog.h \
7994
db/comic_query_result_processor.h \
8095
db/folder_query_result_processor.h \
@@ -165,6 +180,7 @@ HEADERS += comic_flow.h \
165180
SOURCES += comic_flow.cpp \
166181
../common/concurrent_queue.cpp \
167182
../common/cover_utils.cpp \
183+
../common/image_decoders.cpp \
168184
create_library_dialog.cpp \
169185
db/comic_query_result_processor.cpp \
170186
db/folder_query_result_processor.cpp \

YACReaderLibrary/initial_comic_info_extractor.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,14 @@ void InitialComicInfoExtractor::extract()
130130
int index = order.indexOf(fileNames.at(_coverPage - 1));
131131

132132
if (_target == "") {
133-
if (!_cover.loadFromData(archive.getRawDataAtIndex(index))) {
133+
_cover = loadImageFromData(archive.getRawDataAtIndex(index));
134+
if (_cover.isNull()) {
134135
QLOG_WARN() << "Extracting cover: unable to load image from extracted cover " << _fileSource;
135136
_cover.load(":/images/notCover.png");
136137
}
137138
} else {
138-
QImage p;
139-
if (p.loadFromData(archive.getRawDataAtIndex(index))) {
139+
QImage p = loadImageFromData(archive.getRawDataAtIndex(index));
140+
if (!p.isNull()) {
140141
_coverSize = QPair<int, int>(p.width(), p.height());
141142
saveCover(_target, p);
142143
} else {

YACReaderLibraryServer/YACReaderLibraryServer.pro

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ DEFINES += SERVER_RELEASE YACREADER_LIBRARY
1414
# load default build flags
1515
# do a basic dependency check
1616
include(headless_config.pri)
17+
18+
# AVIF and JXL image format support
19+
unix {
20+
CONFIG += link_pkgconfig
21+
PKGCONFIG += libavif libjxl libjxl_threads
22+
}
23+
win32 {
24+
# For Windows, manually specify paths if needed
25+
LIBS += -lavif -ljxl -ljxl_threads
26+
}
27+
macx {
28+
# For macOS, specify library paths if using Homebrew or MacPorts
29+
LIBS += -lavif -ljxl -ljxl_threads
30+
}
1731
include(../dependencies/pdf_backend.pri)
1832
include(../third_party/QrCode/QrCode.pri)
1933

@@ -58,6 +72,7 @@ HEADERS += ../YACReaderLibrary/library_creator.h \
5872
../common/qnaturalsorting.h \
5973
../common/yacreader_global.h \
6074
../common/cover_utils.h \
75+
../common/image_decoders.h \
6176
../YACReaderLibrary/yacreader_local_server.h \
6277
../YACReaderLibrary/comics_remover.h \
6378
../common/http_worker.h \
@@ -89,6 +104,7 @@ SOURCES += ../YACReaderLibrary/library_creator.cpp \
89104
../common/bookmarks.cpp \
90105
../common/qnaturalsorting.cpp \
91106
../common/cover_utils.cpp \
107+
../common/image_decoders.cpp \
92108
../YACReaderLibrary/yacreader_local_server.cpp \
93109
../YACReaderLibrary/comics_remover.cpp \
94110
../common/http_worker.cpp \

common/comic.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ QStringList Comic::getSupportedImageLiteralFormats()
4040
for (QByteArray &item : supportedImageFormats) {
4141
supportedImageFormatStrings.append(QString::fromLocal8Bit(item));
4242
}
43+
// Add custom supported formats (AVIF and JXL)
44+
supportedImageFormatStrings << "avif" << "jxl";
4345
return supportedImageFormatStrings;
4446
}
4547

@@ -50,15 +52,19 @@ const QStringList Comic::imageExtensions = QStringList() << "*.jpg"
5052
<< "*.tiff"
5153
<< "*.tif"
5254
<< "*.bmp"
53-
<< "*.webp";
55+
<< "*.webp"
56+
<< "*.avif"
57+
<< "*.jxl";
5458
const QStringList Comic::literalImageExtensions = QStringList() << "jpg"
5559
<< "jpeg"
5660
<< "png"
5761
<< "gif"
5862
<< "tiff"
5963
<< "tif"
6064
<< "bmp"
61-
<< "webp";
65+
<< "webp"
66+
<< "avif"
67+
<< "jxl";
6268

6369
#ifndef use_unarr
6470
const QStringList ComicArchiveExtensions = QStringList() << "*.cbr"

common/cover_utils.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
#include "cover_utils.h"
2+
#include "image_decoders.h"
3+
4+
QImage YACReader::loadImageFromData(const QByteArray &data)
5+
{
6+
// Try AVIF first
7+
if (isAvif(data)) {
8+
return decodeAvif(data);
9+
}
10+
// Try JXL
11+
if (isJxl(data)) {
12+
return decodeJxl(data);
13+
}
14+
// Fall back to Qt's built-in loaders (JPEG, PNG, etc.)
15+
QImage image;
16+
image.loadFromData(data);
17+
return image;
18+
}
219

320
bool YACReader::saveCover(const QString &path, const QImage &cover)
421
{

common/cover_utils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
#define COVER_UTILS_H
33

44
#include <QImage>
5+
#include <QByteArray>
56

67
namespace YACReader {
78
bool saveCover(const QString &path, const QImage &image);
9+
QImage loadImageFromData(const QByteArray &data);
810
}
911
#endif // COVER_UTILS_H

common/image_decoders.cpp

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#include "image_decoders.h"
2+
3+
#include <avif/avif.h>
4+
#include <jxl/decode.h>
5+
#include <jxl/decode_cxx.h>
6+
#include <jxl/resizable_parallel_runner.h>
7+
#include <jxl/types.h>
8+
#include <vector>
9+
10+
bool isAvif(const QByteArray &data)
11+
{
12+
if (data.size() < 12)
13+
return false;
14+
return (data.at(4) == 'f' && data.at(5) == 't' && data.at(6) == 'y' && data.at(7) == 'p' &&
15+
data.at(8) == 'a' && data.at(9) == 'v' && data.at(10) == 'i' && data.at(11) == 'f');
16+
}
17+
18+
bool isJxl(const QByteArray &data)
19+
{
20+
if (data.size() < 2)
21+
return false;
22+
23+
// Check for raw JXL codestream (starts with FF0A)
24+
if (static_cast<quint8>(data.at(0)) == 0xFF && static_cast<quint8>(data.at(1)) == 0x0A)
25+
return true;
26+
27+
// Check for JXL container format (JXL signature box at offset 4)
28+
if (data.size() >= 12 && data.at(4) == 'J' && data.at(5) == 'X' && data.at(6) == 'L' && data.at(7) == ' ')
29+
return true;
30+
31+
return false;
32+
}
33+
34+
QImage decodeAvif(const QByteArray &data)
35+
{
36+
avifDecoder *decoder = avifDecoderCreate();
37+
avifResult result = avifDecoderSetIOMemory(decoder, (const uint8_t *)data.constData(), data.size());
38+
if (result != AVIF_RESULT_OK) {
39+
avifDecoderDestroy(decoder);
40+
return QImage();
41+
}
42+
43+
result = avifDecoderParse(decoder);
44+
if (result != AVIF_RESULT_OK) {
45+
avifDecoderDestroy(decoder);
46+
return QImage();
47+
}
48+
49+
QImage image;
50+
if (avifDecoderNextImage(decoder) == AVIF_RESULT_OK) {
51+
avifRGBImage rgb;
52+
avifRGBImageSetDefaults(&rgb, decoder->image);
53+
rgb.format = AVIF_RGB_FORMAT_RGBA;
54+
rgb.depth = 8;
55+
56+
avifRGBImageAllocatePixels(&rgb);
57+
avifImageYUVToRGB(decoder->image, &rgb);
58+
image = QImage(rgb.pixels, decoder->image->width, decoder->image->height, QImage::Format_RGBA8888).copy();
59+
avifRGBImageFreePixels(&rgb);
60+
}
61+
62+
avifDecoderDestroy(decoder);
63+
return image.convertToFormat(QImage::Format_ARGB32);
64+
}
65+
66+
QImage decodeJxl(const QByteArray &data)
67+
{
68+
auto dec = JxlDecoderMake(nullptr);
69+
if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE)) {
70+
return QImage();
71+
}
72+
73+
void* runner = JxlResizableParallelRunnerCreate(nullptr);
74+
if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), JxlResizableParallelRunner, runner)) {
75+
JxlResizableParallelRunnerDestroy(runner);
76+
return QImage();
77+
}
78+
79+
JxlBasicInfo info;
80+
JxlPixelFormat format = {4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0};
81+
std::vector<uint8_t> pixels;
82+
83+
JxlDecoderSetInput(dec.get(), (const uint8_t *)data.constData(), data.size());
84+
JxlDecoderCloseInput(dec.get());
85+
86+
for (;;) {
87+
JxlDecoderStatus status = JxlDecoderProcessInput(dec.get());
88+
if (status == JXL_DEC_ERROR) {
89+
JxlResizableParallelRunnerDestroy(runner);
90+
return QImage();
91+
} else if (status == JXL_DEC_NEED_MORE_INPUT) {
92+
JxlResizableParallelRunnerDestroy(runner);
93+
return QImage();
94+
} else if (status == JXL_DEC_BASIC_INFO) {
95+
if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &info)) {
96+
JxlResizableParallelRunnerDestroy(runner);
97+
return QImage();
98+
}
99+
JxlResizableParallelRunnerSetThreads(runner,
100+
JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize));
101+
} else if (status == JXL_DEC_SUCCESS) {
102+
break;
103+
} else if (status == JXL_DEC_FULL_IMAGE) {
104+
// Nothing to do.
105+
} else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
106+
size_t buffer_size;
107+
if (JXL_DEC_SUCCESS !=
108+
JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)) {
109+
JxlResizableParallelRunnerDestroy(runner);
110+
return QImage();
111+
}
112+
if (buffer_size != info.xsize * info.ysize * 4) {
113+
JxlResizableParallelRunnerDestroy(runner);
114+
return QImage();
115+
}
116+
pixels.resize(buffer_size);
117+
if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec.get(), &format, pixels.data(), pixels.size())) {
118+
JxlResizableParallelRunnerDestroy(runner);
119+
return QImage();
120+
}
121+
} else {
122+
JxlResizableParallelRunnerDestroy(runner);
123+
return QImage();
124+
}
125+
}
126+
127+
JxlResizableParallelRunnerDestroy(runner);
128+
129+
if(pixels.empty())
130+
return QImage();
131+
132+
return QImage(pixels.data(), info.xsize, info.ysize, QImage::Format_RGBA8888).copy().convertToFormat(QImage::Format_ARGB32);
133+
}

common/image_decoders.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#ifndef IMAGE_DECODERS_H
2+
#define IMAGE_DECODERS_H
3+
4+
#include <QImage>
5+
#include <QByteArray>
6+
7+
bool isAvif(const QByteArray &data);
8+
bool isJxl(const QByteArray &data);
9+
QImage decodeAvif(const QByteArray &data);
10+
QImage decodeJxl(const QByteArray &data);
11+
12+
#endif // IMAGE_DECODERS_H

0 commit comments

Comments
 (0)