Not yet compiled or tested: work stopped after the feasibility study. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.4 KiB
C++
150 lines
5.4 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "XTC.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <memory>
|
|
|
|
#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, FileCloser>;
|
|
|
|
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<off_t>(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<uint64_t>(width) * height;
|
|
if (depth > 8 && depth <= 16 && XTC_HEADER + FRAME_HEADER + 2 * npixel <= extent
|
|
&& npixel > static_cast<uint64_t>(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<std::string> CollectStreams(const std::string &path) {
|
|
std::error_code ec;
|
|
if (!std::filesystem::is_directory(path, ec))
|
|
return {path};
|
|
std::vector<std::string> 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<Frame> IndexFrames(const std::string &file) {
|
|
File f = Open(file);
|
|
std::vector<Frame> out;
|
|
uint64_t pos = 0;
|
|
uint8_t h[DGRAM_HEADER];
|
|
while (fseeko(f.get(), static_cast<off_t>(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<uint8_t> &scratch) {
|
|
const size_t npixel = static_cast<size_t>(frame.width) * frame.height;
|
|
scratch.resize(2 * npixel);
|
|
File f = Open(frame.file);
|
|
if (fseeko(f.get(), static_cast<off_t>(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
|