big updat:
All checks were successful
Android Build / publish (push) Successful in 57s
Linux Build / publish (push) Successful in 53s

- update dependencies
- add webp support and webp conversion for profile images
This commit is contained in:
olcxja 2026-07-02 22:40:46 +02:00
commit 95aaaa69ea
244 changed files with 121382 additions and 86 deletions

View file

@ -33,9 +33,12 @@
"username.conditions.allowed": "cat name can only include {all}",
"password.not.hashed.properly": "meow word is not hashed properly",
"invalid.username.or.password": "invalid cat name or meow word",
"error.account.not.exist": "Meowww???",
"error.storage.limit.exceeded": "Meow too big!",
"error.username.taken": "Meeeeeow",
"error.account.not.exist": "Meowccount Does Not Exist",
"error.storage.limit.exceeded": "Storage Limit Exceeded, purr",
"error.username.taken": "Meowname is Taken",
"error.network.error": "Network error, check connection or file size, meow.",
"error.body.too.large": "File is too large, meow! (max 10MB)",
"error.unknown.error": "Unknown error, purr",
"account.not.exist": "cat with this name doesn't exist",
"invalid.nonce": "invalid nonce. try again",
"username.changed": "cat name changed successfully",
@ -88,7 +91,9 @@
"desc.invite.dm.sent": "you invited {0} ({1}) to meowchat",
"desc.invite.group.received": "{0} ({1}) invited you to a clowder",
"desc.invite.group.sent": "you invited {0} ({1}) to a clowder",
"action.fetching.invites.sent": "fetching sent invites...",
"action.invite.sending": "Sending mewvite...",
"action.processing": "Purrcessing...",
"action.fetching.invites.sent": "Fetching sent mewvites...",
"action.fetching.invites.recv": "fetching received invites...",
"action.dm.fetch": "fetching direct meowchats...",
"action.auth": "authenticating...",

View file

@ -36,6 +36,9 @@
"error.account.not.exist": "Account Does Not Exist",
"error.storage.limit.exceeded": "Storage Limit Exceeded",
"error.username.taken": "Username is Taken",
"error.network.error": "Network error, check connection or file size.",
"error.body.too.large": "File is too large (max 10MB)",
"error.unknown.error": "Unknown error",
"account.not.exist": "Account with this name doesn't exist",
"invalid.nonce": "Invalid nonce. Try again",
"username.changed": "Username changed successfully",
@ -88,6 +91,8 @@
"desc.invite.dm.sent": "You invited {0} ({1}) to chat",
"desc.invite.group.received": "{0} ({1}) invited you to a group",
"desc.invite.group.sent": "You invited {0} ({1}) to a group",
"action.invite.sending": "Sending invite...",
"action.processing": "Processing...",
"action.fetching.invites.sent": "Fetching sent invites...",
"action.fetching.invites.recv": "Fetching received invites...",
"action.dm.fetch": "Fetching dms...",
@ -169,9 +174,9 @@
"action.bio.saving": "Saving bio...",
"action.account.saving": "Saving account...",
"action.account.deleting": "Deleting account...",
"success:profile.updated": "Profile updated successfully",
"success:avatar.updated": "Avatar updated successfully",
"success:banner.updated": "Banner updated successfully",
"profile.updated": "Profile updated successfully",
"avatar.updated": "Avatar updated successfully",
"banner.updated": "Banner updated successfully",
"action.save": "Save",
"action.delete.account": "Delete account",
"account.deleted.scheduled": "Account scheduled for deletion. It will be permanently deleted after a month of inactivity. Logging in during this time will cancel the deletion process."

1
webroot/gifuct-js.min.js vendored Normal file
View file

@ -0,0 +1 @@
Couldn't find the requested file /dist/gifuct-js.min.js in gifuct-js.

578
webroot/gifuct.js Normal file
View file

