From 866acf32e04af7ca3c5c582d2f0a19df6e87cf5a Mon Sep 17 00:00:00 2001 From: tobid7 Date: Mon, 29 Dec 2025 21:38:07 +0100 Subject: [PATCH] Add bclim support --- CMakeLists.txt | 9 ++-- README.md | 2 +- include/ctrff.hpp | 3 +- include/ctrff/bclim.hpp | 94 ++++++++++++++++++++++++++++++++++++++++ include/ctrff/helper.hpp | 1 + source/bclim.cpp | 55 +++++++++++++++++++++++ source/helper.cpp | 3 +- tool/main.cpp | 76 ++++++++++++++++++++++++++++++++ 8 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 include/ctrff/bclim.hpp create mode 100644 source/bclim.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cf7cc3e..6e2a0c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,13 +22,14 @@ if(${CTRFF_3DS}) endif() add_library(ctrff STATIC + source/3dsx.cpp + source/bclim.cpp + source/bcstm.cpp + source/bcwav.cpp + source/binutil.cpp source/helper.cpp source/lz11.cpp source/smdh.cpp - source/binutil.cpp - source/bcstm.cpp - source/bcwav.cpp - source/3dsx.cpp ) target_include_directories(ctrff PUBLIC include) diff --git a/README.md b/README.md index 6efa807..7d76801 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Not all Planned formates are listed here yet | 3dsx | Basic Loading and Viewing of Meta Data Smdh | | | bcstm | Loading of almost every Data | Fully done and playable by BCSTM-Player | | bcwav | Basic Loading (not tested yet) | Not finished yet | -| bclim | Nothing done yet (Started creating header) | | +| bclim | Creating A8,RGBA32,RGB565 done | WIP | | lz11 | Encoder done, Decoder missing | WIP | | romfs | Nothing Done yet (Started creating header) | | | smdh | Almost done | missing safetey checks | diff --git a/include/ctrff.hpp b/include/ctrff.hpp index 57a40f2..cac186f 100644 --- a/include/ctrff.hpp +++ b/include/ctrff.hpp @@ -1,8 +1,9 @@ #pragma once #include +#include #include #include #include #include -#include +#include \ No newline at end of file diff --git a/include/ctrff/bclim.hpp b/include/ctrff/bclim.hpp new file mode 100644 index 0000000..4b5c8b9 --- /dev/null +++ b/include/ctrff/bclim.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace ctrff { +class CTRFF_API BCLIM : public BinFile { + public: + BCLIM() {} + ~BCLIM() {} + + enum Format : u32 { + L8, + A8, // tested + LA4, + LA8, + HILO8, + RGB565, // tested + RGB888, + RGBA5551, + RGBA4444, + RGBA8888, // tested + ETC1, + ETC1A4, + L4, + A4, + }; + + struct Header { + static Header Default() { + Header h; + h.Magic = 0x4d494c43; + h.Endianness = 0xfeff; + h.HeaderSize = 0x14; + h.Version = 0x2020000; + h.FileSize = 0; + h.NumSections = 0; + return h; + } + u32 Magic; // 0x4d494c43 "CLIM" + u16 Endianness; + u16 HeaderSize; + u32 Version; + u32 FileSize; + u32 NumSections; + }; + + struct ImagHeader { + static ImagHeader Default() { + ImagHeader h; + h.Magic = 0x67616d69; + h.HeaderSize = 0x14; + h.Width = 0; + h.Height = 0; + h.Format = A8; + h.ImageSize = 0; + return h; + } + u32 Magic; // 0x67616d69 "imag" + u32 HeaderSize; + u16 Width; + u16 Height; + u32 Format; // A bit waste of data lol + u32 ImageSize; + }; + + void Load(const std::string& path) { + std::fstream f(path, std::ios::in | std::ios::binary); + Read(f); + f.close(); + } + + void Save(const std::string& path) { + std::fstream f(path, std::ios::out | std::ios::binary); + Write(f); + f.close(); + } + + void CreateByImage(const std::vector& data, int w, int h, Format fmt); + + /** Write not supported btw */ + void Write(std::fstream& f) const override; + void Read(std::fstream& f) override; + + private: + std::vector pBuffer; + Header pCurrent; + ImagHeader pImag; + bool CreateMode = false; +}; +} // namespace ctrff \ No newline at end of file diff --git a/include/ctrff/helper.hpp b/include/ctrff/helper.hpp index b38d6d6..2e2d686 100644 --- a/include/ctrff/helper.hpp +++ b/include/ctrff/helper.hpp @@ -12,4 +12,5 @@ CTRFF_API void RGBA2RGB565(ctrff::u16 *out, const std::vector &img, const int &w, const int &h); CTRFF_API std::vector DownscaleImage( const std::vector &img, int w, int h, int scale); +CTRFF_API ctrff::u32 TileIndex(const int &x, const int &y, const int &w); } // namespace ctrff \ No newline at end of file diff --git a/source/bclim.cpp b/source/bclim.cpp new file mode 100644 index 0000000..d3b6b1b --- /dev/null +++ b/source/bclim.cpp @@ -0,0 +1,55 @@ +#include + +namespace ctrff { +CTRFF_API void BCLIM::Write(std::fstream& f) const { + f.write(reinterpret_cast(pBuffer.data()), pBuffer.size()); + f.write((const char*)&pCurrent.Magic, sizeof(pCurrent.Magic)); + f.write((const char*)&pCurrent.Endianness, sizeof(pCurrent.Endianness)); + f.write((const char*)&pCurrent.HeaderSize, sizeof(pCurrent.HeaderSize)); + f.write((const char*)&pCurrent.Version, sizeof(pCurrent.Version)); + f.write((const char*)&pCurrent.FileSize, sizeof(pCurrent.FileSize)); + f.write((const char*)&pCurrent.NumSections, sizeof(pCurrent.NumSections)); + f.write((const char*)&pImag.Magic, sizeof(pImag.Magic)); + f.write((const char*)&pImag.HeaderSize, sizeof(pImag.HeaderSize)); + f.write((const char*)&pImag.Width, sizeof(pImag.Width)); + f.write((const char*)&pImag.Height, sizeof(pImag.Height)); + f.write((const char*)&pImag.Format, sizeof(pImag.Format)); + f.write((const char*)&pImag.ImageSize, sizeof(pImag.ImageSize)); +} + +CTRFF_API void BCLIM::Read(std::fstream& f) { + f.seekg(std::ios::end); + size_t size = f.tellg(); + if (size < (sizeof(Header) + sizeof(ImagHeader))) { + throw std::runtime_error("Invalid File!"); + } + f.seekg(size - sizeof(Header) - sizeof(ImagHeader)); + Header h; + ImagHeader imag; + f.read(reinterpret_cast(&h), sizeof(h)); + f.read(reinterpret_cast(&imag), sizeof(imag)); + if (h.Magic != 0x4d494c43) { + throw std::runtime_error("[ctrff] BCLIM: Not a bclim file!"); + } + if (imag.Magic != 0x67616d69) { + throw std::runtime_error("[ctrff] BCLIM: Invalid Data"); + } +} + +CTRFF_API void BCLIM::CreateByImage(const std::vector& data, int w, int h, + Format fmt) { + CreateMode = true; + pImag = ImagHeader::Default(); + pCurrent = Header::Default(); + pImag.Format = fmt; + pImag.Width = w; + pImag.Height = h; + pImag.ImageSize = data.size(); + pBuffer.resize(data.size()); + for (int i = 0; i < data.size(); i++) { + pBuffer[i] = data[i]; + } + pCurrent.FileSize = pBuffer.size() + pCurrent.HeaderSize + pImag.HeaderSize; + pCurrent.NumSections = 1; +} +} // namespace ctrff \ No newline at end of file diff --git a/source/helper.cpp b/source/helper.cpp index 4f42243..01ba247 100644 --- a/source/helper.cpp +++ b/source/helper.cpp @@ -19,7 +19,8 @@ CTRFF_API ctrff::u16 MakePixel565(const ctrff::u8 &r, const ctrff::u8 &g, return res; } -CTRFF_API ctrff::u32 TileIndex(const int &x, const int &y, const int &w) { +CTRFF_API ctrff::u32 ctrff::TileIndex(const int &x, const int &y, + const int &w) { return (((y >> 3) * (w >> 3) + (x >> 3)) << 6) + ((x & 1) | ((y & 1) << 1) | ((x & 2) << 1) | ((y & 2) << 2) | ((x & 4) << 2) | ((y & 4) << 3)); diff --git a/tool/main.cpp b/tool/main.cpp index 7770a5a..d0ed391 100644 --- a/tool/main.cpp +++ b/tool/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -442,6 +443,73 @@ void LZ11Compress(const cf7::command::ArgumentList &data) { out.close(); } +void BCLIMMaker(const cf7::command::ArgumentList &data) { + std::string i = cf7::command::GetArg(data, "input"); + std::string o = cf7::command::GetArg(data, "output"); + std::string f = cf7::command::GetArg(data, "format"); + if (i.empty() || o.empty()) { + std::cout << "[ctrff] BCLIM: Error, no input or output" << std::endl; + return; + } + if (f.empty() || (f.compare("rgba32") != 0 && f.compare("a8") != 0 && + f.compare("rgb565") != 0)) { + f = "a8"; + } + int w = 0, h = 0, c = 0; + auto ret = stbi_load(i.c_str(), &w, &h, &c, 4); + if (!ret) { + std::cout << "[ctrff] BCLIM: Failed to load image " + i << std::endl; + return; + } + if (!PD::BitUtil::IsSingleBit(w) || !PD::BitUtil::IsSingleBit(h)) { + std::cout << "[ctrff] BCLIM: Image with and height must be a power of 8!"; + return; + } + size_t size = w * h; + if (f == "rgba32") { + size *= 4; + } else if (f == "rgb565") { + size *= 2; + } + std::vector res(size); + if (f == "rgba32" || f == "a8") { + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + int src = (y * w + x) * 4; + int dst = ctrff::TileIndex(x, y, w); + if (f == "rgba32") { + dst *= 4; + res[dst + 3] = ret[src + 0]; + res[dst + 2] = ret[src + 1]; + res[dst + 1] = ret[src + 2]; + res[dst + 0] = ret[src + 3]; + } else { + res[dst] = ret[src + 3]; + } + } + } + } else if (f == "rgb565") { + std::vector res16(w * h); + ctrff::RGBA2RGB565(res16.data(), + std::vector(ret, ret + (w * h * 4)), w, h); + for (int i = 0; i < res16.size(); i++) { + int pos = i * 2; + res[pos] = (ctrff::u8)(res16[i] & 0xff); + res[pos + 1] = (ctrff::u8)((res16[i] >> 8) & 0xff); + } + } + ctrff::BCLIM file; + ctrff::BCLIM::Format fmt = ctrff::BCLIM::A8; + if (f == "rgba32") { + fmt = ctrff::BCLIM::RGBA8888; + } else if (f == "rgb565") { + fmt = ctrff::BCLIM::RGB565; + } + file.CreateByImage(res, w, h, fmt); + file.Save(o); + std::cout << "File " + o + " created" << std::endl; +} + int main(int argc, char *argv[]) { cf7::fancy_print = false; cf7::colors_supported = false; @@ -497,6 +565,14 @@ int main(int argc, char *argv[]) { cf7::command("hex", "Show Hex view of a File") .AddSubEntry(cf7::command::sub("i", "input", "Input File path", true)) .SetFunction(Hex)); + mgr.AddCommand( + cf7::command("makebclim", "Create CTR Layout Image") + .AddSubEntry(cf7::command::sub("i", "input", "Input png|bmp", true)) + .AddSubEntry(cf7::command::sub("o", "output", + "Output path of .bclim file", true)) + .AddSubEntry(cf7::command::sub("f", "format", + "Image format rgba32|rgb565|a8", true)) + .SetFunction(BCLIMMaker)); mgr.Execute(); return 0; } \ No newline at end of file