// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include #include #include // PILATUS miniCBF: an ASCII header, four separator bytes, then one byte-offset compressed image. // No CIF parser and no libcbf are needed - every value is on a "# " comment line or a MIME line. namespace minicbf { // The four bytes that end the MIME header and begin the binary section. inline constexpr unsigned char BINARY_SEPARATOR[4] = {0x0c, 0x1a, 0x04, 0xd5}; struct Header { std::string detector; // "PILATUS3 6M, S/N 60-0136" int64_t nx = 0; // fast dimension (columns) int64_t ny = 0; // slow dimension (rows) int64_t nelem = 0; // pixel count declared by the MIME header double pixel_x_m = 0; double pixel_y_m = 0; double thickness_m = 0; std::string material = "Si"; // NORMALISED: the file says "Silicon", the rest of the code wants "Si" double distance_m = 0; double beam_x_px = 0; double beam_y_px = 0; double wavelength_A = 0; double start_angle_deg = 0; double angle_increment_deg = 0; double two_theta_deg = 0; // Goniometer head angles and the per-image increment of each, in degrees. A writer spells "this // head has no such axis" as -9999, which is a sentinel and not an angle, so an axis the header // does not really carry is absent here rather than 9999 degrees away from zero. std::optional chi_deg; std::optional omega_deg; double chi_increment_deg = 0; double phi_increment_deg = 0; double omega_increment_deg = 0; double exposure_s = 0; double period_s = 0; int64_t count_cutoff = 0; // saturation std::string axis_name = "omega"; bool byte_offset = false; // the only conversion supported // The imgCIF axis table the CBF template block carries after the "# " lines, as the file states // it: vectors in the imgCIF laboratory frame (Z from the sample towards the source, Y opposite // gravity, X completing a right-handed set). Turning them into any other frame is the caller's // business, not this format reader's. Most headers carry no such table and leave all of these // empty, which is not the same as their stating the usual mounting. std::optional> fast_direction; // laboratory direction of a +1 column step std::optional> slow_direction; // ... of a +1 row step std::optional> spindle_axis; // the base goniometer rotation axis std::optional> detector_axis; // the detector's own rotation axis: a 2theta arm // "# Oscillation_axis X.CW +SLOW". Some writers say which of the two image directions the spindle // runs along instead of stating a vector. The axis NAME on that line is a poor discriminator (see // ScansPhi, and note that the one header carrying both a name and a table says "X" where its table // says Y) but this token is not: where both are present they agree. bool spindle_along_slow = false; }; // Whether the sweep turns PHI rather than the base (omega) axis. What moved is the axis with a // non-zero increment - a header routinely carries a "# Phi" angle for a phi that stands still - so // the name is consulted only when no increment is stated at all. It is a poor discriminator: some // writers put a direction convention there ("X.CW") instead of an axis name. bool ScansPhi(const Header &h); // Byte offset of the binary section (just past the separator), or nothing if there is none. std::optional FindBinarySection(const uint8_t *data, size_t size); // Parse the ASCII header. Pass the bytes BEFORE the separator; headers are not a fixed size (one // Diamond I24 set carries 6335 bytes, well past a 4 kB guess), so never parse a fixed prefix. Header ParseHeader(const char *data, size_t size); // x-CBF_BYTE_OFFSET -> int32. Deltas against a running value, smallest container first: int8, // escaping to int16 via -128, to int32 via -32768, to int64 via INT32_MIN. Little-endian, packed. // Throws if the stream ends before n_pixels are produced. out must hold n_pixels. void DecodeByteOffset(const uint8_t *data, size_t size, int32_t *out, size_t n_pixels); // Header + pixels of one file, read from disk. Header Read(const std::string &path, std::vector &out); // The same, decoding into memory the caller already has (one read of the file, no extra copy). // Throws if the image does not fit in capacity pixels. Header ReadInto(const std::string &path, int32_t *out, size_t capacity); // Header only - reads just enough of the file to reach the separator. Cheap enough to call per // frame for the rotation angles. Header ReadHeader(const std::string &path); } // namespace minicbf