@ -0,0 +1,578 @@
"use strict";
var gifuct = (() => {
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
try {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
} catch (e) {
throw mod = 0, e;
}
};
// ../node_modules/js-binary-schema-parser/lib/index.js
var require_lib = __commonJS({
"../node_modules/js-binary-schema-parser/lib/index.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.loop = exports.conditional = exports.parse = void 0;
var parse = function parse2(stream, schema) {
var result = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
var parent = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : result;
if (Array.isArray(schema)) {
schema.forEach(function(partSchema) {
return parse2(stream, partSchema, result, parent);
});
} else if (typeof schema === "function") {
schema(stream, result, parent, parse2);
} else {
var key = Object.keys(schema)[0];
if (Array.isArray(schema[key])) {
parent[key] = {};
parse2(stream, schema[key], result, parent[key]);
} else {
parent[key] = schema[key](stream, result, parent, parse2);
}
}
return result;
};
exports.parse = parse;
var conditional = function conditional2(schema, conditionFunc) {
return function(stream, result, parent, parse2) {
if (conditionFunc(stream, result, parent)) {
parse2(stream, schema, result, parent);
}
};
};
exports.conditional = conditional;
var loop = function loop2(schema, continueFunc) {
return function(stream, result, parent, parse2) {
var arr = [];
var lastStreamPos = stream.pos;
while (continueFunc(stream, result, parent)) {
var newParent = {};
parse2(stream, schema, result, newParent);
if (stream.pos === lastStreamPos) {
break;
}
lastStreamPos = stream.pos;
arr.push(newParent);
}
return arr;
};
};
exports.loop = loop;
}
});
// ../node_modules/js-binary-schema-parser/lib/parsers/uint8.js
var require_uint8 = __commonJS({
"../node_modules/js-binary-schema-parser/lib/parsers/uint8.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.readBits = exports.readArray = exports.readUnsigned = exports.readString = exports.peekBytes = exports.readBytes = exports.peekByte = exports.readByte = exports.buildStream = void 0;
var buildStream = function buildStream2(uint8Data) {
return {
data: uint8Data,
pos: 0
};
};
exports.buildStream = buildStream;
var readByte = function readByte2() {
return function(stream) {
return stream.data[stream.pos++];
};
};
exports.readByte = readByte;
var peekByte = function peekByte2() {
var offset = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : 0;
return function(stream) {
return stream.data[stream.pos + offset];
};
};
exports.peekByte = peekByte;
var readBytes = function readBytes2(length) {
return function(stream) {
return stream.data.subarray(stream.pos, stream.pos += length);
};
};
exports.readBytes = readBytes;
var peekBytes = function peekBytes2(length) {
return function(stream) {
return stream.data.subarray(stream.pos, stream.pos + length);
};
};
exports.peekBytes = peekBytes;
var readString = function readString2(length) {
return function(stream) {
return Array.from(readBytes(length)(stream)).map(function(value) {
return String.fromCharCode(value);
}).join("");
};
};
exports.readString = readString;
var readUnsigned = function readUnsigned2(littleEndian) {
return function(stream) {
var bytes = readBytes(2)(stream);
return littleEndian ? (bytes[1] << 8) + bytes[0] : (bytes[0] << 8) + bytes[1];
};
};
exports.readUnsigned = readUnsigned;
var readArray = function readArray2(byteSize, totalOrFunc) {
return function(stream, result, parent) {
var total = typeof totalOrFunc === "function" ? totalOrFunc(stream, result, parent) : totalOrFunc;
var parser = readBytes(byteSize);
var arr = new Array(total);
for (var i = 0; i < total; i++) {
arr[i] = parser(stream);
}
return arr;
};
};
exports.readArray = readArray;
var subBitsTotal = function subBitsTotal2(bits, startIndex, length) {
var result = 0;
for (var i = 0; i < length; i++) {
result += bits[startIndex + i] && Math.pow(2, length - i - 1);
}
return result;
};
var readBits = function readBits2(schema) {
return function(stream) {
var _byte = readByte()(stream);
var bits = new Array(8);
for (var i = 0; i < 8; i++) {
bits[7 - i] = !!(_byte & 1 << i);
}
return Object.keys(schema).reduce(function(res, key) {
var def = schema[key];
if (def.length) {
res[key] = subBitsTotal(bits, def.index, def.length);
} else {
res[key] = bits[def.index];
}
return res;
}, {});
};
};
exports.readBits = readBits;
}
});
// ../node_modules/js-binary-schema-parser/lib/schemas/gif.js
var require_gif = __commonJS({
"../node_modules/js-binary-schema-parser/lib/schemas/gif.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _ = require_lib();
var _uint = require_uint8();
var subBlocksSchema = {
blocks: function blocks(stream) {
var terminator = 0;
var chunks = [];
var streamSize = stream.data.length;
var total = 0;
for (var size = (0, _uint.readByte)()(stream); size !== terminator; size = (0, _uint.readByte)()(stream)) {
if (!size) break;
if (stream.pos + size >= streamSize) {
var availableSize = streamSize - stream.pos;
chunks.push((0, _uint.readBytes)(availableSize)(stream));
total += availableSize;
break;
}
chunks.push((0, _uint.readBytes)(size)(stream));
total += size;
}
var result = new Uint8Array(total);
var offset = 0;
for (var i = 0; i < chunks.length; i++) {
result.set(chunks[i], offset);
offset += chunks[i].length;
}
return result;
}
};
var gceSchema = (0, _.conditional)({
gce: [{
codes: (0, _uint.readBytes)(2)
}, {
byteSize: (0, _uint.readByte)()
}, {
extras: (0, _uint.readBits)({
future: {
index: 0,
length: 3
},
disposal: {
index: 3,
length: 3
},
userInput: {
index: 6
},
transparentColorGiven: {
index: 7
}
})
}, {
delay: (0, _uint.readUnsigned)(true)
}, {
transparentColorIndex: (0, _uint.readByte)()
}, {
terminator: (0, _uint.readByte)()
}]
}, function(stream) {
var codes = (0, _uint.peekBytes)(2)(stream);
return codes[0] === 33 && codes[1] === 249;
});
var imageSchema = (0, _.conditional)({
image: [{
code: (0, _uint.readByte)()
}, {
descriptor: [{
left: (0, _uint.readUnsigned)(true)
}, {
top: (0, _uint.readUnsigned)(true)
}, {
width: (0, _uint.readUnsigned)(true)
}, {
height: (0, _uint.readUnsigned)(true)
}, {
lct: (0, _uint.readBits)({
exists: {
index: 0
},
interlaced: {
index: 1
},
sort: {
index: 2
},
future: {
index: 3,
length: 2
},
size: {
index: 5,
length: 3
}
})
}]
}, (0, _.conditional)({
lct: (0, _uint.readArray)(3, function(stream, result, parent) {
return Math.pow(2, parent.descriptor.lct.size + 1);
})
}, function(stream, result, parent) {
return parent.descriptor.lct.exists;
}), {
data: [{
minCodeSize: (0, _uint.readByte)()
}, subBlocksSchema]
}]
}, function(stream) {
return (0, _uint.peekByte)()(stream) === 44;
});
var textSchema = (0, _.conditional)({
text: [{
codes: (0, _uint.readBytes)(2)
}, {
blockSize: (0, _uint.readByte)()
}, {
preData: function preData(stream, result, parent) {
return (0, _uint.readBytes)(parent.text.blockSize)(stream);
}
}, subBlocksSchema]
}, function(stream) {
var codes = (0, _uint.peekBytes)(2)(stream);
return codes[0] === 33 && codes[1] === 1;
});
var applicationSchema = (0, _.conditional)({
application: [{
codes: (0, _uint.readBytes)(2)
}, {
blockSize: (0, _uint.readByte)()
}, {
id: function id(stream, result, parent) {
return (0, _uint.readString)(parent.blockSize)(stream);
}
}, subBlocksSchema]
}, function(stream) {
var codes = (0, _uint.peekBytes)(2)(stream);
return codes[0] === 33 && codes[1] === 255;
});
var commentSchema = (0, _.conditional)({
comment: [{
codes: (0, _uint.readBytes)(2)
}, subBlocksSchema]
}, function(stream) {
var codes = (0, _uint.peekBytes)(2)(stream);
return codes[0] === 33 && codes[1] === 254;
});
var schema = [
{
header: [{
signature: (0, _uint.readString)(3)
}, {
version: (0, _uint.readString)(3)
}]
},
{
lsd: [{
width: (0, _uint.readUnsigned)(true)
}, {
height: (0, _uint.readUnsigned)(true)
}, {
gct: (0, _uint.readBits)({
exists: {
index: 0
},
resolution: {
index: 1,
length: 3
},
sort: {
index: 4
},
size: {
index: 5,
length: 3
}
})
}, {
backgroundColorIndex: (0, _uint.readByte)()
}, {
pixelAspectRatio: (0, _uint.readByte)()
}]
},
(0, _.conditional)({
gct: (0, _uint.readArray)(3, function(stream, result) {
return Math.pow(2, result.lsd.gct.size + 1);
})
}, function(stream, result) {
return result.lsd.gct.exists;
}),
// content frames
{
frames: (0, _.loop)([gceSchema, applicationSchema, commentSchema, imageSchema, textSchema], function(stream) {
var nextCode = (0, _uint.peekByte)()(stream);
return nextCode === 33 || nextCode === 44;
})
}
];
var _default = schema;
exports["default"] = _default;
}
});
// ../node_modules/gifuct-js/lib/deinterlace.js
var require_deinterlace = __commonJS({
"../node_modules/gifuct-js/lib/deinterlace.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.deinterlace = void 0;
var deinterlace = function deinterlace2(pixels, width) {
var newPixels = new Array(pixels.length);
var rows = pixels.length / width;
var cpRow = function cpRow2(toRow2, fromRow2) {
var fromPixels = pixels.slice(fromRow2 * width, (fromRow2 + 1) * width);
newPixels.splice.apply(newPixels, [toRow2 * width, width].concat(fromPixels));
};
var offsets = [0, 4, 2, 1];
var steps = [8, 8, 4, 2];
var fromRow = 0;
for (var pass = 0; pass < 4; pass++) {
for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {
cpRow(toRow, fromRow);
fromRow++;
}
}
return newPixels;
};
exports.deinterlace = deinterlace;
}
});
// ../node_modules/gifuct-js/lib/lzw.js
var require_lzw = __commonJS({
"../node_modules/gifuct-js/lib/lzw.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.lzw = void 0;
var lzw = function lzw2(minCodeSize, data, pixelCount) {
var MAX_STACK_SIZE = 4096;
var nullCode = -1;
var npix = pixelCount;
var available, clear, code_mask, code_size, end_of_information, in_code, old_code, bits, code, i, datum, data_size, first, top, bi, pi;
var dstPixels = new Array(pixelCount);
var prefix = new Array(MAX_STACK_SIZE);
var suffix = new Array(MAX_STACK_SIZE);
var pixelStack = new Array(MAX_STACK_SIZE + 1);
data_size = minCodeSize;
clear = 1 << data_size;
end_of_information = clear + 1;
available = clear + 2;
old_code = nullCode;
code_size = data_size + 1;
code_mask = (1 << code_size) - 1;
for (code = 0; code < clear; code++) {
prefix[code] = 0;
suffix[code] = code;
}
var datum, bits, count, first, top, pi, bi;
datum = bits = count = first = top = pi = bi = 0;
for (i = 0; i < npix; ) {
if (top === 0) {
if (bits < code_size) {
datum += data[bi] << bits;
bits += 8;
bi++;
continue;
}
code = datum & code_mask;
datum >>= code_size;
bits -= code_size;
if (code > available || code == end_of_information) {
break;
}
if (code == clear) {
code_size = data_size + 1;
code_mask = (1 << code_size) - 1;
available = clear + 2;
old_code = nullCode;
continue;
}
if (old_code == nullCode) {
pixelStack[top++] = suffix[code];
old_code = code;
first = code;
continue;
}
in_code = code;
if (code == available) {
pixelStack[top++] = first;
code = old_code;
}
while (code > clear) {
pixelStack[top++] = suffix[code];
code = prefix[code];
}
first = suffix[code] & 255;
pixelStack[top++] = first;
if (available < MAX_STACK_SIZE) {
prefix[available] = old_code;
suffix[available] = first;
available++;
if ((available & code_mask) === 0 && available < MAX_STACK_SIZE) {
code_size++;
code_mask += available;
}
}
old_code = in_code;
}
top--;
dstPixels[pi++] = pixelStack[top];
i++;
}
for (i = pi; i < npix; i++) {
dstPixels[i] = 0;
}
return dstPixels;
};
exports.lzw = lzw;
}
});
// ../node_modules/gifuct-js/lib/index.js
var require_index = __commonJS({
"../node_modules/gifuct-js/lib/index.js"(exports) {
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.decompressFrames = exports.decompressFrame = exports.parseGIF = void 0;
var _gif = _interopRequireDefault(require_gif());
var _jsBinarySchemaParser = require_lib();
var _uint = require_uint8();
var _deinterlace = require_deinterlace();
var _lzw = require_lzw();
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { "default": obj };
}
var parseGIF = function parseGIF2(arrayBuffer) {
var byteData = new Uint8Array(arrayBuffer);
return (0, _jsBinarySchemaParser.parse)((0, _uint.buildStream)(byteData), _gif["default"]);
};
exports.parseGIF = parseGIF;
var generatePatch = function generatePatch2(image) {
var totalPixels = image.pixels.length;
var patchData = new Uint8ClampedArray(totalPixels * 4);
for (var i = 0; i < totalPixels; i++) {
var pos = i * 4;
var colorIndex = image.pixels[i];
var color = image.colorTable[colorIndex] || [0, 0, 0];
patchData[pos] = color[0];
patchData[pos + 1] = color[1];
patchData[pos + 2] = color[2];
patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0;
}
return patchData;
};
var decompressFrame = function decompressFrame2(frame, gct, buildImagePatch) {
if (!frame.image) {
console.warn("gif frame does not have associated image.");
return;
}
var image = frame.image;
var totalPixels = image.descriptor.width * image.descriptor.height;
var pixels = (0, _lzw.lzw)(image.data.minCodeSize, image.data.blocks, totalPixels);
if (image.descriptor.lct.interlaced) {
pixels = (0, _deinterlace.deinterlace)(pixels, image.descriptor.width);
}
var resultImage = {
pixels,
dims: {
top: frame.image.descriptor.top,
left: frame.image.descriptor.left,
width: frame.image.descriptor.width,
height: frame.image.descriptor.height
}
};
if (image.descriptor.lct && image.descriptor.lct.exists) {
resultImage.colorTable = image.lct;
} else {
resultImage.colorTable = gct;
}
if (frame.gce) {
resultImage.delay = (frame.gce.delay || 10) * 10;
resultImage.disposalType = frame.gce.extras.disposal;
if (frame.gce.extras.transparentColorGiven) {
resultImage.transparentIndex = frame.gce.transparentColorIndex;
}
}
if (buildImagePatch) {
resultImage.patch = generatePatch(resultImage);
}
return resultImage;
};
exports.decompressFrame = decompressFrame;
var decompressFrames = function decompressFrames2(parsedGif, buildImagePatches) {
return parsedGif.frames.filter(function(f) {
return f.image;
}).map(function(f) {
return decompressFrame(f, parsedGif.gct, buildImagePatches);
});
};
exports.decompressFrames = decompressFrames;
}
});
return require_index();
})();

