Skip to content

Commit

Permalink
Sandbox: Add the Shockky.Sandbox console project (#1 and #2)
Browse files Browse the repository at this point in the history
* A lot of work to-do here.
  • Loading branch information
PaulusParssinen committed Mar 3, 2020
1 parent 5ce332c commit 027c996
Show file tree
Hide file tree
Showing 14 changed files with 319 additions and 3 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,3 @@ __pycache__/
*.btm.cs
*.odx.cs
*.xsd.cs
/Shockky.Sandbox
Binary file added Shockky.Sandbox/Palettes/grey.pal
Binary file not shown.
Binary file added Shockky.Sandbox/Palettes/mac.pal
Binary file not shown.
Binary file added Shockky.Sandbox/Palettes/metallic.pal
Binary file not shown.
Binary file added Shockky.Sandbox/Palettes/ntsc.pal
Binary file not shown.
Binary file added Shockky.Sandbox/Palettes/pastels.pal
Binary file not shown.
Binary file added Shockky.Sandbox/Palettes/rainbow.pal
Binary file not shown.
Binary file added Shockky.Sandbox/Palettes/vivid.pal
Binary file not shown.
Binary file added Shockky.Sandbox/Palettes/web216.pal
Binary file not shown.
Binary file added Shockky.Sandbox/Palettes/win.pal
Binary file not shown.
Binary file added Shockky.Sandbox/Palettes/windir4.pal
Binary file not shown.
290 changes: 290 additions & 0 deletions Shockky.Sandbox/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Diagnostics;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;

using Shockky.Chunks;
using Shockky.Chunks.Cast;

using System.CommandLine;
using System.CommandLine.Invocation;

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

namespace Shockky.Sandbox
{
class Program
{
static int Main(string[] args)
{
Console.Title = "Shockky.Sandbox";
Console.WriteLine("Shockky.Sandbox");
Console.WriteLine();

//TODO: Verbose and Quiet levels, and rest of the resources of course
var rootCommand = new RootCommand()
{
new Argument<IEnumerable<FileInfo>>("input")
{
Arity = ArgumentArity.OneOrMore,
Description = "Director movie (.dir, .dxt, .dcr) or external cast (.cst, .cxt, .cct) file(s)."
}.ExistingOnly(),

new Option<bool>("--images",
getDefaultValue: () => true,
description: "Extract all (with bit depth 8) bitmaps from the file."),

new Option<DirectoryInfo>("--output",
getDefaultValue: () => new DirectoryInfo("Output/"),
description: "Directory for the extracted resources").LegalFilePathsOnly()
};
rootCommand.Handler = CommandHandler.Create<IEnumerable<FileInfo>, bool, DirectoryInfo>(HandleExtractCommand);

return rootCommand.Invoke(args);
}

private static IReadOnlyDictionary<int, System.Drawing.Color[]> ReadPalettes()
{
static System.Drawing.Color[] ReadPalette(string fileName)
{
using var fs = File.OpenRead(fileName);
using var input = new BinaryReader(fs, Encoding.ASCII);

input.ReadChars(4);
input.ReadInt32();

input.ReadChars(4);

input.ReadChars(4);
input.ReadInt32();
input.ReadInt16();

System.Drawing.Color[] colors = new System.Drawing.Color[input.ReadInt16()];
for (int i = 0; i < colors.Length; i++)
{
byte r = input.ReadByte();
byte g = input.ReadByte();
byte b = input.ReadByte();

colors[i] = System.Drawing.Color.FromArgb(r, g, b);

input.ReadByte();
}
return colors;
}

return new Dictionary<int, System.Drawing.Color[]>
{
{ -1, ReadPalette("Palettes/mac.pal") },
{ -2, ReadPalette("Palettes/rainbow.pal") },
{ -3, ReadPalette("Palettes/grey.pal") },
{ -4, ReadPalette("Palettes/pastels.pal") },
{ -5, ReadPalette("Palettes/vivid.pal") },
{ -6, ReadPalette("Palettes/ntsc.pal") },
{ -7, ReadPalette("Palettes/metallic.pal") },
{ -8, ReadPalette("Palettes/web216.pal") },
{ -9, null }, //TODO: "Palettes/VGA.pal"
{ -101, ReadPalette("Palettes/windir4.pal") },
{ -102, ReadPalette("Palettes/win.pal") }
};
}

private static int HandleExtractCommand(IEnumerable<FileInfo> input, bool images, DirectoryInfo output)
{
if (!images)
return 0;

output.Create();

//Load the built-in system palettes
IReadOnlyDictionary<int, System.Drawing.Color[]> systemPalettes = ReadPalettes();

foreach (var file in input)
{
//TODO: Seperate different resource types.
DirectoryInfo fileOutputDirectory = output.CreateSubdirectory(file.Name);

Console.Write($"Disassembling file \"{file.Name}\"..");

using var shockwaveFile = new ShockwaveFile(file.FullName);
shockwaveFile.Disassemble();

Console.WriteLine("Done!");

List<(CastMemberPropertiesChunk Member, ChunkItem Media)> memberMedia = new List<(CastMemberPropertiesChunk, ChunkItem)>();

var associationTable = shockwaveFile.Chunks
.FirstOrDefault(c => c.Kind == ChunkKind.KEYPointer) as AssociationTableChunk;

var castAssociationTable = shockwaveFile.Chunks
.FirstOrDefault(c => c.Kind == ChunkKind.CASPointer) as CastAssociationTableChunk;

if (associationTable == null)
{
Console.WriteLine($"Chunk \"{nameof(AssociationTableChunk)}\" was not found!");
continue;
}

if (castAssociationTable == null)
{
Console.WriteLine($"Chunk \"{nameof(CastAssociationTableChunk)}\" was not found!");
continue;
}
Console.Write("Extracting resources..");

//TODO: Report progress, try some of those System.CommandLine goodies?

//Build a list of the cast member-media pairs.
foreach (int memberId in castAssociationTable.Members)
{
if (memberId == 0) continue;

var castMemberChunk = shockwaveFile[memberId] as CastMemberPropertiesChunk;

var mediaEntries = associationTable.CastEntries.Where(e => e.OwnerId == memberId);

foreach (var mediaEntry in mediaEntries)
{
//TODO: filter mediaEntry.Kind here to extract only wanted resources

memberMedia.Add((castMemberChunk, shockwaveFile[mediaEntry.Id]));
}
}

//TODO: DIB and others..
foreach (var (member, media) in memberMedia.Where(entry => entry.Media?.Kind == ChunkKind.BITD))
{
var bitmapChunk = media as BitmapChunk;
var bitmapProperties = member?.Properties as BitmapCastProperties;

if (bitmapProperties == null) continue;

string outputFileName = CoerceValidFileName(member.Common?.Name)
?? $"NONAME-{member.Header.Id}-{media.Header.Id}";

int paletteIndex = bitmapProperties.Palette - 1; //castMemRef

if (paletteIndex >= 0 && paletteIndex < castAssociationTable.Members.Length)
{
//TODO: Research why these safety checks still fail for some files.. CastMemRef's seems to point to non palette members?

int paletteMemberChunkId = castAssociationTable.Members[paletteIndex];

if (shockwaveFile[paletteMemberChunkId] is CastMemberPropertiesChunk paletteMember)
{
if (memberMedia.FirstOrDefault(entry => entry.Member == paletteMember).Media is PaletteChunk paletteChunk)
{
bitmapChunk.PopulateMedia(bitmapProperties);
if (TryExtractBitmapResource(fileOutputDirectory, outputFileName, bitmapChunk, paletteChunk.Colors))
{
Console.Write('.');
continue;
}
}
}
}
else if (systemPalettes.TryGetValue(paletteIndex, out System.Drawing.Color[] palette))
{
bitmapChunk.PopulateMedia(bitmapProperties);
if (TryExtractBitmapResource(fileOutputDirectory, outputFileName, bitmapChunk, palette))
{
Console.Write('.');
continue;
}
}
Console.Write('x');
}
Console.WriteLine(" Done!");
}

return 0;
}

//TODO: Look more into ImageSharp, could offer some helpful tools to do this
private static bool TryExtractBitmapResource(DirectoryInfo outputDirectory, string name, BitmapChunk bitmap, System.Drawing.Color[] palette)
{
//TODO: Properly render flags etc. TrimWhitespace for example uses flood fill apparently so that could be fun.

Span<byte> buffer = bitmap.Data.AsSpan();

int width = bitmap.Width < bitmap.TotalWidth ? bitmap.Width : bitmap.TotalWidth; //TODO: This is wrong way

using var image = new Image<Bgra32>(bitmap.Width, bitmap.Height);
for (int y = 0; y < bitmap.Height; y++)
{
Span<byte> row = buffer.Slice(y * bitmap.TotalWidth, bitmap.TotalWidth);

if (bitmap.BitDepth == 32) //TODO: Can't get this right yet, probably wrong PixelFormat
{
return false;

//Span<Bgra32> pixels = MemoryMarshal.Cast<byte, Bgra32>(row);
//
//for (int x = 0; x < bitmap.Width; x++)
//{
// image[x, y] = pixels[x];
//}
}
else if (bitmap.BitDepth == 4)
{
return false;

//for (int x = 0; x < width; x++)
//{
// System.Drawing.Color pixelColor = palette[row[x] >> 4];
// System.Drawing.Color secondPixelColor = palette[row[x] & 0xF];
//
// //image[x, y] = new Bgra32(pixelColor.R, pixelColor.G, pixelColor.B);
//}
}
else
{
for (int x = 0; x < width; x++)
{
System.Drawing.Color pixelColor = palette[row[x]];
image[x, y] = new Bgra32(pixelColor.R, pixelColor.G, pixelColor.B);
}
}
}

using var fs = File.Create(Path.GetFullPath(name + ".png", outputDirectory.FullName));
image.SaveAsPng(fs);

return true;
}

/// <summary>
/// Strip illegal chars and reserved words from a candidate filename (should not include the directory path)
/// </summary>
/// <remarks>
/// http://stackoverflow.com/questions/309485/c-sharp-sanitize-file-name
/// </remarks>
public static string CoerceValidFileName(string filename)
{
var invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()));
var invalidReStr = string.Format(@"[{0}]+", invalidChars);

var reservedWords = new[]
{
"CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4",
"COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4",
"LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
};

var sanitisedNamePart = Regex.Replace(filename, invalidReStr, "_");
foreach (var reservedWord in reservedWords)
{
var reservedWordPattern = string.Format("^{0}\\.", reservedWord);
sanitisedNamePart = Regex.Replace(sanitisedNamePart, reservedWordPattern, "_reservedWord_.", RegexOptions.IgnoreCase);
}

return sanitisedNamePart;
}
}
}
21 changes: 21 additions & 0 deletions Shockky.Sandbox/Shockky.Sandbox.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Shockky\Shockky.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0007" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20104.2" />
</ItemGroup>

<ItemGroup>
<None Include="Palettes\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
10 changes: 8 additions & 2 deletions Shockky.sln
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2027
# Visual Studio Version 16
VisualStudioVersion = 16.0.29311.281
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shockky", "Shockky\Shockky.csproj", "{0F807D73-1838-4227-B6D3-932D83E99D9C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shockky.Sandbox", "Shockky.Sandbox\Shockky.Sandbox.csproj", "{0BD542DD-87A2-4BAD-8528-87D209C48BE5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{0F807D73-1838-4227-B6D3-932D83E99D9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F807D73-1838-4227-B6D3-932D83E99D9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F807D73-1838-4227-B6D3-932D83E99D9C}.Release|Any CPU.Build.0 = Release|Any CPU
{0BD542DD-87A2-4BAD-8528-87D209C48BE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0BD542DD-87A2-4BAD-8528-87D209C48BE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0BD542DD-87A2-4BAD-8528-87D209C48BE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0BD542DD-87A2-4BAD-8528-87D209C48BE5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down

0 comments on commit 027c996

Please sign in to comment.