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 = { 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 文件头解析失败", }; } }