diff --git a/README.md b/README.md index b7ee68604..88f73f5eb 100644 --- a/README.md +++ b/README.md @@ -207,14 +207,20 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one - vtfpp + vtfpp + PPL v0 + ✅ + ✅ + + + + VTF v7.0-7.6
Strata Source modifications ✅ ✅ - diff --git a/docs/index.md b/docs/index.md index d6477eeb5..7affa1c5b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -181,14 +181,19 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one ✅ - vtfpp + vtfpp + PPL v0 + ✅ + ✅ + + + VTF v7.0-7.6
Strata Source modifications ✅ ✅ - \endhtmlonly diff --git a/include/vtfpp/ImageConversion.h b/include/vtfpp/ImageConversion.h index 74b42e194..28365a7ac 100644 --- a/include/vtfpp/ImageConversion.h +++ b/include/vtfpp/ImageConversion.h @@ -204,6 +204,8 @@ namespace ImageConversion { /// Converts image data to a PNG or HDR file. HDR output will be used for floating-point formats. [[nodiscard]] std::vector convertImageDataToFile(std::span imageData, ImageFormat format, uint16_t width, uint16_t height); +[[nodiscard]] std::vector convertFileToImageData(std::span fileData, ImageFormat& format, int& width, int& height, int& frameCount); + enum class ResizeEdge { // Matches stbir_edge CLAMP = 0, diff --git a/include/vtfpp/PPL.h b/include/vtfpp/PPL.h new file mode 100644 index 000000000..49b6cf292 --- /dev/null +++ b/include/vtfpp/PPL.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include "ImageConversion.h" + +namespace vtfpp { + +class PPL { +public: + struct Image { + uint32_t width; + uint32_t height; + std::vector data; + }; + + explicit PPL(uint32_t checksum_, ImageFormat format_ = ImageFormat::RGB888, uint32_t version_ = 0); + + explicit PPL(std::span pplData); + + explicit PPL(const std::string& pplPath); + + [[nodiscard]] explicit operator bool() const; + + [[nodiscard]] uint32_t getVersion() const; + + void setVersion(uint32_t newVersion); + + [[nodiscard]] uint32_t getChecksum() const; + + void setChecksum(uint32_t newChecksum); + + [[nodiscard]] ImageFormat getFormat() const; + + void setFormat(ImageFormat newFormat); + + [[nodiscard]] bool hasImageForLOD(uint32_t lod) const; + + [[nodiscard]] std::vector getImageLODs() const; + + [[nodiscard]] const Image* getImageRaw(uint32_t lod = 0) const; + + [[nodiscard]] std::optional getImageAs(ImageFormat newFormat, uint32_t lod = 0) const; + + [[nodiscard]] std::optional getImageAsRGB888(uint32_t lod = 0) const; + + bool setImage(std::span imageData, ImageFormat format_, uint32_t width, uint32_t height, uint32_t lod = 0); + + bool setImage(std::span imageData, ImageFormat format_, uint32_t width, uint32_t height, uint32_t resizedWidth, uint32_t resizedHeight, uint32_t lod = 0, ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::BILINEAR); + + bool setImage(const std::string& imagePath, uint32_t lod); + + bool setImage(const std::string& imagePath, uint32_t resizedWidth, uint32_t resizedHeight, uint32_t lod = 0, ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::BILINEAR); + + [[nodiscard]] std::vector saveImageToFile(uint32_t lod = 0) const; + + bool saveImageToFile(const std::string& imagePath, uint32_t lod = 0) const; // NOLINT(*-use-nodiscard) + + [[nodiscard]] std::vector bake(); + + bool bake(const std::string& pplPath); + +protected: + uint32_t version{}; + uint32_t checksum{}; + ImageFormat format{}; + + std::unordered_map images; +}; + +} // namespace vtfpp diff --git a/include/vtfpp/VTF.h b/include/vtfpp/VTF.h index 91bf66df9..8add677f3 100644 --- a/include/vtfpp/VTF.h +++ b/include/vtfpp/VTF.h @@ -318,9 +318,9 @@ class VTF { bool setImage(const std::string& imagePath, ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::BILINEAR, uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, uint16_t slice = 0); - [[nodiscard]] std::vector convertAndSaveImageDataToFile(uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, uint16_t slice = 0) const; + [[nodiscard]] std::vector saveImageToFile(uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, uint16_t slice = 0) const; - void convertAndSaveImageDataToFile(const std::string& imagePath, uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, uint16_t slice = 0) const; + bool saveImageToFile(const std::string& imagePath, uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, uint16_t slice = 0) const; // NOLINT(*-use-nodiscard) [[nodiscard]] bool hasThumbnailData() const; @@ -334,9 +334,9 @@ class VTF { void removeThumbnail(); - [[nodiscard]] std::vector convertAndSaveThumbnailDataToFile() const; + [[nodiscard]] std::vector saveThumbnailToFile() const; - void convertAndSaveThumbnailDataToFile(const std::string& imagePath) const; + bool saveThumbnailToFile(const std::string& imagePath) const; // NOLINT(*-use-nodiscard) [[nodiscard]] std::vector bake(); diff --git a/include/vtfpp/vtfpp.h b/include/vtfpp/vtfpp.h index 772520651..274ab6625 100644 --- a/include/vtfpp/vtfpp.h +++ b/include/vtfpp/vtfpp.h @@ -7,4 +7,5 @@ #include "ImageConversion.h" #include "ImageFormats.h" +#include "PPL.h" #include "VTF.h" diff --git a/src/vtfpp/ImageConversion.cpp b/src/vtfpp/ImageConversion.cpp index 0f230faae..a9867bc0d 100644 --- a/src/vtfpp/ImageConversion.cpp +++ b/src/vtfpp/ImageConversion.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #ifdef SOURCEPP_BUILD_WITH_TBB @@ -11,6 +12,7 @@ #include #include +#include #include #include @@ -864,6 +866,87 @@ std::vector ImageConversion::convertImageDataToFile(std::span ImageConversion::convertFileToImageData(std::span fileData, ImageFormat& format, int& width, int& height, int& frameCount) { + stbi_convert_iphone_png_to_rgb(true); + + format = ImageFormat::EMPTY; + width = 0; + height = 0; + int channels = 0; + frameCount = 1; + + if (stbi_is_hdr_from_memory(reinterpret_cast(fileData.data()), static_cast(fileData.size()))) { + std::unique_ptr stbImage{ + stbi_loadf_from_memory(reinterpret_cast(fileData.data()), static_cast(fileData.size()), &width, &height, &channels, 0), + &stbi_image_free, + }; + if (!stbImage) { + return {}; + } + + switch (channels) { + case 1: format = ImageFormat::R32F; break; + case 3: format = ImageFormat::RGB323232F; break; + case 4: format = ImageFormat::RGBA32323232F; break; + default: return {}; + } + + return {reinterpret_cast(stbImage.get()), reinterpret_cast(stbImage.get()) + ImageFormatDetails::getDataLength(format, width, height)}; + } else if (stbi_is_16_bit_from_memory(reinterpret_cast(fileData.data()), static_cast(fileData.size()))) { + std::unique_ptr stbImage{ + stbi_load_16_from_memory(reinterpret_cast(fileData.data()), static_cast(fileData.size()), &width, &height, &channels, 0), + &stbi_image_free, + }; + if (!stbImage) { + return {}; + } + + if (channels == 4) { + format = ImageFormat::RGBA16161616; + } else { + return {}; + } + + return {reinterpret_cast(stbImage.get()), reinterpret_cast(stbImage.get()) + ImageFormatDetails::getDataLength(format, width, height)}; + } else if (fileData.size() >= 3 && static_cast(fileData[0]) == 'G' && static_cast(fileData[1]) == 'I' && static_cast(fileData[2]) == 'F') { + std::unique_ptr stbImage{ + stbi_load_gif_from_memory(reinterpret_cast(fileData.data()), static_cast(fileData.size()), nullptr, &width, &height, &frameCount, &channels, 0), + &stbi_image_free, + }; + if (!stbImage || !frameCount) { + return {}; + } + + switch (channels) { + case 1: format = ImageFormat::I8; break; + case 2: format = ImageFormat::UV88; break; + case 3: format = ImageFormat::RGB888; break; + case 4: format = ImageFormat::RGBA8888; break; + default: return {}; + } + + return {reinterpret_cast(stbImage.get()), reinterpret_cast(stbImage.get() + (ImageFormatDetails::getDataLength(format, width, height) * frameCount))}; + } else { + std::unique_ptr stbImage{ + stbi_load_from_memory(reinterpret_cast(fileData.data()), static_cast(fileData.size()), &width, &height, &channels, 0), + &stbi_image_free, + }; + if (!stbImage) { + return {}; + } + + switch (channels) { + case 1: format = ImageFormat::I8; break; + case 2: format = ImageFormat::UV88; break; + case 3: format = ImageFormat::RGB888; break; + case 4: format = ImageFormat::RGBA8888; break; + default: return {}; + } + + return {reinterpret_cast(stbImage.get()), reinterpret_cast(stbImage.get()) + ImageFormatDetails::getDataLength(format, width, height)}; + } +} + uint16_t ImageConversion::getResizedDim(uint16_t n, ResizeMethod method) { switch (method) { case ResizeMethod::NONE: break; diff --git a/src/vtfpp/PPL.cpp b/src/vtfpp/PPL.cpp new file mode 100644 index 000000000..7b4d16372 --- /dev/null +++ b/src/vtfpp/PPL.cpp @@ -0,0 +1,219 @@ +#include + +#include + +#include + +using namespace sourcepp; +using namespace vtfpp; + +PPL::PPL(uint32_t checksum_, ImageFormat format_, uint32_t version_) + : version(version_) + , checksum(checksum_) + , format(format_) {} + +PPL::PPL(std::span pplData) { + BufferStreamReadOnly reader{pplData.data(), pplData.size()}; + reader >> this->version >> this->checksum >> this->format; + + auto imageCount = reader.read(); + reader.skip(4); + for (uint32_t i = 0; i < imageCount; i++) { + auto lod = reader.read(); + auto offset = reader.read(); + auto length = reader.read(); + auto width = reader.read(); + auto height = reader.read(); + reader.skip(3); + + this->images[lod] = { + .width = width, + .height = height, + .data = {pplData.data() + offset, pplData.data() + offset + length}, + }; + } +} + +PPL::PPL(const std::string& pplPath) + : PPL(fs::readFileBuffer(pplPath)) {} + +PPL::operator bool() const { + return !this->images.empty(); +} + +uint32_t PPL::getVersion() const { + return this->version; +} + +void PPL::setVersion(uint32_t newVersion) { + this->version = newVersion; +} + +uint32_t PPL::getChecksum() const { + return this->checksum; +} + +void PPL::setChecksum(uint32_t newChecksum) { + this->checksum = newChecksum; +} + +ImageFormat PPL::getFormat() const { + return this->format; +} + +void PPL::setFormat(ImageFormat newFormat) { + for (auto& [lod, image] : this->images) { + image.data = ImageConversion::convertImageDataToFormat(image.data, this->format, newFormat, image.width, image.height); + } + this->format = newFormat; +} + +bool PPL::hasImageForLOD(uint32_t lod) const { + return this->images.contains(lod); +} + +std::vector PPL::getImageLODs() const { + auto view = std::views::keys(this->images); + return {view.begin(), view.end()}; +} + +const PPL::Image* PPL::getImageRaw(uint32_t lod) const { + if (!this->hasImageForLOD(lod)) { + return nullptr; + } + return &this->images.at(lod); +} + +std::optional PPL::getImageAs(ImageFormat newFormat, uint32_t lod) const { + if (!this->hasImageForLOD(lod)) { + return std::nullopt; + } + const auto& image = this->images.at(lod); + return Image{ + .width = image.width, + .height = image.height, + .data = ImageConversion::convertImageDataToFormat(image.data, this->format, newFormat, image.width, image.height), + }; +} + +std::optional PPL::getImageAsRGB888(uint32_t lod) const { + return this->getImageAs(ImageFormat::RGB888, lod); +} + +bool PPL::setImage(std::span imageData, ImageFormat format_, uint32_t width, uint32_t height, uint32_t lod) { + if (!width || !height) { + return false; + } + this->images[lod] = { + .width = width, + .height = height, + .data = format_ == this->format ? std::vector{imageData.begin(), imageData.end()} : ImageConversion::convertImageDataToFormat(imageData, format_, this->format, width, height), + }; + return true; +} + +bool PPL::setImage(std::span imageData, ImageFormat format_, uint32_t width, uint32_t height, uint32_t resizedWidth, uint32_t resizedHeight, uint32_t lod, ImageConversion::ResizeFilter filter) { + if (!width || !height || !resizedWidth || !resizedHeight) { + return false; + } + const auto unresizedData = format_ == this->format ? std::vector{imageData.begin(), imageData.end()} : ImageConversion::convertImageDataToFormat(imageData, format_, this->format, width, height); + this->images[lod] = { + .width = width, + .height = height, + .data = ImageConversion::resizeImageData(unresizedData, this->format, width, resizedWidth, height, resizedHeight, false, filter), + }; + return true; +} + +bool PPL::setImage(const std::string& imagePath, uint32_t lod) { + ImageFormat inputFormat; + int inputWidth, inputHeight, inputFrameCount; + auto imageData_ = ImageConversion::convertFileToImageData(fs::readFileBuffer(imagePath), inputFormat, inputWidth, inputHeight, inputFrameCount); + + // Unable to decode file + if (inputFormat == ImageFormat::EMPTY || !inputWidth || !inputHeight || !inputFrameCount) { + return false; + } + + // One frame (normal) + if (inputFrameCount == 1) { + return this->setImage(imageData_, inputFormat, inputWidth, inputHeight, lod); + } + + // Multiple frames (GIF) - discard extra frames + return this->setImage({imageData_.data(), ImageFormatDetails::getDataLength(inputFormat, inputWidth, inputHeight)}, inputFormat, inputWidth, inputHeight, lod); +} + +bool PPL::setImage(const std::string& imagePath, uint32_t resizedWidth, uint32_t resizedHeight, uint32_t lod, ImageConversion::ResizeFilter filter) { + ImageFormat inputFormat; + int inputWidth, inputHeight, inputFrameCount; + auto imageData_ = ImageConversion::convertFileToImageData(fs::readFileBuffer(imagePath), inputFormat, inputWidth, inputHeight, inputFrameCount); + + // Unable to decode file + if (inputFormat == ImageFormat::EMPTY || !inputWidth || !inputHeight || !inputFrameCount) { + return false; + } + + // One frame (normal) + if (inputFrameCount == 1) { + return this->setImage(imageData_, inputFormat, inputWidth, inputHeight, resizedWidth, resizedHeight, lod, filter); + } + + // Multiple frames (GIF) - discard extra frames + return this->setImage({imageData_.data(), ImageFormatDetails::getDataLength(inputFormat, inputWidth, inputHeight)}, inputFormat, inputWidth, inputHeight, resizedWidth, resizedHeight, lod, filter); +} + +std::vector PPL::saveImageToFile(uint32_t lod) const { + if (auto image = this->getImageRaw(lod)) { + return ImageConversion::convertImageDataToFile(image->data, this->format, image->width, image->height); + } + return {}; +} + +bool PPL::saveImageToFile(const std::string& imagePath, uint32_t lod) const { + if (auto data = this->saveImageToFile(lod); !data.empty()) { + return fs::writeFileBuffer(imagePath, data); + } + return false; +} + +std::vector PPL::bake() { + static constexpr auto ALIGNMENT = 512; + + std::vector out; + BufferStream writer{out}; + + static constexpr auto headerSize = sizeof(uint32_t) * 8; + writer << this->version << this->checksum << this->format; + writer.write(this->images.size()); + + const auto imageDirectorySize = this->images.size() * sizeof(uint32_t) * 8; + while (writer.tell() < ALIGNMENT) { + writer.write(0); + } + writer.seek(headerSize); + + uint32_t currentOffset = ALIGNMENT; + for (const auto& [lod, image] : this->images) { + writer << lod << currentOffset << static_cast(image.data.size()) << image.width << image.height; + for (int i = 0; i < 3; i++) { + writer.write(0); + } + auto seekPoint = writer.tell(); + writer.seek_u(currentOffset).write(image.data); + const auto alignment = math::getPaddingForAlignment(ALIGNMENT, writer.tell()); + for (int i = 0; i < alignment; i++) { + writer.write(0); + } + writer.seek_u(seekPoint); + currentOffset += image.data.size() + alignment; + } + writer.seek(0, std::ios::end); + + out.resize(writer.size()); + return out; +} + +bool PPL::bake(const std::string& pplPath) { + return fs::writeFileBuffer(pplPath, this->bake()); +} diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp index f64884eed..2ef802ced 100644 --- a/src/vtfpp/VTF.cpp +++ b/src/vtfpp/VTF.cpp @@ -1,12 +1,10 @@ #include #include -#include #include #include #include -#include #include @@ -916,106 +914,44 @@ bool VTF::setImage(std::span imageData_, ImageFormat format_, u } bool VTF::setImage(const std::string& imagePath, ImageConversion::ResizeFilter filter, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) { - const auto imageData_ = fs::readFileBuffer(imagePath); - - stbi_convert_iphone_png_to_rgb(true); - int width_, height_, channels; - if (stbi_is_hdr_from_memory(reinterpret_cast(imageData_.data()), static_cast(imageData_.size()))) { - std::unique_ptr stbImage{ - stbi_loadf_from_memory(reinterpret_cast(imageData_.data()), static_cast(imageData_.size()), &width_, &height_, &channels, 0), - &stbi_image_free, - }; - if (!stbImage) { - return false; - } - - ImageFormat imageFormat; - switch (channels) { - case 1: imageFormat = ImageFormat::R32F; break; - case 3: imageFormat = ImageFormat::RGB323232F; break; - case 4: imageFormat = ImageFormat::RGBA32323232F; break; - default: return false; - } - - return this->setImage({reinterpret_cast(stbImage.get()), reinterpret_cast(stbImage.get()) + ImageFormatDetails::getDataLength(imageFormat, width_, height_)}, imageFormat, width_, height_, filter, mip, frame, face, slice); - } else if (stbi_is_16_bit_from_memory(reinterpret_cast(imageData_.data()), static_cast(imageData_.size()))) { - std::unique_ptr stbImage{ - stbi_load_16_from_memory(reinterpret_cast(imageData_.data()), static_cast(imageData_.size()), &width_, &height_, &channels, 0), - &stbi_image_free, - }; - if (!stbImage) { - return false; - } + ImageFormat inputFormat; + int inputWidth, inputHeight, inputFrameCount; + auto imageData_ = ImageConversion::convertFileToImageData(fs::readFileBuffer(imagePath), inputFormat, inputWidth, inputHeight, inputFrameCount); - ImageFormat imageFormat; - if (channels == 4) { - imageFormat = ImageFormat::RGBA16161616; - } else { - return false; - } - - return this->setImage({reinterpret_cast(stbImage.get()), reinterpret_cast(stbImage.get()) + ImageFormatDetails::getDataLength(imageFormat, width_, height_)}, imageFormat, width_, height_, filter, mip, frame, face, slice); - } else if (imageData_.size() >= 3 && static_cast(imageData_[0]) == 'G' && static_cast(imageData_[1]) == 'I' && static_cast(imageData_[2]) == 'F') { - int frameCount_ = 0; - std::unique_ptr stbImage{ - stbi_load_gif_from_memory(reinterpret_cast(imageData_.data()), static_cast(imageData_.size()), nullptr, &width_, &height_, &frameCount_, &channels, 0), - &stbi_image_free, - }; - if (!stbImage || !frameCount_) { - return false; - } + // Unable to decode file + if (inputFormat == ImageFormat::EMPTY || !inputWidth || !inputHeight || !inputFrameCount) { + return false; + } - ImageFormat imageFormat; - switch (channels) { - case 1: imageFormat = ImageFormat::I8; break; - case 2: imageFormat = ImageFormat::UV88; break; - case 3: imageFormat = ImageFormat::RGB888; break; - case 4: imageFormat = ImageFormat::RGBA8888; break; - default: return false; - } + // One frame (normal) + if (inputFrameCount == 1) { + return this->setImage(imageData_, inputFormat, inputWidth, inputHeight, filter, mip, frame, face, slice); + } - bool allSuccess = true; - const auto frameSize = ImageFormatDetails::getDataLength(imageFormat, width_, height_); - for (int currentFrame = 0; currentFrame < frameCount_; currentFrame++) { - if (!this->setImage({reinterpret_cast(stbImage.get() + currentFrame * frameSize), reinterpret_cast(stbImage.get() + currentFrame * frameSize + frameSize)}, imageFormat, width_, height_, filter, mip, frame + currentFrame, face, slice)) { - allSuccess = false; - } - if (currentFrame == 0 && this->frameCount < frame + frameCount_) { - // Call this after setting the first image, this function is a no-op if no image data is present yet - this->setFrameCount(frame + frameCount_); - } + // Multiple frames (GIF) + bool allSuccess = true; + const auto frameSize = ImageFormatDetails::getDataLength(inputFormat, inputWidth, inputHeight); + for (int currentFrame = 0; currentFrame < inputFrameCount; currentFrame++) { + if (!this->setImage({imageData_.data() + currentFrame * frameSize, imageData_.data() + currentFrame * frameSize + frameSize}, inputFormat, inputWidth, inputHeight, filter, mip, frame + currentFrame, face, slice)) { + allSuccess = false; } - return allSuccess; - } else { - std::unique_ptr stbImage{ - stbi_load_from_memory(reinterpret_cast(imageData_.data()), static_cast(imageData_.size()), &width_, &height_, &channels, 0), - &stbi_image_free, - }; - if (!stbImage) { - return false; + if (currentFrame == 0 && this->frameCount < frame + inputFrameCount) { + // Call this after setting the first image, this function is a no-op if no image data is present yet + this->setFrameCount(frame + inputFrameCount); } - - ImageFormat imageFormat; - switch (channels) { - case 1: imageFormat = ImageFormat::I8; break; - case 2: imageFormat = ImageFormat::UV88; break; - case 3: imageFormat = ImageFormat::RGB888; break; - case 4: imageFormat = ImageFormat::RGBA8888; break; - default: return false; - } - - return this->setImage({reinterpret_cast(stbImage.get()), reinterpret_cast(stbImage.get()) + ImageFormatDetails::getDataLength(imageFormat, width_, height_)}, imageFormat, width_, height_, filter, mip, frame, face, slice); } + return allSuccess; } -std::vector VTF::convertAndSaveImageDataToFile(uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const { +std::vector VTF::saveImageToFile(uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const { return ImageConversion::convertImageDataToFile(this->getImageDataRaw(mip, frame, face, slice), this->format, ImageDimensions::getMipDim(mip, this->width), ImageDimensions::getMipDim(mip, this->height)); } -void VTF::convertAndSaveImageDataToFile(const std::string& imagePath, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const { - if (auto data_ = this->convertAndSaveImageDataToFile(mip, frame, face, slice); !data_.empty()) { - fs::writeFileBuffer(imagePath, data_); +bool VTF::saveImageToFile(const std::string& imagePath, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const { + if (auto data_ = this->saveImageToFile(mip, frame, face, slice); !data_.empty()) { + return fs::writeFileBuffer(imagePath, data_); } + return false; } bool VTF::hasThumbnailData() const { @@ -1058,14 +994,15 @@ void VTF::removeThumbnail() { this->removeResourceInternal(Resource::TYPE_THUMBNAIL_DATA); } -std::vector VTF::convertAndSaveThumbnailDataToFile() const { +std::vector VTF::saveThumbnailToFile() const { return ImageConversion::convertImageDataToFile(this->getThumbnailDataRaw(), this->thumbnailFormat, this->thumbnailWidth, this->thumbnailHeight); } -void VTF::convertAndSaveThumbnailDataToFile(const std::string& imagePath) const { - if (auto data_ = this->convertAndSaveThumbnailDataToFile(); !data_.empty()) { - fs::writeFileBuffer(imagePath, data_); +bool VTF::saveThumbnailToFile(const std::string& imagePath) const { + if (auto data_ = this->saveThumbnailToFile(); !data_.empty()) { + return fs::writeFileBuffer(imagePath, data_); } + return false; } std::vector VTF::bake() { diff --git a/src/vtfpp/_vtfpp.cmake b/src/vtfpp/_vtfpp.cmake index 79a688612..095be2e38 100644 --- a/src/vtfpp/_vtfpp.cmake +++ b/src/vtfpp/_vtfpp.cmake @@ -3,10 +3,12 @@ add_pretty_parser(vtfpp PRECOMPILED_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/ImageConversion.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/ImageFormats.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/PPL.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/VTF.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/vtfpp.h" SOURCES "${CMAKE_CURRENT_LIST_DIR}/ImageConversion.cpp" + "${CMAKE_CURRENT_LIST_DIR}/PPL.cpp" "${CMAKE_CURRENT_LIST_DIR}/VTF.cpp") sourcepp_add_tbb(vtfpp) diff --git a/test/res/vtfpp/test.ppl b/test/res/vtfpp/test.ppl new file mode 100644 index 000000000..7cc89c85a Binary files /dev/null and b/test/res/vtfpp/test.ppl differ diff --git a/test/vtfpp.cpp b/test/vtfpp.cpp index f6cc76708..7464f8b74 100644 --- a/test/vtfpp.cpp +++ b/test/vtfpp.cpp @@ -6,6 +6,28 @@ using namespace sourcepp; using namespace vtfpp; +TEST(vtfpp, read_write_ppl) { + auto in = fs::readFileBuffer(ASSET_ROOT "vtfpp/test.ppl"); + + PPL reader{in}; + EXPECT_EQ(reader.getVersion(), 0); + EXPECT_EQ(reader.getChecksum(), 0xa9230a52); + EXPECT_EQ(reader.getFormat(), ImageFormat::RGB888); + + const auto* image = reader.getImageRaw(); + ASSERT_TRUE(reader); + + PPL writer{reader.getChecksum(), reader.getFormat(), reader.getVersion()}; + writer.setImage(image->data, reader.getFormat(), image->width, image->height); + + auto out = writer.bake(); + + ASSERT_EQ(in.size(), out.size()); + for (int i = 0; i < in.size(); i++) { + EXPECT_EQ(in[i], out[i]); + } +} + #if 0 #define TEST_WRITE_FMT(Format, Flags) \