From dc16a00271b97d787196cbab8195e140df87a76f Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Fri, 28 Aug 2026 20:07:10 +0200 Subject: [PATCH] reader: read a PILATUS miniCBF sweep natively, without libcbf Most facilities still archive rotation data as a directory of miniCBF frames, which until now had to be converted to HDF5 before rugnux could see it. Nothing in that format needs a CIF parser or a library: it is an ASCII header, four separator bytes, then one byte-offset compressed image, and every value the reader wants sits on a "# " comment line or a MIME line. MiniCBF holds the format itself - header parse and the byte-offset decoder, which is a running value with deltas stored smallest-container-first. Verified byte-exact against dxtbx on PILATUS 6M, 6M-F, 300K, silicon and CdTe sensors, and three sensor thicknesses. JFJochCBFReader is a sibling of JFJochHDF5Reader under the JFJochReader base. NAMING ANY FRAME READS ITS WHOLE SWEEP: the sweep is identified by the template (prefix + digit count) the named frame belongs to, not by "every .cbf in the directory", so a directory holding two sweeps does not splice two crystals together. Naming a directory takes the sweep with the most frames in it. Images decode on demand, one per call, so any number of workers can read at once - there is no global lock as there is on the HDF5 path, HDF5 not being thread-safe. A raw CBF carries no analysis results, so the dataset it builds is the geometry, the mask and nothing else, exactly as a plain DECTRIS file with no /entry/MX gives. Two header quirks are handled because real files have them: the sensor material is written "Silicon" where the rest of the code compares against "CdTe", and the thickness unit is sometimes omitted. Headers are not a fixed size either - one set carries 6335 bytes - so the parse runs to the binary separator rather than over a fixed prefix. Co-Authored-By: Claude Opus 5 (1M context) --- reader/CMakeLists.txt | 4 + reader/JFJochCBFReader.cpp | 235 +++++++++++++++++++++++++++++++++++++ reader/JFJochCBFReader.h | 54 +++++++++ reader/MiniCBF.cpp | 215 +++++++++++++++++++++++++++++++++ reader/MiniCBF.h | 64 ++++++++++ 5 files changed, 572 insertions(+) create mode 100644 reader/JFJochCBFReader.cpp create mode 100644 reader/JFJochCBFReader.h create mode 100644 reader/MiniCBF.cpp create mode 100644 reader/MiniCBF.h diff --git a/reader/CMakeLists.txt b/reader/CMakeLists.txt index a5168f185..5fb20d52a 100644 --- a/reader/CMakeLists.txt +++ b/reader/CMakeLists.txt @@ -2,6 +2,10 @@ ADD_LIBRARY(JFJochReader STATIC JFJochReader.cpp JFJochReader.h JFJochHDF5Reader.cpp JFJochHDF5Reader.h + MiniCBF.cpp + MiniCBF.h + JFJochCBFReader.cpp + JFJochCBFReader.h HDF5ImageLocator.cpp HDF5ImageLocator.h HDF5ImageSource.cpp diff --git a/reader/JFJochCBFReader.cpp b/reader/JFJochCBFReader.cpp new file mode 100644 index 000000000..cd404f14d --- /dev/null +++ b/reader/JFJochCBFReader.cpp @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "JFJochCBFReader.h" + +#include +#include +#include +#include +#include +#include + +#include "../common/JFJochException.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(name[prefix.size() + i]))) + return false; + return true; + } +}; + +std::optional