#pragma once /* A header-only implementation of the .ply file format. * https://github.com/nmwsharp/happly * By Nicholas Sharp - nsharp@cs.cmu.edu * * Version 2, July 20, 2019 */ /* MIT License Copyright (c) 2018 Nick Sharp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // clang-format off /* === Changelog === Significant changes to the file recorded here. - Version 5 (Aug 22, 2020) Minor: skip blank lines before properties in ASCII files - Version 4 (Sep 11, 2019) Change internal list format to be flat. Other small perf fixes and cleanup. - Version 3 (Aug 1, 2019) Add support for big endian and obj_info - Version 2 (July 20, 2019) Catch exceptions by const reference. - Version 1 (undated) Initial version. Unnamed changes before version numbering. */ // clang-format on #include #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; } // 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); 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); 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