From ecf0d9d4ef130af43dc551082522ae9e5edb0b1b Mon Sep 17 00:00:00 2001 From: Chenx221 Date: Thu, 10 Oct 2024 17:54:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EscudeLSF.sln | 25 ++ EscudeLSF/EscudeLSF.csproj | 14 ++ EscudeLSF/Program.cs | 276 +++++++++++++++++++++++ EscudeLSF/Properties/launchSettings.json | 7 + README.md | 1 + note.txt | 148 ++++++++++++ 6 files changed, 471 insertions(+) create mode 100644 EscudeLSF.sln create mode 100644 EscudeLSF/EscudeLSF.csproj create mode 100644 EscudeLSF/Program.cs create mode 100644 EscudeLSF/Properties/launchSettings.json create mode 100644 README.md create mode 100644 note.txt diff --git a/EscudeLSF.sln b/EscudeLSF.sln new file mode 100644 index 0000000..6034cd9 --- /dev/null +++ b/EscudeLSF.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35312.102 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EscudeLSF", "EscudeLSF\EscudeLSF.csproj", "{3DD9B6A4-5DD0-43FA-92EF-33DED4622A2B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3DD9B6A4-5DD0-43FA-92EF-33DED4622A2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DD9B6A4-5DD0-43FA-92EF-33DED4622A2B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DD9B6A4-5DD0-43FA-92EF-33DED4622A2B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DD9B6A4-5DD0-43FA-92EF-33DED4622A2B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CAD0F1D4-91F8-4B9F-B70D-90E12F0B04F8} + EndGlobalSection +EndGlobal diff --git a/EscudeLSF/EscudeLSF.csproj b/EscudeLSF/EscudeLSF.csproj new file mode 100644 index 0000000..10c16d4 --- /dev/null +++ b/EscudeLSF/EscudeLSF.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/EscudeLSF/Program.cs b/EscudeLSF/Program.cs new file mode 100644 index 0000000..447eecf --- /dev/null +++ b/EscudeLSF/Program.cs @@ -0,0 +1,276 @@ +using ImageMagick; +using System.Runtime.InteropServices; + +namespace EscudeLSF +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct LSFHDR + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] signature; // 4 bytes + public uint unknown1; // 4 bytes + public uint unknown2; // 4 bytes + public uint width; // 4 bytes + public uint height; // 4 bytes + public uint unknown_width; // 4 bytes + public uint unknown_height; // 4 bytes + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct LSFENTRY + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)] + public char[] name; // 128 bytes + public uint x; // 4 bytes + public uint y; // 4 bytes + public uint baseWidth; // 4 bytes + public uint baseHeight; // 4 bytes + public uint unknown3; // 4 bytes + public uint unknown4; // 4 bytes + public byte type; // 1 byte + public byte id; // 1 byte + public byte unknown5; // 1 byte + public byte unknown6; // 1 byte + public uint unknown7; // 4 bytes + public uint unknown8; // 4 bytes + } + + internal class Program + { + private static readonly byte[] Signature = [0x4C, 0x53, 0x46, 0x00]; + + static void Main(string[] args) + { + if (args.Length == 0 || args.Length > 2) + { + Console.WriteLine("Invalid arguments. Use -h for help."); + return; + } + + string? workDirectory; + string? outputDirectory; + + switch (args[0]) + { + case "-h": + DisplayHelp(); + return; + + case "-r": + case "-d": + case "-s": + workDirectory = GetWorkDirectory(args); + outputDirectory = GetOutputDirectory(workDirectory); + if (args[0] == "-d") + { + ProcessBatch(args[1], workDirectory, outputDirectory); + } + else + { + CombineIMG(ReadLSF(args[1]), workDirectory, outputDirectory); + } + break; + + default: + if (!IsValidFileArgument(args)) + { + Console.WriteLine("Invalid arguments. Use -h for help."); + return; + } + workDirectory = Path.GetDirectoryName(args[0]); + outputDirectory = GetOutputDirectory(workDirectory); + CombineIMG(ReadLSF(args[0]), workDirectory, outputDirectory); + break; + } + } + static void DisplayHelp() + { + Console.WriteLine("Usage: EscudeLSF.exe [-r ] [-d ] [-s ] [-h]"); + Console.WriteLine("Options:"); + Console.WriteLine(" Single lsf process"); + Console.WriteLine(" -r Read single lsf file"); + Console.WriteLine(" -d Process all lsf files in directory"); + Console.WriteLine(" -s Same as "); + Console.WriteLine(" -h Display help info"); + } + + static string GetOutputDirectory(string workDirectory) + { + string outputDirectory = Path.Combine(Path.GetDirectoryName(workDirectory), "output"); + if (!Directory.Exists(outputDirectory)) + { + Directory.CreateDirectory(outputDirectory); + } + return outputDirectory; + } + + static void ProcessBatch(string directory, string workDirectory, string outputDirectory) + { + foreach (string file in Directory.GetFiles(directory, "*.lsf")) + { + CombineIMG(ReadLSF(file), workDirectory, outputDirectory); + } + } + + static string GetWorkDirectory(string[] args) + { + return args[0] == "-d" ? args[1] : Path.GetDirectoryName(args[1]); + } + + static bool IsValidFileArgument(string[] args) + { + return args.Length == 2 || (args.Length == 1 && !Path.Exists(args[0])); + } + static List ReadLSF(string filePath) + { + LSFHDR header; + List entries = []; + + using (FileStream fs = new(filePath, FileMode.Open, FileAccess.Read)) + using (BinaryReader reader = new(fs)) + { + header = ReadLSFHDR(reader); + if (!header.signature.SequenceEqual(Signature)) + { + Console.WriteLine("Invalid LSF file: Signature does not match."); + return []; + } + while (fs.Position < fs.Length) + { + LSFENTRY entry = ReadLSFENTRY(reader); + + // 过滤某些类型(主要是表情,还有审查 + // 没找到合适的处理方法,有主意的老哥欢迎Pull Request + if (entry.type == 0x0B || entry.type == 0x15 || entry.type == 0xFF || (entry.type == 0x00 && entry.unknown5 == 0x03)) + { + continue; + } + // 过滤重复项(没看出重复的意义,也许是前面过滤掉的某个类型需要 + if (entries.Any(e => e.name.SequenceEqual(entry.name))) + { + continue; + } + + entries.Add(entry); + } + } + return entries; + } + + static LSFHDR ReadLSFHDR(BinaryReader reader) + { + byte[] headerData = reader.ReadBytes(Marshal.SizeOf(typeof(LSFHDR))); + GCHandle handle = GCHandle.Alloc(headerData, GCHandleType.Pinned); + try + { + return (LSFHDR)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(LSFHDR)); + } + finally + { + handle.Free(); + } + } + + static LSFENTRY ReadLSFENTRY(BinaryReader reader) + { + byte[] entryData = reader.ReadBytes(Marshal.SizeOf(typeof(LSFENTRY))); + GCHandle handle = GCHandle.Alloc(entryData, GCHandleType.Pinned); + try + { + return (LSFENTRY)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(LSFENTRY)); + } + finally + { + handle.Free(); + } + } + + static void CombineIMG(List entrys, string workDirectory, string storeDir) + { + // 检查是否有多个角色 + bool isMultiCharacter = entrys.Any(e => e.type == 0x14); + + foreach (var entry in entrys.Where(e => e.type == 0x00)) + { + string baseName = new string(entry.name).Trim('\0'); + string targetPath = Path.Combine(storeDir, baseName); + string baseImagePath = Path.Combine(workDirectory, baseName + ".png"); + + if (!File.Exists(baseImagePath)) + { + Console.WriteLine($"Error, File not found: {baseName}"); + continue; + } + + + + foreach (var eyeEntry in entrys.Where(e => e.type == 0x0A)) + { + using var baseImage = new MagickImage(baseImagePath); + ProcessEyeImage(entrys, workDirectory, baseImage, eyeEntry, targetPath, isMultiCharacter); + } + } + } + + private static void ProcessEyeImage(List entrys, string workDirectory, MagickImage baseImage, LSFENTRY eyeEntry, string targetPath, bool isMultiCharacter) + { + string eyeName = new string(eyeEntry.name).Trim('\0'); + string eyeImagePath = Path.Combine(workDirectory, eyeName + ".png"); + + if (!File.Exists(eyeImagePath)) + { + Console.WriteLine($"Error, File not found: {eyeName}"); + return; + } + + using var eyeImage = new MagickImage(eyeImagePath); + var tmpImage = RealProcessIMG(baseImage, eyeImage, (int)eyeEntry.x, (int)eyeEntry.y); + string target2Path = targetPath + "_" + eyeName; + + if (isMultiCharacter) + { + foreach (var eye2Entry in entrys.Where(e => e.type == 0x14)) + { + ProcessOverlayImage(eye2Entry, target2Path, workDirectory, tmpImage); + } + } + else + { + tmpImage.Write(target2Path + ".png"); + } + } + + private static void ProcessOverlayImage(LSFENTRY eye2Entry, string target2Path, string workDirectory, MagickImage tmpImage) + { + string eye2Name = new string(eye2Entry.name).Trim('\0'); + string eye2ImagePath = Path.Combine(workDirectory, eye2Name + ".png"); + + if (!File.Exists(eye2ImagePath)) + { + Console.WriteLine($"Error, File not found: {eye2Name}"); + return; + } + + using var overlayImage = new MagickImage(eye2ImagePath); + RealProcessIMG(tmpImage, overlayImage, (int)eye2Entry.x, (int)eye2Entry.y).Write($"{target2Path}_{eye2Name}.png"); + } + + + static MagickImage RealProcessIMG(MagickImage b, MagickImage o, int x, int y) + { + o.Alpha(AlphaOption.Set); + + // Not working + // 原先是设计给处理那些不透明的图片,但好像有问题 + //if (args[4] == "y") + //{ + // //overlayImage.ColorFuzz = new Percentage(6); // For Face + // //overlayImage.Transparent(MagickColors.White); // For Face + // //overlayImage.Blur(0, 4); // For Face + //} + + b.Composite(o, x, y, CompositeOperator.Over); + return b; + } + } +} diff --git a/EscudeLSF/Properties/launchSettings.json b/EscudeLSF/Properties/launchSettings.json new file mode 100644 index 0000000..fc452a6 --- /dev/null +++ b/EscudeLSF/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "EscudeLSF": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..662c5a6 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# EscudeLSF \ No newline at end of file diff --git a/note.txt b/note.txt new file mode 100644 index 0000000..c800342 --- /dev/null +++ b/note.txt @@ -0,0 +1,148 @@ +struct LSFHDR { 28 + uint8_t signature[4]; // 4 bytes // LSF + uint32_t unknown1; // 4 bytes + uint32_t unknown2; // 4 bytes + uint32_t width; // 4 bytes + uint32_t height; // 4 bytes + uint32_t unknown_width; // 4 bytes + uint32_t unknown_height; // 4 bytes +}; + + +[4C 53 46 00] 02 00 00 00 00 00 09 00 [00 05 00 00] +[D0 02 00 00] [80 02 00 00] [68 01 00 00] + + +struct LSFENTRY { 164 + char name[128]; // 128 bytes + uint32_t x; // 4 bytes + uint32_t y; // 4 bytes + uint32_t baseWidth+Width; // 4 bytes + uint32_t baseHeight+Height; // 4 bytes + uint32_t unknown3; // 4 bytes + uint32_t unknown4; // 4 bytes + uint32_t properties; // 4 bytes + //uint8_t type; // 1 bytes // 00Base 0BFace? 15Face2? 0AEye 14Eye2 FFCensor + //uint8_t id; // 1 bytes + //uint8_t unknown5; // 1 bytes //Layer? 可能决定一对多的关系? + //uint8_t unknown6; // 1 bytes // FF + uint32_t unknown7; // 4 bytes + uint32_t unknown8; // 4 bytes +}; + +未能实现: +0B、15类型脸色、FF类型审查 +00 ** 03基板 + +基板: +[45 56 5F 41 30 31 5F 30 30 31 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00] +00 00 00 00 00 00 00 00 [00 05 00 00] [D0 02 00 00] +00 00 00 00 00 00 00 00 [00 01 00 FF] 00 00 00 00 +00 00 00 00 + +脸色:(需白色转透明+5%颜色容错) +[45 56 5F 41 30 31 5F 30 30 32 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00] +[30 02 00 00] [95 00 00 00] [A5 02 00 00] [C8 00 00 00] +00 00 00 00 00 00 00 00 [0B 01 03 FF] 00 00 00 00 +00 00 00 00 + +560,149 +677,200 + +[45 56 5F 41 30 31 5F 30 30 33 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00] +[30 02 00 00] [96 00 00 00] [A5 02 00 00] [CA 00 00 00] +00 00 00 00 00 00 00 00 [0B 02 03 FF] 00 00 00 00 +00 00 00 00 + +560,150 + +[45 56 5F 41 30 31 5F 30 30 34 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00] +[30 02 00 00] [8E 00 00 00] [A5 02 00 00] [CB 00 00 00] +00 00 00 00 00 00 00 00 [0B 03 03 FF] 00 00 00 00 +00 00 00 00 + +560 142 + +不知道是什么玩意:(这个坐标都一样,遇到这种0B ** 00组合视为一个) +[45 56 5F 41 30 31 5F 30 30 35 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00] +[3E 02 00 00] [A7 00 00 00] [9C 02 00 00] [B0 00 00 00] +00 00 00 00 00 00 00 00 [0B 01 00 FF] 00 00 00 00 +00 00 00 00 + +574 167 + +45 56 5F 41 30 31 5F 30 30 35 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +3E 02 00 00 A7 00 00 00 9C 02 00 00 B0 00 00 00 +00 00 00 00 00 00 00 00 0B 02 00 FF 00 00 00 00 +00 00 00 00 + +眼睛: +[45 56 5F 41 30 31 5F 30 30 36 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00] +[2F 02 00 00] [77 00 00 00] [A6 02 00 00] [CB 00 00 00] +00 00 00 00 00 00 00 00 [0A 01 00 FF] 00 00 00 00 +00 00 00 00 + +559 119 + +[45 56 5F 41 30 31 5F 30 30 37 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00] +[2F 02 00 00] [79 00 00 00] [A7 02 00 00] [CB 00 00 00] +00 00 00 00 00 00 00 00 [0A 02 00 FF] 00 00 00 00 +00 00 00 00 + +559 121 \ No newline at end of file