v1.0.0-rc.166 (#76)
Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / build:windows:nocuda (push) Successful in 18m0s
Build Packages / build:windows:cuda (push) Successful in 20m30s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m39s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:rugnux:windows (push) Successful in 11m25s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m27s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m43s
Build Packages / build:rpm (rocky9) (push) Successful in 13m34s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m19s
Build Packages / DIALS test (push) Successful in 12m36s
Build Packages / XDS test (durin plugin) (push) Successful in 6m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m11s
Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / build:windows:nocuda (push) Successful in 18m0s
Build Packages / build:windows:cuda (push) Successful in 20m30s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m39s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:rugnux:windows (push) Successful in 11m25s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m27s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m43s
Build Packages / build:rpm (rocky9) (push) Successful in 13m34s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m19s
Build Packages / DIALS test (push) Successful in 12m36s
Build Packages / XDS test (durin plugin) (push) Successful in 6m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m11s
* `rugnux --mode calibration` writes `<prefix>.json` beside the `.poni`, whose `dataset_settings` member is a `jfjoch_broker` `dataset_settings` body as it stands. * `rugnux` and `jfjoch_viewer` read PILATUS miniCBF sweeps natively, without conversion. * Masters written by other facilities open, including Eiger 1.x and third-party NXmx variants. * `rugnux` measures the beam centre on every run, and indexes with it when the file's value indexes nothing. * A detector swung out on a 2theta arm is placed where the file says it stands, and the calibration can hold the tilt fixed. * `rugnux` writes the unmerged MTZ by default, and a P1 merge beside it, so a wrong space group can be re-merged without reprocessing. * Significant improvements to symmetry handling in `rugnux`: the lattice, the point group, the setting and the systematic absences. * The `rugnux` report gives the resolution the CC1/2 fit reached, beside the range the reflections were written to. * The `rugnux` report gives the twinning statistics measured before the space group was decided, beside the ones measured after. * The `rugnux` report gives the strong-direction diffraction limit, and warns when CC1/2 is not monotone with resolution. * `rugnux` ranks screw axes on the evidence their absences carry, rather than on how many control reflections a candidate happens to have. * Twinning is no longer reported when the L-test contradicts it. * The `rugnux` report gives the detector tilt, the measured tilt and the direct beam beside the beam centre, and a post-refined beam centre is judged against the run's own measurement rather than the file's. * `--no-refine-tilt` holds the detector tilt at the value in the file, instead of zeroing it, when the calibration starts from the spots. * The `jfjoch_viewer` grid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area. Reviewed-on: #76 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
This commit was merged in pull request #76.
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochCBFReader.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
#include "../common/JFJochException.h"
|
||||
#include "../common/Logger.h"
|
||||
#include "../common/JFJochMath.h"
|
||||
|
||||
namespace {
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
// The sweep a file belongs to, as a template: everything before the trailing run of digits, the
|
||||
// number of digits, and the extension. "o8_1_0042.cbf" -> {"o8_1_", 4}. A directory can hold several
|
||||
// sweeps ("o8_1_*" beside "o8_2_*"), so collecting every .cbf in it would silently splice two
|
||||
// crystals together; matching the template is what makes "point at any frame" safe.
|
||||
struct Template {
|
||||
std::string prefix;
|
||||
size_t digits = 0;
|
||||
|
||||
bool Matches(const std::string &name) const {
|
||||
if (name.size() != prefix.size() + digits + 4) // + ".cbf"
|
||||
return false;
|
||||
if (name.compare(0, prefix.size(), prefix) != 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 &filename) {
|
||||
const std::filesystem::path p(filename);
|
||||
if (!HasCBFExtension(p))
|
||||
return {};
|
||||
const std::string stem = p.stem().string();
|
||||
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};
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectSweep(const std::string &path) {
|
||||
std::filesystem::path p(path);
|
||||
const bool is_dir = std::filesystem::is_directory(p);
|
||||
// A frame named with no directory at all is in this one - parent_path() of a bare filename is
|
||||
// empty, and iterating an empty path finds nothing, so naming a frame from inside its own
|
||||
// directory found no sweep.
|
||||
std::filesystem::path dir = is_dir ? p : p.parent_path();
|
||||
if (dir.empty())
|
||||
dir = ".";
|
||||
|
||||
// Naming a frame selects ITS sweep. Naming a directory selects the sweep with the most frames in
|
||||
// it, which is the one a user pointing at a data directory means.
|
||||
std::optional<Template> want;
|
||||
if (!is_dir)
|
||||
want = TemplateOf(p.filename().string());
|
||||
|
||||
std::map<std::pair<std::string, size_t>, 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()))
|
||||
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->prefix, t->digits}].push_back(e.path().string());
|
||||
}
|
||||
|
||||
std::vector<std::string> out;
|
||||
for (auto &[key, files] : sweeps)
|
||||
if (files.size() > out.size())
|
||||
out = std::move(files);
|
||||
|
||||
// The frame number is zero-padded in every PILATUS naming scheme in use, so within one template a
|
||||
// plain sort is the collection order.
|
||||
std::sort(out.begin(), out.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
// An imgCIF laboratory direction in the internal frame. imgCIF puts Z from the sample towards the
|
||||
// source and Y opposite gravity, while the internal frame has z along the beam and y along increasing
|
||||
// row, so the two differ by a half turn about x - a rotation and not a mirror, so an axis carried
|
||||
// through it turns the same way by the same angle. (writer/HDF5NXmx.cpp states the same relation from
|
||||
// the other side, where it separates this from the McStas one, which is a half turn about z.)
|
||||
Coord ImgCIFToInternal(const std::array<double, 3> &v) {
|
||||
return {static_cast<float>(v[0]), static_cast<float>(-v[1]), static_cast<float>(-v[2])};
|
||||
}
|
||||
|
||||
// The base rotation axis where a header states nothing about it, in the internal frame (x along
|
||||
// increasing detector column, y along increasing row, z along the beam). It is a convention: such a
|
||||
// header names its rotation axis but gives no direction, so this is the sign an NXmx master writes
|
||||
// for the same instruments, and a file that needs the other one is settled from the data by the run's
|
||||
// axis-sign rescue.
|
||||
const Coord ASSUMED_BASE_AXIS(-1.0f, 0.0f, 0.0f);
|
||||
|
||||
// The base rotation axis of the instrument, in the internal frame.
|
||||
//
|
||||
// Two headers in every corpus examined here state it and were being overruled by the assumption. One
|
||||
// beamline's PILATUS turns about the VERTICAL: its axis table says so outright, and its "# Oscillation
|
||||
// _axis" line says so a second way, by naming the image direction the spindle runs along rather than a
|
||||
// vector. Assuming the horizontal axis put the spindle 90 degrees out - which no amount of refinement
|
||||
// recovers, and which a sign rescue cannot reach either, since it is not a sign - and the run indexed
|
||||
// nothing. The sign is still the rescue's business; the DIRECTION is the file's.
|
||||
Coord BaseAxis(const minicbf::Header &h) {
|
||||
if (h.spindle_axis.has_value())
|
||||
return ImgCIFToInternal(*h.spindle_axis);
|
||||
if (h.spindle_along_slow)
|
||||
return {0.0f, -1.0f, 0.0f}; // minus the slow direction, as the default is minus the fast
|
||||
return ASSUMED_BASE_AXIS;
|
||||
}
|
||||
|
||||
// How the stored image sits in the detector plane, where the header's axis table states it - the same
|
||||
// thing the NXmx module directions say, in the form this format says it. Nothing comes back when the
|
||||
// header carries no table, or when what it states is not one of the eight discrete orientations.
|
||||
std::optional<DetectorOrientation> ImageOrientation(const minicbf::Header &h) {
|
||||
if (!h.fast_direction.has_value() || !h.slow_direction.has_value())
|
||||
return {};
|
||||
return DetectorOrientation::Match(ImgCIFToInternal(*h.fast_direction),
|
||||
ImgCIFToInternal(*h.slow_direction));
|
||||
}
|
||||
|
||||
// The axis a miniCBF sweep turns about, in the internal frame (x along increasing detector column,
|
||||
// y along increasing row, z along the beam).
|
||||
//
|
||||
// The base axis comes from the header where it states one (BaseAxis above) and is otherwise assumed.
|
||||
// The sense of the omega rotation below follows the base axis, so a sweep parked at a non-zero omega
|
||||
// inherits whichever direction it turns out to have.
|
||||
//
|
||||
// The head is base -> chi -> phi, so only the axes OUTSIDE the scanned one can tilt it. An omega
|
||||
// scan turns about the base axis however the cradle is set - which is why a header carrying a large
|
||||
// fixed chi still comes out as the base axis here - and only a phi scan is carried by chi and by
|
||||
// omega. Chi turns about the beam, as the imgCIF axis convention has it, pointing back at the
|
||||
// source; internal z points the other way, hence the minus. A kappa arm cannot be expressed at all:
|
||||
// its inclination is a property of the hardware that no miniCBF header states.
|
||||
Coord RotationAxis(const minicbf::Header &h) {
|
||||
const Coord base = BaseAxis(h);
|
||||
if (!minicbf::ScansPhi(h))
|
||||
return base;
|
||||
|
||||
const auto rad = [](double deg) { return static_cast<float>(deg * PI / 180.0); };
|
||||
const Coord chi_axis(0.0f, 0.0f, -1.0f);
|
||||
const Coord tilted = RotMatrix(rad(h.chi_deg.value_or(0.0)), chi_axis) * base;
|
||||
return RotMatrix(rad(h.omega_deg.value_or(0.0)), base) * tilted;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool JFJochCBFReader::CanRead(const std::string &path) {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::is_directory(path, ec))
|
||||
return !CollectSweep(path).empty();
|
||||
if (!HasCBFExtension(std::filesystem::path(path)))
|
||||
return false;
|
||||
try {
|
||||
// A pixel size as well as the compression: a byte-offset CBF with no "# Pixel_size" line has
|
||||
// no geometry, and XDS writes its correction files in exactly that shape. Claiming one here
|
||||
// would open it with a pixel size of zero.
|
||||
const auto h = minicbf::ReadHeader(path);
|
||||
return h.byte_offset && h.pixel_x_m > 0.0 && h.pixel_y_m > 0.0;
|
||||
} catch (const JFJochException &) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void JFJochCBFReader::ReadFiles(const std::string &path) {
|
||||
files_ = CollectSweep(path);
|
||||
if (files_.empty())
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
"No CBF images found for " + path);
|
||||
|
||||
header0_ = minicbf::ReadHeader(files_[0]);
|
||||
if (!header0_.byte_offset)
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
"Unsupported CBF compression (only x-CBF_BYTE_OFFSET)");
|
||||
// Beside the compression, because 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 the default of 0
|
||||
// collapses all of them without a word. A byte-offset CBF with no "# Pixel_size" line is not a
|
||||
// detector image from this family - XDS writes its correction files in exactly that shape.
|
||||
if (!(header0_.pixel_x_m > 0.0) || !(header0_.pixel_y_m > 0.0))
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
files_[0] + " has no pixel size in its header (no '# Pixel_size' line); "
|
||||
"it carries a CBF binary section but is not a detector image");
|
||||
|
||||
dataset_ = std::make_shared<JFJochReaderDataset>();
|
||||
dataset_->experiment = default_experiment;
|
||||
|
||||
DetectorSetup detector = DetDECTRIS(header0_.nx, header0_.ny, header0_.detector, {});
|
||||
detector.PixelSize_um(static_cast<int64_t>(std::lround(header0_.pixel_x_m * 1e6)));
|
||||
detector.SensorThickness_um(static_cast<int64_t>(std::lround(header0_.thickness_m * 1e6)));
|
||||
detector.SensorMaterial(header0_.material);
|
||||
// Only when the header states one. Count_cutoff defaults to 0 and SaturationLimitFromValue(0) is
|
||||
// 1, so an absent line marked EVERY pixel at or above one count as saturated - the integration
|
||||
// accept gate then drops the whole reflection and the run comes out empty for a reason nothing
|
||||
// reports. Left unset, DiffractionExperiment::GetSaturationLimit() falls back to the container's
|
||||
// own overflow, which is the safe direction: it can only fail to call a pixel saturated.
|
||||
if (header0_.count_cutoff > 0)
|
||||
detector.SaturationLimit(SaturationLimitFromValue(header0_.count_cutoff));
|
||||
else
|
||||
Logger("CBFReader").Warning("{} states no Count_cutoff, so no pixel will be called "
|
||||
"saturated; if this detector overloads, its strongest "
|
||||
"reflections will be 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; the real overflow is the header's Count_cutoff, set above.
|
||||
detector.BitDepthImage(32);
|
||||
if (const auto orientation = ImageOrientation(header0_))
|
||||
detector.ImageOrientation(orientation.value());
|
||||
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, which small-molecule collection uses routinely. The arm
|
||||
// turns the detector about the sample, so it carries the square-on geometry with it: the header's
|
||||
// Detector_distance stays the distance along the detector normal and Beam_xy stays the point of
|
||||
// normal incidence, neither of which the swing moves - which is exactly what the PONI convention
|
||||
// wants, so the swing is a PONI rotation and nothing else in the header changes. It turns about
|
||||
// the axis the header's table gives the arm, and otherwise about the base spindle axis: on the
|
||||
// four-circle geometry these headers describe the arm and the spindle are one axis, and the one
|
||||
// header here that states both states them with the same vector.
|
||||
if (header0_.two_theta_deg != 0.0) {
|
||||
const Coord axis = header0_.detector_axis.has_value()
|
||||
? ImgCIFToInternal(*header0_.detector_axis) : BaseAxis(header0_);
|
||||
float rot1 = 0, rot2 = 0, rot3 = 0;
|
||||
PoniAnglesFromMatrix(RotMatrix(static_cast<float>(header0_.two_theta_deg * PI / 180.0), 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_.period_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.
|
||||
std::vector<double> angles(files_.size());
|
||||
for (size_t i = 0; i < files_.size(); i++)
|
||||
angles[i] = minicbf::ReadHeader(files_[i]).start_angle_deg;
|
||||
|
||||
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),
|
||||
RotationAxis(header0_), {}));
|
||||
|
||||
dataset_->error_value = -1;
|
||||
dataset_->experiment.ImagesPerTrigger(static_cast<int64_t>(files_.size()));
|
||||
|
||||
// The untrusted pixels a PILATUS marks with a negative value: module gaps and the bad-pixel map.
|
||||
// They are the same on every frame of a sweep, so frame 0 defines the mask.
|
||||
std::vector<int32_t> first;
|
||||
minicbf::Read(files_[0], first);
|
||||
std::vector<uint32_t> mask(first.size(), 0);
|
||||
for (size_t i = 0; i < first.size(); i++)
|
||||
if (first[i] < 0)
|
||||
mask[i] = 1;
|
||||
dataset_->pixel_mask = std::make_shared<const PixelMask>(mask);
|
||||
|
||||
SetStartMessage(dataset_);
|
||||
}
|
||||
|
||||
uint64_t JFJochCBFReader::GetNumberOfImages() const {
|
||||
return files_.size();
|
||||
}
|
||||
|
||||
void JFJochCBFReader::Close() {
|
||||
files_.clear();
|
||||
dataset_.reset();
|
||||
}
|
||||
|
||||
template <class Buffer>
|
||||
CompressedImage JFJochCBFReader::DecodeInto(int64_t image_number, Buffer &buffer) 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));
|
||||
|
||||
// Decode straight into the caller's bytes: the pixels are plain int32 and nothing downstream has
|
||||
// to decompress them, so NO_COMPRESSION over that buffer is the whole image.
|
||||
const auto h = minicbf::ReadInto(files_[image_number],
|
||||
reinterpret_cast<int32_t *>(buffer.data()), npixel);
|
||||
if (static_cast<size_t>(h.nelem) != npixel)
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
"CBF 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 JFJochCBFReader::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;
|
||||
|
||||
// The image must outlive this call, so it is decoded straight into the caller's buffer - the same
|
||||
// thing the argument is for on the HDF5 path - and message.image only points at it.
|
||||
message.image = DecodeInto(image_number, buffer);
|
||||
message.number = image_number;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<JFJochReaderRawImage> JFJochCBFReader::GetRawImage(int64_t image_number) {
|
||||
auto ret = std::make_shared<JFJochReaderRawImage>();
|
||||
ret->image = DecodeInto(image_number, ret->image_buffer);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<SpotToSave> JFJochCBFReader::ReadSpots(int64_t) const {
|
||||
return {}; // a raw CBF stores no analysis results
|
||||
}
|
||||
Reference in New Issue
Block a user