Moved functionality into the application root

Added a CachedByteArray class that will store compressed (disk-ready) images on the appropriate media
This commit is contained in:
Brychan Dempsey 2021-10-16 16:21:46 +13:00
parent ea5e564352
commit ecbc3978f1
3 changed files with 458 additions and 0 deletions

View File

@ -1,10 +1,15 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Configuration; using System.Configuration;
using System.Data; using System.Data;
using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace RBG_Server namespace RBG_Server
{ {
@ -13,5 +18,194 @@ namespace RBG_Server
/// </summary> /// </summary>
public partial class App : Application public partial class App : Application
{ {
public GameServer BoardGameServer {get;}
public CommunicationHandler communicationHandler { get; private set;}
protected static readonly SolidColorBrush RED_BRUSH = new(Color.FromRgb(255, 0, 0));
protected static readonly SolidColorBrush BLUE_BRUSH = new(Color.FromRgb(0, 0, 255));
protected static readonly SolidColorBrush GREEN_BRUSH = new(Color.FromRgb(0, 255, 0));
/// <summary>
/// Loads the specified files from disk, creating mip maps as appropriate
/// </summary>
/// <param name="selectedFiles"></param>
/// <param name="isSprite"></param>
/// <param name="spriteLimit"></param>
void LoadFiles(string[] selectedFiles, bool isSprite=false, int spriteLimit=128)
{
// Continue in parallel
_ = Parallel.ForEach(selectedFiles, (file) =>
{
LoadFile(file);
});
}
/// <summary>
/// Loads a singular file from disk, generating Mip maps as required
/// </summary>
/// <param name="path"></param>
void LoadFile(string path, bool isSprite=true)
{
// Check if the file is a .gif, first.
using FileStream fs = File.OpenRead(path);
LoadStream(fs, path, isSprite);
}
void LoadStream(Stream source, string path, bool isSprite)
{
long srcStart = source.Position;
byte[] vs = new byte[6]; // First 6 bytes is the gif specifier
source.Read(vs, 0, 6);
source.Position = srcStart;
string magicNumber = Encoding.ASCII.GetString(vs);
if (magicNumber == "GIF87a" || magicNumber == "GIF89a")
{
// Gif file, replace with a layered .png
GifBitmapDecoder decoder = new(source, BitmapCreateOptions.None, BitmapCacheOption.Default);
// Obtain each .gif frame
BitmapFrame[] frames = new BitmapFrame[decoder.Frames.Count];
decoder.Frames.CopyTo(frames, 0);
// Get frame sizes
int height = frames[0].PixelHeight;
int width = frames[0].PixelWidth;
// The lowest mip is static, using either the thumb or first frame
if (decoder.Thumbnail != null)
{
double scaleRate = 32 / (double)Math.Max(height, width);
MemoryStream ms = ScaleImage(decoder.Thumbnail, scaleRate);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_low" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
else
{
double scaleRate = 32 / (double)Math.Max(height, width);
MemoryStream ms = ScaleImage(frames[0], scaleRate);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_low" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
// The remaining mips are animated
if (width >= 128 || height >= 128)
{
double scaleRate = 128 / (double)Math.Max(height, width);
MemoryStream ms = ScaleAnimatedImage(frames, scaleRate);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_med" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
if (width >= 512 || height >= 512)
{
double scaleRate = 512 / (double)Math.Max(height, width);
MemoryStream ms = ScaleAnimatedImage(frames, scaleRate);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_high" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
if (width >= 512 || height >= 512)
{
// Just re-encode the gif to .png
PngBitmapEncoder encoder = new();
foreach (BitmapFrame frame in frames)
{
encoder.Frames.Add(frame);
}
MemoryStream ms = new();
encoder.Save(ms);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_raw" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
}
else
{
// Regular image file
// Create the bitmap to get the current intrinsics
BitmapImage bitmapImage = new();
bitmapImage.BeginInit();
bitmapImage.StreamSource = source;
bitmapImage.CacheOption = BitmapCacheOption.None;
bitmapImage.EndInit();
int height = bitmapImage.PixelHeight;
int width = bitmapImage.PixelWidth;
// Behaviour depends on the required image
if (isSprite)
{
// Sprites should have a low mip-map, and the medium
// Low = 32*32
// Medium = 128*128
// High = 512*512 or image size if less than 1024
if (width >= 32 || height >= 32)
{
double scaleRate = 32 / (double)Math.Max(height, width);
MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_low" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
if (width >= 128 || height >= 128)
{
double scaleRate = 128 / (double)Math.Max(height, width);
MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_med" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
if (width >= 512 || height >= 512)
{
double scaleRate = 512 / (double)Math.Max(height, width);
MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_high" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
if (width >= 512 || height >= 512)
{
MemoryStream ms = new();
source.CopyTo(ms);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_raw" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
}
else
{
// Non-sprites should have a low mip-map, and the medium, but at a larger size than the sprites
// Low = 128*128
// Medium = 512*512
// High = 2048*2048
if (width >= 128 || height >= 128)
{
double scaleRate = 128 / (double)Math.Max(height, width);
MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_low" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
if (width >= 512 || height >= 512)
{
double scaleRate = 512 / (double)Math.Max(height, width);
MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_med" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
if (width >= 2048 || height >= 2048)
{
double scaleRate = 2048 / (double)Math.Max(height, width);
MemoryStream ms = ScaleImage(bitmapImage, scaleRate);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_high" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
if (width >= 2048 || height >= 2048)
{
MemoryStream ms = new();
source.CopyTo(ms);
_ = communicationHandler.ImageCollection.TryAdd(Path.GetFileNameWithoutExtension(path) + "_mip_raw" + Path.GetExtension(path), new CachedByteArray(ms.ToArray()));
}
}
}
}
MemoryStream ScaleImage(BitmapSource source, double scaleRate)
{
TransformedBitmap modified = new(source, new ScaleTransform(source.PixelWidth * scaleRate, source.PixelHeight * scaleRate));
PngBitmapEncoder encoder = new();
encoder.Frames.Add(BitmapFrame.Create(modified));
MemoryStream ms = new();
encoder.Save(ms);
return ms;
}
MemoryStream ScaleAnimatedImage(BitmapFrame[] frames, double scaleRate)
{
PngBitmapEncoder encoder = new();
foreach (BitmapFrame frame in frames)
{
TransformedBitmap modified = new(frame, new ScaleTransform(frame.PixelWidth * scaleRate, frame.PixelHeight * scaleRate));
encoder.Frames.Add(BitmapFrame.Create(modified));
}
MemoryStream ms = new();
encoder.Save(ms);
return ms;
}
} }
} }

View File

@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace RBG_Server
{
/// <summary>
/// Stores data in either memory or disk, depending on the required behaviour of the system, namely, if the
/// byte array exceeds the size of the Memory Limit (objects can be required to stay in RAM)
/// </summary>
public struct CachedByteArray : IDisposable
{
// Static event, so that every instance of CachedByteArray is affected by a change in memory limit
private static event EventHandler MemoryLimitModifiedEvent;
// Properties to keep track of the actual allocated cache amounts
private static long memoryAllocationTotal = 0;
private static long memoryAllocationRAM = 0;
// static fields
private static int memoryLimit = 1024 * 1024 * 1; // Start with a memory limit of 1 MB
private static readonly string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Remote Board Game\\Cache Objects");
// static properties
public static long MemoryAllocationRAM { get { return memoryAllocationRAM; } }
public static long MemoryAllocationTotal { get { return memoryAllocationTotal; } }
public static long MemoryAllocationDisk { get { return memoryAllocationTotal - memoryAllocationRAM; } }
private SemaphoreSlim accessSem;
public bool IsInRam { get; private set; }
// struct data
private bool useOnlyRAM;
private Stream dataStream;
private string dataHashCode;
public bool UseOnlyRAM { get => useOnlyRAM; set => useOnlyRAM = value; }
// Properties from the array itself
public long Length => dataStream.Length;
/// <summary>
/// Set the memory limit for keeping data in RAM, as opposed to storing on disk.
/// Will raise events if modified, so all instances will be refactored to disk if appropriate
/// </summary>
public static int MemoryLimit
{
get
{
return memoryLimit;
}
set
{
memoryLimit = value;
MemoryLimitModifiedEvent?.Invoke(null, EventArgs.Empty);
}
}
public CachedByteArray(byte[] b) : this(b, false)
{
}
public CachedByteArray(byte[] b, bool useOnlyRAM)
{
accessSem = new SemaphoreSlim(0,1);
this.useOnlyRAM = useOnlyRAM;
dataHashCode = GenerateDataHash(b);
if (!useOnlyRAM && b.Length > MemoryLimit)
{
// Save to a file; load the data
string path = Path.Combine(folderPath, dataHashCode);
dataStream = CopyIntoFile(b, path);
IsInRam = false;
}
else
{
dataStream = new MemoryStream(b);
memoryAllocationRAM += dataStream.Length;
IsInRam = true;
}
dataStream.Position = 0;
memoryAllocationTotal += dataStream.Length;
// Signal the semaphore is ready
accessSem.Release();
// Listen to changes in the memory limit
MemoryLimitModifiedEvent += CachedByteArray_memoryLimitModifiedEvent;
}
/// <summary>
/// Generates a SHA256 hash of the data in the array, and returns it in a safe string format
/// </summary>
/// <param name="byteData"></param>
/// <returns></returns>
private static string GenerateDataHash(byte[] byteData)
{
using SHA256 dataHash = SHA256.Create();
byte[] hashCode = dataHash.ComputeHash(byteData);
string generatedHashCode = "";
foreach (byte hashByte in hashCode)
{
generatedHashCode += $"{hashByte:X2}";
}
return generatedHashCode;
}
/// <summary>
/// Copies the given <see cref="byte[]"/> into the file at <paramref name="filePath"/>, opening the stream, and returning it
/// </summary>
/// <param name="byteData"></param>
/// <param name="filePath"></param>
/// <returns></returns>
private static Stream CopyIntoFile(byte[] byteData, string filePath)
{
FileStream s = File.OpenWrite(filePath);
foreach (byte b in byteData)
{
s.WriteByte(b);
}
return s;
}
private void CachedByteArray_memoryLimitModifiedEvent(object sender, EventArgs e)
{
if (!useOnlyRAM)
{
if (dataStream.Length > MemoryLimit && IsInRam)
{
// Memory stream exceeds the limit; create an FS and copy
accessSem.Wait();
// Get the underlying byte[]
byte[] dataBytes = ((MemoryStream)dataStream).ToArray();
// Generate the hashcode from the data
dataHashCode = GenerateDataHash(dataBytes);
// Create the file, and copy the data
dataStream = CopyIntoFile(dataBytes, Path.Combine(folderPath, dataHashCode));
// Disregard the allocation from RAM
memoryAllocationRAM -= dataStream.Length;
accessSem.Release();
}
else if (dataStream.Length <= MemoryLimit && !IsInRam)
{
// Data stream is no larger than the limit, but the file is saved on disk; load to RAM instead
// Obtain a lock on the stream
accessSem.Wait();
// Copy data to memory
dataStream.Position = 0;
MemoryStream ms = new();
dataStream.CopyTo(ms);
// Increase the amount noted in RAM
memoryAllocationRAM += ms.Length;
// Close the FileStream
dataStream.Close();
// Delete the file
DeleteFile(Path.Combine(folderPath, dataHashCode));
// Set the datastream to the memory location
dataStream = ms;
accessSem.Release();
// Check the loaded data matches the hashcode we already have
string loadedHashCode = GenerateDataHash(ms.ToArray());
if (loadedHashCode != dataHashCode)
{
throw new UnauthorizedAccessException("Data was modified");
}
}
// Otherwise, the data is already in the correct spot, so we don't need to do anything else
}
}
private static void DeleteFile(string filePath)
{
try
{
File.Delete(filePath);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
public static implicit operator byte[](CachedByteArray c)
{
byte[] dataBytes = new byte[c.dataStream.Length];
c.accessSem.Wait();
if (c.IsInRam)
{
// We can use the underlying array from the MemoryStream
dataBytes = (c.dataStream as MemoryStream).ToArray();
}
else
{
// Ensure we read from the beginning of the stream
c.dataStream.Position = 0;
c.dataStream.Read(dataBytes, 0, dataBytes.Length);
}
c.accessSem.Release();
return dataBytes;
}
public static explicit operator CachedByteArray(byte[] b) => new CachedByteArray(b);
/// <summary>
/// Array index operator, allows the CachedByteArray to act more completely as a byte[]
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public byte this[int index]
{
get
{
int b;
// Aquire the semaphore, so that the read is effectively atomic (ensures the read is performed on the expected data)
accessSem.Wait();
dataStream.Position = index;
b = dataStream.ReadByte();
accessSem.Release();
if (b == -1)
{
throw new IndexOutOfRangeException(index.ToString());
}
return (byte)b;
}
}
public void Dispose()
{
accessSem.Wait();
// Remove the event listener
MemoryLimitModifiedEvent -= CachedByteArray_memoryLimitModifiedEvent;
// Remove the allocation from the counter
memoryAllocationTotal -= dataStream.Length;
memoryAllocationRAM -= dataStream.Length;
// If the FileStream is opened, close and dispose it
dataStream?.Dispose();
// If the file is on disk, remove it
DeleteFile(Path.Combine(folderPath, dataHashCode));
// Release all resources obtained by the semaphore
accessSem.Dispose();
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RBG_Server
{
/// <summary>
/// Contains all communication data, from both the client's and server's perspective (should be able to switch between each mode as necessary)
/// </summary>
public class CommunicationHandler
{
/// <summary>
/// Image data is stored in memory in a dictionary collection. Each byte[] represents an image file, compressed according to its file type;
/// which helps save space in memory. Uses a CachedByteArray; essentially a normal byte array but automatically stored on disk if it is larger
/// than a set size.
/// This limit can be changed at any time
/// </summary>
public ConcurrentDictionary<string, CachedByteArray> ImageCollection { get; } = new();
}
}