diff --git a/projects/CalcGeometryUV/PrimitivePlyIO.cpp b/projects/CalcGeometryUV/PrimitivePlyIO.cpp index c81433bf4..663d03933 100644 --- a/projects/CalcGeometryUV/PrimitivePlyIO.cpp +++ b/projects/CalcGeometryUV/PrimitivePlyIO.cpp @@ -22,6 +22,7 @@ #define TINYPLY_IMPLEMENTATION #include "primplyio_tinyply.h" +#include "happly.h" /* static void readply( @@ -123,18 +124,72 @@ static void readply( } } */ + static void ReadAllAttrFromPlyFile(std::string &ply_file, std::shared_ptr prim){ std::filesystem::path file_path(ply_file); if(!std::filesystem::exists(file_path)){ throw std::runtime_error(ply_file + " not exsit"); return; } + std::ifstream file_stream; - file_stream.open(file_path); + file_stream.open(file_path,std::ios::binary); if(!file_stream.is_open()){ throw std::runtime_error("fail to open "+ply_file); return; } + + happly::PLYData ply_obj(file_stream); + try{ + ply_obj.validate(); + }catch(std::exception &e){ + std::cerr << e.what() << std::endl; + throw e; + return; + } + std::vector element_names = ply_obj.getElementNames(); + for(std::string element_name : element_names){ + std::cout << element_name << "\n |\n"; + std::vectorproperty_names = ply_obj.getElement(element_name).getPropertyNames(); + for(std::string property_name : property_names){ + std::cout << "\t|--" << property_name << std::endl; + } + + } + happly::Element& vertex = ply_obj.getElement("vertex"); + std::cout << "Vertex count = " <verts.resize(vertex.count); + std::vector property_names = vertex.getPropertyNames(); + for(std::string property_name : property_names){ + std::vector &new_property = prim->add_attr(property_name); + try{ + std::vector data = vertex.getProperty(property_name); + new_property.assign(data.begin(),data.end()); + }catch(std::exception &e){ + std::cerr << e.what() << std::endl; + throw e; + return; + } + } + return; + +} + +/* +static void ReadAllAttrFromPlyFile(std::string &ply_file, std::shared_ptr prim){ + std::filesystem::path file_path(ply_file); + if(!std::filesystem::exists(file_path)){ + throw std::runtime_error(ply_file + " not exsit"); + return; + } + + std::ifstream file_stream; + file_stream.open(file_path,std::ios::binary); + if(!file_stream.is_open()){ + throw std::runtime_error("fail to open "+ply_file); + return; + } + tinyply::PlyFile ply_obj; if(!ply_obj.parse_header(file_stream)){ throw std::runtime_error("fail to parse ply header"); @@ -210,6 +265,7 @@ static void ReadAllAttrFromPlyFile(std::string &ply_file, std::shared_ptr +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// General namespace wrapping all Happly things. +namespace happly { + +// Enum specifying binary or ASCII filetypes. Binary can be little-endian +// (default) or big endian. +enum class DataFormat { ASCII, Binary, BinaryBigEndian }; + +// Type name strings +// clang-format off +template std::string typeName() { return "unknown"; } +template<> inline std::string typeName() { return "char"; } +template<> inline std::string typeName() { return "uchar"; } +template<> inline std::string typeName() { return "short"; } +template<> inline std::string typeName() { return "ushort"; } +template<> inline std::string typeName() { return "int"; } +template<> inline std::string typeName() { return "uint"; } +template<> inline std::string typeName() { return "float"; } +template<> inline std::string typeName() { return "double"; } + +// Template hackery that makes getProperty() and friends pretty while automatically picking up smaller types +namespace { + +// A pointer for the equivalent/smaller equivalent of a type (eg. when a double is requested a float works too, etc) +// long int is intentionally absent to avoid platform confusion +template struct TypeChain { bool hasChildType = false; typedef T type; }; +template <> struct TypeChain { bool hasChildType = true; typedef int32_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef int16_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef int8_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef uint32_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef uint16_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef uint8_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef float type; }; + +template struct CanonicalName { typedef T type; }; +template <> struct CanonicalName { typedef int8_t type; }; +template <> struct CanonicalName { typedef uint8_t type; }; +template <> struct CanonicalName { typedef std::conditional::type, int>::value, uint32_t, uint64_t>::type type; }; + +// Used to change behavior of >> for 8bit ints, which does not do what we want. +template struct SerializeType { typedef T type; }; +template <> struct SerializeType { typedef int32_t type; }; +template <> struct SerializeType< int8_t> { typedef int32_t type; }; + +// Give address only if types are same (used below when conditionally copying data) +// last int/char arg is to resolve ambiguous overloads, just always pass 0 and the int version will be preferred +template +S* addressIfSame(T&, char) { + throw std::runtime_error("tried to take address for types that are not same"); + return nullptr;} +template +S* addressIfSame(S& t, int) {return &t;} + +// clang-format on +} // namespace + +/** + * @brief A generic property, which is associated with some element. Can be plain Property or a ListProperty, of some + * type. Generally, the user should not need to interact with these directly, but they are exposed in case someone + * wants to get clever. + */ +class Property { + +public: + /** + * @brief Create a new Property with the given name. + * + * @param name_ + */ + Property(const std::string& name_) : name(name_){}; + virtual ~Property(){}; + + std::string name; + + /** + * @brief Reserve memory. + * + * @param capacity Expected number of elements. + */ + virtual void reserve(size_t capacity) = 0; + + /** + * @brief (ASCII reading) Parse out the next value of this property from a list of tokens. + * + * @param tokens The list of property tokens for the element. + * @param currEntry Index in to tokens, updated after this property is read. + */ + virtual void parseNext(const std::vector& tokens, size_t& currEntry) = 0; + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNext(std::istream& stream) = 0; + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNextBigEndian(std::istream& stream) = 0; + + /** + * @brief (reading) Write a header entry for this property. + * + * @param outStream Stream to write to. + */ + virtual void writeHeader(std::ostream& outStream) = 0; + + /** + * @brief (ASCII writing) write this property for some element to a stream in plaintext + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataASCII(std::ostream& outStream, size_t iElement) = 0; + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinary(std::ostream& outStream, size_t iElement) = 0; + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinaryBigEndian(std::ostream& outStream, size_t iElement) = 0; + + /** + * @brief Number of element entries for this property + * + * @return + */ + virtual size_t size() = 0; + + /** + * @brief A string naming the type of the property + * + * @return + */ + virtual std::string propertyTypeName() = 0; +}; + +namespace { + +/** + * Check if the platform is little endian. + * (not foolproof, but will work on most platforms) + * + * @return true if little endian + */ +bool isLittleEndian() { + int32_t oneVal = 0x1; + char* numPtr = (char*)&oneVal; + return (numPtr[0] == 1); +} + +/** + * Swap endianness. + * + * @param value Value to swap. + * + * @return Swapped value. + */ +template +T swapEndian(T val) { + char* bytes = reinterpret_cast(&val); + for (unsigned int i = 0; i < sizeof(val) / 2; i++) { + std::swap(bytes[sizeof(val) - 1 - i], bytes[i]); + } + return val; +} + +// The following specializations for single-byte types are used to avoid compiler warnings. +template <> int8_t swapEndian(int8_t val) { return val; } +template <> uint8_t swapEndian(uint8_t val) { return val; } + + +// Unpack flattened list from the convention used in TypedListProperty +template +std::vector> unflattenList(const std::vector& flatList, const std::vector flatListStarts) { + size_t outerCount = flatListStarts.size() - 1; + + // Put the output here + std::vector> outLists(outerCount); + + if (outerCount == 0) { + return outLists; // quick out for empty + } + + // Copy each sublist + for (size_t iOuter = 0; iOuter < outerCount; iOuter++) { + size_t iFlatStart = flatListStarts[iOuter]; + size_t iFlatEnd = flatListStarts[iOuter + 1]; + outLists[iOuter].insert(outLists[iOuter].begin(), flatList.begin() + iFlatStart, flatList.begin() + iFlatEnd); + } + + return outLists; +} + + +}; // namespace + + +/** + * @brief A property which takes a single value (not a list). + */ +template +class TypedProperty : public Property { + +public: + /** + * @brief Create a new Property with the given name. + * + * @param name_ + */ + TypedProperty(const std::string& name_) : Property(name_) { + if (typeName() == "unknown") { + // TODO should really be a compile-time error + throw std::runtime_error("Attempted property type does not match any type defined by the .ply format."); + } + }; + + /** + * @brief Create a new property and initialize with data. + * + * @param name_ + * @param data_ + */ + TypedProperty(const std::string& name_, const std::vector& data_) : Property(name_), data(data_) { + if (typeName() == "unknown") { + throw std::runtime_error("Attempted property type does not match any type defined by the .ply format."); + } + }; + + virtual ~TypedProperty() override{}; + + /** + * @brief Reserve memory. + * + * @param capacity Expected number of elements. + */ + virtual void reserve(size_t capacity) override { data.reserve(capacity); } + + /** + * @brief (ASCII reading) Parse out the next value of this property from a list of tokens. + * + * @param tokens The list of property tokens for the element. + * @param currEntry Index in to tokens, updated after this property is read. + */ + virtual void parseNext(const std::vector& tokens, size_t& currEntry) override { + data.emplace_back(); + std::istringstream iss(tokens[currEntry]); + typename SerializeType::type tmp; // usually the same type as T + iss >> tmp; + data.back() = tmp; + currEntry++; + }; + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNext(std::istream& stream) override { + data.emplace_back(); + stream.read((char*)&data.back(), sizeof(T)); + } + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNextBigEndian(std::istream& stream) override { + data.emplace_back(); + stream.read((char*)&data.back(), sizeof(T)); + data.back() = swapEndian(data.back()); + } + + /** + * @brief (reading) Write a header entry for this property. + * + * @param outStream Stream to write to. + */ + virtual void writeHeader(std::ostream& outStream) override { + outStream << "property " << typeName() << " " << name << "\n"; + } + + /** + * @brief (ASCII writing) write this property for some element to a stream in plaintext + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataASCII(std::ostream& outStream, size_t iElement) override { + outStream.precision(std::numeric_limits::max_digits10); + outStream << static_cast::type>(data[iElement]); // case is usually a no-op + } + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinary(std::ostream& outStream, size_t iElement) override { + outStream.write((char*)&data[iElement], sizeof(T)); + } + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinaryBigEndian(std::ostream& outStream, size_t iElement) override { + auto value = swapEndian(data[iElement]); + outStream.write((char*)&value, sizeof(T)); + } + + /** + * @brief Number of element entries for this property + * + * @return + */ + virtual size_t size() override { return data.size(); } + + + /** + * @brief A string naming the type of the property + * + * @return + */ + virtual std::string propertyTypeName() override { return typeName(); } + + /** + * @brief The actual data contained in the property + */ + std::vector data; +}; + + +/** + * @brief A property which is a list of value (eg, 3 doubles). Note that lists are always variable length per-element. + */ +template +class TypedListProperty : public Property { + +public: + /** + * @brief Create a new Property with the given name. + * + * @param name_ + */ + TypedListProperty(const std::string& name_, int listCountBytes_) : Property(name_), listCountBytes(listCountBytes_) { + if (typeName() == "unknown") { + throw std::runtime_error("Attempted property type does not match any type defined by the .ply format."); + } + + flattenedIndexStart.push_back(0); + }; + + /** + * @brief Create a new property and initialize with data + * + * @param name_ + * @param data_ + */ + TypedListProperty(const std::string& name_, const std::vector>& data_) : Property(name_) { + if (typeName() == "unknown") { + throw std::runtime_error("Attempted property type does not match any type defined by the .ply format."); + } + + // Populate list with data + flattenedIndexStart.push_back(0); + for (const std::vector& vec : data_) { + for (const T& val : vec) { + flattenedData.emplace_back(val); + } + flattenedIndexStart.push_back(flattenedData.size()); + } + }; + + virtual ~TypedListProperty() override{}; + + /** + * @brief Reserve memory. + * + * @param capacity Expected number of elements. + */ + virtual void reserve(size_t capacity) override { + flattenedData.reserve(3 * capacity); // optimize for triangle meshes + flattenedIndexStart.reserve(capacity + 1); + } + + /** + * @brief (ASCII reading) Parse out the next value of this property from a list of tokens. + * + * @param tokens The list of property tokens for the element. + * @param currEntry Index in to tokens, updated after this property is read. + */ + virtual void parseNext(const std::vector& tokens, size_t& currEntry) override { + + std::istringstream iss(tokens[currEntry]); + size_t count; + iss >> count; + currEntry++; + + size_t currSize = flattenedData.size(); + size_t afterSize = currSize + count; + flattenedData.resize(afterSize); + for (size_t iFlat = currSize; iFlat < afterSize; iFlat++) { + std::istringstream iss(tokens[currEntry]); + typename SerializeType::type tmp; // usually the same type as T + iss >> tmp; + flattenedData[iFlat] = tmp; + currEntry++; + } + flattenedIndexStart.emplace_back(afterSize); + } + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNext(std::istream& stream) override { + + // Read the size of the list + size_t count = 0; + stream.read(((char*)&count), listCountBytes); + + // Read list elements + size_t currSize = flattenedData.size(); + size_t afterSize = currSize + count; + flattenedData.resize(afterSize); + if (count > 0) { + stream.read((char*)&flattenedData[currSize], count * sizeof(T)); + } + flattenedIndexStart.emplace_back(afterSize); + } + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNextBigEndian(std::istream& stream) override { + + // Read the size of the list + size_t count = 0; + stream.read(((char*)&count), listCountBytes); + if (listCountBytes == 8) { + count = (size_t)swapEndian((uint64_t)count); + } else if (listCountBytes == 4) { + count = (size_t)swapEndian((uint32_t)count); + } else if (listCountBytes == 2) { + count = (size_t)swapEndian((uint16_t)count); + } + + // Read list elements + size_t currSize = flattenedData.size(); + size_t afterSize = currSize + count; + flattenedData.resize(afterSize); + if (count > 0) { + stream.read((char*)&flattenedData[currSize], count * sizeof(T)); + } + flattenedIndexStart.emplace_back(afterSize); + + // Swap endian order of list elements + for (size_t iFlat = currSize; iFlat < afterSize; iFlat++) { + flattenedData[iFlat] = swapEndian(flattenedData[iFlat]); + } + } + + /** + * @brief (reading) Write a header entry for this property. Note that we already use "uchar" for the list count type. + * + * @param outStream Stream to write to. + */ + virtual void writeHeader(std::ostream& outStream) override { + // NOTE: We ALWAYS use uchar as the list count output type + outStream << "property list uchar " << typeName() << " " << name << "\n"; + } + + /** + * @brief (ASCII writing) write this property for some element to a stream in plaintext + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataASCII(std::ostream& outStream, size_t iElement) override { + size_t dataStart = flattenedIndexStart[iElement]; + size_t dataEnd = flattenedIndexStart[iElement + 1]; + + // Get the number of list elements as a uchar, and ensure the value fits + size_t dataCount = dataEnd - dataStart; + if (dataCount > std::numeric_limits::max()) { + throw std::runtime_error( + "List property has an element with more entries than fit in a uchar. See note in README."); + } + + outStream << dataCount; + outStream.precision(std::numeric_limits::max_digits10); + for (size_t iFlat = dataStart; iFlat < dataEnd; iFlat++) { + outStream << " " << static_cast::type>(flattenedData[iFlat]); // cast is usually a no-op + } + } + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinary(std::ostream& outStream, size_t iElement) override { + size_t dataStart = flattenedIndexStart[iElement]; + size_t dataEnd = flattenedIndexStart[iElement + 1]; + + // Get the number of list elements as a uchar, and ensure the value fits + size_t dataCount = dataEnd - dataStart; + if (dataCount > std::numeric_limits::max()) { + throw std::runtime_error( + "List property has an element with more entries than fit in a uchar. See note in README."); + } + uint8_t count = static_cast(dataCount); + + outStream.write((char*)&count, sizeof(uint8_t)); + outStream.write((char*)&flattenedData[dataStart], count * sizeof(T)); + } + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinaryBigEndian(std::ostream& outStream, size_t iElement) override { + size_t dataStart = flattenedIndexStart[iElement]; + size_t dataEnd = flattenedIndexStart[iElement + 1]; + + // Get the number of list elements as a uchar, and ensure the value fits + size_t dataCount = dataEnd - dataStart; + if (dataCount > std::numeric_limits::max()) { + throw std::runtime_error( + "List property has an element with more entries than fit in a uchar. See note in README."); + } + uint8_t count = static_cast(dataCount); + + outStream.write((char*)&count, sizeof(uint8_t)); + for (size_t iFlat = dataStart; iFlat < dataEnd; iFlat++) { + T value = swapEndian(flattenedData[iFlat]); + outStream.write((char*)&value, sizeof(T)); + } + } + + /** + * @brief Number of element entries for this property + * + * @return + */ + virtual size_t size() override { return flattenedIndexStart.size() - 1; } + + + /** + * @brief A string naming the type of the property + * + * @return + */ + virtual std::string propertyTypeName() override { return typeName(); } + + /** + * @brief The (flattened) data for the property, as formed by concatenating all of the individual element lists + * together. + */ + std::vector flattenedData; + + /** + * @brief Indices in to flattenedData. The i'th element gives the index in to flattenedData where the element's data + * begins. A final entry is included which is the length of flattenedData. Size is N_elem + 1. + */ + std::vector flattenedIndexStart; + + /** + * @brief The number of bytes used to store the count for lists of data. + */ + int listCountBytes = -1; +}; + + +/** + * @brief Helper function to construct a new property of the appropriate type. + * + * @param name The name of the property to construct. + * @param typeStr A string naming the type according to the format. + * @param isList Is this a plain property, or a list property? + * @param listCountTypeStr If a list property, the type of the count varible. + * + * @return A new Property with the proper type. + */ +inline std::unique_ptr createPropertyWithType(const std::string& name, const std::string& typeStr, + bool isList, const std::string& listCountTypeStr) { + + // == Figure out how many bytes the list count field has, if this is a list type + // Note: some files seem to use signed types here, we read the width but always parse as if unsigned + int listCountBytes = -1; + if (isList) { + if (listCountTypeStr == "uchar" || listCountTypeStr == "uint8" || listCountTypeStr == "char" || + listCountTypeStr == "int8") { + listCountBytes = 1; + } else if (listCountTypeStr == "ushort" || listCountTypeStr == "uint16" || listCountTypeStr == "short" || + listCountTypeStr == "int16") { + listCountBytes = 2; + } else if (listCountTypeStr == "uint" || listCountTypeStr == "uint32" || listCountTypeStr == "int" || + listCountTypeStr == "int32") { + listCountBytes = 4; + } else { + throw std::runtime_error("Unrecognized list count type: " + listCountTypeStr); + } + } + + // = Unsigned int + + // 8 bit unsigned + if (typeStr == "uchar" || typeStr == "uint8") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // 16 bit unsigned + else if (typeStr == "ushort" || typeStr == "uint16") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // 32 bit unsigned + else if (typeStr == "uint" || typeStr == "uint32") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // = Signed int + + // 8 bit signed + if (typeStr == "char" || typeStr == "int8") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // 16 bit signed + else if (typeStr == "short" || typeStr == "int16") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // 32 bit signed + else if (typeStr == "int" || typeStr == "int32") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // = Float + + // 32 bit float + else if (typeStr == "float" || typeStr == "float32") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // 64 bit float + else if (typeStr == "double" || typeStr == "float64") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + else { + throw std::runtime_error("Data type: " + typeStr + " cannot be mapped to .ply format"); + } +} + +/** + * @brief An element (more properly an element type) in the .ply object. Tracks the name of the elemnt type (eg, + * "vertices"), the number of elements of that type (eg, 1244), and any properties associated with that element (eg, + * "position", "color"). + */ +class Element { + +public: + /** + * @brief Create a new element type. + * + * @param name_ Name of the element type (eg, "vertices") + * @param count_ Number of instances of this element. + */ + Element(const std::string& name_, size_t count_) : name(name_), count(count_) {} + + std::string name; + size_t count; + std::vector> properties; + + /** + * @brief Check if a property exists. + * + * @param target The name of the property to get. + * + * @return Whether the target property exists. + */ + bool hasProperty(const std::string& target) { + for (std::unique_ptr& prop : properties) { + if (prop->name == target) { + return true; + } + } + return false; + } + + /** + * @brief Check if a property exists with the requested type. + * + * @tparam T The type of the property + * @param target The name of the property to get. + * + * @return Whether the target property exists. + */ + template + bool hasPropertyType(const std::string& target) { + for (std::unique_ptr& prop : properties) { + if (prop->name == target) { + TypedProperty* castedProp = dynamic_cast*>(prop.get()); + if (castedProp) { + return true; + } + return false; + } + } + return false; + } + + /** + * @brief A list of the names of all properties + * + * @return Property names + */ + std::vector getPropertyNames() { + std::vector names; + for (std::unique_ptr& p : properties) { + names.push_back(p->name); + } + return names; + } + + /** + * @brief Low-level method to get a pointer to a property. Users probably don't need to call this. + * + * @param target The name of the property to get. + * + * @return A (unique_ptr) pointer to the property. + */ + std::unique_ptr& getPropertyPtr(const std::string& target) { + for (std::unique_ptr& prop : properties) { + if (prop->name == target) { + return prop; + } + } + throw std::runtime_error("PLY parser: element " + name + " does not have property " + target); + } + + /** + * @brief Add a new (plain, not list) property for this element type. + * + * @tparam T The type of the property + * @param propertyName The name of the property + * @param data The data for the property. Must have the same length as the number of elements. + */ + template + void addProperty(const std::string& propertyName, const std::vector& data) { + + if (data.size() != count) { + throw std::runtime_error("PLY write: new property " + propertyName + " has size which does not match element"); + } + + // If there is already some property with this name, remove it + for (size_t i = 0; i < properties.size(); i++) { + if (properties[i]->name == propertyName) { + properties.erase(properties.begin() + i); + i--; + } + } + + // Copy to canonical type. Often a no-op, but takes care of standardizing widths across platforms. + std::vector::type> canonicalVec(data.begin(), data.end()); + + properties.push_back( + std::unique_ptr(new TypedProperty::type>(propertyName, canonicalVec))); + } + + /** + * @brief Add a new list property for this element type. + * + * @tparam T The type of the property (eg, "double" for a list of doubles) + * @param propertyName The name of the property + * @param data The data for the property. Outer vector must have the same length as the number of elements. + */ + template + void addListProperty(const std::string& propertyName, const std::vector>& data) { + + if (data.size() != count) { + throw std::runtime_error("PLY write: new property " + propertyName + " has size which does not match element"); + } + + // If there is already some property with this name, remove it + for (size_t i = 0; i < properties.size(); i++) { + if (properties[i]->name == propertyName) { + properties.erase(properties.begin() + i); + i--; + } + } + + // Copy to canonical type. Often a no-op, but takes care of standardizing widths across platforms. + std::vector::type>> canonicalListVec; + for (const std::vector& subList : data) { + canonicalListVec.emplace_back(subList.begin(), subList.end()); + } + + properties.push_back(std::unique_ptr( + new TypedListProperty::type>(propertyName, canonicalListVec))); + } + + /** + * @brief Get a vector of a data from a property for this element. Automatically promotes to larger types. Throws if + * requested data is unavailable. + * + * @tparam T The type of data requested + * @param propertyName The name of the property to get. + * + * @return The data. + */ + template + std::vector getProperty(const std::string& propertyName) { + + // Find the property + std::unique_ptr& prop = getPropertyPtr(propertyName); + + // Get a copy of the data with auto-promoting type magic + return getDataFromPropertyRecursive(prop.get()); + } + + /** + * @brief Get a vector of a data from a property for this element. Unlike getProperty(), only returns if the ply + * record contains a type that matches T exactly. Throws if * requested data is unavailable. + * + * @tparam T The type of data requested + * @param propertyName The name of the property to get. + * + * @return The data. + */ + template + std::vector getPropertyType(const std::string& propertyName) { + + // Find the property + std::unique_ptr& prop = getPropertyPtr(propertyName); + TypedProperty* castedProp = dynamic_cast*>(prop.get()); + if (castedProp) { + return castedProp->data; + } + + // No match, failure + throw std::runtime_error("PLY parser: property " + prop->name + " is not of type type " + typeName() + + ". Has type " + prop->propertyTypeName()); + } + + /** + * @brief Get a vector of lists of data from a property for this element. Automatically promotes to larger types. + * Throws if requested data is unavailable. + * + * @tparam T The type of data requested + * @param propertyName The name of the property to get. + * + * @return The data. + */ + template + std::vector> getListProperty(const std::string& propertyName) { + + // Find the property + std::unique_ptr& prop = getPropertyPtr(propertyName); + + // Get a copy of the data with auto-promoting type magic + return getDataFromListPropertyRecursive(prop.get()); + } + + /** + * @brief Get a vector of a data from a property for this element. Unlike getProperty(), only returns if the ply + * record contains a type that matches T exactly. Throws if * requested data is unavailable. + * + * @tparam T The type of data requested + * @param propertyName The name of the property to get. + * + * @return The data. + */ + template + std::vector> getListPropertyType(const std::string& propertyName) { + + // Find the property + std::unique_ptr& prop = getPropertyPtr(propertyName); + TypedListProperty* castedProp = dynamic_cast*>(prop.get()); + if (castedProp) { + return unflattenList(castedProp->flattenedData, castedProp->flattenedIndexStart); + } + + // No match, failure + throw std::runtime_error("PLY parser: list property " + prop->name + " is not of type " + typeName() + + ". Has type " + prop->propertyTypeName()); + } + + + /** + * @brief Get a vector of lists of data from a property for this element. Automatically promotes to larger types. + * Unlike getListProperty(), this method will additionally convert between types of different sign (eg, requesting and + * int32 would get data from a uint32); doing so naively converts between signed and unsigned types. This is typically + * useful for data representing indices, which might be stored as signed or unsigned numbers. + * + * @tparam T The type of data requested + * @param propertyName The name of the property to get. + * + * @return The data. + */ + template + std::vector> getListPropertyAnySign(const std::string& propertyName) { + + // Find the property + std::unique_ptr& prop = getPropertyPtr(propertyName); + + // Get a copy of the data with auto-promoting type magic + try { + // First, try the usual approach, looking for a version of the property with the same signed-ness and possibly + // smaller size + return getDataFromListPropertyRecursive(prop.get()); + } catch (const std::runtime_error& orig_e) { + + // If the usual approach fails, look for a version with opposite signed-ness + try { + + // This type has the oppopsite signeness as the input type + typedef typename CanonicalName::type Tcan; + typedef typename std::conditional::value, typename std::make_unsigned::type, + typename std::make_signed::type>::type OppsignType; + + return getDataFromListPropertyRecursive(prop.get()); + + } catch (const std::runtime_error&) { + throw orig_e; + } + + throw orig_e; + } + } + + + /** + * @brief Performs sanity checks on the element, throwing if any fail. + */ + void validate() { + + // Make sure no properties have duplicate names, and no names have whitespace + for (size_t iP = 0; iP < properties.size(); iP++) { + for (char c : properties[iP]->name) { + if (std::isspace(c)) { + throw std::runtime_error("Ply validate: illegal whitespace in name " + properties[iP]->name); + } + } + for (size_t jP = iP + 1; jP < properties.size(); jP++) { + if (properties[iP]->name == properties[jP]->name) { + throw std::runtime_error("Ply validate: multiple properties with name " + properties[iP]->name); + } + } + } + + // Make sure all properties have right length + for (size_t iP = 0; iP < properties.size(); iP++) { + if (properties[iP]->size() != count) { + throw std::runtime_error("Ply validate: property has wrong size. " + properties[iP]->name + + " does not match element size."); + } + } + } + + /** + * @brief Writes out this element's information to the file header. + * + * @param outStream The stream to use. + */ + void writeHeader(std::ostream& outStream) { + + outStream << "element " << name << " " << count << "\n"; + + for (std::unique_ptr& p : properties) { + p->writeHeader(outStream); + } + } + + /** + * @brief (ASCII writing) Writes out all of the data for every element of this element type to the stream, including + * all contained properties. + * + * @param outStream The stream to write to. + */ + void writeDataASCII(std::ostream& outStream) { + // Question: what is the proper output for an element with no properties? Here, we write a blank line, so there is + // one line per element no matter what. + for (size_t iE = 0; iE < count; iE++) { + for (size_t iP = 0; iP < properties.size(); iP++) { + properties[iP]->writeDataASCII(outStream, iE); + if (iP < properties.size() - 1) { + outStream << " "; + } + } + outStream << "\n"; + } + } + + + /** + * @brief (binary writing) Writes out all of the data for every element of this element type to the stream, including + * all contained properties. + * + * @param outStream The stream to write to. + */ + void writeDataBinary(std::ostream& outStream) { + for (size_t iE = 0; iE < count; iE++) { + for (size_t iP = 0; iP < properties.size(); iP++) { + properties[iP]->writeDataBinary(outStream, iE); + } + } + } + + + /** + * @brief (binary writing) Writes out all of the data for every element of this element type to the stream, including + * all contained properties. + * + * @param outStream The stream to write to. + */ + void writeDataBinaryBigEndian(std::ostream& outStream) { + for (size_t iE = 0; iE < count; iE++) { + for (size_t iP = 0; iP < properties.size(); iP++) { + properties[iP]->writeDataBinaryBigEndian(outStream, iE); + } + } + } + + + /** + * @brief Helper function which does the hard work to implement type promotion for data getters. Throws if type + * conversion fails. + * + * @tparam D The desired output type + * @tparam T The current attempt for the actual type of the property + * @param prop The property to get (does not delete nor share pointer) + * + * @return The data, with the requested type + */ + template + std::vector getDataFromPropertyRecursive(Property* prop) { + + typedef typename CanonicalName::type Tcan; + + { // Try to return data of type D from a property of type T + TypedProperty* castedProp = dynamic_cast*>(prop); + if (castedProp) { + // Succeeded, return a buffer of the data (copy while converting type) + std::vector castedVec; + castedVec.reserve(castedProp->data.size()); + for (Tcan& v : castedProp->data) { + castedVec.push_back(static_cast(v)); + } + return castedVec; + } + } + + TypeChain chainType; + if (chainType.hasChildType) { + return getDataFromPropertyRecursive::type>(prop); + } else { + // No smaller type to try, failure + throw std::runtime_error("PLY parser: property " + prop->name + " cannot be coerced to requested type " + + typeName() + ". Has type " + prop->propertyTypeName()); + } + } + + + /** + * @brief Helper function which does the hard work to implement type promotion for list data getters. Throws if type + * conversion fails. + * + * @tparam D The desired output type + * @tparam T The current attempt for the actual type of the property + * @param prop The property to get (does not delete nor share pointer) + * + * @return The data, with the requested type + */ + template + std::vector> getDataFromListPropertyRecursive(Property* prop) { + typedef typename CanonicalName::type Tcan; + + TypedListProperty* castedProp = dynamic_cast*>(prop); + if (castedProp) { + // Succeeded, return a buffer of the data (copy while converting type) + + // Convert to flat buffer of new type + std::vector* castedFlatVec = nullptr; + std::vector castedFlatVecCopy; // we _might_ make a copy here, depending on is_same below + + if (std::is_same, std::vector>::value) { + // just use the array we already have + castedFlatVec = addressIfSame>(castedProp->flattenedData, 0 /* dummy arg to disambiguate */); + } else { + // make a copy + castedFlatVecCopy.reserve(castedProp->flattenedData.size()); + for (Tcan& v : castedProp->flattenedData) { + castedFlatVecCopy.push_back(static_cast(v)); + } + castedFlatVec = &castedFlatVecCopy; + } + + // Unflatten and return + return unflattenList(*castedFlatVec, castedProp->flattenedIndexStart); + } + + TypeChain chainType; + if (chainType.hasChildType) { + return getDataFromListPropertyRecursive::type>(prop); + } else { + // No smaller type to try, failure + throw std::runtime_error("PLY parser: list property " + prop->name + + " cannot be coerced to requested type list " + typeName() + ". Has type list " + + prop->propertyTypeName()); + } + } +}; + + +// Some string helpers +namespace { + +inline std::string trimSpaces(const std::string& input) { + size_t start = 0; + while (start < input.size() && input[start] == ' ') start++; + size_t end = input.size(); + while (end > start && (input[end - 1] == ' ' || input[end - 1] == '\n' || input[end - 1] == '\r')) end--; + return input.substr(start, end - start); +} + +inline std::vector tokenSplit(const std::string& input) { + std::vector result; + size_t curr = 0; + size_t found = 0; + while ((found = input.find_first_of(' ', curr)) != std::string::npos) { + std::string token = input.substr(curr, found - curr); + token = trimSpaces(token); + if (token.size() > 0) { + result.push_back(token); + } + curr = found + 1; + } + std::string token = input.substr(curr); + token = trimSpaces(token); + if (token.size() > 0) { + result.push_back(token); + } + + return result; +} + +inline bool startsWith(const std::string& input, const std::string& query) { + return input.compare(0, query.length(), query) == 0; +} +}; // namespace + + +/** + * @brief Primary class; represents a set of data in the .ply format. + */ +class PLYData { + +public: + /** + * @brief Create an empty PLYData object. + */ + PLYData(){}; + + /** + * @brief Initialize a PLYData by reading from a file. Throws if any failures occur. + * + * @param filename The file to read from. + * @param verbose If true, print useful info about the file to stdout + */ + PLYData(const std::string& filename, bool verbose = false) { + + using std::cout; + using std::endl; + using std::string; + using std::vector; + + if (verbose) cout << "PLY parser: Reading ply file: " << filename << endl; + + // Open a file in binary always, in case it turns out to have binary data. + std::ifstream inStream(filename, std::ios::binary); + if (inStream.fail()) { + throw std::runtime_error("PLY parser: Could not open file " + filename); + } + + parsePLY(inStream, verbose); + + if (verbose) { + cout << " - Finished parsing file." << endl; + } + } + + /** + * @brief Initialize a PLYData by reading from a stringstream. Throws if any failures occur. + * + * @param inStream The stringstream to read from. + * @param verbose If true, print useful info about the file to stdout + */ + PLYData(std::istream& inStream, bool verbose = false) { + + using std::cout; + using std::endl; + + if (verbose) cout << "PLY parser: Reading ply file from stream" << endl; + + parsePLY(inStream, verbose); + + if (verbose) { + cout << " - Finished parsing stream." << endl; + } + } + + /** + * @brief Perform sanity checks on the file, throwing if any fail. + */ + void validate() { + + for (size_t iE = 0; iE < elements.size(); iE++) { + for (char c : elements[iE].name) { + if (std::isspace(c)) { + throw std::runtime_error("Ply validate: illegal whitespace in element name " + elements[iE].name); + } + } + for (size_t jE = iE + 1; jE < elements.size(); jE++) { + if (elements[iE].name == elements[jE].name) { + throw std::runtime_error("Ply validate: duplcate element name " + elements[iE].name); + } + } + } + + // Do a quick validation sanity check + for (Element& e : elements) { + e.validate(); + } + } + + /** + * @brief Write this data to a .ply file. + * + * @param filename The file to write to. + * @param format The format to use (binary or ascii?) + */ + void write(const std::string& filename, DataFormat format = DataFormat::ASCII) { + outputDataFormat = format; + + validate(); + + // Open stream for writing + std::ofstream outStream(filename, std::ios::out | std::ios::binary); + if (!outStream.good()) { + throw std::runtime_error("Ply writer: Could not open output file " + filename + " for writing"); + } + + writePLY(outStream); + } + + /** + * @brief Write this data to an output stream + * + * @param outStream The output stream to write to. + * @param format The format to use (binary or ascii?) + */ + void write(std::ostream& outStream, DataFormat format = DataFormat::ASCII) { + outputDataFormat = format; + + validate(); + + writePLY(outStream); + } + + /** + * @brief Get an element type by name ("vertices") + * + * @param target The name of the element type to get + * + * @return A reference to the element type. + */ + Element& getElement(const std::string& target) { + for (Element& e : elements) { + if (e.name == target) return e; + } + throw std::runtime_error("PLY parser: no element with name: " + target); + } + + + /** + * @brief Check if an element type exists + * + * @param target The name to check for. + * + * @return True if exists. + */ + bool hasElement(const std::string& target) { + for (Element& e : elements) { + if (e.name == target) return true; + } + return false; + } + + + /** + * @brief A list of the names of all elements + * + * @return Element names + */ + std::vector getElementNames() { + std::vector names; + for (Element& e : elements) { + names.push_back(e.name); + } + return names; + } + + + /** + * @brief Add a new element type to the object + * + * @param name The name of the new element type ("vertices"). + * @param count The number of elements of this type. + */ + void addElement(const std::string& name, size_t count) { elements.emplace_back(name, count); } + + // === Common-case helpers + + + /** + * @brief Common-case helper get mesh vertex positions + * + * @param vertexElementName The element name to use (default: "vertex") + * + * @return A vector of vertex positions. + */ + std::vector> getVertexPositions(const std::string& vertexElementName = "vertex") { + + std::vector xPos = getElement(vertexElementName).getProperty("x"); + std::vector yPos = getElement(vertexElementName).getProperty("y"); + std::vector zPos = getElement(vertexElementName).getProperty("z"); + + std::vector> result(xPos.size()); + for (size_t i = 0; i < result.size(); i++) { + result[i][0] = xPos[i]; + result[i][1] = yPos[i]; + result[i][2] = zPos[i]; + } + + return result; + } + + /** + * @brief Common-case helper get mesh vertex colors + * + * @param vertexElementName The element name to use (default: "vertex") + * + * @return A vector of vertex colors (unsigned chars [0,255]). + */ + std::vector> getVertexColors(const std::string& vertexElementName = "vertex") { + + std::vector r = getElement(vertexElementName).getProperty("red"); + std::vector g = getElement(vertexElementName).getProperty("green"); + std::vector b = getElement(vertexElementName).getProperty("blue"); + + std::vector> result(r.size()); + for (size_t i = 0; i < result.size(); i++) { + result[i][0] = r[i]; + result[i][1] = g[i]; + result[i][2] = b[i]; + } + + return result; + } + + /** + * @brief Common-case helper to get face indices for a mesh. If not template type is given, size_t is used. Naively + * converts to requested signedness, which may lead to unexpected values if an unsigned type is used and file contains + * negative values. + * + * @return The indices into the vertex elements for each face. Usually 0-based, though there are no formal rules. + */ + template + std::vector> getFaceIndices() { + + for (const std::string& f : std::vector{"face"}) { + for (const std::string& p : std::vector{"vertex_indices", "vertex_index"}) { + try { + return getElement(f).getListPropertyAnySign(p); + } catch (const std::runtime_error&) { + // that's fine + } + } + } + throw std::runtime_error("PLY parser: could not find face vertex indices attribute under any common name."); + } + + + /** + * @brief Common-case helper set mesh vertex positons. Creates vertex element, if necessary. + * + * @param vertexPositions A vector of vertex positions + */ + void addVertexPositions(std::vector>& vertexPositions) { + + std::string vertexName = "vertex"; + size_t N = vertexPositions.size(); + + // Create the element + if (!hasElement(vertexName)) { + addElement(vertexName, N); + } + + // De-interleave + std::vector xPos(N); + std::vector yPos(N); + std::vector zPos(N); + for (size_t i = 0; i < vertexPositions.size(); i++) { + xPos[i] = vertexPositions[i][0]; + yPos[i] = vertexPositions[i][1]; + zPos[i] = vertexPositions[i][2]; + } + + // Store + getElement(vertexName).addProperty("x", xPos); + getElement(vertexName).addProperty("y", yPos); + getElement(vertexName).addProperty("z", zPos); + } + + /** + * @brief Common-case helper set mesh vertex colors. Creates a vertex element, if necessary. + * + * @param colors A vector of vertex colors (unsigned chars [0,255]). + */ + void addVertexColors(std::vector>& colors) { + + std::string vertexName = "vertex"; + size_t N = colors.size(); + + // Create the element + if (!hasElement(vertexName)) { + addElement(vertexName, N); + } + + // De-interleave + std::vector r(N); + std::vector g(N); + std::vector b(N); + for (size_t i = 0; i < colors.size(); i++) { + r[i] = colors[i][0]; + g[i] = colors[i][1]; + b[i] = colors[i][2]; + } + + // Store + getElement(vertexName).addProperty("red", r); + getElement(vertexName).addProperty("green", g); + getElement(vertexName).addProperty("blue", b); + } + + /** + * @brief Common-case helper set mesh vertex colors. Creates a vertex element, if necessary. + * + * @param colors A vector of vertex colors as floating point [0,1] values. Internally converted to [0,255] chars. + */ + void addVertexColors(std::vector>& colors) { + + std::string vertexName = "vertex"; + size_t N = colors.size(); + + // Create the element + if (!hasElement(vertexName)) { + addElement(vertexName, N); + } + + auto toChar = [](double v) { + if (v < 0.0) v = 0.0; + if (v > 1.0) v = 1.0; + return static_cast(v * 255.); + }; + + // De-interleave + std::vector r(N); + std::vector g(N); + std::vector b(N); + for (size_t i = 0; i < colors.size(); i++) { + r[i] = toChar(colors[i][0]); + g[i] = toChar(colors[i][1]); + b[i] = toChar(colors[i][2]); + } + + // Store + getElement(vertexName).addProperty("red", r); + getElement(vertexName).addProperty("green", g); + getElement(vertexName).addProperty("blue", b); + } + + + /** + * @brief Common-case helper to set face indices. Creates a face element if needed. The input type will be casted to a + * 32 bit integer of the same signedness. + * + * @param indices The indices into the vertex list around each face. + */ + template + void addFaceIndices(std::vector>& indices) { + + std::string faceName = "face"; + size_t N = indices.size(); + + // Create the element + if (!hasElement(faceName)) { + addElement(faceName, N); + } + + // Cast to 32 bit + typedef typename std::conditional::value, int32_t, uint32_t>::type IndType; + std::vector> intInds; + for (std::vector& l : indices) { + std::vector thisInds; + for (T& val : l) { + IndType valConverted = static_cast(val); + if (valConverted != val) { + throw std::runtime_error("Index value " + std::to_string(val) + + " could not be converted to a .ply integer without loss of data. Note that .ply " + "only supports 32-bit ints."); + } + thisInds.push_back(valConverted); + } + intInds.push_back(thisInds); + } + + // Store + getElement(faceName).addListProperty("vertex_indices", intInds); + } + + + /** + * @brief Comments for the file. When writing, each entry will be written as a sequential comment line. + */ + std::vector comments; + + + /** + * @brief obj_info comments for the file. When writing, each entry will be written as a sequential comment line. + */ + std::vector objInfoComments; + +private: + std::vector elements; + const int majorVersion = 1; // I'll buy you a drink if these ever get bumped + const int minorVersion = 0; + + DataFormat inputDataFormat = DataFormat::ASCII; // set when reading from a file + DataFormat outputDataFormat = DataFormat::ASCII; // option for writing files + + + // === Reading === + + /** + * @brief Parse a PLY file from an input stream + * + * @param inStream + * @param verbose + */ + void parsePLY(std::istream& inStream, bool verbose) { + + // == Process the header + parseHeader(inStream, verbose); + + + // === Parse data from a binary file + if (inputDataFormat == DataFormat::Binary) { + parseBinary(inStream, verbose); + } + // === Parse data from an binary file + else if (inputDataFormat == DataFormat::BinaryBigEndian) { + parseBinaryBigEndian(inStream, verbose); + } + // === Parse data from an ASCII file + else if (inputDataFormat == DataFormat::ASCII) { + parseASCII(inStream, verbose); + } + } + + /** + * @brief Read the header for a file + * + * @param inStream + * @param verbose + */ + void parseHeader(std::istream& inStream, bool verbose) { + + using std::cout; + using std::endl; + using std::string; + using std::vector; + + // First two lines are predetermined + { // First line is magic constant + string plyLine; + std::getline(inStream, plyLine); + if (trimSpaces(plyLine) != "ply") { + throw std::runtime_error("PLY parser: File does not appear to be ply file. First line should be 'ply'"); + } + } + + { // second line is version + string styleLine; + std::getline(inStream, styleLine); + vector tokens = tokenSplit(styleLine); + if (tokens.size() != 3) throw std::runtime_error("PLY parser: bad format line"); + std::string formatStr = tokens[0]; + std::string typeStr = tokens[1]; + std::string versionStr = tokens[2]; + + // "format" + if (formatStr != "format") throw std::runtime_error("PLY parser: bad format line"); + + // ascii/binary + if (typeStr == "ascii") { + inputDataFormat = DataFormat::ASCII; + if (verbose) cout << " - Type: ascii" << endl; + } else if (typeStr == "binary_little_endian") { + inputDataFormat = DataFormat::Binary; + if (verbose) cout << " - Type: binary" << endl; + } else if (typeStr == "binary_big_endian") { + inputDataFormat = DataFormat::BinaryBigEndian; + if (verbose) cout << " - Type: binary big endian" << endl; + } else { + throw std::runtime_error("PLY parser: bad format line"); + } + + // version + if (versionStr != "1.0") { + throw std::runtime_error("PLY parser: encountered file with version != 1.0. Don't know how to parse that"); + } + if (verbose) cout << " - Version: " << versionStr << endl; + } + + // Consume header line by line + while (inStream.good()) { + string line; + std::getline(inStream, line); + + // Parse a comment + if (startsWith(line, "comment")) { + string comment = line.substr(8); + if (verbose) cout << " - Comment: " << comment << endl; + comments.push_back(comment); + continue; + } + + // Parse an obj_info comment + if (startsWith(line, "obj_info")) { + string infoComment = line.substr(9); + if (verbose) cout << " - obj_info: " << infoComment << endl; + objInfoComments.push_back(infoComment); + continue; + } + + // Parse an element + else if (startsWith(line, "element")) { + vector tokens = tokenSplit(line); + if (tokens.size() != 3) throw std::runtime_error("PLY parser: Invalid element line"); + string name = tokens[1]; + size_t count; + std::istringstream iss(tokens[2]); + iss >> count; + elements.emplace_back(name, count); + if (verbose) cout << " - Found element: " << name << " (count = " << count << ")" << endl; + continue; + } + + // Parse a property list + else if (startsWith(line, "property list")) { + vector tokens = tokenSplit(line); + if (tokens.size() != 5) throw std::runtime_error("PLY parser: Invalid property list line"); + if (elements.size() == 0) throw std::runtime_error("PLY parser: Found property list without previous element"); + string countType = tokens[2]; + string type = tokens[3]; + string name = tokens[4]; + elements.back().properties.push_back(createPropertyWithType(name, type, true, countType)); + if (verbose) + cout << " - Found list property: " << name << " (count type = " << countType << ", data type = " << type + << ")" << endl; + continue; + } + + // Parse a property + else if (startsWith(line, "property")) { + vector tokens = tokenSplit(line); + if (tokens.size() != 3) throw std::runtime_error("PLY parser: Invalid property line"); + if (elements.size() == 0) throw std::runtime_error("PLY parser: Found property without previous element"); + string type = tokens[1]; + string name = tokens[2]; + elements.back().properties.push_back(createPropertyWithType(name, type, false, "")); + if (verbose) cout << " - Found property: " << name << " (type = " << type << ")" << endl; + continue; + } + + // Parse end of header + else if (startsWith(line, "end_header")) { + break; + } + + // Error! + else { + throw std::runtime_error("Unrecognized header line: " + line); + } + } + } + + /** + * @brief Read the actual data for a file, in ASCII + * + * @param inStream + * @param verbose + */ + void parseASCII(std::istream& inStream, bool verbose) { + + using std::string; + using std::vector; + + // Read all elements + for (Element& elem : elements) { + + if (verbose) { + std::cout << " - Processing element: " << elem.name << std::endl; + } + + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->reserve(elem.count); + } + for (size_t iEntry = 0; iEntry < elem.count; iEntry++) { + + string line; + std::getline(inStream, line); + + // Some .ply files seem to include empty lines before the start of property data (though this is not specified + // in the format description). We attempt to recover and parse such files by skipping any empty lines. + if (!elem.properties.empty()) { // if the element has no properties, the line _should_ be blank, presumably + while (line.empty()) { // skip lines until we hit something nonempty + std::getline(inStream, line); + } + } + + vector tokens = tokenSplit(line); + size_t iTok = 0; + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->parseNext(tokens, iTok); + } + } + } + } + + /** + * @brief Read the actual data for a file, in binary. + * + * @param inStream + * @param verbose + */ + void parseBinary(std::istream& inStream, bool verbose) { + + if (!isLittleEndian()) { + throw std::runtime_error("binary reading assumes little endian system"); + } + + using std::string; + using std::vector; + + // Read all elements + for (Element& elem : elements) { + + if (verbose) { + std::cout << " - Processing element: " << elem.name << std::endl; + } + + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->reserve(elem.count); + } + for (size_t iEntry = 0; iEntry < elem.count; iEntry++) { + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->readNext(inStream); + } + } + } + } + + /** + * @brief Read the actual data for a file, in binary. + * + * @param inStream + * @param verbose + */ + void parseBinaryBigEndian(std::istream& inStream, bool verbose) { + + if (!isLittleEndian()) { + throw std::runtime_error("binary reading assumes little endian system"); + } + + using std::string; + using std::vector; + + // Read all elements + for (Element& elem : elements) { + + if (verbose) { + std::cout << " - Processing element: " << elem.name << std::endl; + } + + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->reserve(elem.count); + } + for (size_t iEntry = 0; iEntry < elem.count; iEntry++) { + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->readNextBigEndian(inStream); + } + } + } + } + + // === Writing === + + + /** + * @brief write a PLY file to an output stream + * + * @param outStream + */ + void writePLY(std::ostream& outStream) { + + writeHeader(outStream); + + // Write all elements + for (Element& e : elements) { + if (outputDataFormat == DataFormat::Binary) { + if (!isLittleEndian()) { + throw std::runtime_error("binary writing assumes little endian system"); + } + e.writeDataBinary(outStream); + } else if (outputDataFormat == DataFormat::BinaryBigEndian) { + if (!isLittleEndian()) { + throw std::runtime_error("binary writing assumes little endian system"); + } + e.writeDataBinaryBigEndian(outStream); + } else if (outputDataFormat == DataFormat::ASCII) { + e.writeDataASCII(outStream); + } + } + } + + + /** + * @brief Write out a header for a file + * + * @param outStream + */ + void writeHeader(std::ostream& outStream) { + + // Magic line + outStream << "ply\n"; + + // Type line + outStream << "format "; + if (outputDataFormat == DataFormat::Binary) { + outStream << "binary_little_endian "; + } else if (outputDataFormat == DataFormat::BinaryBigEndian) { + outStream << "binary_big_endian "; + } else if (outputDataFormat == DataFormat::ASCII) { + outStream << "ascii "; + } + + // Version number + outStream << majorVersion << "." << minorVersion << "\n"; + + // Write comments + bool hasHapplyComment = false; + std::string happlyComment = "Written with hapPLY (https://github.com/nmwsharp/happly)"; + for (const std::string& comment : comments) { + if (comment == happlyComment) hasHapplyComment = true; + outStream << "comment " << comment << "\n"; + } + if (!hasHapplyComment) { + outStream << "comment " << happlyComment << "\n"; + } + + // Write obj_info comments + for (const std::string& comment : objInfoComments) { + outStream << "obj_info " << comment << "\n"; + } + + // Write elements (and their properties) + for (Element& e : elements) { + e.writeHeader(outStream); + } + + // End header + outStream << "end_header\n"; + } +}; + +} // namespace happly