#include #include #include #include #include "TextureParser.h" #include "WallpaperEngine/Data/Assets/Texture.h" #include "WallpaperEngine/Logging/Log.h" using namespace WallpaperEngine::Data::Assets; using namespace WallpaperEngine::Data::Parsers; TextureUniquePtr TextureParser::parse (const BinaryReader& file) { auto result = std::make_unique (); parseTextureHeader (*result, file); parseContainer (*result, file); for (uint32_t image = 0; image < result->imageCount; image++) { const uint32_t mipmapCount = file.nextUInt32 (); MipmapList mipmaps; for (uint32_t mipmap = 0; mipmap < mipmapCount; mipmap++) { mipmaps.emplace_back (parseMipmap (file, *result)); } result->images.emplace (image, mipmaps); } if (!result->isAnimated ()) { return result; } parseAnimations (*result, file); return result; } MipmapSharedPtr TextureParser::parseMipmap (const BinaryReader& file, const Texture& header) { auto result = std::make_shared (); // TEXB0004 has extra header data if (header.containerVersion == ContainerVersion_TEXB0004) { // two integers that only seem to affect the editor std::ignore = file.nextUInt32 (); std::ignore = file.nextUInt32 (); // json blob, not parsed yet result->json = file.nextNullTerminatedString (); std::ignore = file.nextUInt32 (); } result->width = file.nextUInt32 (); result->height = file.nextUInt32 (); if (header.containerVersion == ContainerVersion_TEXB0004 || header.containerVersion == ContainerVersion_TEXB0003 || header.containerVersion == ContainerVersion_TEXB0002) { result->compression = file.nextUInt32 (); result->uncompressedSize = file.nextInt (); } result->compressedSize = file.nextInt (); if (result->compression == 0) { // misnamed: in uncompressed files compressedSize actually holds the file length result->uncompressedSize = result->compressedSize; } result->uncompressedData = std::unique_ptr (new char[result->uncompressedSize]); if (result->compression == 1) { result->compressedData = std::unique_ptr (new char[result->compressedSize]); file.next (result->compressedData.get (), result->compressedSize); int bytes = LZ4_decompress_safe ( result->compressedData.get (), result->uncompressedData.get (), result->compressedSize, result->uncompressedSize ); if (bytes < 0) { sLog.exception ("Cannot decompress texture data, LZ4_decompress_safe returned an error"); } } else { file.next (result->uncompressedData.get (), result->uncompressedSize); } return result; } FrameSharedPtr TextureParser::parseFrameV1 (const BinaryReader& file) { auto result = std::make_shared (); result->frameNumber = file.nextUInt32 (); result->frametime = file.nextFloat (); result->x = static_cast (file.nextUInt32 ()); result->y = static_cast (file.nextUInt32 ()); result->width1 = static_cast (file.nextUInt32 ()); std::ignore = file.nextUInt32 (); // unknown std::ignore = file.nextUInt32 (); // unknown result->height1 = static_cast (file.nextUInt32 ()); return result; } FrameSharedPtr TextureParser::parseFrame (const BinaryReader& file) { auto result = std::make_shared (); result->frameNumber = file.nextUInt32 (); result->frametime = file.nextFloat (); result->x = file.nextFloat (); result->y = file.nextFloat (); result->width1 = file.nextFloat (); result->width2 = file.nextFloat (); result->height2 = file.nextFloat (); result->height1 = file.nextFloat (); return result; } TextureMap TextureParser::parseTextureMap (const JSON& it) { if (!it.is_array ()) { return {}; } TextureMap result = {}; int textureIndex = -1; for (const auto& cur : it) { textureIndex++; if (cur.is_null ()) { continue; } if (cur.is_object ()) { const auto nameIt = cur.find ("name"); if (nameIt != cur.end () && nameIt->is_string ()) { result.emplace (textureIndex, nameIt->get ()); } } else if (cur.is_string ()) { std::string texName = cur; if (!texName.empty ()) { result.emplace (textureIndex, texName); } } } return result; } TextureFormat TextureParser::parseTextureFormat (uint32_t value) { switch (value) { case TextureFormat_UNKNOWN: case TextureFormat_ARGB8888: case TextureFormat_RGB888: case TextureFormat_RGB565: case TextureFormat_DXT5: case TextureFormat_DXT3: case TextureFormat_DXT1: case TextureFormat_RG88: case TextureFormat_R8: case TextureFormat_RG1616f: case TextureFormat_R16f: case TextureFormat_BC7: case TextureFormat_RGBa1010102: case TextureFormat_RGBA16161616f: case TextureFormat_RGB161616f: return static_cast (value); default: sLog.exception ("unknown texture format: ", value); } } void TextureParser::parseTextureHeader (Texture& header, const BinaryReader& file) { char magic[9] = { 0 }; file.next (magic, 9); if (strncmp (magic, "TEXV0005", 9) != 0) { sLog.exception ("unexpected texture container type: ", std::string_view (magic, 9)); } file.next (magic, 9); if (strncmp (magic, "TEXI0001", 9) != 0) { sLog.exception ("unexpected texture sub-container type: ", std::string_view (magic, 9)); } header.format = parseTextureFormat (file.nextUInt32 ()); header.flags = parseTextureFlags (file.nextUInt32 ()); header.textureWidth = file.nextUInt32 (); header.textureHeight = file.nextUInt32 (); header.width = file.nextUInt32 (); header.height = file.nextUInt32 (); // ignore some more bytes std::ignore = file.nextUInt32 (); } void TextureParser::parseContainer (Texture& header, const BinaryReader& file) { char magic[9] = { 0 }; file.next (magic, 9); header.imageCount = file.nextUInt32 (); if (strncmp (magic, "TEXB0004", 9) == 0) { header.containerVersion = ContainerVersion_TEXB0004; header.freeImageFormat = parseFIF (file.nextUInt32 ()); header.isVideoMp4 = file.nextUInt32 () == 1; if (header.freeImageFormat == FIF_UNKNOWN && header.isVideoMp4) { header.freeImageFormat = FIF_MP4; } // TEXB0004 behaves like TEXB0003 unless it's actually an MP4 if (header.freeImageFormat != FIF_MP4) { header.containerVersion = ContainerVersion_TEXB0003; } } else if (strncmp (magic, "TEXB0003", 9) == 0) { header.containerVersion = ContainerVersion_TEXB0003; header.freeImageFormat = parseFIF (file.nextUInt32 ()); } else if (strncmp (magic, "TEXB0002", 9) == 0) { header.containerVersion = ContainerVersion_TEXB0002; } else if (strncmp (magic, "TEXB0001", 9) == 0) { header.containerVersion = ContainerVersion_TEXB0001; } else { sLog.exception ("unknown texture format type: ", std::string_view (magic, 9)); } } void TextureParser::parseAnimations (Texture& header, const BinaryReader& file) { char magic[9] = { 0 }; file.next (magic, 9); if (strncmp (magic, "TEXS0001", 9) == 0) { header.animatedVersion = AnimatedVersion_TEXS0001; } else if (strncmp (magic, "TEXS0002", 9) == 0) { header.animatedVersion = AnimatedVersion_TEXS0002; } else if (strncmp (magic, "TEXS0003", 9) == 0) { header.animatedVersion = AnimatedVersion_TEXS0003; } else { sLog.exception ("found animation information of unknown type: ", std::string_view (magic, 9)); } uint32_t frameCount = file.nextUInt32 (); if (header.animatedVersion == AnimatedVersion_TEXS0003) { header.gifWidth = file.nextUInt32 (); header.gifHeight = file.nextUInt32 (); } while (frameCount-- > 0) { if (header.animatedVersion == AnimatedVersion_TEXS0001) { header.frames.push_back (parseFrameV1 (file)); } else { header.frames.push_back (parseFrame (file)); } } // TEXS0001/TEXS0002 don't carry gif dimensions in the header, derive them from the first frame if (header.animatedVersion == AnimatedVersion_TEXS0001 || header.animatedVersion == AnimatedVersion_TEXS0002) { header.gifWidth = (*header.frames.begin ())->width1; header.gifHeight = (*header.frames.begin ())->height1; } // spritesheets are grid-based; infer the grid from texture size vs. frame size if (!header.frames.empty () && header.width > 0 && header.height > 0) { auto& firstFrame = *header.frames.front (); float frameWidth = firstFrame.width1; float frameHeight = firstFrame.height1; if (frameWidth > 0.0f && frameHeight > 0.0f) { const uint32_t cols = static_cast (std::round (static_cast (header.width) / frameWidth)); const uint32_t rows = static_cast (std::round (static_cast (header.height) / frameHeight)); const uint32_t frameCount = static_cast (header.frames.size ()); // only accept the grid if it can hold all frames - otherwise plain GIFs (frameWidth == textureWidth) // would get treated as 1x1 spritesheets if (cols > 0 && rows > 0 && cols * rows >= frameCount) { header.spritesheetCols = cols; header.spritesheetRows = rows; header.spritesheetFrames = frameCount; float totalDuration = 0.0f; for (const auto& frame : header.frames) { totalDuration += frame->frametime; } header.spritesheetDuration = totalDuration; } } } } uint32_t TextureParser::parseTextureFlags (uint32_t value) { if (value < TextureFlags_All) { return value; } sLog.exception ("unknown texture flags: ", value); } FIF TextureParser::parseFIF (uint32_t value) { switch (value) { case FIF_UNKNOWN: case FIF_BMP: case FIF_ICO: case FIF_JPEG: case FIF_JNG: case FIF_KOALA: case FIF_LBM: case FIF_MNG: case FIF_PBM: case FIF_PBMRAW: case FIF_PCD: case FIF_PCX: case FIF_PGM: case FIF_PGMRAW: case FIF_PNG: case FIF_PPM: case FIF_PPMRAW: case FIF_RAS: case FIF_TARGA: case FIF_TIFF: case FIF_WBMP: case FIF_PSD: case FIF_CUT: case FIF_XBM: case FIF_XPM: case FIF_DDS: case FIF_GIF: case FIF_HDR: case FIF_FAXG3: case FIF_SGI: case FIF_EXR: case FIF_J2K: case FIF_JP2: case FIF_PFM: case FIF_PICT: case FIF_RAW: case FIF_WEBP: case FIF_JXR: return static_cast (value); default: sLog.exception ("unknown free image format: ", value); } } TextureUniquePtr TextureParser::parse ( const BinaryReader& file, const std::string& filename, std::function metadataLoader ) { auto result = parse (file); if (metadataLoader) { parseSpritesheetMetadata (*result, filename, metadataLoader); } return result; } void TextureParser::parseSpritesheetMetadata ( Texture& header, const std::string& filename, std::function metadataLoader ) { try { std::string texJsonContent = metadataLoader (filename + ".tex-json"); nlohmann::json texJson = nlohmann::json::parse (texJsonContent); if (texJson.contains ("spritesheetsequences") && texJson["spritesheetsequences"].is_array ()) { auto& sequences = texJson["spritesheetsequences"]; if (!sequences.empty ()) { auto& firstSeq = sequences[0]; int frames = firstSeq.value ("frames", 0); float frameWidth = firstSeq.value ("width", 0.0f); float frameHeight = firstSeq.value ("height", 0.0f); float duration = firstSeq.value ("duration", 1.0f); if (frames > 0 && frameWidth > 0.0f && frameHeight > 0.0f && header.width > 0 && header.height > 0) { header.spritesheetCols = static_cast (std::round (header.width / frameWidth)); header.spritesheetRows = static_cast (std::round (header.height / frameHeight)); header.spritesheetFrames = static_cast (frames); header.spritesheetDuration = duration; } } } } catch (const std::exception&) { // .tex-json file is optional, only used for spritesheet data } }