viewer: fix thumbnail render crash (worker-thread exception / LUT UB)
The image-strip thumbnail renderer could take the whole viewer down: - Only the image load was wrapped in try/catch; the rest of RenderThumbnail_i (pixel access, LUT mapping, QPainter) ran unguarded on the worker thread, so any exception there called std::terminate. Wrap the entire function. - std::clamp(idx, 0, lutSize - 1) is undefined behaviour when the colour LUT is empty (lo > hi). Bail out early if the LUT has no entries. A click landing while a later thumbnail is still rendering surfaced this. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1057,73 +1057,82 @@ void JFJochImageReadingWorker::RenderThumbnails(QVector<qint64> image_numbers, b
|
||||
}
|
||||
|
||||
QImage JFJochImageReadingWorker::RenderThumbnail_i(int64_t image_number, bool show_spots) {
|
||||
std::shared_ptr<JFJochReaderImage> img;
|
||||
// Everything runs inside one try: an uncaught exception here would be on the worker thread and
|
||||
// would std::terminate the whole app. On any failure we just skip this thumbnail.
|
||||
try {
|
||||
QMutexLocker locker(&m);
|
||||
if (http_mode || image_number < 0 || image_number >= total_images)
|
||||
std::shared_ptr<JFJochReaderImage> img;
|
||||
{
|
||||
QMutexLocker locker(&m);
|
||||
if (http_mode || image_number < 0 || image_number >= total_images)
|
||||
return {};
|
||||
img = file_reader.LoadImage(image_number, 1);
|
||||
}
|
||||
if (!img)
|
||||
return {};
|
||||
img = file_reader.LoadImage(image_number, 1);
|
||||
} catch (const std::exception &e) {
|
||||
logger.Debug("Thumbnail load failed for image {}: {}", image_number, e.what());
|
||||
return {};
|
||||
}
|
||||
if (!img)
|
||||
return {};
|
||||
|
||||
const int W = static_cast<int>(img->Dataset().experiment.GetXPixelsNum());
|
||||
const int H = static_cast<int>(img->Dataset().experiment.GetYPixelsNum());
|
||||
const auto &px = img->Image();
|
||||
if (W <= 0 || H <= 0 || static_cast<int64_t>(px.size()) < static_cast<int64_t>(W) * H)
|
||||
return {};
|
||||
const int W = static_cast<int>(img->Dataset().experiment.GetXPixelsNum());
|
||||
const int H = static_cast<int>(img->Dataset().experiment.GetYPixelsNum());
|
||||
const auto &px = img->Image();
|
||||
if (W <= 0 || H <= 0 || static_cast<int64_t>(px.size()) < static_cast<int64_t>(W) * H)
|
||||
return {};
|
||||
|
||||
// Downsample by block-maximum (keeps sharp Bragg spots), then map through the colour-scale LUT.
|
||||
constexpr int maxDim = 120;
|
||||
const double s = static_cast<double>(maxDim) / std::max(W, H);
|
||||
const int tw = std::max(1, static_cast<int>(W * s));
|
||||
const int th = std::max(1, static_cast<int>(H * s));
|
||||
const int bx = std::max(1, W / tw);
|
||||
const int by = std::max(1, H / th);
|
||||
const auto &lut = thumb_color_scale_.LUTData();
|
||||
const int lutSize = static_cast<int>(lut.size());
|
||||
if (lutSize <= 0)
|
||||
return {};
|
||||
|
||||
const float fg = std::max(1.0f, static_cast<float>(img->GetAutoContrastValue()));
|
||||
const auto &lut = thumb_color_scale_.LUTData();
|
||||
const int lutSize = static_cast<int>(lut.size());
|
||||
const float invRange = fg > 0 ? (lutSize - 1) / fg : 0.0f;
|
||||
const rgb gap = thumb_color_scale_.Apply(ColorScaleSpecial::Gap);
|
||||
// Downsample by block-maximum (keeps sharp Bragg spots), then map through the LUT.
|
||||
constexpr int maxDim = 120;
|
||||
const double s = static_cast<double>(maxDim) / std::max(W, H);
|
||||
const int tw = std::max(1, static_cast<int>(W * s));
|
||||
const int th = std::max(1, static_cast<int>(H * s));
|
||||
const int bx = std::max(1, W / tw);
|
||||
const int by = std::max(1, H / th);
|
||||
|
||||
QImage out(tw, th, QImage::Format_RGB32);
|
||||
for (int ty = 0; ty < th; ++ty) {
|
||||
QRgb *line = reinterpret_cast<QRgb *>(out.scanLine(ty));
|
||||
const int y1 = std::min(H, ty * by + by);
|
||||
for (int tx = 0; tx < tw; ++tx) {
|
||||
const int x1 = std::min(W, tx * bx + bx);
|
||||
int32_t best = 0;
|
||||
bool any = false;
|
||||
for (int yy = ty * by; yy < y1; ++yy)
|
||||
for (int xx = tx * bx; xx < x1; ++xx) {
|
||||
const int32_t v = px[yy * W + xx];
|
||||
if (v == GAP_PXL_VALUE || v == ERROR_PXL_VALUE) continue;
|
||||
if (v == SATURATED_PXL_VALUE) { best = static_cast<int32_t>(fg); any = true; continue; }
|
||||
if (!any || v > best) { best = v; any = true; }
|
||||
const float fg = std::max(1.0f, static_cast<float>(img->GetAutoContrastValue()));
|
||||
const float invRange = (lutSize - 1) / fg;
|
||||
const rgb gap = thumb_color_scale_.Apply(ColorScaleSpecial::Gap);
|
||||
|
||||
QImage out(tw, th, QImage::Format_RGB32);
|
||||
for (int ty = 0; ty < th; ++ty) {
|
||||
QRgb *line = reinterpret_cast<QRgb *>(out.scanLine(ty));
|
||||
const int y1 = std::min(H, ty * by + by);
|
||||
for (int tx = 0; tx < tw; ++tx) {
|
||||
const int x1 = std::min(W, tx * bx + bx);
|
||||
int32_t best = 0;
|
||||
bool any = false;
|
||||
for (int yy = ty * by; yy < y1; ++yy)
|
||||
for (int xx = tx * bx; xx < x1; ++xx) {
|
||||
const int32_t v = px[yy * W + xx];
|
||||
if (v == GAP_PXL_VALUE || v == ERROR_PXL_VALUE) continue;
|
||||
if (v == SATURATED_PXL_VALUE) { best = static_cast<int32_t>(fg); any = true; continue; }
|
||||
if (!any || v > best) { best = v; any = true; }
|
||||
}
|
||||
rgb c;
|
||||
if (!any) {
|
||||
c = gap;
|
||||
} else {
|
||||
int idx = static_cast<int>(std::max(0, best) * invRange + 0.5f);
|
||||
idx = std::clamp(idx, 0, lutSize - 1);
|
||||
c = lut[idx];
|
||||
}
|
||||
rgb c;
|
||||
if (!any) {
|
||||
c = gap;
|
||||
} else {
|
||||
int idx = static_cast<int>(std::max(0, best) * invRange + 0.5f);
|
||||
idx = std::clamp(idx, 0, lutSize - 1);
|
||||
c = lut[idx];
|
||||
line[tx] = qRgb(c.r, c.g, c.b);
|
||||
}
|
||||
line[tx] = qRgb(c.r, c.g, c.b);
|
||||
}
|
||||
}
|
||||
|
||||
if (show_spots) {
|
||||
QPainter p(&out);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
for (const auto &sp : img->ImageData().spots) {
|
||||
p.setPen(QPen(sp.indexed ? thumb_feature_color_ : thumb_spot_color_, 1.0));
|
||||
p.drawEllipse(QPointF(sp.x * s, sp.y * s), 1.6, 1.6);
|
||||
if (show_spots) {
|
||||
QPainter p(&out);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
for (const auto &sp : img->ImageData().spots) {
|
||||
p.setPen(QPen(sp.indexed ? thumb_feature_color_ : thumb_spot_color_, 1.0));
|
||||
p.drawEllipse(QPointF(sp.x * s, sp.y * s), 1.6, 1.6);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
} catch (const std::exception &e) {
|
||||
logger.Debug("Thumbnail render failed for image {}: {}", image_number, e.what());
|
||||
return {};
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user