fix(io/gpr): 雷达文件按宽字符打开,支持中文目录路径
规范化/Impulse 雷达 reader 的 std::ifstream 用 toLocal8Bit 产出的窄字节(GBK)路径 打开文件。GUI app 链接 QWebEngine(Chromium)/VTK,启动时 setlocale(LC_ALL,"") 把 LC_CTYPE 提升为系统 UTF-8 locale,此后 narrow ifstream 把 GBK 路径字节当 UTF-8 解析 → 打不开 → "打开 .head 失败"。纯 "C" locale 的无头/单测环境用 CP_ACP=GBK 解窄路径, 不触发,故此前未暴露。 新增 io/gpr/LocalPath: Windows 用 MultiByteToWideChar(CP_ACP) 把本地 8 位字节解成 宽字符 std::filesystem::path,使 ifstream/ofstream/file_size 走宽字符打开,与 locale 无关;非 Windows 直接 UTF-8。改到所有外部导入路径 open 点(.head/.data/.iprb/.iprh/ .ord/.gps)。 回归测试 OpensCjkDirectoryPathUnderUtf8Locale: 显式置 .UTF-8 locale 复现 app 运行期 条件,走真实 buildLineVolumeFromNormalized 断言中文目录建体成功(退回 narrow 则抛错, 红/绿已验)。
This commit is contained in:
parent
e9a9866c34
commit
c0b6b31a9a
|
|
@ -16,6 +16,7 @@
|
|||
#include "data/store/ChunkedVolumeStore.hpp"
|
||||
#include "io/gpr/GprSurveyAssembler.hpp"
|
||||
#include "io/gpr/IprHeader.hpp"
|
||||
#include "io/gpr/LocalPath.hpp"
|
||||
|
||||
namespace geopro::data {
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ std::string toHeaderPath(const std::string& iprbPath) {
|
|||
}
|
||||
|
||||
std::string readFileText(const std::string& path) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
std::ifstream f(geopro::io::gpr::localPath(path), std::ios::binary);
|
||||
if (!f) throw std::runtime_error("StreamingVolumeBuilder: 无法打开 " + path);
|
||||
std::ostringstream ss;
|
||||
ss << f.rdbuf();
|
||||
|
|
@ -68,7 +69,7 @@ std::int64_t totalTraces(const std::vector<std::string>& iprb, double& surveyDx)
|
|||
throw std::runtime_error("StreamingVolumeBuilder: samples<=0");
|
||||
if (c == 0) surveyDx = h.distanceInterval;
|
||||
const std::int64_t bytes =
|
||||
static_cast<std::int64_t>(fs::file_size(fs::path(iprb[c])));
|
||||
static_cast<std::int64_t>(fs::file_size(geopro::io::gpr::localPath(iprb[c])));
|
||||
const std::int64_t per = static_cast<std::int64_t>(h.samples) * 2;
|
||||
if (per <= 0 || bytes % per != 0)
|
||||
throw std::runtime_error("StreamingVolumeBuilder: .iprb 字节非整道");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
add_library(geopro_io_gpr STATIC
|
||||
IprHeader.cpp IprbReader.cpp GprGeometry.cpp GprSurveyAssembler.cpp
|
||||
GpsTrack.cpp NormalizedRadarReader.cpp
|
||||
GpsTrack.cpp NormalizedRadarReader.cpp LocalPath.cpp
|
||||
RadarVolumeAssembler.cpp NormalizedRadarVolumeBridge.cpp)
|
||||
target_include_directories(geopro_io_gpr PUBLIC ${CMAKE_SOURCE_DIR}/src)
|
||||
target_compile_features(geopro_io_gpr PUBLIC cxx_std_17)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "io/gpr/GprGeometry.hpp"
|
||||
#include "io/gpr/IprHeader.hpp"
|
||||
#include "io/gpr/IprbReader.hpp"
|
||||
#include "io/gpr/LocalPath.hpp"
|
||||
|
||||
namespace geopro::io::gpr {
|
||||
namespace {
|
||||
|
|
@ -27,7 +28,7 @@ std::string toHeaderPath(const std::string& iprbPath) {
|
|||
}
|
||||
|
||||
std::string readFileText(const std::string& path) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
std::ifstream f(localPath(path), std::ios::binary);
|
||||
if (!f) {
|
||||
throw std::runtime_error("无法打开文件: " + path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "io/gpr/LocalPath.hpp"
|
||||
|
||||
namespace geopro::io::gpr {
|
||||
|
||||
namespace {
|
||||
|
|
@ -26,7 +28,7 @@ bool parseDouble(const std::string& s, double& out) {
|
|||
} // namespace
|
||||
|
||||
GpsTrack parseGps(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
std::ifstream f(localPath(path));
|
||||
if (!f) throw std::runtime_error("parseGps: 打不开 " + path);
|
||||
|
||||
GpsTrack track;
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@
|
|||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "io/gpr/LocalPath.hpp"
|
||||
|
||||
namespace geopro::io::gpr {
|
||||
|
||||
BScan readIprb(const std::string& path, const IprHeader& h) {
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
std::ifstream f(localPath(path), std::ios::binary | std::ios::ate);
|
||||
if (!f) {
|
||||
throw std::runtime_error("readIprb: 无法打开文件: " + path);
|
||||
}
|
||||
|
|
@ -43,7 +45,7 @@ BScan readIprb(const std::string& path, const IprHeader& h) {
|
|||
|
||||
BScan readIprbRange(const std::string& path, const IprHeader& h,
|
||||
std::int64_t t0, std::int64_t t1) {
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
std::ifstream f(localPath(path), std::ios::binary | std::ios::ate);
|
||||
if (!f) {
|
||||
throw std::runtime_error("readIprbRange: 无法打开文件: " + path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
#include "io/gpr/LocalPath.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace geopro::io::gpr {
|
||||
|
||||
std::filesystem::path localPath(const std::string& p) {
|
||||
#ifdef _WIN32
|
||||
if (p.empty()) return std::filesystem::path{};
|
||||
const int wlen = ::MultiByteToWideChar(
|
||||
CP_ACP, 0, p.data(), static_cast<int>(p.size()), nullptr, 0);
|
||||
if (wlen <= 0) return std::filesystem::path(p); // 退化:原样(ASCII 安全)
|
||||
std::wstring w(static_cast<std::size_t>(wlen), L'\0');
|
||||
::MultiByteToWideChar(CP_ACP, 0, p.data(), static_cast<int>(p.size()),
|
||||
w.data(), wlen);
|
||||
return std::filesystem::path(w);
|
||||
#else
|
||||
return std::filesystem::path(p); // POSIX:窄字节即 UTF-8 原生路径
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace geopro::io::gpr
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace geopro::io::gpr {
|
||||
|
||||
// 把"本地 8 位编码"的窄字节路径转成 std::filesystem::path。
|
||||
// Windows:源串按当前 ANSI 代码页(简中=GBK/936,即 QString::toLocal8Bit 产物)
|
||||
// 解码为宽字符 path,使 std::ifstream/ofstream 走宽字符打开 —— 否则
|
||||
// 窄字符 ifstream 在默认 "C" locale 下无法解析含中文的路径,open 失败。
|
||||
// 其它平台:窄字节即原生 UTF-8 路径,直接构造。
|
||||
// 退化保护:转换失败时原样返回(ASCII 路径不受影响)。
|
||||
std::filesystem::path localPath(const std::string& p);
|
||||
|
||||
} // namespace geopro::io::gpr
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
#include "io/gpr/NormalizedRadarReader.hpp"
|
||||
#include "io/gpr/LocalPath.hpp"
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
|
|
@ -76,11 +77,11 @@ std::vector<std::int16_t> readRadarDataCube(const std::string& dataPath,
|
|||
const std::size_t n = static_cast<std::size_t>(h.lastTrace) * h.samples;
|
||||
const std::uintmax_t expect = static_cast<std::uintmax_t>(n) * 2;
|
||||
std::error_code ec;
|
||||
const auto fsize = std::filesystem::file_size(dataPath, ec);
|
||||
const auto fsize = std::filesystem::file_size(localPath(dataPath), ec);
|
||||
if (ec || fsize != expect)
|
||||
throw std::runtime_error("规范化 .data 大小不符: " + dataPath);
|
||||
std::vector<std::int16_t> cube(n);
|
||||
std::ifstream f(dataPath, std::ios::binary);
|
||||
std::ifstream f(localPath(dataPath), std::ios::binary);
|
||||
if (!f) throw std::runtime_error("打开 .data 失败: " + dataPath);
|
||||
f.read(reinterpret_cast<char*>(cube.data()), static_cast<std::streamsize>(expect));
|
||||
if (!f) throw std::runtime_error("读 .data 失败: " + dataPath);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#include "io/gpr/LocalPath.hpp"
|
||||
#include "io/gpr/NormalizedRadarReader.hpp"
|
||||
#include "io/gpr/RadarVolumeAssembler.hpp"
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ geopro::core::BuiltI16 buildLineVolumeFromNormalized(const std::string& lineDir,
|
|||
|
||||
std::string headText;
|
||||
{
|
||||
std::ifstream f(head);
|
||||
std::ifstream f(localPath(head));
|
||||
if (!f) throw std::runtime_error("打开 .head 失败: " + head);
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,29 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <clocale>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include "core/algo/GprVolumeBuilder.hpp"
|
||||
#include "io/gpr/NormalizedRadarVolumeBridge.hpp"
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
// 在指定目录写出一组最小可解析的规范化 .head/.data(K=4 道 M=2 通道 N=3 采样)。
|
||||
void writeMinimalLine(const fs::path& head, const fs::path& data) {
|
||||
{ std::ofstream f(head);
|
||||
f << "SAMPLES:3\nNUMBER_OF_CH:2\nLAST_TRACE:8\nBITS:16\nENDIAN_TYPE:1\n"
|
||||
"DISTANCE_INTERVAL:0.1\nTIMEWINDOW:30\nDIELECTRIC:9\n"; }
|
||||
{ std::ofstream f(data, std::ios::binary);
|
||||
for (int t = 0; t < 4; ++t) for (int c = 0; c < 2; ++c) for (int s = 0; s < 3; ++s) {
|
||||
std::int16_t v = static_cast<std::int16_t>(t * 10 + c * 100 + s);
|
||||
f.write(reinterpret_cast<const char*>(&v), 2); } }
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(NormalizedRadarBridge, BuildsVolumeWithExpectedAxes) {
|
||||
// K=4 道, M=2 通道, N=3 采样, 无通道偏移(不插值), coarse=1。
|
||||
fs::path dir = fs::temp_directory_path() / "radar_bridge_test";
|
||||
|
|
@ -26,3 +44,55 @@ TEST(NormalizedRadarBridge, BuildsVolumeWithExpectedAxes) {
|
|||
EXPECT_GT(b.spacing[2], 0.0); // dz 由 timewindow/dielectric 求得 >0
|
||||
EXPECT_NEAR(b.quant.toPhys(b.vol.at(3, 1, 2)), 132.0, b.quant.scale); // t3c1s2=30+100+2
|
||||
}
|
||||
|
||||
// 回归:中文目录路径必须能打开渲染。app 传入的是 QString::toLocal8Bit(),即当前
|
||||
// ANSI 代码页(简中=GBK)窄字节。关键复现条件——GUI app 链接 QWebEngine(Chromium)/VTK,
|
||||
// 它们在启动时 setlocale(LC_ALL,"") 把 LC_CTYPE 提升为系统 UTF-8 locale;此后窄字符
|
||||
// ifstream 会把 GBK 路径字节当 UTF-8 解析 → open 失败(即"打开 .head 失败")。
|
||||
// 故本测试显式置 UTF-8 locale 复现该失败面,走宽字符打开(见 io/gpr/LocalPath)守护回归。
|
||||
// (纯 "C" locale 下 UCRT 用 CP_ACP=GBK 解窄路径,反而不失败,无法复现——须置 UTF-8。)
|
||||
TEST(NormalizedRadarBridge, OpensCjkDirectoryPathUnderUtf8Locale) {
|
||||
#ifdef _WIN32
|
||||
const std::wstring wname = L"radar_cjk_南同大道";
|
||||
const fs::path dir = fs::temp_directory_path() / wname;
|
||||
fs::remove_all(dir);
|
||||
fs::create_directories(dir);
|
||||
writeMinimalLine(dir / L"南同大道_000.head", dir / L"南同大道_000.data");
|
||||
|
||||
// 模拟 app:宽字符 → 当前 ANSI 代码页(GBK)窄字节,等价 QString::toLocal8Bit()。
|
||||
auto toAcp = [](const std::wstring& w) {
|
||||
const int n = ::WideCharToMultiByte(CP_ACP, 0, w.data(),
|
||||
static_cast<int>(w.size()), nullptr, 0,
|
||||
nullptr, nullptr);
|
||||
std::string s(static_cast<std::size_t>(n), '\0');
|
||||
::WideCharToMultiByte(CP_ACP, 0, w.data(), static_cast<int>(w.size()),
|
||||
s.data(), n, nullptr, nullptr);
|
||||
return s;
|
||||
};
|
||||
const std::string dirAcp = toAcp(dir.wstring());
|
||||
const std::string prefixAcp = toAcp(L"南同大道_000");
|
||||
|
||||
// 复现 app 运行期的 UTF-8 C locale(QWebEngine/VTK 所置)——不修复则 narrow open 失败。
|
||||
const char* prevC = std::setlocale(LC_ALL, nullptr);
|
||||
const std::string savedC = prevC ? prevC : "C";
|
||||
std::setlocale(LC_ALL, ".UTF-8");
|
||||
|
||||
geopro::core::BuiltI16 b;
|
||||
try {
|
||||
b = geopro::io::gpr::buildLineVolumeFromNormalized(
|
||||
dirAcp, prefixAcp, /*coarse=*/1, /*targetDy=*/0.0);
|
||||
} catch (...) {
|
||||
std::setlocale(LC_ALL, savedC.c_str());
|
||||
fs::remove_all(dir);
|
||||
throw; // 未修复时会在此抛"打开 .head 失败"→ 测试红,正是回归守护
|
||||
}
|
||||
std::setlocale(LC_ALL, savedC.c_str());
|
||||
|
||||
EXPECT_EQ(b.vol.nx(), 4);
|
||||
EXPECT_EQ(b.vol.ny(), 2);
|
||||
EXPECT_EQ(b.vol.nz(), 3);
|
||||
fs::remove_all(dir);
|
||||
#else
|
||||
GTEST_SKIP() << "中文窄路径打开问题仅 Windows 相关";
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue