// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include // marCCD: an ordinary uncompressed TIFF whose 3072-byte instrument header sits in the gap between // the TIFF header and the pixels. The image is read through libtiff, which the build already // carries; the instrument header is a fixed-offset block of little-endian int32, so it is read // directly. Rayonix MX-series and mar Mosaic detectors both write it, under half a dozen file // extensions (.mccd, .img, .tif, or a bare frame number). // // Every value the header stores is an integer times a fixed scale: lengths in micrometres or // nanometres, angles in millidegrees, the wavelength in 1e-5 A. They are converted here, so the // rest of the code sees metres, degrees and angstroms like every other reader. namespace marccd { // Byte offset of the instrument header when the file does not say. Every writer seen puts it here // and states it in TIFF tag 34710 as well. inline constexpr uint32_t DEFAULT_HEADER_OFFSET = 1024; // The private TIFF tag in which marCCD states where its own header begins. inline constexpr uint16_t TIFFTAG_MARCCD_HEADER_OFFSET = 34710; struct Header { std::string detector; // from the TIFF Model/Software tags where they say anything int64_t nx = 0; // fast dimension (columns) int64_t ny = 0; // slow dimension (rows) double pixel_x_m = 0; double pixel_y_m = 0; 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; double exposure_s = 0; int64_t saturated_value = 0; std::string axis_name = "phi"; // the circle that moved between start and end }; // True if the path names something this reader can open: a marCCD file, or a directory holding at // least one. Reads a few kB at most. bool CanRead(const std::string &path); // The files of one sweep, in collection order. path is a directory, or one frame inside the sweep // to take the whole sweep from. Empty where there is none. std::vector CollectSweep(const std::string &path); // Header only - reads the first few kB of the file. Cheap enough to call per frame for the // rotation angles. Header ReadHeader(const std::string &path); // Header + pixels of one file. out is resized to nx * ny. Header Read(const std::string &path, std::vector &out); // The same, widening the stored uint16 into memory the caller already has. scratch carries the // 16-bit strip between frames, so a worker walking a sweep allocates once. Throws if the image // does not fit in capacity pixels. Header ReadInto(const std::string &path, int32_t *out, size_t capacity, std::vector &scratch); } // namespace marccd