// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include #include #include #include // 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. // // Rigaku's d*TREK writes the same container with a different vocabulary: the detector and the // goniometer as named circles with their vectors (CCD_GONIO_VALUES, ROTATION_VECTOR, ...), the beam // position in pixels, and its own overflow encoding for 16-bit pixels. A header naming // DETECTOR_NAMES is read that way. 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; int64_t saturation = 0; // CCD_IMAGE_SATURATION: the value a saturated pixel carries std::string detector; // DETECTOR_SN, where stated std::string axis_name = "phi"; // d*TREK only (Rigaku CrystalClear): the geometry is stated as vectors rather than the ADSC // scalars, in the imgCIF laboratory frame - z from the sample to the source, y up. Absent on an // ADSC header, where two_theta_deg above carries the one detector circle there is. std::optional> fast_direction; std::optional> slow_direction; std::optional> spindle_axis; struct Circle { std::array axis; double angle_deg; }; std::vector detector_circles; // header order: the outermost circle first // RAXIS_COMPRESSION_RATIO: a stored value above 32767 is (value - 32768) * ratio. 0 = not used. int64_t overflow_ratio = 0; std::map 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 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 &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 &scratch); } // namespace smv