// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "XTC.h" #include #include #include #include #include #include "../common/JFJochException.h" namespace { constexpr size_t DGRAM_HEADER = 40; // clock time 8, timestamp 8, env 4, Xtc header 20 constexpr size_t XTC_HEADER = 20; constexpr size_t FRAME_HEADER = 16; // Camera::FrameV1: width, height, depth, offset constexpr uint32_t ID_XTC = 1; constexpr uint32_t ID_FRAME = 2; constexpr uint32_t TRANSITION_L1ACCEPT = 12; constexpr uint32_t TRANSITION_LAST = 12; struct FileCloser { void operator()(FILE *f) const { if (f) fclose(f); } }; using File = std::unique_ptr; File Open(const std::string &path) { File f(fopen(path.c_str(), "rb")); if (!f) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Cannot open " + path); return f; } uint32_t Word(const uint8_t *p, size_t i) { uint32_t v; memcpy(&v, p + 4 * i, 4); return v; } // The transition a datagram header names, from the top bits of its timestamp's first word. uint32_t Transition(const uint8_t *dgram) { return (Word(dgram, 2) >> 24) & 0xf; } // A first datagram is a Configure with a container as its outermost Xtc, whose extent covers at // least its own header. bool PlausibleFirstDatagram(const uint8_t *h) { return Transition(h) <= TRANSITION_LAST && (Word(h, 8) & 0xffff) == ID_XTC && Word(h, 9) >= XTC_HEADER; } // Walks the Xtcs in [begin, end) of the file, looking for camera frames; the largest found is kept. void FindFrame(FILE *f, uint64_t begin, uint64_t end, xtc::Frame &best) { uint64_t pos = begin; while (pos + XTC_HEADER <= end) { uint8_t h[XTC_HEADER]; if (fseeko(f, static_cast(pos), SEEK_SET) != 0 || fread(h, 1, XTC_HEADER, f) != XTC_HEADER) return; const uint32_t type = Word(h, 3) & 0xffff; const uint32_t extent = Word(h, 4); if (extent < XTC_HEADER || pos + extent > end) return; if (type == ID_XTC) FindFrame(f, pos + XTC_HEADER, pos + extent, best); else if (type == ID_FRAME && extent >= XTC_HEADER + FRAME_HEADER) { uint8_t fh[FRAME_HEADER]; if (fread(fh, 1, FRAME_HEADER, f) == FRAME_HEADER) { const uint32_t width = Word(fh, 0), height = Word(fh, 1), depth = Word(fh, 2); const uint64_t npixel = static_cast(width) * height; if (depth > 8 && depth <= 16 && XTC_HEADER + FRAME_HEADER + 2 * npixel <= extent && npixel > static_cast(best.width) * best.height) best = {"", pos + XTC_HEADER + FRAME_HEADER, width, height}; } } pos += extent; } } } // namespace namespace xtc { bool CanRead(const std::string &path) { std::error_code ec; if (std::filesystem::is_directory(path, ec)) return !CollectStreams(path).empty(); if (!std::filesystem::is_regular_file(path, ec) || !path.ends_with(".xtc") || path.ends_with(".smd.xtc")) return false; File f(fopen(path.c_str(), "rb")); uint8_t h[DGRAM_HEADER]; return f && fread(h, 1, DGRAM_HEADER, f.get()) == DGRAM_HEADER && PlausibleFirstDatagram(h); } std::vector CollectStreams(const std::string &path) { std::error_code ec; if (!std::filesystem::is_directory(path, ec)) return {path}; std::vector out; for (const auto &e: std::filesystem::directory_iterator(path, ec)) { const std::string p = e.path().string(); if (e.is_regular_file() && p.ends_with(".xtc") && !p.ends_with(".smd.xtc")) out.push_back(p); } std::sort(out.begin(), out.end()); return out; } std::vector IndexFrames(const std::string &file) { File f = Open(file); std::vector out; uint64_t pos = 0; uint8_t h[DGRAM_HEADER]; while (fseeko(f.get(), static_cast(pos), SEEK_SET) == 0 && fread(h, 1, DGRAM_HEADER, f.get()) == DGRAM_HEADER) { const uint32_t extent = Word(h, 9); if (extent < XTC_HEADER) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, file + ": a datagram's extent is shorter than its own header"); const uint64_t payload = pos + DGRAM_HEADER, end = pos + DGRAM_HEADER - XTC_HEADER + extent; if (Transition(h) == TRANSITION_L1ACCEPT) { Frame frame; FindFrame(f.get(), payload, end, frame); if (frame.width > 0) { frame.file = file; out.push_back(frame); } } pos = end; } return out; } void ReadFrame(const Frame &frame, int32_t *out, std::vector &scratch) { const size_t npixel = static_cast(frame.width) * frame.height; scratch.resize(2 * npixel); File f = Open(frame.file); if (fseeko(f.get(), static_cast(frame.offset), SEEK_SET) != 0 || fread(scratch.data(), 1, scratch.size(), f.get()) != scratch.size()) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, frame.file + ": an image ends past the end of the file"); for (size_t i = 0; i < npixel; i++) { uint16_t v; memcpy(&v, scratch.data() + 2 * i, 2); out[i] = v; } } } // namespace xtc