static void Decompile(string path, bool keepRaw = false, PsbImageFormat format = PsbImageFormat.png, uint?key = null, PsbType type = PsbType.PSB) { var name = Path.GetFileNameWithoutExtension(path); Console.WriteLine($"Decompiling: {name}"); #if !DEBUG try #endif { if (keepRaw) { PsbDecompiler.DecompileToFile(path, key: key, type: type); } else { PsbDecompiler.DecompileToFile(path, PsbExtractOption.Extract, format, key: key, type: type); } } #if !DEBUG catch (Exception e) { Console.WriteLine(e); } #endif }
public static void CombineImagesToFile(string inputPath, PsbImageFormat extractFormat = PsbImageFormat.png) { if (!File.Exists(inputPath)) { return; } var psb = new PSB(inputPath); if (psb.Type != PsbType.Tachie) { return; } var name = Path.GetFileNameWithoutExtension(inputPath); var dirPath = Path.Combine(Path.GetDirectoryName(inputPath), name); if (File.Exists(dirPath)) { name += "-resources"; dirPath += "-resources"; } if (!Directory.Exists(dirPath)) //ensure there is no file with same name! { Directory.CreateDirectory(dirPath); } var bitmaps = CombineTachie(psb); foreach (var kv in bitmaps) { kv.Value.Save(Path.Combine(dirPath, $"{kv.Key}.{extractFormat}"), extractFormat.ToImageFormat()); } }
public static ImageFormat ToImageFormat(this PsbImageFormat imageFormat) { switch (imageFormat) { case PsbImageFormat.bmp: return(ImageFormat.Bmp); case PsbImageFormat.png: default: return(ImageFormat.Png); } }
/// <summary> /// Decompile to files /// </summary> /// <param name="psb">PSB</param> /// <param name="outputPath">Output json file name, should end with .json</param> /// <param name="additionalContext">additional context used in decompilation</param> /// <param name="extractOption">whether to extract image to common format</param> /// <param name="extractFormat">if extract, what format do you want</param> /// <param name="useResx">if false, use array-based resource json (legacy)</param> /// <param name="key">PSB CryptKey</param> public static void DecompileToFile(PSB psb, string outputPath, Dictionary <string, object> additionalContext = null, PsbExtractOption extractOption = PsbExtractOption.Original, PsbImageFormat extractFormat = PsbImageFormat.png, bool useResx = true, uint?key = null) { var context = FreeMount.CreateContext(additionalContext); if (key != null) { context.Context[Consts.Context_CryptKey] = key; } File.WriteAllText(outputPath, Decompile(psb)); //MARK: breaking change for json path OutputResources(psb, context, outputPath, extractOption, extractFormat, useResx); }
public static string DefaultExtension(this PsbImageFormat imageFormat) { switch (imageFormat) { case PsbImageFormat.png: return(".png"); case PsbImageFormat.bmp: return(".bmp"); default: return($".{imageFormat}"); } }
/// <summary> /// Decompile to files /// </summary> /// <param name="inputPath">PSB file path</param> /// <param name="extractOption">whether to extract image to common format</param> /// <param name="extractFormat">if extract, what format do you want</param> /// <param name="useResx">if false, use array-based resource json (legacy)</param> /// <param name="key">PSB CryptKey</param> /// <param name="type">Specify PSB type, if not set, infer type automatically</param> public static void DecompileToFile(string inputPath, PsbExtractOption extractOption = PsbExtractOption.Original, PsbImageFormat extractFormat = PsbImageFormat.png, bool useResx = true, uint?key = null, PsbType type = PsbType.PSB) { var context = FreeMount.CreateContext(); if (key != null) { context.Context[Consts.Context_CryptKey] = key; } File.WriteAllText(ChangeExtensionForOutputJson(inputPath, ".json"), Decompile(inputPath, out var psb, context.Context)); OutputResources(psb, context, inputPath, extractOption, extractFormat, useResx); }
public static void DecompressToImageFile(byte[] data, string path, int height, int width, PsbImageFormat format = PsbImageFormat.png, PsbPixelFormat colorFormat = PsbPixelFormat.None, int align = 4) { byte[] bytes; try { bytes = Decompress(data, height, width, align); } catch (Exception e) { throw new PsbBadFormatException(PsbBadFormatReason.Resources, "data incorrect", e); } ConvertToImageFile(bytes, path, height, width, format, colorFormat); }
/// <summary> /// Decompile to files /// </summary> /// <param name="inputPath">PSB file path</param> /// <param name="imageOption">whether to extract image to common format</param> /// <param name="extractFormat">if extract, what format do you want</param> /// <param name="useResx">if false, use array-based resource json (legacy)</param> /// <param name="key">PSB CryptKey</param> public static void DecompileToFile(string inputPath, PsbImageOption imageOption = PsbImageOption.Original, PsbImageFormat extractFormat = PsbImageFormat.Png, bool useResx = true, uint?key = null) { var context = FreeMount.CreateContext(); if (key != null) { context.Context[FreeMount.CryptKey] = key; } File.WriteAllText(Path.ChangeExtension(inputPath, ".json"), Decompile(inputPath, out var psb, context.Context)); //MARK: breaking change for json path OutputResources(psb, context, inputPath, imageOption, extractFormat, useResx); }
/// <summary> /// Save (most user friendly) images /// </summary> /// <param name="inputPath"></param> /// <param name="format"></param> public static void ExtractImageFiles(string inputPath, PsbImageFormat format = PsbImageFormat.png) { if (!File.Exists(inputPath)) { return; } var name = Path.GetFileNameWithoutExtension(inputPath); var dirPath = Path.Combine(Path.GetDirectoryName(inputPath), name); if (File.Exists(dirPath)) { name += "-resources"; dirPath += "-resources"; } if (!Directory.Exists(dirPath)) //ensure there is no file with same name! { Directory.CreateDirectory(dirPath); } var texExt = format == PsbImageFormat.bmp ? ".bmp" : ".png"; var texFormat = format.ToImageFormat(); var psb = new PSB(inputPath); if (psb.Type == PsbType.Tachie) { var bitmaps = TextureCombiner.CombineTachie(psb); foreach (var kv in bitmaps) { kv.Value.Save(Path.Combine(dirPath, $"{kv.Key}{texExt}"), texFormat); } return; } var texs = PsbResHelper.UnlinkImages(psb); foreach (var tex in texs) { tex.Save(Path.Combine(dirPath, tex.Tag + texExt), texFormat); } }
internal static void OutputResources(PSB psb, FreeMountContext context, string filePath, PsbExtractOption extractOption = PsbExtractOption.Original, PsbImageFormat extractFormat = PsbImageFormat.png, bool useResx = true) { var name = Path.GetFileNameWithoutExtension(filePath); var dirPath = Path.Combine(Path.GetDirectoryName(filePath), name); PsbResourceJson resx = new PsbResourceJson(psb, context.Context); if (File.Exists(dirPath)) { name += "-resources"; dirPath += "-resources"; } if (!Directory.Exists(dirPath)) //ensure there is no file with same name! { if (psb.Resources.Count != 0) { Directory.CreateDirectory(dirPath); } } var resDictionary = psb.TypeHandler.OutputResources(psb, context, name, dirPath, extractOption); //MARK: We use `.resx.json` to distinguish from psbtools' `.res.json` if (useResx) { resx.Resources = resDictionary; resx.Context = context.Context; File.WriteAllText(Path.ChangeExtension(filePath, ".resx.json"), JsonConvert.SerializeObject(resx, Formatting.Indented)); } else { File.WriteAllText(Path.ChangeExtension(filePath, ".res.json"), JsonConvert.SerializeObject(resDictionary.Values.ToList(), Formatting.Indented)); } }
internal static void OutputResources(PSB psb, FreeMountContext context, string filePath, PsbImageOption imageOption = PsbImageOption.Original, PsbImageFormat extractFormat = PsbImageFormat.Png, bool useResx = true) { var name = Path.GetFileNameWithoutExtension(filePath); var dirPath = Path.Combine(Path.GetDirectoryName(filePath), name); var resources = psb.CollectResources(); PsbResourceJson resx = new PsbResourceJson(psb, context.Context); if (File.Exists(dirPath)) { name += "-resources"; dirPath += "-resources"; } if (!Directory.Exists(dirPath)) //ensure there is no file with same name! { if (psb.Resources.Count != 0 || resources.Count != 0) { Directory.CreateDirectory(dirPath); } } Dictionary <string, string> resDictionary = new Dictionary <string, string>(); if (imageOption == PsbImageOption.Original) { for (int i = 0; i < psb.Resources.Count; i++) { var relativePath = psb.Resources[i].Index == null ? $"#{i}.bin" : $"{psb.Resources[i].Index}.bin"; File.WriteAllBytes( Path.Combine(dirPath, relativePath), psb.Resources[i].Data); resDictionary.Add(Path.GetFileNameWithoutExtension(relativePath), $"{name}/{relativePath}"); } } else { for (int i = 0; i < resources.Count; i++) { var resource = resources[i]; //Generate Friendly Name string relativePath = resource.GetFriendlyName(psb.Type); switch (imageOption) { case PsbImageOption.Extract: ImageFormat pixelFormat; switch (extractFormat) { case PsbImageFormat.Png: relativePath += ".png"; pixelFormat = ImageFormat.Png; break; default: relativePath += ".bmp"; pixelFormat = ImageFormat.Bmp; break; } relativePath = CheckPath(relativePath, i); if (resource.Compress == PsbCompressType.RL) { RL.DecompressToImageFile(resource.Data, Path.Combine(dirPath, relativePath), resource.Height, resource.Width, extractFormat, resource.PixelFormat); } else if (resource.Compress == PsbCompressType.Tlg || resource.Compress == PsbCompressType.ByName) { var bmp = context.ResourceToBitmap(resource.Compress == PsbCompressType.Tlg ? ".tlg" : Path.GetExtension(resource.Name), resource.Data); if (bmp == null) { if (resource.Compress == PsbCompressType.Tlg) //Fallback to managed TLG decoder { using (var ms = new MemoryStream(resource.Data)) using (var br = new BinaryReader(ms)) { bmp = new TlgImageConverter().Read(br); bmp.Save(Path.Combine(dirPath, relativePath), pixelFormat); bmp.Dispose(); } } relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name)); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else { bmp.Save(Path.Combine(dirPath, relativePath), pixelFormat); bmp.Dispose(); } } //else if (resource.Compress == PsbCompressType.ByName) //{ // relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name)); // File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); //} else { RL.ConvertToImageFile(resource.Data, Path.Combine(dirPath, relativePath), resource.Height, resource.Width, extractFormat, resource.PixelFormat); } break; case PsbImageOption.Original: if (resources[i].Compress == PsbCompressType.RL) { relativePath += ".rl"; relativePath = CheckPath(relativePath, i); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else if (resource.Compress == PsbCompressType.Tlg) { relativePath += ".tlg"; relativePath = CheckPath(relativePath, i); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else { relativePath += ".raw"; relativePath = CheckPath(relativePath, i); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } break; case PsbImageOption.Decompress: relativePath += ".raw"; relativePath = CheckPath(relativePath, i); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resources[i].Compress == PsbCompressType.RL ? RL.Decompress(resource.Data) : resource.Data); break; case PsbImageOption.Compress: relativePath += ".rl"; relativePath = CheckPath(relativePath, i); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resources[i].Compress != PsbCompressType.RL ? RL.Compress(resource.Data) : resource.Data); break; default: throw new ArgumentOutOfRangeException(nameof(imageOption), imageOption, null); } try { resDictionary.Add(i.ToString(), $"{name}/{relativePath}"); } catch (ArgumentException e) { throw new PsbBadFormatException(PsbBadFormatReason.Resources, "There are resources with same names! Try Raw export mode.", e); } } } //MARK: We use `.resx.json` to distinguish from psbtools' `.res.json` if (useResx) { resx.Resources = resDictionary; resx.Context = context.Context; File.WriteAllText(Path.ChangeExtension(filePath, ".resx.json"), JsonConvert.SerializeObject(resx, Formatting.Indented)); } else { File.WriteAllText(Path.ChangeExtension(filePath, ".res.json"), JsonConvert.SerializeObject(resDictionary.Values.ToList(), Formatting.Indented)); } string CheckPath(string rPath, int id) { var k = Path.GetFileNameWithoutExtension(rPath); if (resDictionary.ContainsKey(k)) { return($"{id}{Path.GetExtension(rPath)}"); } return(rPath); } }
/// <summary> /// Convert a PSB to External Texture PSB. /// </summary> /// <param name="inputPath"></param> /// <param name="outputUnlinkedPsb">output unlinked PSB; otherwise only output textures</param> /// <param name="order"></param> /// <param name="format"></param> /// <returns>The unlinked PSB path</returns> public static string UnlinkToFile(string inputPath, bool outputUnlinkedPsb = true, PsbLinkOrderBy order = PsbLinkOrderBy.Name, PsbImageFormat format = PsbImageFormat.png) { if (!File.Exists(inputPath)) { return(null); } var name = Path.GetFileNameWithoutExtension(inputPath); var dirPath = Path.Combine(Path.GetDirectoryName(inputPath), name); var psbSavePath = inputPath; if (File.Exists(dirPath)) { name += "-resources"; dirPath += "-resources"; } if (!Directory.Exists(dirPath)) //ensure there is no file with same name! { Directory.CreateDirectory(dirPath); } var context = FreeMount.CreateContext(); context.ImageFormat = format; var psb = new PSB(inputPath); if (psb.TypeHandler is BaseImageType imageType) { imageType.UnlinkToFile(psb, context, name, dirPath, outputUnlinkedPsb, order); } psb.TypeHandler.UnlinkToFile(psb, context, name, dirPath, outputUnlinkedPsb, order); if (outputUnlinkedPsb) { psb.Merge(); psbSavePath = Path.ChangeExtension(inputPath, ".unlinked.psb"); //unlink only works with motion.psb so no need for ext rename File.WriteAllBytes(psbSavePath, psb.Build()); } return(psbSavePath); }
static void Main(string[] args) { Console.WriteLine("FreeMote PSB Decompiler"); Console.WriteLine("by Ulysses, [email protected]"); FreeMount.Init(); Console.WriteLine($"{FreeMount.PluginsCount} Plugins Loaded."); PsbConstants.InMemoryLoading = true; Console.WriteLine(); var app = new CommandLineApplication(); app.OptionsComparison = StringComparison.OrdinalIgnoreCase; //help app.HelpOption(); //do not inherit app.ExtendedHelpText = PrintHelp(); //options var optKey = app.Option <uint>("-k|--key", "Set PSB key (uint, dec)", CommandOptionType.SingleValue); var optFormat = app.Option <PsbImageFormat>("-e|--extract <FORMAT>", "Convert textures to Png/Bmp. Default=Png", CommandOptionType.SingleValue, true); var optRaw = app.Option("-raw|--raw", "Keep raw textures", CommandOptionType.NoValue); //メモリ足りない もうどうしよう : https://soundcloud.com/ulysses-wu/Heart-Chrome var optOom = app.Option("-oom|--memory-limit", "Disable In-Memory Loading", CommandOptionType.NoValue); var optHex = app.Option("-hex|--json-hex", "(Json) Use hex numbers", CommandOptionType.NoValue, true); var optArray = app.Option("-indent|--json-array-indent", "(Json) Indent arrays", CommandOptionType.NoValue, true); //args var argPath = app.Argument("Files", "File paths", multipleValues: true); //command: unlink app.Command("unlink", linkCmd => { //help linkCmd.Description = "Unlink textures from PSBs"; linkCmd.HelpOption(); linkCmd.ExtendedHelpText = @" Example: PsbDecompile unlink sample.psb "; //options var optOrder = linkCmd.Option <PsbLinkOrderBy>("-o|--order <ORDER>", "Set texture unlink order (ByName/ByOrder/Convention). Default=ByName", CommandOptionType.SingleValue); //args var argPsbPath = linkCmd.Argument("PSB", "PSB Path").IsRequired(); //var argTexPath = linkCmd.Argument("Textures", "Texture Paths").IsRequired(); linkCmd.OnExecute(() => { PsbImageFormat format = optFormat.HasValue() ? optFormat.ParsedValue : PsbImageFormat.Png; //var order = optOrder.HasValue() ? optOrder.ParsedValue : PsbLinkOrderBy.Name; var psbPaths = argPsbPath.Values; foreach (var psbPath in psbPaths) { if (File.Exists(psbPath)) { try { PsbDecompiler.UnlinkToFile(psbPath, format: format); } catch (Exception e) { Console.WriteLine(e); } } } }); }); app.OnExecute(() => { if (optOom.HasValue()) { PsbConstants.InMemoryLoading = false; } if (optArray.HasValue()) { PsbConstants.JsonArrayCollapse = false; } if (optHex.HasValue()) { PsbConstants.JsonUseHexNumber = true; } bool useRaw = optRaw.HasValue(); PsbImageFormat format = optFormat.HasValue() ? optFormat.ParsedValue : PsbImageFormat.Png; uint?key = optKey.HasValue() ? optKey.ParsedValue : (uint?)null; foreach (var s in argPath.Values) { if (File.Exists(s)) { Decompile(s, useRaw, format, key); } else if (Directory.Exists(s)) { foreach (var file in Directory.EnumerateFiles(s, "*.psb") .Union(Directory.EnumerateFiles(s, "*.mmo")) .Union(Directory.EnumerateFiles(s, "*.pimg")) .Union(Directory.EnumerateFiles(s, "*.scn")) .Union(Directory.EnumerateFiles(s, "*.dpak")) .Union(Directory.EnumerateFiles(s, "*.psz")) .Union(Directory.EnumerateFiles(s, "*.psp")) ) { Decompile(s, useRaw, format, key); } } } }); if (args.Length == 0) { app.ShowHelp(); return; } app.Execute(args); Console.WriteLine("Done."); }
/// <summary> /// Decompile to files /// </summary> /// <param name="inputPath">PSB file path</param> /// <param name="imageOption">whether to extract image to common format</param> /// <param name="extractFormat">if extract, what format do you want</param> /// <param name="useResx">if false, use array-based resource json (legacy)</param> public static void DecompileToFile(string inputPath, PsbImageOption imageOption = PsbImageOption.Original, PsbImageFormat extractFormat = PsbImageFormat.Png, bool useResx = true) { var name = Path.GetFileNameWithoutExtension(inputPath); var dirPath = Path.Combine(Path.GetDirectoryName(inputPath), name); File.WriteAllText(inputPath + ".json", Decompile(inputPath, out var psb)); var resources = psb.CollectResources(); PsbResourceJson resx = new PsbResourceJson { PsbVersion = psb.Header.Version, PsbType = psb.Type, Platform = psb.Platform, ExternalTextures = psb.Type == PsbType.Motion && psb.Resources.Count <= 0 }; if (!Directory.Exists(dirPath)) //ensure no file with same name! { Directory.CreateDirectory(dirPath); } Dictionary <string, string> resDictionary = new Dictionary <string, string>(); if (imageOption == PsbImageOption.Original) { for (int i = 0; i < psb.Resources.Count; i++) { var relativePath = psb.Resources[i].Index == null ? $"#{i}.bin" : $"{psb.Resources[i].Index}.bin"; File.WriteAllBytes( Path.Combine(dirPath, relativePath), psb.Resources[i].Data); resDictionary.Add(Path.GetFileNameWithoutExtension(relativePath), $"{name}/{relativePath}"); } } else { for (int i = 0; i < resources.Count; i++) { var resource = resources[i]; //Generate Friendly Name string relativePath; if (psb.Type == PsbType.Pimg && !string.IsNullOrWhiteSpace(resource.Name)) { relativePath = Path.GetFileNameWithoutExtension(resource.Name); } else if (string.IsNullOrWhiteSpace(resource.Name) || string.IsNullOrWhiteSpace(resource.Part)) { relativePath = resource.Index.ToString(); } else { relativePath = $"{resource.Part}{PsbResCollector.ResourceNameDelimiter}{resource.Name}"; } switch (imageOption) { case PsbImageOption.Extract: //var pixelFormat = resource.Spec.DefaultPixelFormat(); //MARK: PixelFormat should refer `type` switch (extractFormat) { case PsbImageFormat.Png: relativePath += ".png"; if (resource.Compress == PsbCompressType.RL) { RL.UncompressToImageFile(resource.Data, Path.Combine(dirPath, relativePath), resource.Height, resource.Width, PsbImageFormat.Png, resource.PixelFormat); } else if (resource.Compress == PsbCompressType.Tlg || resource.Compress == PsbCompressType.ByName && resource.Name.EndsWith(".tlg", true, null)) { TlgImageConverter converter = new TlgImageConverter(); using (var ms = new MemoryStream(resource.Data)) { BinaryReader br = new BinaryReader(ms); converter.Read(br).Save(Path.Combine(dirPath, relativePath), ImageFormat.Png); } //WARN: tlg is kept and recorded in resource json for compile relativePath = Path.ChangeExtension(relativePath, ".tlg"); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else if (resource.Compress == PsbCompressType.ByName) { relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name)); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else { RL.ConvertToImageFile(resource.Data, Path.Combine(dirPath, relativePath), resource.Height, resource.Width, extractFormat, resource.PixelFormat); } break; default: relativePath += ".bmp"; if (resource.Compress == PsbCompressType.RL) { RL.UncompressToImageFile(resource.Data, Path.Combine(dirPath, relativePath), resource.Height, resource.Width, PsbImageFormat.Bmp, resource.PixelFormat); } else if (resource.Compress == PsbCompressType.Tlg || resource.Compress == PsbCompressType.ByName && resource.Name.EndsWith(".tlg", true, null)) { TlgImageConverter converter = new TlgImageConverter(); using (var ms = new MemoryStream(resource.Data)) { BinaryReader br = new BinaryReader(ms); converter.Read(br).Save(Path.Combine(dirPath, relativePath), ImageFormat.Bmp); } //WARN: tlg is kept and recorded in resource json for compile relativePath = Path.ChangeExtension(relativePath, ".tlg"); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else if (resource.Compress == PsbCompressType.ByName) { relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name)); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else { RL.ConvertToImageFile(resource.Data, Path.Combine(dirPath, relativePath), resource.Height, resource.Width, extractFormat, resource.PixelFormat); } break; } break; case PsbImageOption.Original: if (resources[i].Compress == PsbCompressType.RL) { relativePath += ".rl"; File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else if (resource.Compress == PsbCompressType.Tlg) { relativePath += ".tlg"; File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else { relativePath += ".raw"; File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } break; case PsbImageOption.Uncompress: File.WriteAllBytes(Path.Combine(dirPath, relativePath), resources[i].Compress == PsbCompressType.RL ? RL.Uncompress(resource.Data) : resource.Data); relativePath += ".raw"; break; case PsbImageOption.Compress: File.WriteAllBytes(Path.Combine(dirPath, relativePath), resources[i].Compress != PsbCompressType.RL ? RL.Compress(resource.Data) : resource.Data); relativePath += ".rl"; break; default: throw new ArgumentOutOfRangeException(nameof(imageOption), imageOption, null); } try { resDictionary.Add(Path.GetFileNameWithoutExtension(relativePath), $"{name}/{relativePath}"); } catch (ArgumentException e) { throw new BadImageFormatException("There are resources with same names! Try Raw export mode.", e); } } } //MARK: We use `.resx.json` to distinguish from psbtools' `.res.json` if (useResx) { resx.Resources = resDictionary; File.WriteAllText(inputPath + ".resx.json", JsonConvert.SerializeObject(resx, Formatting.Indented)); } else { File.WriteAllText(inputPath + ".res.json", JsonConvert.SerializeObject(resDictionary.Values.ToList(), Formatting.Indented)); } }
internal static void OutputResources(PSB psb, FreeMountContext context, string filePath, PsbExtractOption extractOption = PsbExtractOption.Original, PsbImageFormat extractFormat = PsbImageFormat.png, bool useResx = true) { var name = Path.GetFileNameWithoutExtension(filePath); var dirPath = Path.Combine(Path.GetDirectoryName(filePath), name); PsbResourceJson resx = new PsbResourceJson(psb, context.Context); if (File.Exists(dirPath)) { name += "-resources"; dirPath += "-resources"; } var extraDir = Path.Combine(dirPath, Consts.ExtraResourceFolderName); if (!Directory.Exists(dirPath)) //ensure there is no file with same name! { if (psb.Resources.Count != 0) { Directory.CreateDirectory(dirPath); } } if (psb.ExtraResources.Count > 0) { var extraDic = PsbResHelper.OutputExtraResources(psb, context, name, extraDir, out var flattenArrays, extractOption); resx.ExtraResources = extraDic; if (flattenArrays != null && flattenArrays.Count > 0) { resx.ExtraFlattenArrays = flattenArrays; } } var resDictionary = psb.TypeHandler.OutputResources(psb, context, name, dirPath, extractOption); //MARK: We use `.resx.json` to distinguish from psbtools' `.res.json` if (useResx) { resx.Resources = resDictionary; resx.Context = context.Context; string json; if (Consts.JsonArrayCollapse) { json = ArrayCollapseJsonTextWriter.SerializeObject(resx); } else { json = JsonConvert.SerializeObject(resx, Formatting.Indented); } File.WriteAllText(ChangeExtensionForOutputJson(filePath, ".resx.json"), json); } else { if (psb.ExtraResources.Count > 0) { throw new NotSupportedException("PSBv4 cannot use legacy res.json format."); } File.WriteAllText(ChangeExtensionForOutputJson(filePath, ".res.json"), JsonConvert.SerializeObject(resDictionary.Values.ToList(), Formatting.Indented)); } }
public static void ConvertToImageFile(byte[] data, string path, int height, int width, PsbImageFormat format, PsbPixelFormat colorFormat = PsbPixelFormat.None) { var bmp = ConvertToImage(data, height, width, colorFormat); switch (format) { case PsbImageFormat.Bmp: bmp.Save(path, ImageFormat.Bmp); break; case PsbImageFormat.Png: bmp.Save(path, ImageFormat.Png); break; } }
/// <summary> /// Split textures into parts and save to files /// </summary> /// <param name="psb">PSB</param> /// <param name="path">Save directory</param> /// <param name="option">Save option</param> /// <param name="imageFormat">Save format</param> /// <param name="pixelFormat">When save to PSB special formats, specific pixel format to use</param> public static void SplitTextureToFiles(this PSB psb, string path, PsbImageOption option = PsbImageOption.Extract, PsbImageFormat imageFormat = PsbImageFormat.Png, PsbPixelFormat pixelFormat = PsbPixelFormat.None) { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } var source = (PsbDictionary)psb.Objects["source"]; foreach (var texPair in source) { if (!(texPair.Value is PsbDictionary tex)) { continue; } var name = texPair.Key; if (!Directory.Exists(Path.Combine(path, name))) { Directory.CreateDirectory(Path.Combine(path, name)); } var icon = (PsbDictionary)tex["icon"]; var texture = (PsbDictionary)tex["texture"]; //var mipmap = (PsbDictionary)texture["mipMap"]; //TODO: Mipmap? var md = PsbResCollector.GenerateMotionResMetadata(texture, (PsbResource)texture["pixel"]); md.Spec = psb.Platform; //Important Bitmap bmp = md.ToImage(); foreach (var iconPair in icon) { var savePath = Path.Combine(path, name, iconPair.Key); var info = (PsbDictionary)iconPair.Value; var width = (int)(PsbNumber)info["width"]; var height = (int)(PsbNumber)info["height"]; var top = (int)(PsbNumber)info["top"]; var left = (int)(PsbNumber)info["left"]; var attr = (int)(PsbNumber)info["attr"]; Bitmap b = new Bitmap(width, height, PixelFormat.Format32bppArgb); #if USE_FASTBITMAP using (FastBitmap f = b.FastLock()) { f.CopyRegion(bmp, new Rectangle(left, top, width, height), new Rectangle(0, 0, b.Width, b.Height)); } #else Graphics g = Graphics.FromImage(b); //g.InterpolationMode = InterpolationMode.NearestNeighbor; //g.PixelOffsetMode = PixelOffsetMode.Half; g.DrawImage(bmp, new Rectangle(0, 0, b.Width, b.Height), new Rectangle(left, top, width, height), GraphicsUnit.Pixel); g.Dispose(); #endif switch (option) { case PsbImageOption.Decompress: File.WriteAllBytes(savePath + ".raw", RL.GetPixelBytesFromImage(b, pixelFormat)); break; case PsbImageOption.Compress: File.WriteAllBytes(savePath + ".rl", RL.CompressImage(b, pixelFormat)); break; case PsbImageOption.Original: case PsbImageOption.Extract: default: switch (imageFormat) { case PsbImageFormat.Bmp: b.Save(savePath + ".bmp", ImageFormat.Bmp); break; case PsbImageFormat.Png: default: b.Save(savePath + ".png", ImageFormat.Png); //b.Save(savePath + $"_{attr}.png", ImageFormat.Png); break; } break; } } bmp.Dispose(); } }
public static void ConvertToImageFile(byte[] data, string path, int height, int width, PsbImageFormat format, PsbPixelFormat colorFormat = PsbPixelFormat.None, byte[] palette = null, PsbPixelFormat paletteColorFormat = PsbPixelFormat.None) { Bitmap bmp = ConvertToImage(data, palette, height, width, colorFormat, paletteColorFormat); switch (format) { case PsbImageFormat.bmp: bmp.Save(path, ImageFormat.Bmp); break; case PsbImageFormat.png: bmp.Save(path, ImageFormat.Png); break; } }
/// <summary> /// Convert a PSB to External Texture PSB. /// </summary> /// <param name="inputPath"></param> /// <param name="outputUnlinkedPsb">output unlinked PSB if you need</param> /// <param name="order"></param> /// <param name="format"></param> public static void UnlinkToFile(string inputPath, bool outputUnlinkedPsb = true, PsbLinkOrderBy order = PsbLinkOrderBy.Name, PsbImageFormat format = PsbImageFormat.Png) { if (!File.Exists(inputPath)) { return; } var name = Path.GetFileNameWithoutExtension(inputPath); var dirPath = Path.Combine(Path.GetDirectoryName(inputPath), name); if (File.Exists(dirPath)) { name += "-resources"; dirPath += "-resources"; } if (!Directory.Exists(dirPath)) //ensure there is no file with same name! { Directory.CreateDirectory(dirPath); } var psb = new PSB(inputPath); var texs = psb.Unlink(); if (outputUnlinkedPsb) { psb.Merge(); var psbSavePath = Path.ChangeExtension(inputPath, ".unlinked.psb"); File.WriteAllBytes(psbSavePath, psb.Build()); } var texExt = format == PsbImageFormat.Bmp ? ".bmp" :".png"; var texFormat = format == PsbImageFormat.Bmp ? ImageFormat.Bmp : ImageFormat.Png; switch (order) { case PsbLinkOrderBy.Convention: foreach (var tex in texs) { tex.Save(Path.Combine(dirPath, tex.Tag + texExt), texFormat); } break; case PsbLinkOrderBy.Name: foreach (var tex in texs) { tex.Save(Path.Combine(dirPath, $"{name}_{tex.Tag}{texExt}"), texFormat); } break; case PsbLinkOrderBy.Order: for (var i = 0; i < texs.Count; i++) { var tex = texs[i]; tex.Save(Path.Combine(dirPath, $"{i}{texExt}"), texFormat); } break; } }
public static Dictionary <string, string> OutputImageResources(PSB psb, FreeMountContext context, string name, string dirPath, PsbExtractOption extractOption = PsbExtractOption.Original, PsbImageFormat extractFormat = PsbImageFormat.png) { var resources = psb.CollectResources <ImageMetadata>(); Dictionary <string, string> resDictionary = new Dictionary <string, string>(); ImageFormat pixelFormat; switch (extractFormat) { case PsbImageFormat.png: pixelFormat = ImageFormat.Png; break; default: pixelFormat = ImageFormat.Bmp; break; } if (extractOption == PsbExtractOption.Original) { for (int i = 0; i < psb.Resources.Count; i++) { var relativePath = psb.Resources[i].Index == null ? $"#{Consts.ResourceIdentifierChar}{i}.bin" : $"{psb.Resources[i].Index}.bin"; File.WriteAllBytes( Path.Combine(dirPath, relativePath), psb.Resources[i].Data); resDictionary.Add(Path.GetFileNameWithoutExtension(relativePath), $"{name}/{relativePath}"); } } else { for (int i = 0; i < resources.Count; i++) { var resource = resources[i]; //Generate Friendly Name var friendlyName = resource.GetFriendlyName(psb.Type); string relativePath = friendlyName; if (string.IsNullOrWhiteSpace(friendlyName)) { relativePath = resource.Resource.Index?.ToString() ?? $"({i})"; friendlyName = i.ToString(); } var currentExtractOption = extractOption; if (resource.Compress != PsbCompressType.Tlg && resource.Compress != PsbCompressType.ByName && (resource.Width <= 0 || resource.Height <= 0)) //impossible to extract, just keep raw { if (currentExtractOption == PsbExtractOption.Extract) { currentExtractOption = PsbExtractOption.Original; } } switch (currentExtractOption) { case PsbExtractOption.Extract: switch (extractFormat) { case PsbImageFormat.png: relativePath += ".png"; break; default: relativePath += ".bmp"; break; } relativePath = CheckPath(relativePath, i); if (resource.Compress == PsbCompressType.RL) { RL.DecompressToImageFile(resource.Data, Path.Combine(dirPath, relativePath), resource.Height, resource.Width, extractFormat, resource.PixelFormat); } else if (resource.Compress == PsbCompressType.Tlg || resource.Compress == PsbCompressType.ByName) { var bmp = context.ResourceToBitmap(resource.Compress == PsbCompressType.Tlg ? ".tlg" : Path.GetExtension(resource.Name), resource.Data); if (bmp == null) { if (resource.Compress == PsbCompressType.Tlg) //Fallback to managed TLG decoder { using var ms = new MemoryStream(resource.Data); using var br = new BinaryReader(ms); bmp = new TlgImageConverter().Read(br); bmp.Save(Path.Combine(dirPath, relativePath), pixelFormat); bmp.Dispose(); } relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name)); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else { bmp.Save(Path.Combine(dirPath, relativePath), pixelFormat); bmp.Dispose(); } } //else if (resource.Compress == PsbCompressType.ByName) //{ // relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name)); // File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); //} else { RL.ConvertToImageFile(resource.Data, Path.Combine(dirPath, relativePath), resource.Height, resource.Width, extractFormat, resource.PixelFormat, resource.PalData, resource.PalettePixelFormat); } break; case PsbExtractOption.Original: if (resources[i].Compress == PsbCompressType.RL) { relativePath += ".rl"; relativePath = CheckPath(relativePath, i); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else if (resource.Compress == PsbCompressType.Tlg) { relativePath += ".tlg"; relativePath = CheckPath(relativePath, i); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } else { relativePath += ".raw"; relativePath = CheckPath(relativePath, i); File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data); } break; //case PsbExtractOption.Decompress: // relativePath += ".raw"; // relativePath = CheckPath(relativePath, i); // File.WriteAllBytes(Path.Combine(dirPath, relativePath), // resources[i].Compress == PsbCompressType.RL // ? RL.Decompress(resource.Data) // : resource.Data); // break; //case PsbExtractOption.Compress: // relativePath += ".rl"; // relativePath = CheckPath(relativePath, i); // File.WriteAllBytes(Path.Combine(dirPath, relativePath), // resources[i].Compress != PsbCompressType.RL // ? RL.Compress(resource.Data) // : resource.Data); // break; default: throw new ArgumentOutOfRangeException(nameof(currentExtractOption), currentExtractOption, null); } //Determine save name try { bool indexConflict = false; uint conflictedIndex = 0; if (resource.Resource.Index != null) //try index as name first { if (resDictionary.ContainsKey(resource.Index.ToString())) //index is used before { Console.WriteLine( $"[WARN] Resource Index {resource.Index} conflict. May be resource sharing, but may also be something wrong."); //continue; indexConflict = true; conflictedIndex = resource.Resource.Index.Value; resource.Resource.Index = null; //have another try on friendly name } } if (resource.Resource.Index == null) { if (resDictionary.ContainsKey(friendlyName)) // friendly name is also used (most likely its the same res reused), no name can be used to save { Console.WriteLine( $"[WARN] Resource Name {friendlyName} conflict. May be resource sharing, but may also be something wrong."); continue; //just skip } if (indexConflict) //index is used but friendly name is not (maybe they are different?), save using friendly name { Console.WriteLine($"[FIXED] Resource {friendlyName} is sharing same data with Index {conflictedIndex}"); } } resDictionary.Add(resource.Resource.Index == null ? friendlyName : resource.Index.ToString(), $"{name}/{relativePath}"); } catch (ArgumentException e) { throw new PsbBadFormatException(PsbBadFormatReason.Resources, "Resource Export Error: Name conflict, or Index is not specified. Try Raw export mode.", e); } } } string CheckPath(string rPath, int id) { var k = Path.GetFileNameWithoutExtension(rPath); if (resDictionary.ContainsKey(k)) { return($"{id}{Path.GetExtension(rPath)}"); } return(rPath); } return(resDictionary); }
static void Main(string[] args) { Console.WriteLine("FreeMote PSB Decompiler"); Console.WriteLine("by Ulysses, [email protected]"); FreeMount.Init(); Console.WriteLine($"{FreeMount.PluginsCount} Plugins Loaded."); PsbConstants.InMemoryLoading = true; Console.WriteLine(); var app = new CommandLineApplication(); app.OptionsComparison = StringComparison.OrdinalIgnoreCase; //help app.HelpOption(); //do not inherit app.ExtendedHelpText = PrintHelp(); //options var optKey = app.Option <uint>("-k|--key", "Set PSB key (uint, dec)", CommandOptionType.SingleValue); var optFormat = app.Option <PsbImageFormat>("-e|--extract <FORMAT>", "Convert textures to Png/Bmp. Default=Png", CommandOptionType.SingleValue, true); var optRaw = app.Option("-raw|--raw", "Keep raw textures", CommandOptionType.NoValue, inherited: true); //メモリ足りない もうどうしよう : https://soundcloud.com/ulysses-wu/Heart-Chrome var optOom = app.Option("-oom|--memory-limit", "Disable In-Memory Loading", CommandOptionType.NoValue, inherited: true); var optHex = app.Option("-hex|--json-hex", "(Json) Use hex numbers", CommandOptionType.NoValue, true); var optArray = app.Option("-indent|--json-array-indent", "(Json) Indent arrays", CommandOptionType.NoValue, true); //args var argPath = app.Argument("Files", "File paths", multipleValues: true); //command: unlink app.Command("unlink", linkCmd => { //help linkCmd.Description = "Unlink textures from PSBs"; linkCmd.HelpOption(); linkCmd.ExtendedHelpText = @" Example: PsbDecompile unlink sample.psb "; //options var optOrder = linkCmd.Option <PsbLinkOrderBy>("-o|--order <ORDER>", "Set texture unlink order (ByName/ByOrder/Convention). Default=ByName", CommandOptionType.SingleValue); //args var argPsbPath = linkCmd.Argument("PSB", "PSB Path").IsRequired(); //var argTexPath = linkCmd.Argument("Textures", "Texture Paths").IsRequired(); linkCmd.OnExecute(() => { PsbImageFormat format = optFormat.HasValue() ? optFormat.ParsedValue : PsbImageFormat.Png; var order = optOrder.HasValue() ? optOrder.ParsedValue : PsbLinkOrderBy.Name; var psbPaths = argPsbPath.Values; foreach (var psbPath in psbPaths) { if (File.Exists(psbPath)) { try { PsbDecompiler.UnlinkToFile(psbPath, format: format, order: order); } catch (Exception e) { Console.WriteLine(e); } } } }); }); //info-psb app.Command("info-psb", archiveCmd => { //help archiveCmd.Description = "Extract files from info.psb.m & body.bin (FreeMote.Plugins required)"; archiveCmd.HelpOption(); archiveCmd.ExtendedHelpText = @" Example: PsbDecompile info-psb -k 1234567890ab -l 131 -a sample_info.psb.m PsbDecompile info-psb -s 1234567890absample_info.psb.m -l 131 sample_info.psb Hint: The body.bin should exist in the same folder and keep both file names correct. "; //options //var optMdfSeed = archiveCmd.Option("-s|--seed <SEED>", // "Set complete seed (Key+FileName)", // CommandOptionType.SingleValue); var optExtractAll = archiveCmd.Option("-a|--all", "Decompile all contents in body.bin if possible (can be slow)", CommandOptionType.NoValue); var optMdfKey = archiveCmd.Option("-k|--key <KEY>", "Set key (Infer file name from path)", CommandOptionType.SingleValue); var optMdfKeyLen = archiveCmd.Option <int>("-l|--length <LEN>", "Set key length. Default=131", CommandOptionType.SingleValue); var optInfoOom = archiveCmd.Option("-1by1|--enumerate", "Disable parallel processing when using `-a` (can be very slow)", CommandOptionType.NoValue); //args var argPsbPaths = archiveCmd.Argument("PSB", "Archive Info PSB Paths", true); archiveCmd.OnExecute(() => { bool extractAll = optExtractAll.HasValue(); bool enableParallel = PsbConstants.FastMode; if (optInfoOom.HasValue()) { enableParallel = false; } string key = optMdfKey.HasValue() ? optMdfKey.Value() : null; //string seed = optMdfSeed.HasValue() ? optMdfSeed.Value() : null; if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key), "No key or seed specified."); } int keyLen = optMdfKeyLen.HasValue() ? optMdfKeyLen.ParsedValue : 0x83; Dictionary <string, object> context = new Dictionary <string, object>(); if (keyLen >= 0) { context["MdfKeyLength"] = (uint)keyLen; } foreach (var s in argPsbPaths.Values) { if (File.Exists(s)) { var fileName = Path.GetFileName(s); context["MdfKey"] = key + fileName; try { var dir = Path.GetDirectoryName(Path.GetFullPath(s)); var name = fileName.Substring(0, fileName.IndexOf("_info.")); var body = Path.Combine(dir, name + "_body.bin"); if (!File.Exists(body)) { Console.WriteLine($"Can not find body: {body}"); continue; } PSB psb = null; using (var fs = File.OpenRead(s)) { psb = new PSB(MdfConvert(fs, context)); } File.WriteAllText(Path.GetFullPath(s) + ".json", PsbDecompiler.Decompile(psb)); PsbResourceJson resx = new PsbResourceJson(psb, context); File.WriteAllText(Path.GetFullPath(s) + ".resx.json", resx.SerializeToJson()); var dic = psb.Objects["file_info"] as PsbDictionary; var suffixList = ((PsbCollection)psb.Objects["expire_suffix_list"]); var suffix = ""; if (suffixList.Count > 0) { suffix = suffixList[0] as PsbString ?? ""; } Console.WriteLine($"Extracting info from {fileName} ..."); var bodyBytes = File.ReadAllBytes(body); var extractDir = Path.Combine(dir, name); if (!Directory.Exists(extractDir)) { Directory.CreateDirectory(extractDir); } #if DEBUG Stopwatch sw = Stopwatch.StartNew(); #endif if (enableParallel) //parallel! { int count = 0; Parallel.ForEach(dic, pair => { count++; //Console.WriteLine($"{(extractAll ? "Decompiling" : "Extracting")} {pair.Key} ..."); var range = ((PsbCollection)pair.Value); var start = ((PsbNumber)range[0]).IntValue; var len = ((PsbNumber)range[1]).IntValue; using (var ms = new MemoryStream(bodyBytes, start, len)) { var bodyContext = new Dictionary <string, object>(context) { ["MdfKey"] = key + pair.Key + suffix }; var mms = MdfConvert(ms, bodyContext); if (extractAll) { try { PSB bodyPsb = new PSB(mms); PsbDecompiler.DecompileToFile(bodyPsb, Path.Combine(extractDir, pair.Key + suffix + ".json"), bodyContext, PsbImageOption.Extract); } catch (Exception e) { Console.WriteLine($"Decompile failed: {pair.Key}"); File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } } else { File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } } }); Console.WriteLine($"{count} files {(extractAll ? "decompiled" : "extracted")}."); } else { //no parallel foreach (var pair in dic) { Console.WriteLine( $"{(extractAll ? "Decompiling" : "Extracting")} {pair.Key} ..."); var range = ((PsbCollection)pair.Value); var start = ((PsbNumber)range[0]).IntValue; var len = ((PsbNumber)range[1]).IntValue; using (var ms = new MemoryStream(bodyBytes, start, len)) { context["MdfKey"] = key + pair.Key + suffix; var mms = MdfConvert(ms, context); if (extractAll) { try { PSB bodyPsb = new PSB(mms); PsbDecompiler.DecompileToFile(bodyPsb, Path.Combine(extractDir, pair.Key + suffix + ".json"), context, PsbImageOption.Extract); } catch (Exception e) { Console.WriteLine($"Decompile failed: {pair.Key}"); File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } } else { File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } } } } #if DEBUG sw.Stop(); Console.WriteLine($"Process time: {sw.Elapsed:g}"); #endif } catch (Exception e) { Console.WriteLine(e); } } } }); }); app.OnExecute(() => { if (optOom.HasValue()) { PsbConstants.InMemoryLoading = false; } if (optArray.HasValue()) { PsbConstants.JsonArrayCollapse = false; } if (optHex.HasValue()) { PsbConstants.JsonUseHexNumber = true; } bool useRaw = optRaw.HasValue(); PsbImageFormat format = optFormat.HasValue() ? optFormat.ParsedValue : PsbImageFormat.Png; uint?key = optKey.HasValue() ? optKey.ParsedValue : (uint?)null; foreach (var s in argPath.Values) { if (File.Exists(s)) { Decompile(s, useRaw, format, key); } else if (Directory.Exists(s)) { foreach (var file in Directory.EnumerateFiles(s, "*.psb") .Union(Directory.EnumerateFiles(s, "*.mmo")) .Union(Directory.EnumerateFiles(s, "*.pimg")) .Union(Directory.EnumerateFiles(s, "*.scn")) .Union(Directory.EnumerateFiles(s, "*.dpak")) .Union(Directory.EnumerateFiles(s, "*.psz")) .Union(Directory.EnumerateFiles(s, "*.psp")) ) { Decompile(s, useRaw, format, key); } } } }); if (args.Length == 0) { app.ShowHelp(); return; } app.Execute(args); Console.WriteLine("Done."); }