summary refs log tree commit diff
path: root/extra/admin-api/Spacebar.Cdn.Worker/DiscordImageResizeService.cs
blob: 5a70bfd6e34a9bbeb56215692367c76589051911 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Serialization;
using ImageMagick;
using Microsoft.AspNetCore.Mvc;

namespace Spacebar.AdminApi.TestClient.Services.Services;

public class DiscordImageResizeParams {
    public uint? Size { get; set; }
    public DiscordImageResizeQuality Quality { get; set; } = DiscordImageResizeQuality.High;
    public bool KeepAspectRatio { get; set; } = true;
    public bool Passthrough { get; set; } = true;
    public bool Animated { get; set; } = true;

    public bool SpacebarAllowUpscale { get; set; } = false;
    public bool SpacebarOptimiseGif { get; set; } = true;
    public string Format { get; set; } = "webp";

    public string ToSerializedName() {
        return $"{(Animated ? "a_" : "")}{Size}px_{Quality.ToString()}_u.{SpacebarAllowUpscale}_o.{SpacebarOptimiseGif}.{Format}";
    }
}

public static class HttpRequestExtensions {
    extension(HttpRequest request) {
        public DiscordImageResizeParams GetResizeParams() {
            return new() {
                Size = request.Query.ContainsKey("size") && uint.TryParse(request.Query["size"], out uint size) ? size : null,
                Quality = request.Query.ContainsKey("quality") && Enum.TryParse<DiscordImageResizeQuality>(request.Query["quality"], true, out var quality)
                    ? quality
                    : DiscordImageResizeQuality.High,
                KeepAspectRatio = !request.Query.ContainsKey("keepAspectRatio") || !bool.TryParse(request.Query["keepAspectRatio"], out bool kar) || kar,
                Passthrough = request.Query.ContainsKey("passthrough") && bool.TryParse(request.Query["passthrough"], out bool pt) && pt,
                Animated = request.Query.ContainsKey("animated") && bool.TryParse(request.Query["animated"], out bool an) && an,
                SpacebarAllowUpscale = request.Query.ContainsKey("allowUpscale") && bool.TryParse(request.Query["allowUpscale"], out bool au) && au,
                SpacebarOptimiseGif = request.Query.ContainsKey("optimiseGif") && bool.TryParse(request.Query["optimiseGif"], out bool og) && og,
                Format = request.Query.ContainsKey("format") ? request.Query["format"]! : "webp",
            };
        }
    }
    
    extension (HttpResponse response) {
        public void SetSuccessCacheHeader() {
            int cacheDuration = (int)TimeSpan.FromHours(6).TotalSeconds;
            response.Headers.CacheControl = $"public, max-age={cacheDuration}, s-maxage={cacheDuration}, immutable";
        }

        public void SetFailureCacheHeader() {
            int cacheDuration = (int)TimeSpan.FromMinutes(5).TotalSeconds;
            response.Headers.CacheControl = $"public, max-age={cacheDuration}, s-maxage={cacheDuration}, immutable";
        }
    }
}

public enum DiscordImageResizeQuality {
    [EnumMember(Value = "low")] Low,
    [EnumMember(Value = "high")] High,
    [EnumMember(Value = "lossless")] Lossless
}

public class DiscordImageResizeService {
    //(PixelArtDetectionService pads) {
    [SuppressMessage("ReSharper", "AccessToModifiedClosure")]
    public async Task<MagickImageCollection> Apply(MagickImageCollection img, DiscordImageResizeParams resizeParams) {
        if (resizeParams.Passthrough) return img;
        if (img.First().Format == MagickFormat.Gif) {
            var t = new Thread(() => {
                Console.WriteLine("Coalescing gif for resize");
                img.Coalesce();
            });
            t.Start();
            while (t.IsAlive) await Task.Delay(100);
        }

        if (!resizeParams.Animated) {
            var oldImg = img;
            img = new MagickImageCollection([oldImg.First().Clone()]);
            oldImg.Dispose();
        }

        if (resizeParams.Size.HasValue) {
            if (resizeParams.Size > 4096)
                resizeParams.Size = 4096;

            if (img.Max(x => Math.Max(x.Height, x.Width)) > resizeParams.Size || resizeParams.SpacebarAllowUpscale) {
                Parallel.ForEach(img, new ParallelOptions() { MaxDegreeOfParallelism = 16 }, frame => {
                    if (resizeParams.Size.HasValue) {
                        uint oldWidth = frame.Width, oldHeight = frame.Height;
                        // pads.IsPixelArt(frame)
                        frame.Resize(resizeParams.Size.Value, resizeParams.Size.Value,
                            resizeParams.Quality == DiscordImageResizeQuality.Low ? FilterType.Point : FilterType.Gaussian);
                        Console.WriteLine($"Resized frame from {oldWidth}x{oldHeight} to {frame.Width}x{frame.Height}: {img.IndexOf(frame)+1}/{img.Count}");
                    }
                });
            }
        }

        if (img.First().Format == MagickFormat.Gif && resizeParams.SpacebarOptimiseGif) {
            var t = new Thread(() => {
                Console.WriteLine("Optimizing gif after resize");
                img.OptimizePlus();
            });
            t.Start();
            while (t.IsAlive) await Task.Delay(100);
        }

        return img;
    }
}