UNPKG

ply-js

Version:

A TypeScript port based on python-plyfile for reading and writing .ply files

192 lines (191 loc) 9.44 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PlyData = void 0; /* * This file is part of python-plyfile (original work Copyright © 2014-2025 Darsh Ranjan * and plyfile authors). TypeScript port © 2025 Gustavo Diogo Silva (GitHub: GustavoDiogo). * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this * program. If not, see <http://www.gnu.org/licenses/>. */ const element_1 = require("./element"); const header_1 = require("./header"); const utils_1 = require("./utils"); const fs_1 = __importDefault(require("fs")); class PlyData { constructor(elements = [], text = false, byteOrder = '=', comments = [], objInfo = []) { this._elements = []; this._elementLookup = new Map(); this._comments = []; this._objInfo = []; this._text = false; this._byteOrder = '='; this._byteOrder = byteOrder; this._text = text; this.comments = comments; this.objInfo = objInfo; this.elements = elements; } get elements() { return this._elements; } set elements(v) { this._elements = [...v]; this._index(); } get text() { return this._text; } set text(v) { this._text = v; } get byteOrder() { return (!this._text && this._byteOrder === '=') ? utils_1.nativeByteOrder : this._byteOrder; } set byteOrder(v) { if (!['<', '>', '='].includes(v)) throw new Error("byte order must be '<', '>', or '='"); this._byteOrder = v; } get comments() { return [...this._comments]; } set comments(v) { this._comments = [...v]; } get objInfo() { return [...this._objInfo]; } set objInfo(v) { this._objInfo = [...v]; } _index() { this._elementLookup = new Map(this._elements.map(e => [e.name, e])); if (this._elementLookup.size !== this._elements.length) throw new Error('two elements with same name'); } static _parseHeader(stream) { const parser = new header_1.PlyHeaderParser(new header_1.PlyHeaderLines(stream)); const elements = parser.elements.map(e => new element_1.PlyElement(e.name, e.properties, e.count, e.comments)); const pd = new PlyData(elements, parser.format === 'ascii', utils_1.byteOrderMap[parser.format], parser.comments, parser.objInfo); return pd; } static read(pathOrStream, opts = {}) { const mustClose = typeof pathOrStream === 'string'; try { if (typeof pathOrStream === 'string') { const file = fs_1.default.readFileSync(pathOrStream); // buffer-backed reader that tracks offset consumed by PlyHeaderLines let offset = 0; const rawReader = { read(n) { if (offset >= file.length) return null; const end = Math.min(offset + n, file.length); const chunk = file.slice(offset, end); offset = end; return chunk; } }; // PlyHeaderLines expects a reader returning string|Buffer; adapter converts null->'' const reader = { read(n) { const r = rawReader.read(n); return r === null ? '' : r; } }; // Use PlyHeaderLines against our buffer reader to reliably parse header const headerLines = []; for (const line of new header_1.PlyHeaderLines(reader)) headerLines.push(line); const parser = new header_1.PlyHeaderParser(headerLines); const elements = parser.elements.map(e => new element_1.PlyElement(e.name, e.properties, e.count, e.comments)); const headerParsed = new PlyData(elements, parser.format === 'ascii', utils_1.byteOrderMap[parser.format], parser.comments, parser.objInfo); const dataBuf = file.subarray(offset); // debug: show header offset, header lines and element summaries // eslint-disable-next-line no-console console.error('DEBUG header offset=', offset); // eslint-disable-next-line no-console console.error('DEBUG headerLines[0..5]=', headerLines.slice(0, 6)); // eslint-disable-next-line no-console console.error('DEBUG parsed elements=', headerParsed.elements.map(e => ({ name: e.name, count: e.count, props: e.properties.map((p) => p.name) }))); // eslint-disable-next-line no-console console.error('DEBUG dataBuf length=', dataBuf.length, 'first32=', dataBuf.slice(0, 32).toString('hex')); // debug: estimate expected bytes per element (scalar-only elements) try { const sizes = { i1: 1, u1: 1, i2: 2, u2: 2, i4: 4, u4: 4, f4: 4, f8: 8 }; const per = headerParsed.elements.map(e => { const hasList = e.properties.some((p) => p.constructor && p.constructor.name === 'PlyListProperty'); if (hasList) return { name: e.name, count: e.count, estBytes: null, hasList: true }; let rowBytes = 0; for (const p of e.properties) { const code = p.valDtype; rowBytes += sizes[code]; } return { name: e.name, count: e.count, estBytes: rowBytes * e.count, hasList: false }; }); // eslint-disable-next-line no-console console.error('DEBUG expected per-element bytes=', per); } catch (e) { /* ignore */ } if (headerParsed.text) { const s = dataBuf.toString('utf8'); const lines = s.split(/\r?\n/).filter(Boolean); let lineCursor = 0; for (const elt of headerParsed) { const need = elt.count; const slice = lines.slice(lineCursor, lineCursor + need).join('\n') + '\n'; elt._read(Buffer.from(slice, 'utf8'), true, headerParsed.byteOrder, opts.mmap, opts.knownListLen?.[elt.name] || {}); lineCursor += need; } } else { let cursor = 0; for (const elt of headerParsed) { const bufSlice = dataBuf.subarray(cursor); const consumed = elt._read(bufSlice, false, headerParsed.byteOrder, opts.mmap, opts.knownListLen?.[elt.name] || {}); if (typeof consumed !== 'number') throw new Error(`element ${elt.name} did not return consumed byte count`); cursor += consumed; } } return headerParsed; } throw new Error('Readable stream version of read() not implemented in this minimal port. Provide a filename path.'); } finally { // nothing to close for readFileSync } } write(pathOrStream, _opts = {}) { const text = this._text; const binaryStream = typeof pathOrStream !== 'string' ? pathOrStream : fs_1.default.createWriteStream(pathOrStream); const header = this.header; const outChunks = []; if (text) outChunks.push(header + '\n'); else outChunks.push(Buffer.from(header + '\n', 'ascii')); for (const elt of this._elements) { elt._write({ push: (b) => outChunks.push(b) }, text, this.byteOrder); } for (const c of outChunks) binaryStream.write(c); if (typeof pathOrStream === 'string') binaryStream.end(); } get header() { const lines = ['ply']; if (this._text) lines.push('format ascii 1.0'); else lines.push(`format ${utils_1.byteOrderReverse[this.byteOrder]} 1.0`); for (const c of this._comments) lines.push('comment ' + c); for (const c of this._objInfo) lines.push('obj_info ' + c); for (const e of this._elements) lines.push(e.header()); lines.push('end_header'); return lines.join('\n'); } [Symbol.iterator]() { return this._elements[Symbol.iterator](); } get length() { return this._elements.length; } has(name) { return this._elementLookup.has(name); } get(name) { const e = this._elementLookup.get(name); if (!e) throw new Error('KeyError'); return e; } toString() { return this.header; } } exports.PlyData = PlyData;