rugnux and the viewer read SMV and gzipped miniCBF

Two more of the formats deposited data actually arrives in, found by processing a
corpus of it: every PETRA III EMBL set is .cbf.gz, and NSRRC and the whole ADSC
Quantum era are SMV. Both were previously "no native input".

SMV is an ASCII "KEY=value;" block between braces, then the pixels - no container,
no compression, nothing to decode by offset - so reader/SMV.{h,cpp} and
JFJochSMVReader are a smaller job than the marCCD pair they sit beside, and need no
new dependency at all. Two things the format does not give us, both said out loud
rather than papered over:

* It states no saturation value, so overloads are judged on the 16-bit container
  alone. That can only fail to call a pixel saturated, never condemn a good one,
  but a CCD at the top of its range does saturate, so the reader warns once.
* Its beam centre is in MILLIMETRES and which of X/Y is the fast direction is a
  convention rather than a rule. Measured on one ALS ADSC sweep the file's value is
  TRANSPOSED: as stated it indexes 2/60 frames, and the run's own beam-centre
  measurement (which adopts the right one automatically) indexes 60/60. Swapping it
  here would fit that writer and might break another, so the header is read as the
  format defines it and the measurement stays the arbiter. Revisit with a second
  vendor's SMV in hand.

.cbf.gz needed only Slurp() in MiniCBF.cpp, through which every read already passes:
it sniffs the two-byte gzip magic - not the file name - and takes a zlib path when it
is there, leaving the plain path free of zlib's buffer copy. zlib-ng is already in the
build, so this is a link line, not a dependency. The sweep template grew a suffix,
because ".cbf" and ".cbf.gz" are separate sweeps and std::filesystem cannot split the
double extension on its own.

The viewer's single cbf_reader becomes three, dispatched by CanRead() in the same
order as rugnux. Dispatch is by CONTENT in both: ".img" is used by miniCBF, marCCD
AND SMV depending on the writer, and a PDB detector label has now been wrong about
the format four times, so an extension decides nothing.

Measured, de novo, no flags: 9fcg (1800 gzipped frames) gives P4 and a cell 0.06%
from the deposited one at 1.37 A against a deposited 1.54; 6oel (ADSC SMV) gives
F4132 - 96 operations, the most a protein space group can have - and a cell 0.05%
out, 100% indexed. Tests cover both formats and the transposed-beam-centre case with
fixtures written byte for byte, so they need no external data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
leonarski_fandClaude Opus 5 committed 2026-09-16 22:35:05 +02:00
1 parent a1889c45e9
commit 2ab8c55dfa
15 files changed
+867 -27

No files matched your search

