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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user