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:
parent
ea5e564352
commit
ecbc3978f1
@ -1,10 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace RBG_Server
|
||||
{
|
||||
@ -13,5 +18,194 @@ namespace RBG_Server
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
240
RBG_Server.Core/CachedByteArray.cs
Normal file
240
RBG_Server.Core/CachedByteArray.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
24
RBG_Server.Core/CommunicationHandler.cs
Normal file
24
RBG_Server.Core/CommunicationHandler.cs
Normal 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();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user