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