添加项目文件。

This commit is contained in:
Chenx221 2024-10-10 17:54:47 +08:00
parent 960bb19fb6
commit ecf0d9d4ef
6 changed files with 471 additions and 0 deletions

25
EscudeLSF.sln Normal file
View File

@ -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

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.0.0" />
</ItemGroup>
</Project>

276
EscudeLSF/Program.cs Normal file
View File

@ -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 <filepath>] [-d <directory>] [-s <filepath>] [-h]");
Console.WriteLine("Options:");
Console.WriteLine(" <filepath> Single lsf process");
Console.WriteLine(" -r <filepath> Read single lsf file");
Console.WriteLine(" -d <directory> Process all lsf files in directory");
Console.WriteLine(" -s <filepath> Same as <filepath>");
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<LSFENTRY> ReadLSF(string filePath)
{
LSFHDR header;
List<LSFENTRY> 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<LSFENTRY> 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<LSFENTRY> 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;
}
}
}

View File

@ -0,0 +1,7 @@
{
"profiles": {
"EscudeLSF": {
"commandName": "Project"
}
}
}

1
README.md Normal file
View File

@ -0,0 +1 @@
# EscudeLSF

148
note.txt Normal file
View File

@ -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