From 819f456d6e1da74659a9d7c360f72addfe662b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20San=20Mart=C3=ADn?= Date: Sat, 31 Aug 2024 13:04:39 +0200 Subject: [PATCH 01/16] Implement lanczos for upscaling to get crispier images --- YACReader/YACReader.pro | 67 ++++-- YACReader/viewer.cpp | 16 +- image_processing/image_processing.pri | 8 + image_processing/resize_image.cpp | 317 ++++++++++++++++++++++++++ image_processing/resize_image.h | 20 ++ 5 files changed, 401 insertions(+), 27 deletions(-) create mode 100644 image_processing/image_processing.pri create mode 100644 image_processing/resize_image.cpp create mode 100644 image_processing/resize_image.h diff --git a/YACReader/YACReader.pro b/YACReader/YACReader.pro index 08502677b..08ebf1506 100644 --- a/YACReader/YACReader.pro +++ b/YACReader/YACReader.pro @@ -10,7 +10,23 @@ DEFINES += YACREADER #load default build flags include (../config.pri) + +CONFIG(7zip) { +include(../compressed_archive/wrapper.pri) +} else:CONFIG(unarr) { +include(../compressed_archive/unarr/unarr-wrapper.pri) +} else:CONFIG(libarchive) { +include(../compressed_archive/libarchive/libarchive-wrapper.pri) +} else { + error(No compression backend specified. Did you mess with the build system?) +} + +include(../custom_widgets/custom_widgets_yacreader.pri) +include(../image_processing/image_processing.pri) include (../dependencies/pdf_backend.pri) +include(../shortcuts_management/shortcuts_management.pri) + +include(../third_party/QsLog/QsLog.pri) CONFIG(force_angle) { contains(QMAKE_TARGET.arch, x86_64) { @@ -30,7 +46,7 @@ CONFIG(force_angle) { } } -SOURCES += main.cpp +SOURCES += main.cpp \ INCLUDEPATH += ../common \ ../custom_widgets @@ -55,6 +71,14 @@ win32 { msvc { QMAKE_CXXFLAGS_RELEASE += /MP /Ob2 /Oi /Ot /GT /GL QMAKE_LFLAGS_RELEASE += /LTCG + + # Enable AVX and AVX2 support + QMAKE_CXXFLAGS += /arch:AVX + DEFINES += __AVX__ + + # Enable AVX2 if supported + win32:QMAKE_CXXFLAGS += /arch:AVX2 + DEFINES += __AVX2__ } CONFIG -= embed_manifest_exe } @@ -67,7 +91,27 @@ macx { lessThan(QT_MAJOR_VERSION, 6): QT += macextras } -QT += network widgets core multimedia svg +unix|mingw { + # Enable general SIMD optimizations + QMAKE_CXXFLAGS += -msse2 # Baseline for x86 + + # Architecture-specific optimizations (adjust as needed) + contains(QMAKE_TARGET.arch, x86_64) { + QMAKE_CXXFLAGS += -mavx2 -mfma + DEFINES += __AVX__ __AVX2__ + } else { # Assuming x86 (32-bit) + QMAKE_CXXFLAGS += -msse4.2 + DEFINES += __SSE4_2__ + } + + # ARM + contains(QMAKE_HOST.arch, arm) { + QMAKE_CXXFLAGS += -mfpu=neon -mfloat-abi=hard + DEFINES += __ARM_NEON__ + } +} + +QT += network widgets core multimedia svg concurrent greaterThan(QT_MAJOR_VERSION, 5): QT += openglwidgets core5compat @@ -106,7 +150,7 @@ HEADERS += ../common/comic.h \ ../common/exit_check.h \ ../common/scroll_management.h \ ../common/opengl_checker.h \ - ../common/pdf_comic.h + ../common/pdf_comic.h \ !CONFIG(no_opengl) { HEADERS += ../common/gl/yacreader_flow_gl.h \ @@ -143,31 +187,16 @@ SOURCES += ../common/comic.cpp \ ../common/yacreader_global_gui.cpp \ ../common/exit_check.cpp \ ../common/scroll_management.cpp \ - ../common/opengl_checker.cpp + ../common/opengl_checker.cpp \ !CONFIG(no_opengl) { SOURCES += ../common/gl/yacreader_flow_gl.cpp \ goto_flow_gl.cpp } -include(../custom_widgets/custom_widgets_yacreader.pri) - -CONFIG(7zip) { -include(../compressed_archive/wrapper.pri) -} else:CONFIG(unarr) { -include(../compressed_archive/unarr/unarr-wrapper.pri) -} else:CONFIG(libarchive) { -include(../compressed_archive/libarchive/libarchive-wrapper.pri) -} else { - error(No compression backend specified. Did you mess with the build system?) -} -include(../shortcuts_management/shortcuts_management.pri) - RESOURCES += yacreader_images.qrc \ yacreader_files.qrc -include(../third_party/QsLog/QsLog.pri) - RC_FILE = icon.rc macx { diff --git a/YACReader/viewer.cpp b/YACReader/viewer.cpp index 7d6fe242f..82f857a38 100644 --- a/YACReader/viewer.cpp +++ b/YACReader/viewer.cpp @@ -16,6 +16,7 @@ #include "notifications_label_widget.h" #include "comic_db.h" #include "shortcuts_manager.h" +#include "resize_image.h" #include "opengl_checker.h" @@ -387,16 +388,15 @@ void Viewer::updateContentSize() if (zoom != 100) { pagefit.scale(floor(pagefit.width() * zoom / 100.0f), 0, Qt::KeepAspectRatioByExpanding); } - // apply scaling + // apply size to the container content->resize(pagefit); - // TODO: updtateContentSize should only scale the pixmap once - if (devicePixelRatioF() > 1) // only in HDPI displays - { - QPixmap page = currentPage->scaled(content->width() * devicePixelRatioF(), content->height() * devicePixelRatioF(), Qt::KeepAspectRatio, Qt::SmoothTransformation); - page.setDevicePixelRatio(devicePixelRatioF()); - content->setPixmap(page); - } + // scale the image to fit the container + auto devicePixelRatioF = content->devicePixelRatioF(); + QLOG_ERROR() << "src size: " << currentPage->size() << " content size: " << content->size() << " target size " << QSize(content->width() * devicePixelRatioF, content->height() * devicePixelRatioF); + QPixmap page = smartScalePixmap(*currentPage, content->width() * devicePixelRatioF, content->height() * devicePixelRatioF); // currentPage->scaled(content->width() * devicePixelRatioF(), content->height() * devicePixelRatioF(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + page.setDevicePixelRatio(devicePixelRatioF); + content->setPixmap(page); emit backgroundChanges(); } diff --git a/image_processing/image_processing.pri b/image_processing/image_processing.pri new file mode 100644 index 000000000..d2248f4de --- /dev/null +++ b/image_processing/image_processing.pri @@ -0,0 +1,8 @@ +INCLUDEPATH += $$PWD +DEPENDPATH += $$PWD + +SOURCES += \ + $$PWD/resize_image.cpp + +HEADERS += \ + $$PWD/resize_image.h diff --git a/image_processing/resize_image.cpp b/image_processing/resize_image.cpp new file mode 100644 index 000000000..a7ca7b612 --- /dev/null +++ b/image_processing/resize_image.cpp @@ -0,0 +1,317 @@ +#include "resize_image.h" + +#include +#include +#include + +QPixmap scalePixmapBicubic(const QPixmap &pixmap, int width, int height); +QPixmap scalePixmapLanczos(const QPixmap &pixmap, int width, int height); +QPixmap scalePixmapArea(const QPixmap &pixmap, int width, int height); +QPixmap scalePixmapLanczosQt(const QPixmap &pixmap, int targetWidth, int targetHeight, int a = 3); + +QPixmap smartScalePixmap(const QPixmap &pixmap, int width, int height) +{ + const int w = pixmap.width(); + const int h = pixmap.height(); + if ((w == width && h == height) || pixmap.isNull()) { + return pixmap; + } + + if (w <= width && h <= height) { // upscaling + return scalePixmapLanczos(pixmap, width, height); + } + + return pixmap; +} + +QPixmap scalePixmap(const QPixmap &pixmap, int width, int height, ScaleMethod method) +{ + const int w = pixmap.width(); + const int h = pixmap.height(); + if (w == width && h == height) { + return pixmap; + } + + switch (method) { + case ScaleMethod::QtFast: + return pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::FastTransformation); + case ScaleMethod::QtSmooth: + return pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation); + case ScaleMethod::Bicubic: + return scalePixmapBicubic(pixmap, width, height); + case ScaleMethod::Lanczos: + return scalePixmapLanczos(pixmap, width, height); + case ScaleMethod::Area: + return scalePixmapArea(pixmap, width, height); + } +} + +QPixmap scalePixmapBicubic(const QPixmap &pixmap, int width, int height) +{ + // TODO: implement + return pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation); +} + +QPixmap scalePixmapLanczos(const QPixmap &pixmap, int width, int height) +{ + return scalePixmapLanczosQt(pixmap, width, height); +} + +QPixmap scalePixmapArea(const QPixmap &pixmap, int width, int height) +{ + // TODO: implement + return pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation); +} + +// Platform-specific SIMD includes and checks +#if defined(__AVX__) || defined(__AVX2__) +#include // For x86 SSE/AVX +#elif defined(__ARM_NEON) || defined(__ARM_NEON__) +#include // For ARM NEON +#else +#warning "No SIMD instructions detected, falling back to scalar implementation." +#endif + +// Define SIMD intrinsics for different platforms +#if defined(__AVX__) || defined(__AVX2__) + +inline __m256d lanczosKernelAVX(const __m256d &x, const __m256d &a_val) +{ + __m256d zero = _mm256_setzero_pd(); + __m256d one = _mm256_set1_pd(1.0); + __m256d pix = _mm256_mul_pd(_mm256_set1_pd(M_PI), x); + __m256d sin_pix = _mm256_sin_pd(pix); + __m256d sin_pix_a = _mm256_sin_pd(_mm256_div_pd(pix, a_val)); + __m256d numerator = _mm256_mul_pd(_mm256_mul_pd(a_val, sin_pix), sin_pix_a); + __m256d denominator = _mm256_mul_pd(pix, pix); + __m256d result = _mm256_div_pd(numerator, denominator); + result = _mm256_blendv_pd(result, one, _mm256_cmp_pd(x, zero, _CMP_EQ_OQ)); + return result; +} + +QVector processRow(int y, int targetWidth, int targetHeight, const QImage &sourceImage, int a) +{ + QVector resultRow(targetWidth); + int sourceWidth = sourceImage.width(); + int sourceHeight = sourceImage.height(); + __m256d a_vec = _mm256_set1_pd(a); + + for (int x = 0; x < targetWidth; ++x) { + double gx = ((double)x / targetWidth) * (sourceWidth - 1); + double gy = ((double)y / targetHeight) * (sourceHeight - 1); + + __m256d red_vec = _mm256_setzero_pd(); + __m256d green_vec = _mm256_setzero_pd(); + __m256d blue_vec = _mm256_setzero_pd(); + __m256d alpha_vec = _mm256_setzero_pd(); + __m256d weight_vec = _mm256_setzero_pd(); + + for (int ix = (int)gx - a + 1; ix <= (int)gx + a; ++ix) { + for (int iy = (int)gy - a + 1; iy <= (int)gy + a; ++iy) { + if (ix >= 0 && ix < sourceWidth && iy >= 0 && iy < sourceHeight) { + __m256d gx_vec = _mm256_set1_pd(gx - ix); + __m256d gy_vec = _mm256_set1_pd(gy - iy); + __m256d weight_x = lanczosKernelAVX(gx_vec, a_vec); + __m256d weight_y = lanczosKernelAVX(gy_vec, a_vec); + __m256d weight = _mm256_mul_pd(weight_x, weight_y); + + QColor color(sourceImage.pixel(ix, iy)); + __m256d color_red = _mm256_set1_pd(color.red()); + __m256d color_green = _mm256_set1_pd(color.green()); + __m256d color_blue = _mm256_set1_pd(color.blue()); + __m256d color_alpha = _mm256_set1_pd(color.alpha()); + + red_vec = _mm256_add_pd(red_vec, _mm256_mul_pd(weight, color_red)); + green_vec = _mm256_add_pd(green_vec, _mm256_mul_pd(weight, color_green)); + blue_vec = _mm256_add_pd(blue_vec, _mm256_mul_pd(weight, color_blue)); + alpha_vec = _mm256_add_pd(alpha_vec, _mm256_mul_pd(weight, color_alpha)); + + weight_vec = _mm256_add_pd(weight_vec, weight); + } + } + } + + double red = _mm256_cvtsd_f64(_mm256_hadd_pd(red_vec, red_vec)); + double green = _mm256_cvtsd_f64(_mm256_hadd_pd(green_vec, green_vec)); + double blue = _mm256_cvtsd_f64(_mm256_hadd_pd(blue_vec, blue_vec)); + double alpha = _mm256_cvtsd_f64(_mm256_hadd_pd(alpha_vec, alpha_vec)); + double sumWeights = _mm256_cvtsd_f64(_mm256_hadd_pd(weight_vec, weight_vec)); + + if (sumWeights > 0.0) { + red = std::clamp(red / sumWeights, 0.0, 255.0); + green = std::clamp(green / sumWeights, 0.0, 255.0); + blue = std::clamp(blue / sumWeights, 0.0, 255.0); + alpha = std::clamp(alpha / sumWeights, 0.0, 255.0); + } + + resultRow[x] = qRgba(static_cast(red), static_cast(green), static_cast(blue), static_cast(alpha)); + } + + return resultRow; +} + +#elif defined(__ARM_NEON) || defined(__ARM_NEON__) + +inline float64x2_t lanczosKernelNEON(const float64x2_t &x, int a) +{ + float64x2_t zero = vdupq_n_f64(0.0); + float64x2_t one = vdupq_n_f64(1.0); + float64x2_t a_val = vdupq_n_f64(a); + float64x2_t pix = vmulq_f64(vdupq_n_f64(M_PI), x); + float64x2_t sin_pix = vsin_f64(pix); + float64x2_t sin_pix_a = vsin_f64(vdivq_f64(pix, a_val)); + float64x2_t numerator = vmulq_f64(vmulq_f64(a_val, sin_pix), sin_pix_a); + float64x2_t denominator = vmulq_f64(pix, pix); + float64x2_t result = vdivq_f64(numerator, denominator); + uint64x2_t mask = vceqq_f64(x, zero); + result = vbslq_f64(mask, one, result); + return result; +} + +QVector processRow(int y, int targetWidth, int targetHeight, const QImage &sourceImage, int a) +{ + QVector resultRow(targetWidth); + int sourceWidth = sourceImage.width(); + int sourceHeight = sourceImage.height(); + + for (int x = 0; x < targetWidth; ++x) { + double gx = ((double)x / targetWidth) * (sourceWidth - 1); + double gy = ((double)y / targetHeight) * (sourceHeight - 1); + + float64x2_t red_vec = vdupq_n_f64(0.0); + float64x2_t green_vec = vdupq_n_f64(0.0); + float64x2_t blue_vec = vdupq_n_f64(0.0); + float64x2_t alpha_vec = vdupq_n_f64(0.0); + float64x2_t weight_vec = vdupq_n_f64(0.0); + + for (int ix = (int)gx - a + 1; ix <= (int)gx + a; ++ix) { + for (int iy = (int)gy - a + 1; iy <= (int)gy + a; ++iy) { + if (ix >= 0 && ix < sourceWidth && iy >= 0 && iy < sourceHeight) { + float64x2_t gx_vec = vdupq_n_f64(gx - ix); + float64x2_t gy_vec = vdupq_n_f64(gy - iy); + float64x2_t weight_x = lanczosKernelNEON(gx_vec, a); + float64x2_t weight_y = lanczosKernelNEON(gy_vec, a); + float64x2_t weight = vmulq_f64(weight_x, weight_y); + + QColor color(sourceImage.pixel(ix, iy)); + float64x2_t color_red = vdupq_n_f64(color.red()); + float64x2_t color_green = vdupq_n_f64(color.green()); + float64x2_t color_blue = vdupq_n_f64(color.blue()); + float64x2_t color_alpha = vdupq_n_f64(color.alpha()); + + red_vec = vmlaq_f64(red_vec, weight, color_red); + green_vec = vmlaq_f64(green_vec, weight, color_green); + blue_vec = vmlaq_f64(blue_vec, weight, color_blue); + alpha_vec = vmlaq_f64(alpha_vec, weight, color_alpha); + weight_vec = vaddq_f64(weight_vec, weight); + } + } + } + + double red = vgetq_lane_f64(red_vec, 0) + vgetq_lane_f64(red_vec, 1); + double green = vgetq_lane_f64(green_vec, 0) + vgetq_lane_f64(green_vec, 1); + double blue = vgetq_lane_f64(blue_vec, 0) + vgetq_lane_f64(blue_vec, 1); + double alpha = vgetq_lane_f64(alpha_vec, 0) + vgetq_lane_f64(alpha_vec, 1); + double sumWeights = vgetq_lane_f64(weight_vec, 0) + vgetq_lane_f64(weight_vec, 1); + + if (sumWeights > 0.0) { + red = std::clamp(red / sumWeights, 0.0, 255.0); + green = std::clamp(green / sumWeights, 0.0, 255.0); + blue = std::clamp(blue / sumWeights, 0.0, 255.0); + alpha = std::clamp(alpha / sumWeights, 0.0, 255.0); + } + + resultRow[x] = qRgba(static_cast(red), static_cast(green), static_cast(blue), static_cast(alpha)); + } + + return resultRow; +} + +#else + +// Scalar fallback for unsupported platforms +double lanczosKernel(double x, int a) +{ + if (x == 0.0) + return 1.0; + if (x < -a || x > a) + return 0.0; + double pix = M_PI * x; + return a * std::sin(pix) * std::sin(pix / a) / (pix * pix); +} + +QVector processRow(int y, int targetWidth, int targetHeight, const QImage &sourceImage, int a) +{ + QVector resultRow(targetWidth); + int sourceWidth = sourceImage.width(); + int sourceHeight = sourceImage.height(); + + for (int x = 0; x < targetWidth; ++x) { + double gx = ((double)x / targetWidth) * (sourceWidth - 1); + double gy = ((double)y / targetHeight) * (sourceHeight - 1); + + double red = 0.0, green = 0.0, blue = 0.0, alpha = 0.0, sumWeights = 0.0; + + for (int ix = (int)gx - a + 1; ix <= (int)gx + a; ++ix) { + for (int iy = (int)gy - a + 1; iy <= (int)gy + a; ++iy) { + if (ix >= 0 && ix < sourceWidth && iy >= 0 && iy < sourceHeight) { + double weight = lanczosKernel(gx - ix, a) * lanczosKernel(gy - iy, a); + + QColor color(sourceImage.pixel(ix, iy)); + + red += weight * color.red(); + green += weight * color.green(); + blue += weight * color.blue(); + alpha += weight * color.alpha(); + sumWeights += weight; + } + } + } + + if (sumWeights > 0.0) { + red = std::clamp(red / sumWeights, 0.0, 255.0); + green = std::clamp(green / sumWeights, 0.0, 255.0); + blue = std::clamp(blue / sumWeights, 0.0, 255.0); + alpha = std::clamp(alpha / sumWeights, 0.0, 255.0); + } + + resultRow[x] = qRgba(static_cast(red), static_cast(green), static_cast(blue), static_cast(alpha)); + } + + return resultRow; +} + +#endif + +// Main function to scale the image +QImage scaleImageLanczos(const QImage &sourceImage, int targetWidth, int targetHeight, int a = 3) +{ + QImage targetImage(targetWidth, targetHeight, QImage::Format_ARGB32); + + QVector rows(targetHeight); + for (int i = 0; i < targetHeight; ++i) { + rows[i] = i; + } + + QFuture> future = QtConcurrent::mapped(rows, [targetWidth, targetHeight, &sourceImage, a](int y) { + return processRow(y, targetWidth, targetHeight, sourceImage, a); + }); + + future.waitForFinished(); + + for (int y = 0; y < targetHeight; ++y) { + QVector row = future.resultAt(y); + for (int x = 0; x < targetWidth; ++x) { + targetImage.setPixel(x, y, row[x]); + } + } + + return targetImage; +} + +QPixmap scalePixmapLanczosQt(const QPixmap &pixmap, int targetWidth, int targetHeight, int a) +{ + QImage sourceImage = pixmap.toImage(); + QImage scaledImage = scaleImageLanczos(sourceImage, targetWidth, targetHeight, a); + return QPixmap::fromImage(scaledImage); +} diff --git a/image_processing/resize_image.h b/image_processing/resize_image.h new file mode 100644 index 000000000..17ea04a36 --- /dev/null +++ b/image_processing/resize_image.h @@ -0,0 +1,20 @@ +#ifndef RESIZE_IMAGE_H +#define RESIZE_IMAGE_H + +#include +#include +#include + +enum class ScaleMethod { + QtFast, + QtSmooth, // Bilinear + Bicubic, + Lanczos, + Area + +}; + +QPixmap smartScalePixmap(const QPixmap &pixmap, int width, int height); +QPixmap scalePixmap(const QPixmap &pixmap, int width, int height, ScaleMethod method = ScaleMethod::QtSmooth); + +#endif // RESIZE_IMAGE_H From 8f42aae53aa456361b3573a670cc309b724fa7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20San=20Mart=C3=ADn?= Date: Sat, 31 Aug 2024 14:02:17 +0200 Subject: [PATCH 02/16] Fix Qt5 compatibility --- image_processing/resize_image.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/image_processing/resize_image.cpp b/image_processing/resize_image.cpp index a7ca7b612..167117e2b 100644 --- a/image_processing/resize_image.cpp +++ b/image_processing/resize_image.cpp @@ -1,5 +1,6 @@ #include "resize_image.h" +#include "qmath.h" #include #include #include @@ -293,14 +294,17 @@ QImage scaleImageLanczos(const QImage &sourceImage, int targetWidth, int targetH rows[i] = i; } - QFuture> future = QtConcurrent::mapped(rows, [targetWidth, targetHeight, &sourceImage, a](int y) { - return processRow(y, targetWidth, targetHeight, sourceImage, a); - }); + // Use a QVector to store the results of the processed rows + QVector> results(targetHeight); - future.waitForFinished(); + // Launch concurrent tasks using QtConcurrent::map, which modifies the container in place + QtConcurrent::blockingMap(rows, [&results, targetWidth, targetHeight, &sourceImage, a](int y) { + results[y] = processRow(y, targetWidth, targetHeight, sourceImage, a); + }); + // Set the pixels in the target image for (int y = 0; y < targetHeight; ++y) { - QVector row = future.resultAt(y); + const QVector &row = results[y]; for (int x = 0; x < targetWidth; ++x) { targetImage.setPixel(x, y, row[x]); } @@ -308,7 +312,6 @@ QImage scaleImageLanczos(const QImage &sourceImage, int targetWidth, int targetH return targetImage; } - QPixmap scalePixmapLanczosQt(const QPixmap &pixmap, int targetWidth, int targetHeight, int a) { QImage sourceImage = pixmap.toImage(); From 35c88edf2d010c8ea4b9846a12c7e6e56486f4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20San=20Mart=C3=ADn?= Date: Sat, 31 Aug 2024 14:27:27 +0200 Subject: [PATCH 03/16] More portable code --- image_processing/resize_image.cpp | 44 +++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/image_processing/resize_image.cpp b/image_processing/resize_image.cpp index 167117e2b..a121cca30 100644 --- a/image_processing/resize_image.cpp +++ b/image_processing/resize_image.cpp @@ -76,13 +76,53 @@ QPixmap scalePixmapArea(const QPixmap &pixmap, int width, int height) // Define SIMD intrinsics for different platforms #if defined(__AVX__) || defined(__AVX2__) +// Function to normalize angles in radians to the range [-PI, PI] +__m256d normalize_angle(__m256d x) +{ + const __m256d pi = _mm256_set1_pd(M_PI); + const __m256d two_pi = _mm256_set1_pd(2 * M_PI); + // Calculate the quotient of x / (2*PI) + __m256d quotient = _mm256_div_pd(x, two_pi); + // Use floor to get the nearest lower integer + quotient = _mm256_floor_pd(quotient); + // Calculate the remainder + __m256d remainder = _mm256_sub_pd(x, _mm256_mul_pd(quotient, two_pi)); + // Adjust the range to [-PI, PI] + __m256d adjust = _mm256_cmp_pd(remainder, pi, _CMP_GT_OS); + remainder = _mm256_sub_pd(remainder, _mm256_and_pd(adjust, two_pi)); + return remainder; +} + +// Improved sine approximation function for __m256d using the normalized angle +__m256d sin_pd_approx(__m256d x) +{ + x = normalize_angle(x); // Normalize x to the range [-PI, PI] + + // Sine approximation coefficients + const __m256d a0 = _mm256_set1_pd(-0.16666666666666666); + const __m256d a1 = _mm256_set1_pd(0.008333333333333333); + const __m256d a2 = _mm256_set1_pd(-0.0001984126984126984); + + __m256d x2 = _mm256_mul_pd(x, x); + __m256d x3 = _mm256_mul_pd(x2, x); + __m256d x5 = _mm256_mul_pd(x3, x2); + __m256d x7 = _mm256_mul_pd(x5, x2); + + // Compute the polynomial approximation + __m256d result = _mm256_add_pd(x, _mm256_mul_pd(a0, x3)); + result = _mm256_add_pd(result, _mm256_mul_pd(a1, x5)); + result = _mm256_add_pd(result, _mm256_mul_pd(a2, x7)); + + return result; +} + inline __m256d lanczosKernelAVX(const __m256d &x, const __m256d &a_val) { __m256d zero = _mm256_setzero_pd(); __m256d one = _mm256_set1_pd(1.0); __m256d pix = _mm256_mul_pd(_mm256_set1_pd(M_PI), x); - __m256d sin_pix = _mm256_sin_pd(pix); - __m256d sin_pix_a = _mm256_sin_pd(_mm256_div_pd(pix, a_val)); + __m256d sin_pix = sin_pd_approx(pix); + __m256d sin_pix_a = sin_pd_approx(_mm256_div_pd(pix, a_val)); __m256d numerator = _mm256_mul_pd(_mm256_mul_pd(a_val, sin_pix), sin_pix_a); __m256d denominator = _mm256_mul_pd(pix, pix); __m256d result = _mm256_div_pd(numerator, denominator); From eacbd5f49922892e5c8043ac63de4904ce9a8f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20San=20Mart=C3=ADn?= Date: Sat, 31 Aug 2024 14:41:30 +0200 Subject: [PATCH 04/16] Don't enable x86 specific feature in arm builds --- YACReader/YACReader.pro | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/YACReader/YACReader.pro b/YACReader/YACReader.pro index 08ebf1506..cd9352422 100644 --- a/YACReader/YACReader.pro +++ b/YACReader/YACReader.pro @@ -92,22 +92,21 @@ macx { } unix|mingw { - # Enable general SIMD optimizations - QMAKE_CXXFLAGS += -msse2 # Baseline for x86 - - # Architecture-specific optimizations (adjust as needed) - contains(QMAKE_TARGET.arch, x86_64) { - QMAKE_CXXFLAGS += -mavx2 -mfma - DEFINES += __AVX__ __AVX2__ - } else { # Assuming x86 (32-bit) - QMAKE_CXXFLAGS += -msse4.2 - DEFINES += __SSE4_2__ - } - - # ARM contains(QMAKE_HOST.arch, arm) { QMAKE_CXXFLAGS += -mfpu=neon -mfloat-abi=hard DEFINES += __ARM_NEON__ + } else { + # Enable general SIMD optimizations + QMAKE_CXXFLAGS += -msse2 # Baseline for x86 + + # Architecture-specific optimizations (adjust as needed) + contains(QMAKE_TARGET.arch, x86_64) { + QMAKE_CXXFLAGS += -mavx2 -mfma + DEFINES += __AVX__ __AVX2__ + } else { # Assuming x86 (32-bit) + QMAKE_CXXFLAGS += -msse4.2 + DEFINES += __SSE4_2__ + } } } From 9fc3ab49814ab0e668fcb5a508f47919624956a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20A=CC=81ngel=20San=20Marti=CC=81n=20Rodri=CC=81guez?= Date: Sat, 31 Aug 2024 15:20:37 +0200 Subject: [PATCH 05/16] More portable code --- image_processing/resize_image.cpp | 32 +++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/image_processing/resize_image.cpp b/image_processing/resize_image.cpp index a121cca30..3898cff56 100644 --- a/image_processing/resize_image.cpp +++ b/image_processing/resize_image.cpp @@ -195,17 +195,45 @@ QVector processRow(int y, int targetWidth, int targetHeight, const QImage inline float64x2_t lanczosKernelNEON(const float64x2_t &x, int a) { + // Load constants float64x2_t zero = vdupq_n_f64(0.0); float64x2_t one = vdupq_n_f64(1.0); float64x2_t a_val = vdupq_n_f64(a); + + // Convert to scalar arrays + double x_array[2]; + vst1q_f64(x_array, x); + double a_val_array[2] = { static_cast(a), static_cast(a) }; + + // Compute sin(x * pi) float64x2_t pix = vmulq_f64(vdupq_n_f64(M_PI), x); - float64x2_t sin_pix = vsin_f64(pix); - float64x2_t sin_pix_a = vsin_f64(vdivq_f64(pix, a_val)); + double pix_array[2]; + vst1q_f64(pix_array, pix); + + double sin_pix_array[2]; + sin_pix_array[0] = std::sin(pix_array[0]); + sin_pix_array[1] = std::sin(pix_array[1]); + float64x2_t sin_pix = vld1q_f64(sin_pix_array); + + // Compute sin(x * pi / a) + float64x2_t pix_div_a = vdivq_f64(pix, a_val); + double pix_div_a_array[2]; + vst1q_f64(pix_div_a_array, pix_div_a); + + double sin_pix_div_a_array[2]; + sin_pix_div_a_array[0] = std::sin(pix_div_a_array[0]); + sin_pix_div_a_array[1] = std::sin(pix_div_a_array[1]); + float64x2_t sin_pix_a = vld1q_f64(sin_pix_div_a_array); + + // Compute Lanczos kernel float64x2_t numerator = vmulq_f64(vmulq_f64(a_val, sin_pix), sin_pix_a); float64x2_t denominator = vmulq_f64(pix, pix); float64x2_t result = vdivq_f64(numerator, denominator); + + // Handle the case where x is zero uint64x2_t mask = vceqq_f64(x, zero); result = vbslq_f64(mask, one, result); + return result; } From 7a430cc136bb3c90daf105edd16fc24be961b8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20A=CC=81ngel=20San=20Marti=CC=81n=20Rodri=CC=81guez?= Date: Sat, 31 Aug 2024 15:20:46 +0200 Subject: [PATCH 06/16] Remove log output --- YACReader/viewer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/YACReader/viewer.cpp b/YACReader/viewer.cpp index 82f857a38..69221b9e9 100644 --- a/YACReader/viewer.cpp +++ b/YACReader/viewer.cpp @@ -393,7 +393,6 @@ void Viewer::updateContentSize() // scale the image to fit the container auto devicePixelRatioF = content->devicePixelRatioF(); - QLOG_ERROR() << "src size: " << currentPage->size() << " content size: " << content->size() << " target size " << QSize(content->width() * devicePixelRatioF, content->height() * devicePixelRatioF); QPixmap page = smartScalePixmap(*currentPage, content->width() * devicePixelRatioF, content->height() * devicePixelRatioF); // currentPage->scaled(content->width() * devicePixelRatioF(), content->height() * devicePixelRatioF(), Qt::KeepAspectRatio, Qt::SmoothTransformation); page.setDevicePixelRatio(devicePixelRatioF); content->setPixmap(page); From 7248390f683c2f5e6041a0722718592491f49e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20A=CC=81ngel=20San=20Marti=CC=81n=20Rodri=CC=81guez?= Date: Sat, 31 Aug 2024 15:20:59 +0200 Subject: [PATCH 07/16] Try to handle universal builds for macos better --- YACReader/YACReader.pro | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/YACReader/YACReader.pro b/YACReader/YACReader.pro index cd9352422..6d375d7c5 100644 --- a/YACReader/YACReader.pro +++ b/YACReader/YACReader.pro @@ -89,9 +89,19 @@ macx { LIBS += -framework Foundation -framework ApplicationServices -framework AppKit lessThan(QT_MAJOR_VERSION, 6): QT += macextras + + contains(QMAKE_APPLE_DEVICE_ARCHS, arm64) { + QMAKE_CXXFLAGS += -mfpu=neon -mfloat-abi=hard + DEFINES += __ARM_NEON__ + } + + contains(QMAKE_APPLE_DEVICE_ARCHS, x86_64) { + QMAKE_CXXFLAGS += -msse4.2 -mavx2 -mfma + DEFINES += __AVX__ __AVX2__ + } } -unix|mingw { +unix|mingw:!macx { contains(QMAKE_HOST.arch, arm) { QMAKE_CXXFLAGS += -mfpu=neon -mfloat-abi=hard DEFINES += __ARM_NEON__ From 238e7a16bd259db65f2a35439e700c617910eadc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20A=CC=81ngel=20San=20Marti=CC=81n=20Rodri=CC=81guez?= Date: Sat, 31 Aug 2024 15:38:49 +0200 Subject: [PATCH 08/16] Try another approach to configure smid flags --- YACReader/YACReader.pro | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/YACReader/YACReader.pro b/YACReader/YACReader.pro index 6d375d7c5..78f3c57e1 100644 --- a/YACReader/YACReader.pro +++ b/YACReader/YACReader.pro @@ -99,23 +99,23 @@ macx { QMAKE_CXXFLAGS += -msse4.2 -mavx2 -mfma DEFINES += __AVX__ __AVX2__ } -} - -unix|mingw:!macx { - contains(QMAKE_HOST.arch, arm) { - QMAKE_CXXFLAGS += -mfpu=neon -mfloat-abi=hard - DEFINES += __ARM_NEON__ - } else { - # Enable general SIMD optimizations - QMAKE_CXXFLAGS += -msse2 # Baseline for x86 - - # Architecture-specific optimizations (adjust as needed) - contains(QMAKE_TARGET.arch, x86_64) { - QMAKE_CXXFLAGS += -mavx2 -mfma - DEFINES += __AVX__ __AVX2__ - } else { # Assuming x86 (32-bit) - QMAKE_CXXFLAGS += -msse4.2 - DEFINES += __SSE4_2__ +} else { + unix|mingw { + contains(QMAKE_HOST.arch, arm) { + QMAKE_CXXFLAGS += -mfpu=neon -mfloat-abi=hard + DEFINES += __ARM_NEON__ + } else { + # Enable general SIMD optimizations + QMAKE_CXXFLAGS += -msse2 # Baseline for x86 + + # Architecture-specific optimizations (adjust as needed) + contains(QMAKE_TARGET.arch, x86_64) { + QMAKE_CXXFLAGS += -mavx2 -mfma + DEFINES += __AVX__ __AVX2__ + } else { # Assuming x86 (32-bit) + QMAKE_CXXFLAGS += -msse4.2 + DEFINES += __SSE4_2__ + } } } } From b908558d14108e99cb400fcaf51b3fab4c6d22cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20A=CC=81ngel=20San=20Marti=CC=81n=20Rodri=CC=81guez?= Date: Sat, 31 Aug 2024 15:55:35 +0200 Subject: [PATCH 09/16] Use QMAKE_TARGET.arch --- YACReader/YACReader.pro | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/YACReader/YACReader.pro b/YACReader/YACReader.pro index 78f3c57e1..2391fd928 100644 --- a/YACReader/YACReader.pro +++ b/YACReader/YACReader.pro @@ -90,18 +90,18 @@ macx { lessThan(QT_MAJOR_VERSION, 6): QT += macextras - contains(QMAKE_APPLE_DEVICE_ARCHS, arm64) { + contains(QMAKE_TARGET.arch, arm64) { QMAKE_CXXFLAGS += -mfpu=neon -mfloat-abi=hard DEFINES += __ARM_NEON__ } - contains(QMAKE_APPLE_DEVICE_ARCHS, x86_64) { + contains(QMAKE_TARGET.arch, x86_64) { QMAKE_CXXFLAGS += -msse4.2 -mavx2 -mfma DEFINES += __AVX__ __AVX2__ } } else { unix|mingw { - contains(QMAKE_HOST.arch, arm) { + contains(QMAKE_TARGET.arch, arm) { QMAKE_CXXFLAGS += -mfpu=neon -mfloat-abi=hard DEFINES += __ARM_NEON__ } else { From 0e341d7eaa38644d059f119aba9440a7741f78ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20A=CC=81ngel=20San=20Marti=CC=81n=20Rodri=CC=81guez?= Date: Sat, 31 Aug 2024 16:03:12 +0200 Subject: [PATCH 10/16] Add some debug messages --- YACReader/YACReader.pro | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/YACReader/YACReader.pro b/YACReader/YACReader.pro index 2391fd928..abaa2ec03 100644 --- a/YACReader/YACReader.pro +++ b/YACReader/YACReader.pro @@ -55,6 +55,12 @@ INCLUDEPATH += ../common \ INCLUDEPATH += ../common/gl } +message (ANDROID:$$ANDROID_TARGET_ARCH) +message (HOST:$$QMAKE_HOST) +message (HOST:$$QMAKE_HOST.arch) +message (TARGET:$$QMAKE_TARGET) +message (TARGET:$$QMAKE_TARGET.arch) + #there are going to be two builds for windows, OpenGL based and ANGLE based win32 { CONFIG(force_angle) { From b12e1212b5a8c9bb1abf5d9b47d8459817ebb531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20A=CC=81ngel=20San=20Marti=CC=81n=20Rodri=CC=81guez?= Date: Sat, 31 Aug 2024 16:21:27 +0200 Subject: [PATCH 11/16] Avoid defining macros, they should come from the compiler --- YACReader/YACReader.pro | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/YACReader/YACReader.pro b/YACReader/YACReader.pro index abaa2ec03..37e4092a9 100644 --- a/YACReader/YACReader.pro +++ b/YACReader/YACReader.pro @@ -55,10 +55,7 @@ INCLUDEPATH += ../common \ INCLUDEPATH += ../common/gl } -message (ANDROID:$$ANDROID_TARGET_ARCH) -message (HOST:$$QMAKE_HOST) message (HOST:$$QMAKE_HOST.arch) -message (TARGET:$$QMAKE_TARGET) message (TARGET:$$QMAKE_TARGET.arch) #there are going to be two builds for windows, OpenGL based and ANGLE based @@ -80,11 +77,9 @@ win32 { # Enable AVX and AVX2 support QMAKE_CXXFLAGS += /arch:AVX - DEFINES += __AVX__ # Enable AVX2 if supported win32:QMAKE_CXXFLAGS += /arch:AVX2 - DEFINES += __AVX2__ } CONFIG -= embed_manifest_exe } @@ -98,18 +93,15 @@ macx { contains(QMAKE_TARGET.arch, arm64) { QMAKE_CXXFLAGS += -mfpu=neon -mfloat-abi=hard - DEFINES += __ARM_NEON__ } contains(QMAKE_TARGET.arch, x86_64) { QMAKE_CXXFLAGS += -msse4.2 -mavx2 -mfma - DEFINES += __AVX__ __AVX2__ } } else { unix|mingw { contains(QMAKE_TARGET.arch, arm) { QMAKE_CXXFLAGS += -mfpu=neon -mfloat-abi=hard - DEFINES += __ARM_NEON__ } else { # Enable general SIMD optimizations QMAKE_CXXFLAGS += -msse2 # Baseline for x86 @@ -117,10 +109,8 @@ macx { # Architecture-specific optimizations (adjust as needed) contains(QMAKE_TARGET.arch, x86_64) { QMAKE_CXXFLAGS += -mavx2 -mfma - DEFINES += __AVX__ __AVX2__ } else { # Assuming x86 (32-bit) QMAKE_CXXFLAGS += -msse4.2 - DEFINES += __SSE4_2__ } } } From 68bf76a8b23c745b9a85583aa97f2be2f26205c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20San=20Mart=C3=ADn?= Date: Sat, 31 Aug 2024 17:11:28 +0200 Subject: [PATCH 12/16] Faster image processing --- image_processing/resize_image.cpp | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/image_processing/resize_image.cpp b/image_processing/resize_image.cpp index 3898cff56..787feda97 100644 --- a/image_processing/resize_image.cpp +++ b/image_processing/resize_image.cpp @@ -352,7 +352,6 @@ QVector processRow(int y, int targetWidth, int targetHeight, const QImage #endif -// Main function to scale the image QImage scaleImageLanczos(const QImage &sourceImage, int targetWidth, int targetHeight, int a = 3) { QImage targetImage(targetWidth, targetHeight, QImage::Format_ARGB32); @@ -362,24 +361,18 @@ QImage scaleImageLanczos(const QImage &sourceImage, int targetWidth, int targetH rows[i] = i; } - // Use a QVector to store the results of the processed rows - QVector> results(targetHeight); + uchar *imgBits = targetImage.bits(); + int bytesPerLine = targetImage.bytesPerLine(); - // Launch concurrent tasks using QtConcurrent::map, which modifies the container in place - QtConcurrent::blockingMap(rows, [&results, targetWidth, targetHeight, &sourceImage, a](int y) { - results[y] = processRow(y, targetWidth, targetHeight, sourceImage, a); + QtConcurrent::blockingMap(rows, [targetWidth, targetHeight, &sourceImage, a, imgBits, bytesPerLine](int y) { + QVector rowPixels = processRow(y, targetWidth, targetHeight, sourceImage, a); + uchar *rowPtr = imgBits + y * bytesPerLine; + std::memcpy(rowPtr, rowPixels.constData(), targetWidth * sizeof(QRgb)); }); - // Set the pixels in the target image - for (int y = 0; y < targetHeight; ++y) { - const QVector &row = results[y]; - for (int x = 0; x < targetWidth; ++x) { - targetImage.setPixel(x, y, row[x]); - } - } - return targetImage; } + QPixmap scalePixmapLanczosQt(const QPixmap &pixmap, int targetWidth, int targetHeight, int a) { QImage sourceImage = pixmap.toImage(); From af8496c1df2d432f837cae10e6dd28c529377f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20San=20Mart=C3=ADn?= Date: Sat, 31 Aug 2024 17:13:45 +0200 Subject: [PATCH 13/16] Leave more room for the version number --- compileOSX.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compileOSX.sh b/compileOSX.sh index bd92627fc..6277766ac 100755 --- a/compileOSX.sh +++ b/compileOSX.sh @@ -86,7 +86,7 @@ fi echo "Preparing apps for release, Done." -dest="YACReader-$VERSION.$BUILD_NUMBER MacOSX-$ARCH_NAME ${QT_VERSION}" +dest="YACReader-$VERSION.$BUILD_NUMBER macos-$ARCH_NAME ${QT_VERSION}" echo "Copying to destination folder ${dest}" mkdir -p "$dest" cp -R YACReader.app "${dest}/YACReader.app" From 06cc63ee86d82b3248d9e1ac8462163cb55700fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20San=20Mart=C3=ADn?= Date: Sat, 31 Aug 2024 17:50:02 +0200 Subject: [PATCH 14/16] Include cstring explicitly --- image_processing/resize_image.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/image_processing/resize_image.cpp b/image_processing/resize_image.cpp index 787feda97..3f9d858da 100644 --- a/image_processing/resize_image.cpp +++ b/image_processing/resize_image.cpp @@ -4,6 +4,7 @@ #include #include #include +#include QPixmap scalePixmapBicubic(const QPixmap &pixmap, int width, int height); QPixmap scalePixmapLanczos(const QPixmap &pixmap, int width, int height); From 95e751370f57601f64dc16cc96604755c252997c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20San=20Mart=C3=ADn?= Date: Sat, 31 Aug 2024 17:50:16 +0200 Subject: [PATCH 15/16] Use macos in dmg.json --- dmg.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dmg.json b/dmg.json index 2c4caffeb..898193495 100644 --- a/dmg.json +++ b/dmg.json @@ -19,31 +19,31 @@ "x": 80, "y": 90, "type": "file", - "path": "YACReader-#VERSION#.#BUILD_NUMBER# MacOSX-#ARCH_NAME# #QT_VERSION#/YACReader.app" + "path": "YACReader-#VERSION#.#BUILD_NUMBER# macos-#ARCH_NAME# #QT_VERSION#/YACReader.app" }, { "x": 235, "y": 90, "type": "file", - "path": "YACReader-#VERSION#.#BUILD_NUMBER# MacOSX-#ARCH_NAME# #QT_VERSION#/YACReaderLibrary.app" + "path": "YACReader-#VERSION#.#BUILD_NUMBER# macos-#ARCH_NAME# #QT_VERSION#/YACReaderLibrary.app" }, { "x": 470, "y": 295, "type": "file", - "path": "YACReader-#VERSION#.#BUILD_NUMBER# MacOSX-#ARCH_NAME# #QT_VERSION#/YACReaderLibraryServer" + "path": "YACReader-#VERSION#.#BUILD_NUMBER# macos-#ARCH_NAME# #QT_VERSION#/YACReaderLibraryServer" }, { "x": 120, "y": 295, "type": "file", - "path": "YACReader-#VERSION#.#BUILD_NUMBER# MacOSX-#ARCH_NAME# #QT_VERSION#/README.md" + "path": "YACReader-#VERSION#.#BUILD_NUMBER# macos-#ARCH_NAME# #QT_VERSION#/README.md" }, { "x": 290, "y": 295, "type": "file", - "path": "YACReader-#VERSION#.#BUILD_NUMBER# MacOSX-#ARCH_NAME# #QT_VERSION#/COPYING.txt" + "path": "YACReader-#VERSION#.#BUILD_NUMBER# macos-#ARCH_NAME# #QT_VERSION#/COPYING.txt" } ] } From 420cea7571d9f375fc70fa9135611da6e4538c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20A=CC=81ngel=20San=20Marti=CC=81n=20Rodri=CC=81guez?= Date: Sat, 31 Aug 2024 19:16:01 +0200 Subject: [PATCH 16/16] 27 characters limit sucks --- dmg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dmg.json b/dmg.json index 898193495..e7408b0ee 100644 --- a/dmg.json +++ b/dmg.json @@ -1,5 +1,5 @@ { - "title": "YACReader-#VERSION#.#BUILD_NUMBER##QT_VERSION#", + "title": "YACReader#VERSION#.#BUILD_NUMBER##QT_VERSION#", "icon": "icon.icns", "background": "background.png", "window": {