- /api/v1/media and /api/v2/media (and GET/PUT /api/v1/media/:id):
images go through libvips (NetVips, its native build bundled):
autorotated, every kind of metadata dropped (EXIF, GPS, XMP, IPTC,
comments), capped at 4096 px, with a 640 px preview and a blurhash
(own encoder, the reference algorithm); animated GIFs are re-encoded;
video and audio are remuxed by ffmpeg with -map_metadata -1, never
re-encoded, and a video gets a still preview. Files get random names
under /var/lib/privapub/media, outside the web root deploys replace, and
are served at /media/files with nosniff and a sandbox CSP.
- media_ids on create and edit (four at most, the persona's own, each used
once); notes carry them as Document attachments with alt text, blurhash,
focalPoint and size; inbound attachments were already kept.
- avatar and header uploads in update_credentials, cropped to 400x400 and
1500x500, federated with Update{Person}.
- Remote media reaches clients only through /media/proxy/{hmac}/{url},
fetched by the guarded client (no SVG, 40 MB cap) and cached outside the
served root, trimmed to 5 GB; foreign avatars and headers use it too, so
a client never contacts another server.
- MediaJanitor deletes uploads left unattached for a day.
- nginx accepts 100 MB bodies on the upload endpoints only (applied on Max).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
92 lines
3.0 KiB
C#
92 lines
3.0 KiB
C#
namespace PrivaPub.Domain.Media
|
|
{
|
|
public static class Blurhash
|
|
{
|
|
const string Characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~";
|
|
|
|
public static string Encode(byte[] rgb, int width, int height, int componentsX = 4, int componentsY = 3)
|
|
{
|
|
var factors = new double[componentsY, componentsX, 3];
|
|
for (var j = 0; j < componentsY; j++)
|
|
for (var i = 0; i < componentsX; i++)
|
|
{
|
|
var normalisation = i == 0 && j == 0 ? 1 : 2;
|
|
double r = 0, g = 0, b = 0;
|
|
for (var y = 0; y < height; y++)
|
|
for (var x = 0; x < width; x++)
|
|
{
|
|
var basis = normalisation * Math.Cos(Math.PI * i * x / width) * Math.Cos(Math.PI * j * y / height);
|
|
var offset = (y * width + x) * 3;
|
|
r += basis * ToLinear(rgb[offset]);
|
|
g += basis * ToLinear(rgb[offset + 1]);
|
|
b += basis * ToLinear(rgb[offset + 2]);
|
|
}
|
|
var scale = 1.0 / (width * height);
|
|
factors[j, i, 0] = r * scale;
|
|
factors[j, i, 1] = g * scale;
|
|
factors[j, i, 2] = b * scale;
|
|
}
|
|
|
|
var hash = new System.Text.StringBuilder();
|
|
hash.Append(Base83((componentsX - 1) + (componentsY - 1) * 9, 1));
|
|
|
|
double maximum = 0;
|
|
for (var j = 0; j < componentsY; j++)
|
|
for (var i = 0; i < componentsX; i++)
|
|
if (i != 0 || j != 0)
|
|
for (var c = 0; c < 3; c++)
|
|
maximum = Math.Max(maximum, Math.Abs(factors[j, i, c]));
|
|
double maximumValue;
|
|
if (componentsX * componentsY > 1)
|
|
{
|
|
var quantised = (int)Math.Max(0, Math.Min(82, Math.Floor(maximum * 166 - 0.5)));
|
|
maximumValue = (quantised + 1) / 166.0;
|
|
hash.Append(Base83(quantised, 1));
|
|
}
|
|
else
|
|
{
|
|
maximumValue = 1;
|
|
hash.Append(Base83(0, 1));
|
|
}
|
|
|
|
var dc = (ToSrgb(factors[0, 0, 0]) << 16) + (ToSrgb(factors[0, 0, 1]) << 8) + ToSrgb(factors[0, 0, 2]);
|
|
hash.Append(Base83(dc, 4));
|
|
for (var j = 0; j < componentsY; j++)
|
|
for (var i = 0; i < componentsX; i++)
|
|
{
|
|
if (i == 0 && j == 0)
|
|
continue;
|
|
int Quantise(double value) => (int)Math.Max(0, Math.Min(18, Math.Floor(SignPow(value / maximumValue, 0.5) * 9 + 9.5)));
|
|
var ac = Quantise(factors[j, i, 0]) * 19 * 19 + Quantise(factors[j, i, 1]) * 19 + Quantise(factors[j, i, 2]);
|
|
hash.Append(Base83(ac, 2));
|
|
}
|
|
return hash.ToString();
|
|
}
|
|
|
|
static double ToLinear(byte value)
|
|
{
|
|
var v = value / 255.0;
|
|
return v <= 0.04045 ? v / 12.92 : Math.Pow((v + 0.055) / 1.055, 2.4);
|
|
}
|
|
|
|
static int ToSrgb(double value)
|
|
{
|
|
var v = Math.Max(0, Math.Min(1, value));
|
|
return v <= 0.0031308 ? (int)(v * 12.92 * 255 + 0.5) : (int)((1.055 * Math.Pow(v, 1 / 2.4) - 0.055) * 255 + 0.5);
|
|
}
|
|
|
|
static double SignPow(double value, double exponent) => Math.CopySign(Math.Pow(Math.Abs(value), exponent), value);
|
|
|
|
static string Base83(int value, int length)
|
|
{
|
|
var result = new char[length];
|
|
for (var i = 1; i <= length; i++)
|
|
{
|
|
var digit = value / (int)Math.Pow(83, length - i) % 83;
|
|
result[i - 1] = Characters[digit];
|
|
}
|
|
return new string(result);
|
|
}
|
|
}
|
|
}
|