View file

@ -203,5 +203,6 @@
}
</script>
<script src="gifuct.js"></script>
<script src="main.js"></script>
<script src="userscript.js"></script>

View file

@ -268,10 +268,9 @@
</main>
</body>
</html>
<script src="../main.js"></script>
<script>
const container = document.getElementById('auth-container');
const authContainer = document.getElementById('auth-container');
const toRegister = document.getElementById('to-register');
@ -307,15 +306,15 @@
toRegister.addEventListener('click', () => {
container.className = 'auth-container show-register';
authContainer.className = 'auth-container show-register';
});
toLogin.addEventListener('click', () => {
container.className = 'auth-container';
authContainer.className = 'auth-container';
});
backToReg.addEventListener('click', () => {
container.className = 'auth-container show-register';
authContainer.className = 'auth-container show-register';
});
@ -349,7 +348,7 @@
} catch (error) {
console.log(error);
showBlahNotification("error:something.wrong.mayb.pass");
container.className = 'auth-container';
authContainer.className = 'auth-container';
}
});
@ -363,17 +362,17 @@
if (!registerUsername.value || registerUsername.value.trim() === '') {
showBlahNotification("error:username.cant.empty");
container.className = 'auth-container show-register';
authContainer.className = 'auth-container show-register';
return;
}
if (!registerPassword.value || registerPassword.value.trim() === '') {
showBlahNotification("error:password.cant.empty");
container.className = 'auth-container show-register';
authContainer.className = 'auth-container show-register';
return;
}
if (registerPassword.value != registerPasswordConfirm.value) {
showBlahNotification("error:passwords.not.match");
container.className = 'auth-container show-register';
authContainer.className = 'auth-container show-register';
return;
}
@ -402,20 +401,20 @@
}
const imageObjectURL = URL.createObjectURL(imageBlob);
document.getElementById('captcha-image').src = imageObjectURL;
container.className = 'auth-container show-captcha';
authContainer.className = 'auth-container show-captcha';
} catch (error) {
console.log(error);
if (res.length > 64 || res.length <= 1) {
throw new Error();
}
showBlahNotification(res);
container.className = 'auth-container show-register';
authContainer.className = 'auth-container show-register';
}
} catch (error) {
console.log(error);
showBlahNotification("error:something.wrong");
container.className = 'auth-container show-register';
authContainer.className = 'auth-container show-register';
}
});
@ -451,12 +450,12 @@
} else {
showBlahNotification(res);
container.className = 'auth-container show-register';
authContainer.className = 'auth-container show-register';
}
}
} catch (error) {
showBlahNotification("error:something.wrong");
container.className = 'auth-container show-register';
authContainer.className = 'auth-container show-register';
}
});
@ -507,4 +506,5 @@
}
</script>
<script src="../main.js"></script>
<script src="../userscript.js"></script>

View file

