diff --git a/src/Imghoard.Tests/ClientTests.cs b/src/Imghoard.Tests/ClientTests.cs
deleted file mode 100644
index 40d896a..0000000
--- a/src/Imghoard.Tests/ClientTests.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using Xunit;
-
-namespace Imghoard.Tests
-{
- public class ClientTests
- {
- [Fact]
- public void ConstructClientTest()
- {
- var i = new ImghoardClient(ImghoardClient.Config.Default());
-
- Assert.NotNull(i);
- }
- }
-}
diff --git a/src/Imghoard.Tests/Formatters/Base64FileFormatterTest.cs b/src/Imghoard.Tests/Formatters/Base64FileFormatterTest.cs
new file mode 100644
index 0000000..713307e
--- /dev/null
+++ b/src/Imghoard.Tests/Formatters/Base64FileFormatterTest.cs
@@ -0,0 +1,30 @@
+using System.Text;
+using System.Text.Json;
+using Imghoard.Models;
+using Xunit;
+
+namespace Imghoard.Tests.Formatters
+{
+ public class Base64FileFormatterTest
+ {
+ [Fact]
+ public void FormatTest()
+ {
+ using var pixel = typeof(Base64FileFormatterTest).Assembly
+ .GetManifestResourceStream("Imghoard.Tests.Resources.BlackPixel.png");
+
+ var bytes = JsonSerializer.SerializeToUtf8Bytes(
+ new PostImage
+ {
+ Stream = pixel
+ },
+ ImghoardClient.JsonSerializerOptions
+ );
+
+ Assert.Equal(
+ @"{""Data"":""data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==""}",
+ Encoding.UTF8.GetString(bytes)
+ );
+ }
+ }
+}
diff --git a/src/Imghoard.Tests/Imghoard.Tests.csproj b/src/Imghoard.Tests/Imghoard.Tests.csproj
index 787828d..e3f4c60 100644
--- a/src/Imghoard.Tests/Imghoard.Tests.csproj
+++ b/src/Imghoard.Tests/Imghoard.Tests.csproj
@@ -1,12 +1,18 @@
- WinExe
netcoreapp3.0
-
-
+ false
+
+
+
+
+
+
+
+
diff --git a/src/Imghoard.Tests/Program.cs b/src/Imghoard.Tests/Program.cs
deleted file mode 100644
index eabdc22..0000000
--- a/src/Imghoard.Tests/Program.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace Imghoard.Tests
-{
- class Program
- {
- static async Task RunAsync()
- {
- var cli = new ImghoardClient();
- }
- }
-}
diff --git a/src/Imghoard.Tests/Resources/BlackPixel.png b/src/Imghoard.Tests/Resources/BlackPixel.png
new file mode 100644
index 0000000..17f1d2e
Binary files /dev/null and b/src/Imghoard.Tests/Resources/BlackPixel.png differ
diff --git a/src/Imghoard/Formatters/Base64FileFormatter.cs b/src/Imghoard/Formatters/Base64FileFormatter.cs
new file mode 100644
index 0000000..274db90
--- /dev/null
+++ b/src/Imghoard/Formatters/Base64FileFormatter.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Buffers;
+using System.Buffers.Text;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Miki.Utils.Imaging.Headers;
+using Miki.Utils.Imaging.Headers.Models;
+
+namespace Imghoard.Formatters
+{
+ internal sealed class Base64FileFormatter : JsonConverter
+ {
+ public override Stream Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ throw new NotSupportedException("Reading a Base64 stream is not supported.");
+ }
+
+ public override void Write(Utf8JsonWriter writer, Stream value, JsonSerializerOptions options)
+ {
+ byte[] data;
+
+ if (value is MemoryStream memoryStream)
+ {
+ data = memoryStream.ToArray();
+ }
+ else
+ {
+ // TODO: Check if this can be improved.
+ using var ms = new MemoryStream();
+ value.Position = 0;
+ value.CopyTo(ms);
+ data = ms.ToArray();
+ }
+
+ var (supported, name) = IsSupported(data);
+
+ if (!supported)
+ {
+ throw new NotSupportedException($"File extension {name} is not supported. Supported extensions: png, jpeg, gif.");
+ }
+
+ var prefix = $"data:image/{name};base64,";
+
+ var prefixLength = Encoding.UTF8.GetByteCount(prefix);
+ var encodingLength = Base64.GetMaxEncodedToUtf8Length(data.Length);
+ var length = prefixLength + encodingLength;
+
+ var destination = ArrayPool.Shared.Rent(length);
+ var span = destination.AsSpan();
+
+ try
+ {
+ Encoding.UTF8.GetBytes(prefix, span);
+
+ var status = Base64.EncodeToUtf8(data, span.Slice(prefixLength), out var consumed, out var written);
+ Debug.Assert(status == OperationStatus.Done);
+ Debug.Assert(consumed == data.Length);
+
+ writer.WriteStringValue(span.Slice(0, prefixLength + written));
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(destination);
+ }
+ }
+
+ private static (bool supported, string name) IsSupported(byte[] image)
+ {
+ if (ImageHeaders.Validate(image, ImageType.Png))
+ {
+ return (true, "png");
+ }
+
+ if (ImageHeaders.Validate(image, ImageType.Jpeg))
+ {
+ return (true, "jpeg");
+ }
+
+ if (ImageHeaders.Validate(image, ImageType.Gif89a) || ImageHeaders.Validate(image, ImageType.Gif87a))
+ {
+ return (true, "gif");
+ }
+
+ return (false, null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Imghoard/Imghoard.csproj b/src/Imghoard/Imghoard.csproj
index c899b6f..fafb819 100644
--- a/src/Imghoard/Imghoard.csproj
+++ b/src/Imghoard/Imghoard.csproj
@@ -5,10 +5,9 @@
-
-
+
diff --git a/src/Imghoard/ImghoardClient.cs b/src/Imghoard/ImghoardClient.cs
index a5c10e4..c9245c8 100644
--- a/src/Imghoard/ImghoardClient.cs
+++ b/src/Imghoard/ImghoardClient.cs
@@ -1,157 +1,153 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
using System.Text;
+using System.Text.Json;
using System.Threading.Tasks;
using Imghoard.Models;
-using Miki.Net.Http;
-using Miki.Utils.Imaging.Headers;
-using Miki.Utils.Imaging.Headers.Models;
-using Newtonsoft.Json;
namespace Imghoard
{
- public class ImghoardClient
+ public class ImghoardClient : IDisposable
{
- private HttpClient apiClient;
- private Config config;
+ internal static readonly MediaTypeHeaderValue JsonHeaderValue = new MediaTypeHeaderValue("application/json");
+ internal static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions
+ {
+ IgnoreNullValues = true
+ };
+
+ private readonly HttpClient apiClient;
- public ImghoardClient(ImghoardClient.Config config) {
- this.config = config;
+ public ImghoardClient(Config config)
+ {
+ apiClient = new HttpClient
+ {
+ BaseAddress = config.Endpoint,
+ DefaultRequestHeaders =
+ {
+ {"x-miki-tenancy", config.Tenancy}
+ }
+ };
}
- public ImghoardClient(Uri Endpoint)
+ public ImghoardClient(Uri endpoint)
+ : this (new Config { Endpoint = endpoint })
{
- apiClient = new HttpClientFactory()
- .HasBaseUri(config.Endpoint)
- .CreateNew();
- apiClient.AddHeader("x-miki-tenancy", config.Tenancy);
}
- public async Task> GetImagesAsync(params string[] Tags)
+ public ImghoardClient()
+ : this(Config.Default())
{
- StringBuilder query = null;
- if (Tags.Any())
+ }
+
+ private static void ValidateResponse(HttpResponseMessage response)
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new Exception(response.ReasonPhrase);
+ }
+ }
+
+ public async Task> GetImagesAsync(params string[] tags)
+ {
+ string url;
+
+ if (tags.Length == 0)
{
- query = new StringBuilder();
- foreach (string tag in Tags)
+ url = "/images";
+ }
+ else
+ {
+ var query = new StringBuilder();
+
+ for (var i = 0; i < tags.Length; i++)
{
- if (tag.StartsWith("-"))
+ var tag = tags[i];
+
+ if (string.IsNullOrEmpty(tag))
+ {
+ throw new ArgumentException("Cannot provide an empty tag name.", nameof(tags));
+ }
+
+ if (tag[0] == '-')
{
query.Append(tag);
- continue;
+ }
+ else if (i > 0)
+ {
+ query.Append($"+{tag}");
}
else
{
- if (tag != Tags.FirstOrDefault())
- {
- query.Append($"+{tag}");
- continue;
- }
-
query.Append(tag);
}
}
- }
-
- StringBuilder urlBuilder = new StringBuilder("/images");
- if(query != null)
- {
- urlBuilder.Append($"?tags={query}");
+ url = $"/images?tags={query}";
}
- var response = await apiClient.GetAsync(urlBuilder.ToString());
- if (response.Success)
- {
- return JsonConvert.DeserializeObject>(response.Body);
- }
+ var response = await apiClient.GetAsync(url);
+ ValidateResponse(response);
- // TODO(velddev): Add better error handling.
- throw new Exception(response.HttpResponseMessage.ReasonPhrase);
- }
-
- public async Task GetImageAsync(ulong Id)
- {
- var response = await apiClient.GetAsync("/images?id={Id}");
- if (response.Success)
- {
- return JsonConvert.DeserializeObject(response.Body);
- }
- throw new Exception(response.HttpResponseMessage.ReasonPhrase);
+ var stream = await response.Content.ReadAsStreamAsync();
+
+ return await JsonSerializer.DeserializeAsync(stream);
}
- public async Task PostImageAsync(Stream image, params string[] Tags)
+ public async Task GetImageAsync(ulong id)
{
- byte[] bytes;
+ var response = await apiClient.GetAsync($"/images/{id}");
+ ValidateResponse(response);
- using (var mStream = new MemoryStream())
- {
- await image.CopyToAsync(mStream);
- bytes = mStream.ToArray();
- }
+ var stream = await response.Content.ReadAsStreamAsync();
- var imgd = IsSupported(bytes);
+ return await JsonSerializer.DeserializeAsync(stream);
+ }
- if (!imgd.Item1)
- {
- throw new NotSupportedException(
- "You have given an incorrect image format, currently supported formats are: png, jpeg, gif");
- }
+ public async Task PostImageAsync(Stream image, params string[] tags)
+ {
+ if (image == null) throw new ArgumentNullException(nameof(image));
- var b64 = Convert.ToBase64String(bytes);
- image.Position = 0;
+ // TODO: Check if we can stream this.
- var body = JsonConvert.SerializeObject(
+ var body = JsonSerializer.SerializeToUtf8Bytes(
new PostImage
{
- Data = $"data:image/{imgd.Item2};base64,{b64}",
- Tags = Tags
+ Stream = image,
+ Tags = tags
},
- new JsonSerializerSettings
- {
- DefaultValueHandling = DefaultValueHandling.Ignore,
- NullValueHandling = NullValueHandling.Ignore
- }
+ JsonSerializerOptions
);
- var response = await apiClient.PostAsync("/images", body);
+ var content = new ByteArrayContent(body);
+ content.Headers.ContentType = JsonHeaderValue;
- if (response.Success)
- {
- return JsonConvert.DeserializeObject(response.Body);
- }
- return null;
- }
+ var response = await apiClient.PostAsync("/images", content);
+ ValidateResponse(response);
- (bool, string) IsSupported(byte[] image)
- {
- if(ImageHeaders.Validate(image, ImageType.Png))
- {
- return (true, "png");
- }
- if(ImageHeaders.Validate(image, ImageType.Jpeg))
- {
- return (true, "jpeg");
- }
- if(ImageHeaders.Validate(image, ImageType.Gif89a)
- || ImageHeaders.Validate(image, ImageType.Gif87a))
- {
- return (true, "gif");
- }
- return (false, null);
+ var stream = await response.Content.ReadAsStreamAsync();
+ var result = await JsonSerializer.DeserializeAsync(stream);
+
+ return new Uri(result.File);
}
public class Config
{
public string Tenancy { get; set; } = "prod";
- public string Endpoint { get; set; } = "https://imgh.miki.ai/";
+
+ public Uri Endpoint { get; set; } = new Uri("https://imgh.miki.ai/");
public static Config Default()
{
return new Config();
}
}
+
+ public void Dispose()
+ {
+ apiClient.Dispose();
+ }
}
}
diff --git a/src/Imghoard/Models/Image.cs b/src/Imghoard/Models/Image.cs
index 6289198..735cc10 100644
--- a/src/Imghoard/Models/Image.cs
+++ b/src/Imghoard/Models/Image.cs
@@ -1,14 +1,16 @@
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
namespace Imghoard.Models
{
public class Image
{
- [JsonProperty("ID")]
+ [JsonPropertyName("id")]
public ulong Id { get; internal set; }
- [JsonProperty("Tags")]
+
+ [JsonPropertyName("tags")]
public string[] Tags { get; internal set; }
- [JsonProperty("URL")]
+
+ [JsonPropertyName("url")]
public string Url { get; internal set; }
}
}
diff --git a/src/Imghoard/Models/ImagePostResult.cs b/src/Imghoard/Models/ImagePostResult.cs
new file mode 100644
index 0000000..2ab1382
--- /dev/null
+++ b/src/Imghoard/Models/ImagePostResult.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace Imghoard.Models
+{
+ public class ImagePostResult
+ {
+ [JsonPropertyName("file")]
+ public string File { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Imghoard/Models/PostImage.cs b/src/Imghoard/Models/PostImage.cs
index 61c0f84..7de58be 100644
--- a/src/Imghoard/Models/PostImage.cs
+++ b/src/Imghoard/Models/PostImage.cs
@@ -1,12 +1,16 @@
-using Newtonsoft.Json;
+using System.IO;
+using System.Text.Json.Serialization;
+using Imghoard.Formatters;
namespace Imghoard.Models
{
- internal class PostImage
+ internal struct PostImage
{
- [JsonProperty("Tags")]
+ [JsonPropertyName("Tags")]
public string[] Tags { get; internal set; }
- [JsonProperty("Data")]
- public string Data { get; internal set; }
+
+ [JsonPropertyName("Data")]
+ [JsonConverter(typeof(Base64FileFormatter))]
+ public Stream Stream { get; internal set; }
}
}
diff --git a/src/Imghoard/Properties/AssemblyInfo.cs b/src/Imghoard/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..36f3362
--- /dev/null
+++ b/src/Imghoard/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly:InternalsVisibleTo("Imghoard.Tests")]
\ No newline at end of file