Viewer: link one libjpeg - drop Qt's JPEG and TIFF plugins

With the static Qt of the Linux viewer, QJpegPlugin brings Qt's bundled libjpeg-turbo
(3.1.4, JPEG_LIB_VERSION 80) next to ours from preview/ (3.2.0, ABI 62). Both export the
same jpeg_* symbols and the linker silently kept Qt's, so WriteJPEGToMem - compiled against
our headers - failed with "Wrong JPEG library version". In the viewer that aborted an
in-process rugnux run at the end, where _detector.jpg is collected. The rc173 artifact shows
it directly: jpeg_CreateCompress checks for 80 and the only libjpeg version string is 3.1.4.

The viewer now excludes Qt::QJpegPlugin and the unused Qt::QTiffPlugin (which bound to
whichever libtiff the link offered), and its clipboard copy and "Save image as JPEG" encode
through WriteJPEGToMem instead of QImage::save. The Linux viewer CI job checks the binary
holds no Qt JPEG/TIFF handler and exactly one libjpeg-turbo.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-05 12:47:55 +02:00
co-authored by Claude Opus 5.5
parent 2624a69726
commit 9459444b6a
4 changed files with 44 additions and 11 deletions
+12
View File
@@ -79,6 +79,18 @@ jobs:
# still built because the viewer links the JFJochRugnux library for its in-process jobs.
ninja -j16
cpack
- name: Verify one libjpeg in the viewer
shell: bash
run: |
set -euo pipefail
# With the static Qt, Qt's JPEG plugin brings its own libjpeg-turbo (another ABI) and the
# linker silently mixes it with ours (see viewer/CMakeLists.txt). Files, not pipes into
# `grep -q`, for the SIGPIPE reason given in build:rugnux-tgz.
strings build/viewer/jfjoch_viewer > /tmp/jfjv.strings
if grep -qE 'QJpegHandler|QTiffHandler' /tmp/jfjv.strings; then echo "a Qt JPEG/TIFF plugin is linked"; exit 1; fi
grep -c 'libjpeg-turbo version' /tmp/jfjv.strings > /tmp/jfjv.njpeg || true
if [ "$(cat /tmp/jfjv.njpeg)" != 1 ]; then echo "expected one libjpeg-turbo, found $(cat /tmp/jfjv.njpeg)"; exit 1; fi
echo "one libjpeg-turbo, no Qt JPEG/TIFF plugin"
- name: Upload viewer tgz to release
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
shell: bash
+1
View File
@@ -3,6 +3,7 @@
### 1.0.0-rc.174
* jfjoch_viewer (Linux): processing no longer fails at the end with "Wrong JPEG library version" when writing the detector image.
* Rugnux masks a patch of detector pixels that reads high on every frame, not only a single hot pixel or a pair.
* Rugnux: `.hkl` now holds unmerged, scaled full reflections (SHELX HKLF 4) on rotation data, so SHELXL computes Rint itself.
* Rugnux reads Rigaku d*TREK SMV images (Saturn CCD), including detector 2theta, image orientation and encoded pixel overflows.
+9
View File
@@ -219,6 +219,15 @@ ELSE()
qt_import_plugins(jfjoch_viewer INCLUDE Qt::QXcbIntegrationPlugin)
ENDIF()
# No Qt JPEG or TIFF image plugins: the viewer encodes JPEG and reads/writes TIFF through our own
# libjpeg-turbo and libtiff (preview/). With a static Qt the JPEG plugin brings Qt's bundled
# libjpeg-turbo, built for another ABI (JPEG_LIB_VERSION 80, ours is 62). Both export the same
# jpeg_* symbols, the linker silently keeps the first, and our encoder - the one rugnux writes
# _detector.jpg with - then fails with "Wrong JPEG library version". The TIFF plugin is unused and
# would bind to whichever libtiff the link happens to offer. A shared Qt never loads either plugin,
# as nothing goes through QImage for these formats.
qt_import_plugins(jfjoch_viewer EXCLUDE Qt::QJpegPlugin Qt::QTiffPlugin)
# Bundle the Qt runtime into the install tree at install time: this drives windeployqt on Windows
# and macdeployqt on macOS, copying the Qt shared libraries + plugins next to the app. With a
# static Qt (e.g. our Linux build) there is nothing to copy, so it is a no-op. Requires Qt >= 6.5,
+22 -11
View File
@@ -3,6 +3,7 @@
#include "JFJochImage.h"
#include "../RemoteDisplayMode.h"
#include "../../preview/JFJochJPEG.h"
#include <QGraphicsSimpleTextItem>
#include <QScrollBar>
@@ -14,11 +15,12 @@
#include <QClipboard>
#include <QGuiApplication>
#include <QMimeData>
#include <QBuffer>
#include <QFile>
#include <QFileDialog>
#include <QElapsedTimer>
#include <QPainter>
#include <QtConcurrent/QtConcurrent>
#include <cstring>
QRectF JFJochImageItem::boundingRect() const {
return img_ ? QRectF(0, 0, img_->width(), img_->height()) : QRectF();
@@ -510,13 +512,23 @@ void JFJochImage::contextMenuEvent(QContextMenuEvent *event) {
}
}
// JPEG goes through our own libjpeg-turbo, not QImage::save - the viewer links no Qt JPEG plugin
// (see viewer/CMakeLists.txt). JPEG cannot store alpha, so it is dropped. Empty on failure.
static QByteArray encodeJPEG(const QImage &img, int quality) {
const QImage src = img.convertToFormat(QImage::Format_RGB888);
std::vector<rgb> pixels(size_t(src.width()) * src.height());
for (int y = 0; y < src.height(); y++)
memcpy(pixels.data() + size_t(y) * src.width(), src.constScanLine(y), size_t(src.width()) * sizeof(rgb));
try {
const std::string jpeg = WriteJPEGToMem(CompressedImage(pixels, src.width(), src.height()), quality);
return {jpeg.data(), qsizetype(jpeg.size())};
} catch (const std::exception &) {
return {};
}
}
static void setClipboardAsJpegAndImage(const QImage &img, int quality = 95) {
// Provide both "image/jpeg" and generic image flavors for better compatibility
QByteArray ba;
ba.reserve(img.width() * img.height() * 3 / 2);
QBuffer buf(&ba);
buf.open(QIODevice::WriteOnly);
QImage toSave = img;
// Force 1:1 pixel ratio and standard DPI (96) to avoid scaling in consumer apps
toSave.setDevicePixelRatio(1.0);
@@ -525,10 +537,9 @@ static void setClipboardAsJpegAndImage(const QImage &img, int quality = 95) {
toSave.setDotsPerMeterY(dotsPerMeter96DPI);
toSave = toSave.convertToFormat(QImage::Format_ARGB32); // ensure a known format for encoding
toSave.save(&buf, "JPEG", quality);
auto *mime = new QMimeData();
mime->setData("image/jpeg", ba);
mime->setData("image/jpeg", encodeJPEG(toSave, quality));
mime->setImageData(toSave); // also set as generic bitmap
QGuiApplication::clipboard()->setMimeData(mime);
@@ -583,9 +594,9 @@ void JFJochImage::saveImageToFile(bool with_overlay) {
if (!file_name.endsWith(".jpg", Qt::CaseInsensitive) && !file_name.endsWith(".jpeg", Qt::CaseInsensitive))
file_name += ".jpg";
// JPEG cannot store alpha; flatten to RGB before encoding.
const QImage img = renderToImage(with_overlay).convertToFormat(QImage::Format_RGB32);
if (img.save(file_name, "JPEG", 95))
const QByteArray jpeg = encodeJPEG(renderToImage(with_overlay), 95);
QFile file(file_name);
if (!jpeg.isEmpty() && file.open(QIODevice::WriteOnly) && file.write(jpeg) == jpeg.size())
emit writeStatusBar(tr("Saved %1").arg(file_name), 3000);
else
emit writeStatusBar(tr("Failed to save %1").arg(file_name), 3000);