ply-js
Version:
A TypeScript port based on python-plyfile for reading and writing .ply files
192 lines (191 loc) • 9.44 kB
JavaScript
"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;