feat: add local access control and managed point clouds
This commit is contained in:
153
server/pointcloudMetadata.ts
Normal file
153
server/pointcloudMetadata.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import type {
|
||||
PointAttribute,
|
||||
PointcloudDataFormat,
|
||||
PointcloudMetadataValues,
|
||||
} from "../shared/pointcloudMetadata";
|
||||
|
||||
export const MAX_PLY_HEADER_BYTES = 16 * 1024;
|
||||
|
||||
type ScalarType = { type: string; bytes: number };
|
||||
|
||||
const PLY_SCALAR_TYPES: Record<string, ScalarType> = {
|
||||
char: { type: "int8", bytes: 1 },
|
||||
int8: { type: "int8", bytes: 1 },
|
||||
uchar: { type: "uint8", bytes: 1 },
|
||||
uint8: { type: "uint8", bytes: 1 },
|
||||
short: { type: "int16", bytes: 2 },
|
||||
int16: { type: "int16", bytes: 2 },
|
||||
ushort: { type: "uint16", bytes: 2 },
|
||||
uint16: { type: "uint16", bytes: 2 },
|
||||
int: { type: "int32", bytes: 4 },
|
||||
int32: { type: "int32", bytes: 4 },
|
||||
uint: { type: "uint32", bytes: 4 },
|
||||
uint32: { type: "uint32", bytes: 4 },
|
||||
float: { type: "float32", bytes: 4 },
|
||||
float32: { type: "float32", bytes: 4 },
|
||||
double: { type: "float64", bytes: 8 },
|
||||
float64: { type: "float64", bytes: 8 },
|
||||
};
|
||||
|
||||
export class PointcloudMetadataParseError extends Error {}
|
||||
|
||||
export type ParsedPlyMetadata = {
|
||||
pointCount: number;
|
||||
pointAttributes: PointAttribute[];
|
||||
pointDataFormat: PointcloudDataFormat;
|
||||
bytesPerPoint: number;
|
||||
};
|
||||
|
||||
/** 解析内存中的 PLY 文件头。调用方应保证输入不超过 MAX_PLY_HEADER_BYTES。 */
|
||||
export function parsePlyHeader(buffer: Buffer): ParsedPlyMetadata {
|
||||
const text = buffer.toString("latin1");
|
||||
const endMatch = /(?:^|\r?\n)end_header(?:\r?\n|$)/.exec(text);
|
||||
if (!endMatch) {
|
||||
throw new PointcloudMetadataParseError("未在 16KB 内找到 PLY 文件头结束标记");
|
||||
}
|
||||
|
||||
const headerEnd = endMatch.index + endMatch[0].length;
|
||||
const lines = text.slice(0, headerEnd).split(/\r?\n/).map((line) => line.trim());
|
||||
if (lines[0] !== "ply") {
|
||||
throw new PointcloudMetadataParseError("不是有效的 PLY 文件");
|
||||
}
|
||||
|
||||
let pointDataFormat: PointcloudDataFormat | null = null;
|
||||
let pointCount: number | null = null;
|
||||
let currentElement: string | null = null;
|
||||
const pointAttributes: PointAttribute[] = [];
|
||||
|
||||
for (const line of lines.slice(1)) {
|
||||
if (!line || line === "end_header" || line.startsWith("comment ") || line.startsWith("obj_info ")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tokens = line.split(/\s+/);
|
||||
if (tokens[0] === "format") {
|
||||
const value = tokens[1];
|
||||
if (value !== "ascii" && value !== "binary_little_endian" && value !== "binary_big_endian") {
|
||||
throw new PointcloudMetadataParseError("不支持的 PLY 编码");
|
||||
}
|
||||
pointDataFormat = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tokens[0] === "element") {
|
||||
currentElement = tokens[1] ?? null;
|
||||
if (currentElement === "vertex") {
|
||||
const count = Number(tokens[2]);
|
||||
if (!Number.isSafeInteger(count) || count < 0) {
|
||||
throw new PointcloudMetadataParseError("PLY 顶点数无效");
|
||||
}
|
||||
pointCount = count;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tokens[0] === "property" && currentElement === "vertex") {
|
||||
if (tokens[1] === "list") {
|
||||
throw new PointcloudMetadataParseError("暂不支持列表型顶点属性");
|
||||
}
|
||||
const scalar = PLY_SCALAR_TYPES[tokens[1]];
|
||||
const name = tokens[2];
|
||||
if (!scalar || !name) {
|
||||
throw new PointcloudMetadataParseError("PLY 包含未知的顶点属性类型");
|
||||
}
|
||||
pointAttributes.push({ name, type: scalar.type, bytes: scalar.bytes });
|
||||
}
|
||||
}
|
||||
|
||||
if (!pointDataFormat) {
|
||||
throw new PointcloudMetadataParseError("PLY 文件头缺少编码信息");
|
||||
}
|
||||
if (pointCount === null) {
|
||||
throw new PointcloudMetadataParseError("PLY 文件头缺少顶点数量");
|
||||
}
|
||||
if (pointAttributes.length === 0) {
|
||||
throw new PointcloudMetadataParseError("PLY 文件头缺少顶点属性");
|
||||
}
|
||||
|
||||
return {
|
||||
pointCount,
|
||||
pointAttributes,
|
||||
pointDataFormat,
|
||||
bytesPerPoint: pointAttributes.reduce((sum, attribute) => sum + attribute.bytes, 0),
|
||||
};
|
||||
}
|
||||
|
||||
export function inspectPointcloudMetadata(filePath: string, displayName = filePath): PointcloudMetadataValues {
|
||||
const extension = path.extname(displayName || filePath).toLowerCase();
|
||||
if (extension !== ".ply") {
|
||||
return {
|
||||
metadataStatus: "unsupported",
|
||||
pointCount: null,
|
||||
pointAttributes: null,
|
||||
pointDataFormat: null,
|
||||
bytesPerPoint: null,
|
||||
metadataError: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const fd = fs.openSync(filePath, "r");
|
||||
try {
|
||||
const buffer = Buffer.allocUnsafe(MAX_PLY_HEADER_BYTES);
|
||||
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
||||
const parsed = parsePlyHeader(buffer.subarray(0, bytesRead));
|
||||
return { metadataStatus: "ready", ...parsed, metadataError: null };
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
metadataStatus: "error",
|
||||
pointCount: null,
|
||||
pointAttributes: null,
|
||||
pointDataFormat: null,
|
||||
bytesPerPoint: null,
|
||||
metadataError: error instanceof PointcloudMetadataParseError
|
||||
? error.message
|
||||
: "PLY 文件头解析失败",
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user