+3
View File
@@ -4,6 +4,9 @@
### 1.0.0-rc.171
* rugnux reads marCCD images natively: naming any frame of a Rayonix or mar Mosaic sweep processes the whole sweep, with no conversion step.
* rugnux reads SMV images natively - the format ADSC Quantum detectors wrote and Rayonix still writes.
* rugnux reads gzipped miniCBF (`.cbf.gz`) directly, so a sweep archived that way needs no decompression pass.
* `jfjoch_viewer` opens marCCD, SMV and gzipped miniCBF frames alongside HDF5 and plain miniCBF.
### 1.0.0-rc.170
+13 -3
View File
@@ -1,10 +1,10 @@
# What rugnux reads
Which data `rugnux` opens, before anything is typed. The short answer: an HDF5 master (NXmx or
DECTRIS, from any facility), a PILATUS miniCBF sweep or a marCCD sweep — nothing else is read, so
any other format has to be converted to one of these three first.
DECTRIS, from any facility), a PILATUS miniCBF sweep, a marCCD sweep or an SMV sweep — nothing else
is read, so any other format has to be converted to one of these first.
**Input** is an HDF5 master file, or a directory of PILATUS miniCBF or marCCD frames. One input is
**Input** is an HDF5 master file, or a directory of PILATUS miniCBF, marCCD or SMV frames. One input is
**one sweep of one crystal** — rugnux does not combine sweeps or crystals in a run; process each
sweep to its own `_unmerged.mtz` and merge them downstream
(see [Taking the data onward](RUGNUX_INTEGRATION.md#taking-the-data-onward)).
@@ -26,6 +26,9 @@ sweep to its own `_unmerged.mtz` and merge them downstream
header, including the imgCIF axis table where the header carries one (see
[Detector geometry](DETECTOR_GEOMETRY.md)). A raw CBF carries no analysis results, so
`--mode scale` — which re-scales the reflections stored in a `_process.h5` — does not accept one.
**Gzipped frames (`.cbf.gz`) are read directly**, with no decompression step: EMBL Hamburg's
beamlines write that form by default, and expanding a sweep first would double the disk it needs.
The two forms are separate sweeps, so a directory holding both is not spliced into one crystal.
* **marCCD sweep** — what Rayonix MX-series and mar Mosaic detectors write, and what a decade of
deposited CCD data is archived as: an uncompressed TIFF with the instrument header in the gap
before the pixels. One frame per file, read natively. Naming a frame or its directory selects the
@@ -36,6 +39,13 @@ sweep to its own `_unmerged.mtz` and merge them downstream
from the data as it does for a miniCBF that carries no axis table. A CCD frame marks no untrusted
pixels, so the sweep starts with nothing masked. Like a raw CBF, it carries no analysis results
and `--mode scale` does not accept one.
* **SMV sweep** — what ADSC Quantum detectors wrote and what Rayonix and others still write, so most
archived CCD data from the 2000s is in this form: an ASCII `KEY=value;` block between braces, then
the pixels. One frame per file, read natively, the sweep selected exactly as above. The beam centre
it states is in **millimetres** and is converted here; its convention varies between writers, and a
file that states it transposed indexes nothing until the run's own beam-centre measurement replaces
it, which it does automatically. SMV states no saturation value at all, so overloads are judged on
the 16-bit container alone and the reader says so once per sweep.
Spots are always found by `rugnux` itself, including for the two-pass rotation first pass — the
spot lists a dataset may already carry were found online, at the acquisition's threshold and with
+5 -1
View File
@@ -10,6 +10,10 @@ ADD_LIBRARY(JFJochReader STATIC
MarCCD.h
JFJochMarCCDReader.cpp
JFJochMarCCDReader.h
SMV.cpp
SMV.h
JFJochSMVReader.cpp
JFJochSMVReader.h
HDF5ImageLocator.cpp
HDF5ImageLocator.h
HDF5ImageSource.cpp
@@ -29,5 +33,5 @@ ADD_LIBRARY(JFJochReader STATIC
# tiff: the marCCD reader's files are ordinary uncompressed TIFFs, so libtiff - which the build
# already fetches for JFJochPreview in every mode - reads the pixels and there is no new dependency.
TARGET_LINK_LIBRARIES(JFJochReader JFJochImageAnalysis JFJochAPI JFJochCommon JFJochZMQ JFJochLogger
JFJochHDF5Wrappers CBORStream2FrameSerialize tiff
JFJochHDF5Wrappers CBORStream2FrameSerialize tiff ZLIB::ZLIB
${CMAKE_DL_LIBS})
+30 -9
View File
@@ -12,6 +12,7 @@
#include <map>
#include <optional>
#include <thread>
#include <tuple>
#include "../common/JFJochException.h"
#include "../common/Logger.h"
@@ -19,10 +20,23 @@
namespace {
// ".cbf", or ".cbf.gz" - EMBL Hamburg's beamlines write the gzipped form by default, and the
// reader decompresses it in place, so a sweep of those is named here rather than converted first.
// Returns the suffix that matched, because the sweep template needs its length.
std::optional<std::string> CBFSuffix(const std::filesystem::path &p) {
std::string name = p.filename().string();
std::transform(name.begin(), name.end(), name.begin(),
[](unsigned char c) { return std::tolower(c); });
for (const char *suffix : {".cbf.gz", ".cbf"}) {
const std::string s(suffix);
if (name.size() > s.size() && name.compare(name.size() - s.size(), s.size(), s) == 0)
return s;
}
return {};
}
bool HasCBFExtension(const std::filesystem::path &p) {
std::string ext = p.extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return std::tolower(c); });
return ext == ".cbf";
return CBFSuffix(p).has_value();
}
// The sweep a file belongs to, as a template: everything before the trailing run of digits, the
@@ -32,12 +46,15 @@ bool HasCBFExtension(const std::filesystem::path &p) {
struct Template {
std::string prefix;
size_t digits = 0;
std::string suffix = ".cbf"; // ".cbf" or ".cbf.gz"; the two are not one sweep
bool Matches(const std::string &name) const {
if (name.size() != prefix.size() + digits + 4) // + ".cbf"
if (name.size() != prefix.size() + digits + suffix.size())
return false;
if (name.compare(0, prefix.size(), prefix) != 0)
return false;
if (name.compare(prefix.size() + digits, suffix.size(), suffix) != 0)
return false;
for (size_t i = 0; i < digits; i++)
if (!std::isdigit(static_cast<unsigned char>(name[prefix.size() + i])))
return false;
@@ -47,15 +64,19 @@ struct Template {
std::optional<Template> TemplateOf(const std::string &filename) {
const std::filesystem::path p(filename);
if (!HasCBFExtension(p))
const auto suffix = CBFSuffix(p);
if (!suffix.has_value())
return {};
const std::string stem = p.stem().string();
// The stem is the name with the whole matched suffix removed, which std::filesystem cannot do
// for ".cbf.gz" - its extension() there is ".gz" and its stem() still ends in ".cbf".
const std::string name = p.filename().string();
const std::string stem = name.substr(0, name.size() - suffix->size());
size_t end = stem.size();
while (end > 0 && std::isdigit(static_cast<unsigned char>(stem[end - 1])))
end--;
if (end == stem.size())
return {}; // no trailing number: not part of a numbered sweep
return Template{stem.substr(0, end), stem.size() - end};
return Template{stem.substr(0, end), stem.size() - end, *suffix};
}
std::vector<std::string> CollectSweep(const std::string &path) {
@@ -74,7 +95,7 @@ std::vector<std::string> CollectSweep(const std::string &path) {
if (!is_dir)
want = TemplateOf(p.filename().string());
std::map<std::pair<std::string, size_t>, std::vector<std::string>> sweeps;
std::map<std::tuple<std::string, size_t, std::string>, std::vector<std::string>> sweeps;
std::error_code ec;
for (const auto &e : std::filesystem::directory_iterator(dir, ec)) {
if (!e.is_regular_file() || !HasCBFExtension(e.path()))
@@ -85,7 +106,7 @@ std::vector<std::string> CollectSweep(const std::string &path) {
continue;
if (want.has_value() && !want->Matches(name))
continue;
sweeps[{t->prefix, t->digits}].push_back(e.path().string());
sweeps[{t->prefix, t->digits, t->suffix}].push_back(e.path().string());
}
std::vector<std::string> out;
+191
View File
@@ -0,0 +1,191 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochSMVReader.h"
#include <cmath>
#include <future>
#include <thread>
#include "../common/JFJochException.h"
#include "../common/JFJochMath.h"
#include "../common/Logger.h"
namespace {
// The base rotation axis in the internal frame (x along increasing detector column, y along
// increasing row, z along the beam). A SMV header names the circle that turned but never states
// a direction for it, so this is the convention an NXmx master writes for the same instruments, and
// a file that needs the other sign is settled from the data by the run's axis-sign rescue - the
// same arrangement JFJochCBFReader makes for a miniCBF that states no axis table.
const Coord ASSUMED_BASE_AXIS(-1.0f, 0.0f, 0.0f);
} // namespace
bool JFJochSMVReader::CanRead(const std::string &path) {
return smv::CanRead(path);
}
void JFJochSMVReader::ReadFiles(const std::string &path) {
files_ = smv::CollectSweep(path);
if (files_.empty())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"No SMV images found for " + path);
header0_ = smv::ReadHeader(files_[0]);
// A pixel size is what makes the file an IMAGE: every resolution, every scattering vector and
// the beam centre in millimetres scale by it, and a default of 0 collapses all of them without
// a word.
if (!(header0_.pixel_x_m > 0.0) || !(header0_.pixel_y_m > 0.0))
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
files_[0] + " states no pixel size in its SMV header");
if (!(header0_.wavelength_A > 0.0))
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
files_[0] + " states no wavelength in its SMV header");
if (!(header0_.distance_m > 0.0))
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
files_[0] + " states no detector distance in its SMV header");
dataset_ = std::make_shared<JFJochReaderDataset>();
dataset_->experiment = default_experiment;
DetectorSetup detector = DetDECTRIS(header0_.nx, header0_.ny,
header0_.detector.empty() ? "SMV" : header0_.detector, {});
// Not rounded to whole micrometres, as the miniCBF path can afford to be: a PILATUS pixel is
// exactly 172 um, but these are 73.242 um, and rounding that to 73 is a 0.33% scale error on
// every cell edge the run reports.
detector.PixelSize_um(static_cast<float>(header0_.pixel_x_m * 1e6));
// A CCD has no sensor thickness worth correcting for: the phosphor converts at the surface and
// the fibre optic carries light, not X-rays, so the parallax correction a silicon sensor needs
// does not apply. Left at zero, which is what the geometry means by "no depth".
detector.SensorThickness_um(0);
// SMV states no saturation value at all - unlike marCCD, which at least carries one - so the
// container's own overflow is what decides, which is the safe direction: it can only fail to
// call a pixel saturated, never call a valid one an overload. Said out loud because a CCD at
// the top of its range really does saturate.
Logger("SMVReader").Warning("{}: the SMV format states no saturation value, so saturation is "
"judged on the 16-bit container alone; a detector that overloads "
"below 65535 will have its strongest reflections integrated as if "
"they were valid.", files_[0]);
// Images are handed out as signed 32-bit whatever the file stored, so that is the depth the
// rest of the code must see.
detector.BitDepthImage(32);
detector.MinFrameTime(std::chrono::microseconds(0));
detector.MinCountTime(std::chrono::microseconds(0));
detector.ReadOutTime(std::chrono::nanoseconds(0));
dataset_->experiment.Detector(detector);
dataset_->experiment.BeamX_pxl(static_cast<float>(header0_.beam_x_px));
dataset_->experiment.BeamY_pxl(static_cast<float>(header0_.beam_y_px));
dataset_->experiment.DetectorDistance_mm(static_cast<float>(header0_.distance_m * 1000.0));
// A detector swung out on a 2theta arm. The arm turns the detector about the sample and so
// carries the square-on geometry with it: the header's distance stays the distance along the
// detector normal and the beam centre stays the point of normal incidence, which is exactly
// what the PONI convention wants, so the swing is a PONI rotation and nothing else changes.
// The arm turns about the same axis as the spindle on the geometries these headers describe.
if (header0_.two_theta_deg != 0.0) {
float rot1 = 0, rot2 = 0, rot3 = 0;
PoniAnglesFromMatrix(RotMatrix(static_cast<float>(header0_.two_theta_deg * PI / 180.0),
ASSUMED_BASE_AXIS), rot1, rot2, rot3);
dataset_->experiment.PoniRot1_rad(rot1).PoniRot2_rad(rot2).PoniRot3_rad(rot3);
}
dataset_->experiment.IncidentEnergy_keV(WVL_1A_IN_KEV / static_cast<float>(header0_.wavelength_A));
dataset_->experiment.FrameTime(
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::duration<double>(header0_.exposure_s)),
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::duration<double>(header0_.exposure_s)));
// The rotation angle of every image, from its own header. Reading one costs a 4 kB read, so on
// a sweep of several thousand frames this is worth spreading over the cores, as the CBF path
// does for the same reason.
std::vector<double> angles(files_.size());
{
const size_t nthreads = std::min<size_t>(std::max(1u, std::thread::hardware_concurrency()), 8);
std::vector<std::future<void>> futures;
for (size_t t = 0; t < nthreads; t++)
futures.push_back(std::async(std::launch::async, [&, t] {
for (size_t i = t; i < files_.size(); i += nthreads)
angles[i] = smv::ReadHeader(files_[i]).start_angle_deg;
}));
for (auto &f : futures)
f.get();
}
double increment = header0_.angle_increment_deg;
if (files_.size() > 1) {
// Prefer the measured step over the header's nominal one, and unwrap a sweep that passes 360.
double d = angles[1] - angles[0];
if (d < -180.0) d += 360.0;
if (std::abs(d) > 1e-6) increment = d;
}
dataset_->experiment.Goniometer(GoniometerAxis(header0_.axis_name,
static_cast<float>(angles.front()),
static_cast<float>(increment),
ASSUMED_BASE_AXIS, {}));
dataset_->error_value = -1;
dataset_->experiment.ImagesPerTrigger(static_cast<int64_t>(files_.size()));
// A CCD frame stores no untrusted-pixel marker - every value is a real reading, and the
// detector has no module gaps - so the sweep starts with nothing masked.
dataset_->pixel_mask = std::make_shared<const PixelMask>(static_cast<size_t>(header0_.nx),
static_cast<size_t>(header0_.ny));
SetStartMessage(dataset_);
}
uint64_t JFJochSMVReader::GetNumberOfImages() const {
return files_.size();
}
void JFJochSMVReader::Close() {
files_.clear();
dataset_.reset();
}
template <class Buffer>
CompressedImage JFJochSMVReader::DecodeInto(int64_t image_number, Buffer &buffer,
std::vector<uint8_t> &scratch) const {
if (image_number < 0 || static_cast<size_t>(image_number) >= files_.size())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Image number out of range");
const size_t npixel = static_cast<size_t>(header0_.nx) * static_cast<size_t>(header0_.ny);
buffer.resize(npixel * sizeof(int32_t));
const auto h = smv::ReadInto(files_[image_number],
reinterpret_cast<int32_t *>(buffer.data()), npixel, scratch);
if (h.nx != header0_.nx || h.ny != header0_.ny)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"SMV image size differs from the first image of the sweep");
return CompressedImage(buffer.data(), buffer.size(),
static_cast<size_t>(header0_.nx), static_cast<size_t>(header0_.ny),
CompressedImageMode::Int32, CompressionAlgorithm::NO_COMPRESSION);
}
bool JFJochSMVReader::LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset,
DataMessage &message,
std::vector<uint8_t> &buffer,
int64_t image_number,
bool update_dataset) {
(void) update_dataset;
if (!dataset)
return false;
std::vector<uint8_t> scratch;
message.image = DecodeInto(image_number, buffer, scratch);
message.number = image_number;
return true;
}
bool JFJochSMVReader::ReadRawImage(int64_t image_number, JFJochReaderRawImage &image) {
image.image = DecodeInto(image_number, image.image_buffer, image.read_buffer);
return true;
}
std::vector<SpotToSave> JFJochSMVReader::ReadSpots(int64_t) const {
return {}; // a raw SMV file stores no analysis results
}
+57
View File
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <string>
#include <vector>
#include "JFJochReader.h"
#include "SMV.h"
// Reads a rotation sweep straight from a directory of SMV files - what ADSC Quantum detectors
// wrote and what Rayonix and others still write, so most archived CCD data from the 2000s is in
// this format.
//
// The same shape as JFJochMarCCDReader, which it is otherwise a sibling of: the sweep's geometry
// comes from the first file's header, the rotation angle of each image from its own, and images
// are decoded on demand. A raw SMV file carries no analysis results, so no spots and no
// reflections.
//
// SMV states no saturation value and marks no untrusted pixels, so nothing is masked and no pixel
// is called an overload from the file alone - unlike marCCD, which at least carries a saturated
// value. The beam centre it states is in millimetres and its convention varies between writers;
// see the note in smv::Header.
class JFJochSMVReader : public JFJochReader {
std::vector<std::string> files_;
std::shared_ptr<JFJochReaderDataset> dataset_;
smv::Header header0_;
bool LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset,
DataMessage &message,
std::vector<uint8_t> &buffer,
int64_t image_number,
bool update_dataset) override;
// Decodes one image into the caller's byte buffer and returns the image that points at it. The
// stored 16-bit strip is read through scratch, which the caller keeps between frames.
template <class Buffer>
CompressedImage DecodeInto(int64_t image_number, Buffer &buffer, std::vector<uint8_t> &scratch) const;
public:
~JFJochSMVReader() override = default;
// True if the path names something this reader can open: a marCCD file, or a directory holding
// at least one. Cheap - it reads a few kB at most.
static bool CanRead(const std::string &path);
// path is a directory of marCCD frames, or one frame inside the sweep to take the whole sweep
// from.
void ReadFiles(const std::string &path);
[[nodiscard]] uint64_t GetNumberOfImages() const override;
void Close() override;
bool ReadRawImage(int64_t image_number, JFJochReaderRawImage &image) override;
[[nodiscard]] std::vector<SpotToSave> ReadSpots(int64_t image) const override;
};
+49
View File
@@ -3,6 +3,8 @@
#include "MiniCBF.h"
#include <zlib.h>
#include <algorithm>
#include <cctype>
#include <cstring>
@@ -374,7 +376,54 @@ void DecodeByteOffset(const uint8_t *data, size_t size, int32_t *out, size_t n_p
namespace {
// Whether the file is gzip-wrapped, from its first two bytes rather than its name - a sweep is
// named consistently but the magic is what decides how to read it.
bool IsGzip(const std::string &path) {
std::ifstream f(path, std::ios::binary);
unsigned char m[2] = {0, 0};
if (f)
f.read(reinterpret_cast<char *>(m), sizeof(m));
return m[0] == 0x1f && m[1] == 0x8b;
}
// The gzip path. EMBL Hamburg's beamlines write .cbf.gz by default, so a sweep from there is
// otherwise twice the disk and a decompression pass before anything can read a frame. The
// decompressed size is not in the stream, so the buffer grows in chunks instead of being sized
// up front the way the plain path can.
void SlurpGz(const std::string &path, size_t max_bytes, std::vector<uint8_t> &buf) {
gzFile g = gzopen(path.c_str(), "rb");
if (g == nullptr)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Cannot open gzipped CBF file " + path);
buf.clear();
constexpr size_t CHUNK = 1u << 20;
while (buf.size() < max_bytes) {
const size_t want = std::min(CHUNK, max_bytes - buf.size());
const size_t at = buf.size();
buf.resize(at + want);
const int got = gzread(g, buf.data() + at, static_cast<unsigned>(want));
if (got < 0) {
int err = 0;
const std::string msg = gzerror(g, &err);
gzclose(g);
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Error decompressing " + path + ": " + msg);
}
buf.resize(at + static_cast<size_t>(got));
if (static_cast<size_t>(got) < want)
break; // end of the stream
}
gzclose(g);
}
void Slurp(const std::string &path, size_t max_bytes, std::vector<uint8_t> &buf) {
// The plain path is kept exactly as it was rather than reading everything through zlib: a
// gzFile over an uncompressed file works, but it copies every byte through zlib's own buffer,
// and the overwhelming majority of sweeps are not compressed.
if (IsGzip(path)) {
SlurpGz(path, max_bytes, buf);
return;
}
std::ifstream f(path, std::ios::binary);
if (!f)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
+283
View File
@@ -0,0 +1,283 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "SMV.h"
#include <algorithm>
#include <cctype>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <map>
#include <optional>
#include <tuple>
#include "../common/JFJochException.h"
namespace smv {
namespace {
// The brace block is never long - 512 or 1024 bytes in everything seen - but HEADER_BYTES states
// the real length and is itself inside the block, so read a generous prefix and trust the value.
constexpr size_t PROBE_BYTES = 8192;
std::vector<uint8_t> ReadPrefix(const std::string &path, size_t bytes) {
std::ifstream f(path, std::ios::binary);
if (!f)
throw JFJochException(JFJochExceptionCategory::MockFileOpenError, "Cannot open " + path);
std::vector<uint8_t> out(bytes);
f.read(reinterpret_cast<char *>(out.data()), static_cast<std::streamsize>(bytes));
out.resize(static_cast<size_t>(f.gcount()));
return out;
}
std::string Trim(std::string s) {
const auto ws = [](unsigned char c) { return std::isspace(c) != 0; };
while (!s.empty() && ws(s.front())) s.erase(s.begin());
while (!s.empty() && ws(s.back())) s.pop_back();
return s;
}
// "{ KEY=value; KEY=value; }" -> the pairs. Nothing here is a CIF or a JSON: a key is everything
// before the first '=', a value everything to the next ';', and the block ends at the closing brace.
std::optional<std::map<std::string, std::string>> ParseBlock(const std::vector<uint8_t> &buf) {
if (buf.empty() || buf.front() != '{')
return {};
const std::string text(reinterpret_cast<const char *>(buf.data()), buf.size());
const size_t end = text.find('}');
if (end == std::string::npos)
return {};
std::map<std::string, std::string> kv;
size_t at = 1;
while (at < end) {
const size_t semi = text.find(';', at);
if (semi == std::string::npos || semi > end)
break;
const std::string item = text.substr(at, semi - at);
const size_t eq = item.find('=');
if (eq != std::string::npos) {
std::string key = Trim(item.substr(0, eq));
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return std::toupper(c); });
kv[key] = Trim(item.substr(eq + 1));
}
at = semi + 1;
}
return kv;
}
double Num(const std::map<std::string, std::string> &kv, const std::string &key, double dflt = 0.0) {
const auto it = kv.find(key);
if (it == kv.end()) return dflt;
try { return std::stod(it->second); } catch (const std::exception &) { return dflt; }
}
// The first key of `keys` the header actually carries. SMV accumulated synonyms over twenty years
// of writers and the spellings are not interchangeable between files, only between vendors.
double NumAny(const std::map<std::string, std::string> &kv,
std::initializer_list<const char *> keys, double dflt = 0.0) {
for (const char *k : keys) {
const auto it = kv.find(k);
if (it != kv.end()) {
try { return std::stod(it->second); } catch (const std::exception &) {}
}
}
return dflt;
}
std::optional<std::map<std::string, std::string>> HeaderBlock(const std::string &path) {
return ParseBlock(ReadPrefix(path, PROBE_BYTES));
}
Header Parse(const std::map<std::string, std::string> &kv, const std::string &path) {
Header h;
h.raw = kv;
h.nx = static_cast<int64_t>(Num(kv, "SIZE1"));
h.ny = static_cast<int64_t>(Num(kv, "SIZE2"));
if (h.nx <= 0 || h.ny <= 0)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
path + ": SMV header states no image size");
h.data_offset = static_cast<size_t>(Num(kv, "HEADER_BYTES", 512));
const auto order = kv.find("BYTE_ORDER");
h.little_endian = (order == kv.end()) || order->second.find("little") != std::string::npos;
const auto type = kv.find("TYPE");
if (type != kv.end() && type->second.find("short") == std::string::npos)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
path + ": only 16-bit SMV images are supported (TYPE=" + type->second + ")");
h.bytes_per_pixel = 2;
// A single PIXEL_SIZE is the usual spelling; the split pair appears on a few writers.
const double px = NumAny(kv, {"PIXEL_SIZE", "PIXEL_SIZE_X", "PIXELSIZE"});
const double py = NumAny(kv, {"PIXEL_SIZE_Y", "PIXEL_SIZE", "PIXELSIZE"});
h.pixel_x_m = px * 1e-3; // millimetres in the file
h.pixel_y_m = (py > 0 ? py : px) * 1e-3;
h.distance_m = NumAny(kv, {"DISTANCE", "DETECTOR_DISTANCE"}) * 1e-3;
// Millimetres in the file, pixels everywhere in this program. BEAM_CENTRE is the British
// spelling some writers use; ADSC's own header uses BEAM_CENTER.
const double bx_mm = NumAny(kv, {"BEAM_CENTER_X", "BEAM_CENTRE_X", "BEAM_X"});
const double by_mm = NumAny(kv, {"BEAM_CENTER_Y", "BEAM_CENTRE_Y", "BEAM_Y"});
h.beam_x_px = h.pixel_x_m > 0 ? bx_mm * 1e-3 / h.pixel_x_m : 0.0;
h.beam_y_px = h.pixel_y_m > 0 ? by_mm * 1e-3 / h.pixel_y_m : 0.0;
h.wavelength_A = NumAny(kv, {"WAVELENGTH", "SOURCE_WAVELENGTH"});
h.angle_increment_deg = NumAny(kv, {"OSC_RANGE", "OSCILLATION_RANGE"});
// OSC_START is the angle of THIS image; PHI is where the circle stands, which is the same
// thing on the single-axis goniometers that write this format, and is the only value some
// writers give.
h.start_angle_deg = NumAny(kv, {"OSC_START", "PHI", "START_PHI", "OMEGA"});
h.two_theta_deg = NumAny(kv, {"TWOTHETA", "TWO_THETA", "DETECTOR_2THETA"});
h.exposure_s = NumAny(kv, {"TIME", "EXPOSURE_TIME"});
const auto sn = kv.find("DETECTOR_SN");
h.detector = sn != kv.end() ? ("SMV detector S/N " + sn->second) : "SMV";
// Which circle the file says moved. OSC_AXIS names it where present; otherwise these are
// single-axis collections and the name is the conventional one.
const auto axis = kv.find("OSC_AXIS");
if (axis != kv.end() && !axis->second.empty()) {
std::string a = axis->second;
std::transform(a.begin(), a.end(), a.begin(), [](unsigned char c) { return std::tolower(c); });
h.axis_name = a;
}
return h;
}
// Extensions an SMV frame is written under. The content test below is what actually decides - .img
// is also used by miniCBF and by marCCD - so this is only a cheap pre-filter.
bool PlausibleExtension(const std::filesystem::path &p) {
std::string ext = p.extension().string();
if (ext.empty())
return false;
ext.erase(ext.begin());
if (std::all_of(ext.begin(), ext.end(), [](unsigned char c) { return std::isdigit(c); }))
return true;
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
return ext == "img" || ext == "smv" || ext == "osc";
}
// The same sweep rule as the marCCD reader: the last run of digits in the whole file name, so
// "xtal_1_00042.img" and "xtal.042" are both handled by one rule.
struct Template {
std::string prefix;
size_t digits = 0;
std::string suffix;
bool operator<(const Template &o) const {
return std::tie(prefix, digits, suffix) < std::tie(o.prefix, o.digits, o.suffix);
}
bool Matches(const std::string &name) const {
if (name.size() != prefix.size() + digits + suffix.size()) return false;
if (name.compare(0, prefix.size(), prefix) != 0) return false;
if (name.compare(prefix.size() + digits, suffix.size(), suffix) != 0) return false;
for (size_t i = 0; i < digits; i++)
if (!std::isdigit(static_cast<unsigned char>(name[prefix.size() + i]))) return false;
return true;
}
};
std::optional<Template> TemplateOf(const std::string &name) {
size_t end = name.size();
while (end > 0 && !std::isdigit(static_cast<unsigned char>(name[end - 1]))) end--;
if (end == 0) return {};
size_t start = end;
while (start > 0 && std::isdigit(static_cast<unsigned char>(name[start - 1]))) start--;
return Template{name.substr(0, start), end - start, name.substr(end)};
}
} // namespace
std::vector<std::string> CollectSweep(const std::string &path) {
std::filesystem::path p(path);
std::error_code ec;
const bool is_dir = std::filesystem::is_directory(p, ec);
std::filesystem::path dir = is_dir ? p : p.parent_path();
if (dir.empty()) dir = ".";
std::optional<Template> want;
if (!is_dir) want = TemplateOf(p.filename().string());
std::map<Template, std::vector<std::string>> sweeps;
for (const auto &e : std::filesystem::directory_iterator(dir, ec)) {
if (!e.is_regular_file() || !PlausibleExtension(e.path())) continue;
const std::string name = e.path().filename().string();
const auto t = TemplateOf(name);
if (!t.has_value()) continue;
if (want.has_value() && !want->Matches(name)) continue;
sweeps[*t].push_back(e.path().string());
}
std::vector<std::string> out;
for (auto &[key, files] : sweeps)
if (files.size() > out.size()) out = std::move(files);
std::sort(out.begin(), out.end());
// The name rule matches plenty that is not SMV, so the chosen sweep is confirmed against the
// one thing only an SMV file has: a brace block declaring an image size.
if (!out.empty()) {
const auto kv = HeaderBlock(out.front());
if (!kv.has_value() || !kv->count("SIZE1"))
return {};
}
return out;
}
bool CanRead(const std::string &path) {
std::error_code ec;
if (std::filesystem::is_directory(path, ec))
return !CollectSweep(path).empty();
if (!PlausibleExtension(std::filesystem::path(path)))
return false;
try {
const auto kv = HeaderBlock(path);
return kv.has_value() && kv->count("SIZE1") && kv->count("SIZE2");
} catch (const JFJochException &) {
return false;
}
}
Header ReadHeader(const std::string &path) {
const auto kv = HeaderBlock(path);
if (!kv.has_value())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
path + " carries no SMV header block");
return Parse(*kv, path);
}
Header ReadInto(const std::string &path, int32_t *out, size_t capacity, std::vector<uint8_t> &scratch) {
const Header h = ReadHeader(path);
const size_t npixel = static_cast<size_t>(h.nx) * static_cast<size_t>(h.ny);
if (npixel > capacity)
throw JFJochException(JFJochExceptionCategory::ArrayOutOfBounds,
path + ": image is larger than the buffer given for it");
std::ifstream f(path, std::ios::binary);
if (!f)
throw JFJochException(JFJochExceptionCategory::MockFileOpenError, "Cannot open " + path);
f.seekg(static_cast<std::streamoff>(h.data_offset), std::ios::beg);
scratch.resize(npixel * sizeof(uint16_t));
f.read(reinterpret_cast<char *>(scratch.data()), static_cast<std::streamsize>(scratch.size()));
if (static_cast<size_t>(f.gcount()) != scratch.size())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
path + ": file ends before the image does");
// Stored unsigned 16-bit, handed out as the signed 32-bit the rest of the code reads.
const uint8_t *src = scratch.data();
if (h.little_endian) {
for (size_t i = 0; i < npixel; i++)
out[i] = static_cast<int32_t>(static_cast<uint16_t>(src[2 * i] | (src[2 * i + 1] << 8)));
} else {
for (size_t i = 0; i < npixel; i++)
out[i] = static_cast<int32_t>(static_cast<uint16_t>((src[2 * i] << 8) | src[2 * i + 1]));
}
return h;
}
Header Read(const std::string &path, std::vector<int32_t> &out) {
const Header h = ReadHeader(path);
out.resize(static_cast<size_t>(h.nx) * static_cast<size_t>(h.ny));
std::vector<uint8_t> scratch;
return ReadInto(path, out.data(), out.size(), scratch);
}
} // namespace smv
+70
View File
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <cstdint>
#include <map>
#include <string>
#include <vector>
// SMV: the format ADSC Quantum detectors wrote, and which Rayonix and several others still write.
// A plain ASCII header of "KEY=value;" lines between braces, then the pixels - no container, no
// compression, no binary header to decode by offset. The header states its own length, so even
// that is not assumed.
//
// {
// HEADER_BYTES= 512; DIM=2; BYTE_ORDER=little_endian; TYPE=unsigned_short;
// SIZE1=3072; SIZE2=3072; PIXEL_SIZE=0.102588; DISTANCE=250.000000;
// OSC_START=325.000000; OSC_RANGE=0.200000; WAVELENGTH=0.999839;
// BEAM_CENTER_X=157.500000; BEAM_CENTER_Y=157.500000;
// }
//
// Note the beam centre is in MILLIMETRES, not pixels, and its convention is the one thing writers
// disagree about - see the comment on beam_x_mm in the struct.
namespace smv {
struct Header {
int64_t nx = 0; // SIZE1, the fast dimension
int64_t ny = 0; // SIZE2, the slow dimension
size_t data_offset = 0; // HEADER_BYTES
bool little_endian = true;
int64_t bytes_per_pixel = 2; // TYPE: unsigned_short
double pixel_x_m = 0;
double pixel_y_m = 0;
double distance_m = 0;
// BEAM_CENTER_X/Y, converted to pixels here. Writers disagree about this field more than about
// any other: it is in millimetres, and which of the two is the fast direction is a convention
// rather than a rule. Where a file also carries the ADSC-era synonyms (BEAM_CENTRE_X, or the
// MOSFLM-ordered pair) they are read too. A run whose centre is wrong is recoverable - the
// beam-centre estimator measures it from the data - but a wrong one is not detectable here.
double beam_x_px = 0;
double beam_y_px = 0;
double wavelength_A = 0;
double start_angle_deg = 0; // OSC_START, falling back to PHI
double angle_increment_deg = 0; // OSC_RANGE
double two_theta_deg = 0;
double exposure_s = 0;
std::string detector; // DETECTOR_SN, where stated
std::string axis_name = "phi";
std::map<std::string, std::string> raw; // every key, for anything not modelled above
};
// True if the path names something this reader can open: an SMV file, or a directory holding at
// least one. Reads a few hundred bytes at most.
bool CanRead(const std::string &path);
// The files of one sweep, in collection order. path is a directory, or one frame inside it.
std::vector<std::string> CollectSweep(const std::string &path);
// Header only - reads just the leading brace block. Cheap enough to call per frame.
Header ReadHeader(const std::string &path);
// Header + pixels. out is resized to nx * ny.
Header Read(const std::string &path, std::vector<int32_t> &out);
// The same, widening the stored pixels into memory the caller already has. scratch carries the
// raw bytes between frames so a worker walking a sweep allocates once.
Header ReadInto(const std::string &path, int32_t *out, size_t capacity, std::vector<uint8_t> &scratch);
} // namespace smv
+9 -1
View File
@@ -20,6 +20,7 @@
#include "../reader/JFJochHDF5Reader.h"
#include "../reader/JFJochCBFReader.h"
#include "../reader/JFJochMarCCDReader.h"
#include "../reader/JFJochSMVReader.h"
#include "../common/Logger.h"
#include "../common/Definitions.h"
#include "../common/DiffractionExperiment.h"
@@ -1386,9 +1387,11 @@ static int RunRugnux(int argc, char **argv) {
JFJochHDF5Reader hdf5_reader;
JFJochCBFReader cbf_reader;
JFJochMarCCDReader marccd_reader;
JFJochSMVReader smv_reader;
JFJochReader *reader_ptr = nullptr;
const bool input_is_cbf = JFJochCBFReader::CanRead(input_file);
const bool input_is_marccd = !input_is_cbf && JFJochMarCCDReader::CanRead(input_file);
const bool input_is_smv = !input_is_cbf && !input_is_marccd && JFJochSMVReader::CanRead(input_file);
try {
if (input_is_cbf) {
cbf_reader.ReadFiles(input_file);
@@ -1400,6 +1403,11 @@ static int RunRugnux(int argc, char **argv) {
reader_ptr = &marccd_reader;
logger.Info("Read {} marCCD images of the sweep containing {}",
marccd_reader.GetNumberOfImages(), input_file);
} else if (input_is_smv) {
smv_reader.ReadFiles(input_file);
reader_ptr = &smv_reader;
logger.Info("Read {} SMV images of the sweep containing {}",
smv_reader.GetNumberOfImages(), input_file);
} else {
hdf5_reader.ReadFile(input_file);
reader_ptr = &hdf5_reader;
@@ -1492,7 +1500,7 @@ static int RunRugnux(int argc, char **argv) {
"runs no space-group search. Use --mode mx, which builds the P1 cross-check "
"merge the ledger is folded from.");
// Re-scaling reads reflections a previous run integrated, which only a _process.h5 holds.
if (input_is_cbf || input_is_marccd) {
if (input_is_cbf || input_is_marccd || input_is_smv) {
logger.Error("--mode scale needs the integrated reflections in a _process.h5; "
"a raw image sweep holds none. Run --mode mx on it first.");
exit(EXIT_FAILURE);
+116
View File
@@ -9,6 +9,7 @@
#include "../reader/JFJochHDF5Reader.h"
#include "../reader/JFJochCBFReader.h"
#include "../reader/JFJochMarCCDReader.h"
#include "../reader/JFJochSMVReader.h"
#include "../reader/MiniCBF.h"
#include "../compression/JFJochCompressor.h"
@@ -4291,3 +4292,118 @@ TEST_CASE("JFJochMarCCDReader_Geometry", "[HDF5][Full]") {
remove("marccdplain_001.tif");
}
}
// SMV: an ASCII "KEY=value;" block between braces, then the pixels. The fixtures are written by
// hand here, as for marCCD, because the layout is what the reader has to cope with.
namespace {
struct SMVFields {
int nx = 24, ny = 16;
double pixel_mm = 0.1;
double distance_mm = 250.0;
double osc_start = 5.0, osc_range = 0.2;
double wavelength = 0.9998;
double beam_x_mm = 1.2, beam_y_mm = 0.8;
std::string byte_order = "little_endian";
bool write_header = true;
};
void WriteSMVFrame(const std::string &name, const SMVFields &f) {
std::ostringstream h;
h << "{\n";
if (f.write_header) {
h << "HEADER_BYTES= 512;\nDIM=2;\nBYTE_ORDER=" << f.byte_order << ";\n"
<< "TYPE=unsigned_short;\nSIZE1=" << f.nx << ";\nSIZE2=" << f.ny << ";\n"
<< "PIXEL_SIZE=" << f.pixel_mm << ";\nDISTANCE=" << f.distance_mm << ";\n"
<< "OSC_START=" << f.osc_start << ";\nOSC_RANGE=" << f.osc_range << ";\n"
<< "WAVELENGTH=" << f.wavelength << ";\n"
<< "BEAM_CENTER_X=" << f.beam_x_mm << ";\nBEAM_CENTER_Y=" << f.beam_y_mm << ";\n";
} else {
h << "SOMETHING_ELSE=1;\n"; // a brace block that is not an image header
}
h << "}\n";
std::string head = h.str();
head.resize(512, '\f'); // pad to HEADER_BYTES, as every writer does
std::ofstream out(name, std::ios::binary);
out.write(head.data(), static_cast<std::streamsize>(head.size()));
const std::vector<char> pixels(static_cast<size_t>(f.nx) * f.ny * 2, 0);
out.write(pixels.data(), static_cast<std::streamsize>(pixels.size()));
}
void WriteSMVSweep(const std::string &prefix, const std::string &suffix, SMVFields f = {}) {
for (int frame = 1; frame <= 2; frame++) {
f.osc_start = 5.0 + (frame - 1) * 0.2;
std::ostringstream name;
name << prefix << std::setfill('0') << std::setw(3) << frame << suffix;
WriteSMVFrame(name.str(), f);
}
}
void RemoveSMVSweep(const std::string &prefix, const std::string &suffix) {
for (int frame = 1; frame <= 2; frame++) {
std::ostringstream name;
name << prefix << std::setfill('0') << std::setw(3) << frame << suffix;
remove(name.str().c_str());
}
}
}
TEST_CASE("JFJochSMVReader_Geometry", "[HDF5][Full]") {
SECTION("the header's geometry reaches the experiment in the units the rest of the code uses") {
WriteSMVSweep("smv_", ".img");
REQUIRE(JFJochSMVReader::CanRead("smv_001.img"));
JFJochSMVReader reader;
REQUIRE_NOTHROW(reader.ReadFiles("smv_001.img"));
CHECK(reader.GetNumberOfImages() == 2);
const auto x = reader.GetDataset()->experiment;
CHECK(x.GetDetectorDistance_mm() == Catch::Approx(250.0));
CHECK(x.GetPixelSize_mm() == Catch::Approx(0.1));
CHECK(x.GetWavelength_A() == Catch::Approx(0.9998).epsilon(1e-4));
// The file states the beam centre in MILLIMETRES; the rest of the code wants pixels.
CHECK(x.GetBeamX_pxl() == Catch::Approx(12.0));
CHECK(x.GetBeamY_pxl() == Catch::Approx(8.0));
REQUIRE(x.GetGoniometer().has_value());
CHECK(x.GetGoniometer()->GetStart_deg() == Catch::Approx(5.0));
CHECK(x.GetGoniometer()->GetIncrement_deg() == Catch::Approx(0.2));
reader.Close();
RemoveSMVSweep("smv_", ".img");
}
SECTION("a brace block that is not an image header is refused") {
SMVFields f; f.write_header = false;
WriteSMVFrame("smvbare_001.img", f);
CHECK_FALSE(JFJochSMVReader::CanRead("smvbare_001.img"));
remove("smvbare_001.img");
}
}
TEST_CASE("JFJochCBFReader_GzippedSweep", "[HDF5][Full]") {
// EMBL Hamburg writes .cbf.gz by default. The reader decompresses in place, so a gzipped sweep
// opens exactly as a plain one does and needs no conversion step.
const int64_t nx = 24, ny = 16;
WriteMiniCBFSweep("cbfgz", "# Oscillation_axis OMEGA\n", nx, ny);
for (int frame = 1; frame <= 2; frame++) {
std::ostringstream name;
name << "cbfgz_" << std::setfill('0') << std::setw(4) << frame << ".cbf";
// gzip the frame in place, so the directory holds only the .cbf.gz form
const std::string cmd = "gzip -f " + name.str();
REQUIRE(std::system(cmd.c_str()) == 0);
}
REQUIRE(JFJochCBFReader::CanRead("cbfgz_0001.cbf.gz"));
JFJochCBFReader reader;
REQUIRE_NOTHROW(reader.ReadFiles("cbfgz_0001.cbf.gz"));
CHECK(reader.GetNumberOfImages() == 2);
const auto x = reader.GetDataset()->experiment;
CHECK(x.GetDetectorDistance_mm() == Catch::Approx(331.61));
CHECK(x.GetBeamX_pxl() == Catch::Approx(12.0));
CHECK(x.GetWavelength_A() == Catch::Approx(0.96864).epsilon(1e-5));
JFJochReaderRawImage image;
REQUIRE(reader.ReadRawImage(0, image));
CHECK(image.image.GetWidth() == nx);
CHECK(image.image.GetHeight() == ny);
reader.Close();
for (int frame = 1; frame <= 2; frame++) {
std::ostringstream name;
name << "cbfgz_" << std::setfill('0') << std::setw(4) << frame << ".cbf.gz";
remove(name.str().c_str());
}
}
+17 -5
View File
@@ -108,6 +108,8 @@ JFJochImageReadingWorker::JFJochImageReadingWorker(const SpotFindingSettings &se
http_reader.Experiment(experiment);
hdf5_reader.Experiment(experiment);
cbf_reader.Experiment(experiment);
marccd_reader.Experiment(experiment);
smv_reader.Experiment(experiment);
autoload_timer = new QTimer(this);
autoload_timer->setInterval(autoload_interval);
@@ -277,16 +279,26 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_
file_open_retry_attempts++;
}
// Naming one miniCBF frame opens the sweep it belongs to; anything else is HDF5.
// Naming one frame of a raw sweep opens the whole sweep it belongs to; anything else
// is HDF5. The three raw formats are told apart by their CONTENT, never by the file
// name - ".img" is used by all of miniCBF, marCCD and SMV depending on the writer.
// Release the previously open file first, so switching format leaves nothing behind.
file_reader->Close();
cbf_mode = JFJochCBFReader::CanRead(filename.toStdString());
if (cbf_mode) {
const std::string path = filename.toStdString();
cbf_mode = true;
if (JFJochCBFReader::CanRead(path)) {
file_reader = &cbf_reader;
cbf_reader.ReadFiles(filename.toStdString());
cbf_reader.ReadFiles(path);
} else if (JFJochMarCCDReader::CanRead(path)) {
file_reader = &marccd_reader;
marccd_reader.ReadFiles(path);
} else if (JFJochSMVReader::CanRead(path)) {
file_reader = &smv_reader;
smv_reader.ReadFiles(path);
} else {
cbf_mode = false;
file_reader = &hdf5_reader;
hdf5_reader.ReadFile(filename.toStdString());
hdf5_reader.ReadFile(path);
}
total_images = file_reader->GetNumberOfImages();
dataset = file_reader->GetDataset();
+9 -4
View File
@@ -13,6 +13,8 @@
#include "../reader/JFJochHDF5Reader.h"
#include "../reader/JFJochCBFReader.h"
#include "../reader/JFJochMarCCDReader.h"
#include "../reader/JFJochSMVReader.h"
#include "../common/Logger.h"
#include "JFJochHttpReader.h"
#include "../image_analysis/MXAnalysisWithoutFPGA.h"
@@ -68,13 +70,16 @@ private:
AutoloadMode autoload_mode = AutoloadMode::None;
// A stored file is read by one of these two, whichever its format needs; file_reader points at
// the one in use. The HDF5-only extras - calibration images and reprocessing snapshots - always
// go through hdf5_reader, and a raw CBF sweep simply has none of them.
// A stored file is read by one of these, whichever its format needs; file_reader points at the
// one in use. The HDF5-only extras - calibration images and reprocessing snapshots - always go
// through hdf5_reader, and a raw image sweep simply has none of them, so raw_mode covers all
// three of the raw formats rather than CBF alone.
JFJochHDF5Reader hdf5_reader;
JFJochCBFReader cbf_reader;
JFJochMarCCDReader marccd_reader;
JFJochSMVReader smv_reader;
JFJochReader *file_reader = &hdf5_reader;
bool cbf_mode = false;
bool cbf_mode = false; // "a raw sweep is open", whichever of the three formats it is
JFJochHttpReader http_reader;
+14 -3
View File
@@ -3,6 +3,8 @@
#include "JFJochProcessController.h"
#include "../reader/JFJochCBFReader.h"
#include "../reader/JFJochMarCCDReader.h"
#include "../reader/JFJochSMVReader.h"
#include "../reader/JFJochHDF5Reader.h"
#include "../rugnux/ResultReport.h"
@@ -49,12 +51,21 @@ void JFJochProcessController::run_(QString file_path, DiffractionExperiment expe
try {
JFJochHDF5Reader hdf5_reader;
JFJochCBFReader cbf_reader;
JFJochMarCCDReader marccd_reader;
JFJochSMVReader smv_reader;
JFJochReader *reader_ptr;
if (JFJochCBFReader::CanRead(file_path.toStdString())) {
cbf_reader.ReadFiles(file_path.toStdString());
const std::string path = file_path.toStdString();
if (JFJochCBFReader::CanRead(path)) {
cbf_reader.ReadFiles(path);
reader_ptr = &cbf_reader;
} else if (JFJochMarCCDReader::CanRead(path)) {
marccd_reader.ReadFiles(path);
reader_ptr = &marccd_reader;
} else if (JFJochSMVReader::CanRead(path)) {
smv_reader.ReadFiles(path);
reader_ptr = &smv_reader;
} else {
hdf5_reader.ReadFile(file_path.toStdString());
hdf5_reader.ReadFile(path);
reader_ptr = &hdf5_reader;
}
JFJochReader &reader = *reader_ptr;
+1 -1
View File
@@ -238,7 +238,7 @@ void JFJochViewerMenu::openSelected() {
"Open File", // Dialog title
"", // Default folder
// A CBF is one frame per file: picking any frame opens the sweep it belongs to.
"HDF5 Master Files (*_master.h5 *_process.h5);; HDF5 Files (*.h5);; CBF Images (*.cbf);;All Files (*)"
"HDF5 Master Files (*_master.h5 *_process.h5);; HDF5 Files (*.h5);; CBF Images (*.cbf *.cbf.gz);; CCD Images (*.img *.mccd *.smv);;All Files (*)"
);
if (!fileName.isEmpty())