@ -449,6 +449,29 @@ async function encryptString(plainText, passphrase) {
return uint8ToBase64(combined);
}
async function encryptBytesWithNonce(dataUint8, key, nonce) {
const encoder = new TextEncoder();
const passphrase = nonce + key;
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
const importedKey = await crypto.subtle.importKey(
'raw', pwHash, {name: 'AES-CBC'}, false, ['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(16));
const encrypted = await crypto.subtle.encrypt(
{name: 'AES-CBC', iv: iv},
importedKey,
dataUint8
);
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return combined;
}
async function decryptString(base64Text, passphrase) {
const encoder = new TextEncoder();
const combined = new Uint8Array(atob(base64Text).split("").map(c => c.charCodeAt(0)));
@ -608,11 +631,43 @@ async function fetchEncrypted(request, body = "") {
let data = await response.text();
if (data.startsWith("error:")) return data;
return await decryptString(data, passwordHash);
} catch (e) {
console.error("fetchEncrypted error:", e);
return "error:network.error";
} finally {
release();
}
}
async function fetchEncryptedUpload(target, fileBytesUint8) {
let release;
const lock = new Promise(resolve => release = resolve);
const prevMutex = requestMutex;
requestMutex = prevMutex.then(() => lock);
await prevMutex;
try {
let nonce = await getNonce(id, passwordHash);
let response = await fetch(`${url}/upload?id=${id}`, {
method: "POST",
body: await encryptBytesWithNonce(fileBytesUint8, passwordHash, nonce),
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce),
"target": await encryptWithNonce(target, passwordHash, nonce)
}
});
let data = await response.text();
if (data.startsWith("error:")) return data;
return await decryptString(data, passwordHash);
} catch (e) {
console.error("fetchEncryptedUpload error:", e);
return "error:network.error";
} finally {
release();
}
}
function power(base, exponent, mod) {
let res = 1n;
base = base % mod;
@ -914,6 +969,9 @@ function processBlah(blahmessage) {
}
return message;
} catch (e) {
if (blahmessage.startsWith("error:")) {
return blah["error.unknown.error"] || "Unknown error";
}
if (prepended) {
return blahmessage.substring(1);
}
@ -1092,7 +1150,12 @@ id = localStorage.getItem('id');
username = localStorage.getItem('username');
password = localStorage.getItem('password');
host = localStorage.getItem('host');
mainJS();
window.addEventListener("load", () => {
mainJS();
});
function showFixedContextMenu(rect, html) {
@ -2848,14 +2911,19 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
async function gotoSettings(tab = 'account') {
fixedContextMenu.classList.remove("show");
if (roomsBarContainer) roomsBarContainer.style.display = "";
switchRoomsBar("title.settings", settingsBar);
switchSettingsTab(tab);
await switchRoomsBar("title.settings", settingsBar);
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
if (window.innerWidth > 52 * rem) {
switchSettingsTab(tab);
}
}
function switchSettingsTab(tabName) {
setActiveRoombarItem('tab-settings-' + tabName);
if (tabName === 'account') {
switchRoomContent("title.account", accountSettings, false, null, true); //skip mobile slide
switchRoomContent("title.account", accountSettings, false);
} else if (tabName === 'profile') {
switchRoomContent("title.profile", profileSettings, false);
setTimeout(async () => {
@ -3093,38 +3161,126 @@ async function confirmCropper() {
let zoom = parseFloat(document.getElementById('cropper-zoom').value);
let container = document.getElementById('cropper-viewport-container');
let targetWidth = cropperType === 'avatar' ? 512 : 988;
let targetHeight = cropperType === 'avatar' ? 512 : 400;
let scaleRatio = targetWidth / container.clientWidth;
let drawX = cropperPosX * scaleRatio;
let drawY = cropperPosY * scaleRatio;
let drawW = cropperImageObj.width * zoom * scaleRatio;
let drawH = cropperImageObj.height * zoom * scaleRatio;
let b64;
let newSrc;
if (cropperOriginalType === 'image/gif') {
b64 = cropperOriginalB64.split(',')[1];
newSrc = cropperOriginalB64;
showAction("action.processing", "uploadmedia");
try {
let res = await fetch(cropperOriginalB64);
let buffer = await res.arrayBuffer();
let parsedGif = gifuct.parseGIF(buffer);
let frames = gifuct.decompressFrames(parsedGif, true);
let webpFrames = [];
let tempCanvas = document.createElement('canvas');
let tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
tempCanvas.width = parsedGif.lsd.width;
tempCanvas.height = parsedGif.lsd.height;
let gifCanvas = document.createElement('canvas');
let gifCtx = gifCanvas.getContext('2d', { willReadFrequently: true });
gifCanvas.width = targetWidth;
gifCanvas.height = targetHeight;
let frameImageData = null;
let previousImageData = null;
for (let i = 0; i < frames.length; i++) {
let frame = frames[i];
if (frame.disposalType === 3) {
previousImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
}
if (!frameImageData || frameImageData.width !== frame.dims.width || frameImageData.height !== frame.dims.height) {
frameImageData = tempCtx.createImageData(frame.dims.width, frame.dims.height);
}
frameImageData.data.set(frame.patch);
let patchCanvas = document.createElement('canvas');
patchCanvas.width = frame.dims.width;
patchCanvas.height = frame.dims.height;
patchCanvas.getContext('2d').putImageData(frameImageData, 0, 0);
tempCtx.drawImage(patchCanvas, frame.dims.left, frame.dims.top);
gifCtx.fillStyle = '#000';
gifCtx.fillRect(0, 0, targetWidth, targetHeight);
gifCtx.drawImage(tempCanvas, drawX, drawY, drawW, drawH);
let frameImgData = gifCtx.getImageData(0, 0, targetWidth, targetHeight);
webpFrames.push({
data: frameImgData.data,
duration: Math.max(20, frame.delay || 0),
config: { lossless: 0, quality: 90 }
});
if (frame.disposalType === 2) {
tempCtx.clearRect(frame.dims.left, frame.dims.top, frame.dims.width, frame.dims.height);
} else if (frame.disposalType === 3 && previousImageData) {
tempCtx.putImageData(previousImageData, 0, 0);
}
}
let webpUint8 = await new Promise((resolve, reject) => {
let worker = new Worker('webp-worker.js', { type: 'module' });
worker.onmessage = (e) => {
if (e.data.success) {
resolve(e.data.data);
} else {
reject(new Error(e.data.error));
}
worker.terminate();
};
worker.onerror = (err) => {
reject(err);
worker.terminate();
};
worker.postMessage({ targetWidth, targetHeight, webpFrames });
});
let webpBlob = new Blob([webpUint8], {type: 'image/webp'});
b64 = await new Promise((resolve) => {
let reader = new FileReader();
reader.onload = function() {
resolve(reader.result.split(',')[1]);
};
reader.readAsDataURL(webpBlob);
});
newSrc = 'data:image/webp;base64,' + b64;
} catch (e) {
console.error("GIF crop failed", e);
b64 = cropperOriginalB64.split(',')[1];
newSrc = cropperOriginalB64;
}
} else {
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
let targetWidth = cropperType === 'avatar' ? 512 : 988;
let targetHeight = cropperType === 'avatar' ? 512 : 400;
canvas.width = targetWidth;
canvas.height = targetHeight;
let scaleRatio = targetWidth / container.clientWidth;
let drawX = cropperPosX * scaleRatio;
let drawY = cropperPosY * scaleRatio;
let drawW = cropperImageObj.width * zoom * scaleRatio;
let drawH = cropperImageObj.height * zoom * scaleRatio;
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, targetWidth, targetHeight);
ctx.drawImage(cropperImageObj, drawX, drawY, drawW, drawH);
b64 = canvas.toDataURL('image/jpeg', 0.9).split(',')[1];
newSrc = 'data:image/jpeg;base64,' + b64;
b64 = canvas.toDataURL('image/webp', 0.9).split(',')[1];
newSrc = 'data:image/webp;base64,' + b64;
}
let payloadKey = cropperType === 'avatar' ? 'larp.profile.pfp' : 'larp.profile.banner';
let payload = { string1: payloadKey, string2: b64 };
let fileBytes = base64ToUint8(b64);
showAction("action.uploading", "uploadmedia");
let res = await fetchEncrypted("user/storage/public/update", JSON.stringify(payload));
let res = await fetchEncryptedUpload(payloadKey, fileBytes);
clearAction("uploadmedia");
if (res && res.startsWith("error:")) {
showBlahNotification(res);

807
webroot/omggif.js Normal file
View file

@ -0,0 +1,807 @@
// (c) Dean McNamee <dean@gmail.com>, 2013.
//
// https://github.com/deanm/omggif
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
// omggif is a JavaScript implementation of a GIF 89a encoder and decoder,
// including animation and compression. It does not rely on any specific
// underlying system, so should run in the browser, Node, or Plask.
"use strict";
function GifWriter(buf, width, height, gopts) {
var p = 0;
var gopts = gopts === undefined ? { } : gopts;
var loop_count = gopts.loop === undefined ? null : gopts.loop;
var global_palette = gopts.palette === undefined ? null : gopts.palette;
if (width <= 0 || height <= 0 || width > 65535 || height > 65535)
throw new Error("Width/Height invalid.");
function check_palette_and_num_colors(palette) {
var num_colors = palette.length;
if (num_colors < 2 || num_colors > 256 || num_colors & (num_colors-1)) {
throw new Error(
"Invalid code/color length, must be power of 2 and 2 .. 256.");
}
return num_colors;
}
// - Header.
buf[p++] = 0x47; buf[p++] = 0x49; buf[p++] = 0x46; // GIF
buf[p++] = 0x38; buf[p++] = 0x39; buf[p++] = 0x61; // 89a
// Handling of Global Color Table (palette) and background index.
var gp_num_colors_pow2 = 0;
var background = 0;
if (global_palette !== null) {
var gp_num_colors = check_palette_and_num_colors(global_palette);
while (gp_num_colors >>= 1) ++gp_num_colors_pow2;
gp_num_colors = 1 << gp_num_colors_pow2;
--gp_num_colors_pow2;
if (gopts.background !== undefined) {
background = gopts.background;
if (background >= gp_num_colors)
throw new Error("Background index out of range.");
// The GIF spec states that a background index of 0 should be ignored, so
// this is probably a mistake and you really want to set it to another
// slot in the palette. But actually in the end most browsers, etc end
// up ignoring this almost completely (including for dispose background).
if (background === 0)
throw new Error("Background index explicitly passed as 0.");
}
}
// - Logical Screen Descriptor.
// NOTE(deanm): w/h apparently ignored by implementations, but set anyway.
buf[p++] = width & 0xff; buf[p++] = width >> 8 & 0xff;
buf[p++] = height & 0xff; buf[p++] = height >> 8 & 0xff;
// NOTE: Indicates 0-bpp original color resolution (unused?).
buf[p++] = (global_palette !== null ? 0x80 : 0) | // Global Color Table Flag.
gp_num_colors_pow2; // NOTE: No sort flag (unused?).
buf[p++] = background; // Background Color Index.
buf[p++] = 0; // Pixel aspect ratio (unused?).
// - Global Color Table
if (global_palette !== null) {
for (var i = 0, il = global_palette.length; i < il; ++i) {
var rgb = global_palette[i];
buf[p++] = rgb >> 16 & 0xff;
buf[p++] = rgb >> 8 & 0xff;
buf[p++] = rgb & 0xff;
}
}
if (loop_count !== null) { // Netscape block for looping.
if (loop_count < 0 || loop_count > 65535)
throw new Error("Loop count invalid.")
// Extension code, label, and length.
buf[p++] = 0x21; buf[p++] = 0xff; buf[p++] = 0x0b;
// NETSCAPE2.0
buf[p++] = 0x4e; buf[p++] = 0x45; buf[p++] = 0x54; buf[p++] = 0x53;
buf[p++] = 0x43; buf[p++] = 0x41; buf[p++] = 0x50; buf[p++] = 0x45;
buf[p++] = 0x32; buf[p++] = 0x2e; buf[p++] = 0x30;
// Sub-block
buf[p++] = 0x03; buf[p++] = 0x01;
buf[p++] = loop_count & 0xff; buf[p++] = loop_count >> 8 & 0xff;
buf[p++] = 0x00; // Terminator.
}
var ended = false;
this.addFrame = function(x, y, w, h, indexed_pixels, opts) {
if (ended === true) { --p; ended = false; } // Un-end.
opts = opts === undefined ? { } : opts;
// TODO(deanm): Bounds check x, y. Do they need to be within the virtual
// canvas width/height, I imagine?
if (x < 0 || y < 0 || x > 65535 || y > 65535)
throw new Error("x/y invalid.")
if (w <= 0 || h <= 0 || w > 65535 || h > 65535)
throw new Error("Width/Height invalid.")
if (indexed_pixels.length < w * h)
throw new Error("Not enough pixels for the frame size.");
var using_local_palette = true;
var palette = opts.palette;
if (palette === undefined || palette === null) {
using_local_palette = false;
palette = global_palette;
}
if (palette === undefined || palette === null)
throw new Error("Must supply either a local or global palette.");
var num_colors = check_palette_and_num_colors(palette);
// Compute the min_code_size (power of 2), destroying num_colors.
var min_code_size = 0;
while (num_colors >>= 1) ++min_code_size;
num_colors = 1 << min_code_size; // Now we can easily get it back.
var delay = opts.delay === undefined ? 0 : opts.delay;
// From the spec:
// 0 - No disposal specified. The decoder is
// not required to take any action.
// 1 - Do not dispose. The graphic is to be left
// in place.
// 2 - Restore to background color. The area used by the
// graphic must be restored to the background color.
// 3 - Restore to previous. The decoder is required to
// restore the area overwritten by the graphic with
// what was there prior to rendering the graphic.
// 4-7 - To be defined.
// NOTE(deanm): Dispose background doesn't really work, apparently most
// browsers ignore the background palette index and clear to transparency.
var disposal = opts.disposal === undefined ? 0 : opts.disposal;
if (disposal < 0 || disposal > 3) // 4-7 is reserved.
throw new Error("Disposal out of range.");
var use_transparency = false;
var transparent_index = 0;
if (opts.transparent !== undefined && opts.transparent !== null) {
use_transparency = true;
transparent_index = opts.transparent;
if (transparent_index < 0 || transparent_index >= num_colors)
throw new Error("Transparent color index.");
}
if (disposal !== 0 || use_transparency || delay !== 0) {
// - Graphics Control Extension
buf[p++] = 0x21; buf[p++] = 0xf9; // Extension / Label.
buf[p++] = 4; // Byte size.
buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0);
buf[p++] = delay & 0xff; buf[p++] = delay >> 8 & 0xff;
buf[p++] = transparent_index; // Transparent color index.
buf[p++] = 0; // Block Terminator.
}
// - Image Descriptor
buf[p++] = 0x2c; // Image Seperator.
buf[p++] = x & 0xff; buf[p++] = x >> 8 & 0xff; // Left.
buf[p++] = y & 0xff; buf[p++] = y >> 8 & 0xff; // Top.
buf[p++] = w & 0xff; buf[p++] = w >> 8 & 0xff;
buf[p++] = h & 0xff; buf[p++] = h >> 8 & 0xff;
// NOTE: No sort flag (unused?).
// TODO(deanm): Support interlace.
buf[p++] = using_local_palette === true ? (0x80 | (min_code_size-1)) : 0;
// - Local Color Table
if (using_local_palette === true) {
for (var i = 0, il = palette.length; i < il; ++i) {
var rgb = palette[i];
buf[p++] = rgb >> 16 & 0xff;
buf[p++] = rgb >> 8 & 0xff;
buf[p++] = rgb & 0xff;
}
}
p = GifWriterOutputLZWCodeStream(
buf, p, min_code_size < 2 ? 2 : min_code_size, indexed_pixels);
return p;
};
this.end = function() {
if (ended === false) {
buf[p++] = 0x3b; // Trailer.
ended = true;
}
return p;
};
this.getOutputBuffer = function() { return buf; };
this.setOutputBuffer = function(v) { buf = v; };
this.getOutputBufferPosition = function() { return p; };
this.setOutputBufferPosition = function(v) { p = v; };
}
// Main compression routine, palette indexes -> LZW code stream.
// |index_stream| must have at least one entry.
function GifWriterOutputLZWCodeStream(buf, p, min_code_size, index_stream) {
buf[p++] = min_code_size;
var cur_subblock = p++; // Pointing at the length field.
var clear_code = 1 << min_code_size;
var code_mask = clear_code - 1;
var eoi_code = clear_code + 1;
var next_code = eoi_code + 1;
var cur_code_size = min_code_size + 1; // Number of bits per code.
var cur_shift = 0;
// We have at most 12-bit codes, so we should have to hold a max of 19
// bits here (and then we would write out).
var cur = 0;
function emit_bytes_to_buffer(bit_block_size) {
while (cur_shift >= bit_block_size) {
buf[p++] = cur & 0xff;
cur >>= 8; cur_shift -= 8;
if (p === cur_subblock + 256) { // Finished a subblock.
buf[cur_subblock] = 255;
cur_subblock = p++;
}
}
}
function emit_code(c) {
cur |= c << cur_shift;
cur_shift += cur_code_size;
emit_bytes_to_buffer(8);
}
// I am not an expert on the topic, and I don't want to write a thesis.
// However, it is good to outline here the basic algorithm and the few data
// structures and optimizations here that make this implementation fast.
// The basic idea behind LZW is to build a table of previously seen runs
// addressed by a short id (herein called output code). All data is
// referenced by a code, which represents one or more values from the
// original input stream. All input bytes can be referenced as the same
// value as an output code. So if you didn't want any compression, you
// could more or less just output the original bytes as codes (there are
// some details to this, but it is the idea). In order to achieve
// compression, values greater then the input range (codes can be up to
// 12-bit while input only 8-bit) represent a sequence of previously seen
// inputs. The decompressor is able to build the same mapping while
// decoding, so there is always a shared common knowledge between the
// encoding and decoder, which is also important for "timing" aspects like
// how to handle variable bit width code encoding.
//
// One obvious but very important consequence of the table system is there
// is always a unique id (at most 12-bits) to map the runs. 'A' might be
// 4, then 'AA' might be 10, 'AAA' 11, 'AAAA' 12, etc. This relationship
// can be used for an effecient lookup strategy for the code mapping. We
// need to know if a run has been seen before, and be able to map that run
// to the output code. Since we start with known unique ids (input bytes),
// and then from those build more unique ids (table entries), we can
// continue this chain (almost like a linked list) to always have small
// integer values that represent the current byte chains in the encoder.
// This means instead of tracking the input bytes (AAAABCD) to know our
// current state, we can track the table entry for AAAABC (it is guaranteed
// to exist by the nature of the algorithm) and the next character D.
// Therefor the tuple of (table_entry, byte) is guaranteed to also be
// unique. This allows us to create a simple lookup key for mapping input
// sequences to codes (table indices) without having to store or search
// any of the code sequences. So if 'AAAA' has a table entry of 12, the
// tuple of ('AAAA', K) for any input byte K will be unique, and can be our
// key. This leads to a integer value at most 20-bits, which can always
// fit in an SMI value and be used as a fast sparse array / object key.
// Output code for the current contents of the index buffer.
var ib_code = index_stream[0] & code_mask; // Load first input index.
var code_table = { }; // Key'd on our 20-bit "tuple".
emit_code(clear_code); // Spec says first code should be a clear code.
// First index already loaded, process the rest of the stream.
for (var i = 1, il = index_stream.length; i < il; ++i) {
var k = index_stream[i] & code_mask;
var cur_key = ib_code << 8 | k; // (prev, k) unique tuple.
var cur_code = code_table[cur_key]; // buffer + k.
// Check if we have to create a new code table entry.
if (cur_code === undefined) { // We don't have buffer + k.
// Emit index buffer (without k).
// This is an inline version of emit_code, because this is the core
// writing routine of the compressor (and V8 cannot inline emit_code
// because it is a closure here in a different context). Additionally
// we can call emit_byte_to_buffer less often, because we can have
// 30-bits (from our 31-bit signed SMI), and we know our codes will only
// be 12-bits, so can safely have 18-bits there without overflow.
// emit_code(ib_code);
cur |= ib_code << cur_shift;
cur_shift += cur_code_size;
while (cur_shift >= 8) {
buf[p++] = cur & 0xff;
cur >>= 8; cur_shift -= 8;
if (p === cur_subblock + 256) { // Finished a subblock.
buf[cur_subblock] = 255;
cur_subblock = p++;
}
}
if (next_code === 4096) { // Table full, need a clear.
emit_code(clear_code);
next_code = eoi_code + 1;
cur_code_size = min_code_size + 1;
code_table = { };
} else { // Table not full, insert a new entry.
// Increase our variable bit code sizes if necessary. This is a bit
// tricky as it is based on "timing" between the encoding and
// decoder. From the encoders perspective this should happen after
// we've already emitted the index buffer and are about to create the
// first table entry that would overflow our current code bit size.
if (next_code >= (1 << cur_code_size)) ++cur_code_size;
code_table[cur_key] = next_code++; // Insert into code table.
}
ib_code = k; // Index buffer to single input k.
} else {
ib_code = cur_code; // Index buffer to sequence in code table.
}
}
emit_code(ib_code); // There will still be something in the index buffer.
emit_code(eoi_code); // End Of Information.
// Flush / finalize the sub-blocks stream to the buffer.
emit_bytes_to_buffer(1);
// Finish the sub-blocks, writing out any unfinished lengths and
// terminating with a sub-block of length 0. If we have already started
// but not yet used a sub-block it can just become the terminator.
if (cur_subblock + 1 === p) { // Started but unused.
buf[cur_subblock] = 0;
} else { // Started and used, write length and additional terminator block.
buf[cur_subblock] = p - cur_subblock - 1;
buf[p++] = 0;
}
return p;
}
function GifReader(buf) {
var p = 0;
// - Header (GIF87a or GIF89a).
if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 ||
buf[p++] !== 0x38 || (buf[p++]+1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) {
throw new Error("Invalid GIF 87a/89a header.");
}
// - Logical Screen Descriptor.
var width = buf[p++] | buf[p++] << 8;
var height = buf[p++] | buf[p++] << 8;
var pf0 = buf[p++]; // <Packed Fields>.
var global_palette_flag = pf0 >> 7;
var num_global_colors_pow2 = pf0 & 0x7;
var num_global_colors = 1 << (num_global_colors_pow2 + 1);
var background = buf[p++];
buf[p++]; // Pixel aspect ratio (unused?).
var global_palette_offset = null;
var global_palette_size = null;
if (global_palette_flag) {
global_palette_offset = p;
global_palette_size = num_global_colors;
p += num_global_colors * 3; // Seek past palette.
}
var no_eof = true;
var frames = [ ];
var delay = 0;
var transparent_index = null;
var disposal = 0; // 0 - No disposal specified.
var loop_count = null;
this.width = width;
this.height = height;
while (no_eof && p < buf.length) {
switch (buf[p++]) {
case 0x21: // Graphics Control Extension Block
switch (buf[p++]) {
case 0xff: // Application specific block
// Try if it's a Netscape block (with animation loop counter).
if (buf[p ] !== 0x0b || // 21 FF already read, check block size.
// NETSCAPE2.0
buf[p+1 ] == 0x4e && buf[p+2 ] == 0x45 && buf[p+3 ] == 0x54 &&
buf[p+4 ] == 0x53 && buf[p+5 ] == 0x43 && buf[p+6 ] == 0x41 &&
buf[p+7 ] == 0x50 && buf[p+8 ] == 0x45 && buf[p+9 ] == 0x32 &&
buf[p+10] == 0x2e && buf[p+11] == 0x30 &&
// Sub-block
buf[p+12] == 0x03 && buf[p+13] == 0x01 && buf[p+16] == 0) {
p += 14;
loop_count = buf[p++] | buf[p++] << 8;
p++; // Skip terminator.
} else { // We don't know what it is, just try to get past it.
p += 12;
while (true) { // Seek through subblocks.
var block_size = buf[p++];
// Bad block size (ex: undefined from an out of bounds read).
if (!(block_size >= 0)) throw Error("Invalid block size");
if (block_size === 0) break; // 0 size is terminator
p += block_size;
}
}
break;
case 0xf9: // Graphics Control Extension
if (buf[p++] !== 0x4 || buf[p+4] !== 0)
throw new Error("Invalid graphics extension block.");
var pf1 = buf[p++];
delay = buf[p++] | buf[p++] << 8;
transparent_index = buf[p++];
if ((pf1 & 1) === 0) transparent_index = null;
disposal = pf1 >> 2 & 0x7;
p++; // Skip terminator.
break;
case 0xfe: // Comment Extension.
while (true) { // Seek through subblocks.
var block_size = buf[p++];
// Bad block size (ex: undefined from an out of bounds read).
if (!(block_size >= 0)) throw Error("Invalid block size");
if (block_size === 0) break; // 0 size is terminator
// console.log(buf.slice(p, p+block_size).toString('ascii'));
p += block_size;
}
break;
default:
throw new Error(
"Unknown graphic control label: 0x" + buf[p-1].toString(16));
}
break;
case 0x2c: // Image Descriptor.
var x = buf[p++] | buf[p++] << 8;
var y = buf[p++] | buf[p++] << 8;
var w = buf[p++] | buf[p++] << 8;
var h = buf[p++] | buf[p++] << 8;
var pf2 = buf[p++];
var local_palette_flag = pf2 >> 7;
var interlace_flag = pf2 >> 6 & 1;
var num_local_colors_pow2 = pf2 & 0x7;
var num_local_colors = 1 << (num_local_colors_pow2 + 1);
var palette_offset = global_palette_offset;
var palette_size = global_palette_size;
var has_local_palette = false;
if (local_palette_flag) {
var has_local_palette = true;
palette_offset = p; // Override with local palette.
palette_size = num_local_colors;
p += num_local_colors * 3; // Seek past palette.
}
var data_offset = p;
p++; // codesize
while (true) {
var block_size = buf[p++];
// Bad block size (ex: undefined from an out of bounds read).
if (!(block_size >= 0)) throw Error("Invalid block size");
if (block_size === 0) break; // 0 size is terminator
p += block_size;
}
frames.push({x: x, y: y, width: w, height: h,
has_local_palette: has_local_palette,
palette_offset: palette_offset,
palette_size: palette_size,
data_offset: data_offset,
data_length: p - data_offset,
transparent_index: transparent_index,
interlaced: !!interlace_flag,
delay: delay,
disposal: disposal});
break;
case 0x3b: // Trailer Marker (end of file).
no_eof = false;
break;
default:
throw new Error("Unknown gif block: 0x" + buf[p-1].toString(16));
break;
}
}
this.numFrames = function() {
return frames.length;
};
this.loopCount = function() {
return loop_count;
};
this.frameInfo = function(frame_num) {
if (frame_num < 0 || frame_num >= frames.length)
throw new Error("Frame index out of range.");
return frames[frame_num];
}
this.decodeAndBlitFrameBGRA = function(frame_num, pixels) {
var frame = this.frameInfo(frame_num);
var num_pixels = frame.width * frame.height;
var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
GifReaderLZWOutputIndexStream(
buf, frame.data_offset, index_stream, num_pixels);
var palette_offset = frame.palette_offset;
// NOTE(deanm): It seems to be much faster to compare index to 256 than
// to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
// the profile, not sure if it's related to using a Uint8Array.
var trans = frame.transparent_index;
if (trans === null) trans = 256;
// We are possibly just blitting to a portion of the entire frame.
// That is a subrect within the framerect, so the additional pixels
// must be skipped over after we finished a scanline.
var framewidth = frame.width;
var framestride = width - framewidth;
var xleft = framewidth; // Number of subrect pixels left in scanline.
// Output indicies of the top left and bottom right corners of the subrect.
var opbeg = ((frame.y * width) + frame.x) * 4;
var opend = ((frame.y + frame.height) * width + frame.x) * 4;
var op = opbeg;
var scanstride = framestride * 4;
// Use scanstride to skip past the rows when interlacing. This is skipping
// 7 rows for the first two passes, then 3 then 1.
if (frame.interlaced === true) {
scanstride += width * 4 * 7; // Pass 1.
}
var interlaceskip = 8; // Tracking the row interval in the current pass.
for (var i = 0, il = index_stream.length; i < il; ++i) {
var index = index_stream[i];
if (xleft === 0) { // Beginning of new scan line
op += scanstride;
xleft = framewidth;
if (op >= opend) { // Catch the wrap to switch passes when interlacing.
scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
// interlaceskip / 2 * 4 is interlaceskip << 1.
op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
interlaceskip >>= 1;
}
}
if (index === trans) {
op += 4;
} else {
var r = buf[palette_offset + index * 3];
var g = buf[palette_offset + index * 3 + 1];
var b = buf[palette_offset + index * 3 + 2];
pixels[op++] = b;
pixels[op++] = g;
pixels[op++] = r;
pixels[op++] = 255;
}
--xleft;
}
};
// I will go to copy and paste hell one day...
this.decodeAndBlitFrameRGBA = function(frame_num, pixels) {
var frame = this.frameInfo(frame_num);
var num_pixels = frame.width * frame.height;
var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
GifReaderLZWOutputIndexStream(
buf, frame.data_offset, index_stream, num_pixels);
var palette_offset = frame.palette_offset;
// NOTE(deanm): It seems to be much faster to compare index to 256 than
// to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
// the profile, not sure if it's related to using a Uint8Array.
var trans = frame.transparent_index;
if (trans === null) trans = 256;
// We are possibly just blitting to a portion of the entire frame.
// That is a subrect within the framerect, so the additional pixels
// must be skipped over after we finished a scanline.
var framewidth = frame.width;
var framestride = width - framewidth;
var xleft = framewidth; // Number of subrect pixels left in scanline.
// Output indicies of the top left and bottom right corners of the subrect.
var opbeg = ((frame.y * width) + frame.x) * 4;
var opend = ((frame.y + frame.height) * width + frame.x) * 4;
var op = opbeg;
var scanstride = framestride * 4;
// Use scanstride to skip past the rows when interlacing. This is skipping
// 7 rows for the first two passes, then 3 then 1.
if (frame.interlaced === true) {
scanstride += width * 4 * 7; // Pass 1.
}
var interlaceskip = 8; // Tracking the row interval in the current pass.
for (var i = 0, il = index_stream.length; i < il; ++i) {
var index = index_stream[i];
if (xleft === 0) { // Beginning of new scan line
op += scanstride;
xleft = framewidth;
if (op >= opend) { // Catch the wrap to switch passes when interlacing.
scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
// interlaceskip / 2 * 4 is interlaceskip << 1.
op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
interlaceskip >>= 1;
}
}
if (index === trans) {
op += 4;
} else {
var r = buf[palette_offset + index * 3];
var g = buf[palette_offset + index * 3 + 1];
var b = buf[palette_offset + index * 3 + 2];
pixels[op++] = r;
pixels[op++] = g;
pixels[op++] = b;
pixels[op++] = 255;
}
--xleft;
}
};
}
function GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) {
var min_code_size = code_stream[p++];
var clear_code = 1 << min_code_size;
var eoi_code = clear_code + 1;
var next_code = eoi_code + 1;
var cur_code_size = min_code_size + 1; // Number of bits per code.
// NOTE: This shares the same name as the encoder, but has a different
// meaning here. Here this masks each code coming from the code stream.
var code_mask = (1 << cur_code_size) - 1;
var cur_shift = 0;
var cur = 0;
var op = 0; // Output pointer.
var subblock_size = code_stream[p++];
// TODO(deanm): Would using a TypedArray be any faster? At least it would
// solve the fast mode / backing store uncertainty.
// var code_table = Array(4096);
var code_table = new Int32Array(4096); // Can be signed, we only use 20 bits.
var prev_code = null; // Track code-1.
while (true) {
// Read up to two bytes, making sure we always 12-bits for max sized code.
while (cur_shift < 16) {
if (subblock_size === 0) break; // No more data to be read.
cur |= code_stream[p++] << cur_shift;
cur_shift += 8;
if (subblock_size === 1) { // Never let it get to 0 to hold logic above.
subblock_size = code_stream[p++]; // Next subblock.
} else {
--subblock_size;
}
}
// TODO(deanm): We should never really get here, we should have received
// and EOI.
if (cur_shift < cur_code_size)
break;
var code = cur & code_mask;
cur >>= cur_code_size;
cur_shift -= cur_code_size;
// TODO(deanm): Maybe should check that the first code was a clear code,
// at least this is what you're supposed to do. But actually our encoder
// now doesn't emit a clear code first anyway.
if (code === clear_code) {
// We don't actually have to clear the table. This could be a good idea
// for greater error checking, but we don't really do any anyway. We
// will just track it with next_code and overwrite old entries.
next_code = eoi_code + 1;
cur_code_size = min_code_size + 1;
code_mask = (1 << cur_code_size) - 1;
// Don't update prev_code ?
prev_code = null;
continue;
} else if (code === eoi_code) {
break;
}
// We have a similar situation as the decoder, where we want to store
// variable length entries (code table entries), but we want to do in a
// faster manner than an array of arrays. The code below stores sort of a
// linked list within the code table, and then "chases" through it to
// construct the dictionary entries. When a new entry is created, just the
// last byte is stored, and the rest (prefix) of the entry is only
// referenced by its table entry. Then the code chases through the
// prefixes until it reaches a single byte code. We have to chase twice,
// first to compute the length, and then to actually copy the data to the
// output (backwards, since we know the length). The alternative would be
// storing something in an intermediate stack, but that doesn't make any
// more sense. I implemented an approach where it also stored the length
// in the code table, although it's a bit tricky because you run out of
// bits (12 + 12 + 8), but I didn't measure much improvements (the table
// entries are generally not the long). Even when I created benchmarks for
// very long table entries the complexity did not seem worth it.
// The code table stores the prefix entry in 12 bits and then the suffix
// byte in 8 bits, so each entry is 20 bits.
var chase_code = code < next_code ? code : prev_code;
// Chase what we will output, either {CODE} or {CODE-1}.
var chase_length = 0;
var chase = chase_code;
while (chase > clear_code) {
chase = code_table[chase] >> 8;
++chase_length;
}
var k = chase;
var op_end = op + chase_length + (chase_code !== code ? 1 : 0);
if (op_end > output_length) {
console.log("Warning, gif stream longer than expected.");
return;
}
// Already have the first byte from the chase, might as well write it fast.
output[op++] = k;
op += chase_length;
var b = op; // Track pointer, writing backwards.
if (chase_code !== code) // The case of emitting {CODE-1} + k.
output[op++] = k;
chase = chase_code;
while (chase_length--) {
chase = code_table[chase];
output[--b] = chase & 0xff; // Write backwards.
chase >>= 8; // Pull down to the prefix code.
}
if (prev_code !== null && next_code < 4096) {
code_table[next_code++] = prev_code << 8 | k;
// TODO(deanm): Figure out this clearing vs code growth logic better. I
// have an feeling that it should just happen somewhere else, for now it
// is awkward between when we grow past the max and then hit a clear code.
// For now just check if we hit the max 12-bits (then a clear code should
// follow, also of course encoded in 12-bits).
if (next_code >= code_mask+1 && cur_code_size < 12) {
++cur_code_size;
code_mask = code_mask << 1 | 1;
}
}
prev_code = code;
}
if (op !== output_length) {
console.log("Warning, gif stream shorter than expected.");
}
return output;
}
// CommonJS.
try { exports.GifWriter = GifWriter; exports.GifReader = GifReader } catch(e) {}

View file

@ -268,7 +268,7 @@ hr {
top: 1.5rem;
left: 50%;
transform: translateX(-50%);
z-index: 1500;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 0.8rem;

View file

@ -1,4 +1,7 @@
//write your custom deployment scripts here
loginHost.value = "olcxja.ovh";
loginHostChanged();
window.addEventListener("load", () => {
loginHost.value = "olcxja.ovh";
loginHostChanged();
});

View file

@ -0,0 +1,75 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
// @ts-ignore
import Module from './webp-wasm.js';
// default webp config
const defaultWebpConfig = {
lossless: 0,
quality: 100,
};
export const encoderVersion = () => __awaiter(void 0, void 0, void 0, function* () {
const module = yield Module();
return module.encoder_version();
});
export const encodeRGB = (rgb, width, height, quality) => __awaiter(void 0, void 0, void 0, function* () {
const module = yield Module();
quality = typeof quality !== 'number' ? 100 : Math.min(100, Math.max(0, quality));
return module.encodeRGB(rgb, width, height, quality);
});
export const encodeRGBA = (rgba, width, height, quality) => __awaiter(void 0, void 0, void 0, function* () {
const module = yield Module();
quality = typeof quality !== 'number' ? 100 : Math.min(100, Math.max(0, quality));
return module.encodeRGBA(rgba, width, height, quality);
});
export const encode = (data, width, height, hasAlpha, config) => __awaiter(void 0, void 0, void 0, function* () {
const module = yield Module();
const webpConfig = Object.assign(Object.assign({}, defaultWebpConfig), config);
webpConfig.lossless = Math.min(1, Math.max(0, webpConfig.lossless));
webpConfig.quality = Math.min(100, Math.max(0, webpConfig.quality));
return module.encode(data, width, height, hasAlpha, webpConfig);
});
export const encodeAnimation = (width, height, hasAlpha, frames) => __awaiter(void 0, void 0, void 0, function* () {
const module = yield Module();
const frameVector = new module.VectorWebPAnimationFrame();
frames.forEach((frame) => {
const hasConfig = frame.config !== undefined;
const config = Object.assign(Object.assign({}, defaultWebpConfig), frame.config);
config.lossless = Math.min(1, Math.max(0, config.lossless));
config.quality = Math.min(100, Math.max(0, config.quality));
frameVector.push_back({
duration: frame.duration,
data: frame.data,
config,
has_config: hasConfig,
});
});
return module.encodeAnimation(width, height, hasAlpha, frameVector);
});
export const decoderVersion = () => __awaiter(void 0, void 0, void 0, function* () {
const module = yield Module();
return module.decoder_version();
});
export const decodeRGB = (data) => __awaiter(void 0, void 0, void 0, function* () {
const module = yield Module();
return module.decodeRGB(data);
});
export const decodeRGBA = (data) => __awaiter(void 0, void 0, void 0, function* () {
const module = yield Module();
return module.decodeRGBA(data);
});
// TODO:
// export const decode = async (data: Uint8Array, hasAlpha: boolean) => {
// const module = await Module()
// return module.decode(data, hasAlpha)
// }
export const decodeAnimation = (data, hasAlpha) => __awaiter(void 0, void 0, void 0, function* () {
const module = yield Module();
return module.decodeAnimation(data, hasAlpha);
});

File diff suppressed because it is too large Load diff

BIN
webroot/wasm-webp/webp-wasm.wasm Executable file

Binary file not shown.

11
webroot/webp-worker.js Normal file
View file

@ -0,0 +1,11 @@
import { encodeAnimation } from './wasm-webp/index.js';
self.onmessage = async (e) => {
let { targetWidth, targetHeight, webpFrames } = e.data;
try {
let webpUint8 = await encodeAnimation(targetWidth, targetHeight, true, webpFrames);
self.postMessage({ success: true, data: webpUint8 }, [webpUint8.buffer]);
} catch (err) {
self.postMessage({ success: false, error: err.toString() });
}
};