Clamp blurhash aspect ratio

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2026-08-03 20:30:00 +00:00
committed by GitHub
co-authored by trevor-signal
parent 60929351aa
commit 5cdb0d6afd
2 changed files with 71 additions and 5 deletions
@@ -0,0 +1,57 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { computeBlurHashUrl } from '../../util/computeBlurHashUrl.std.ts';
import { defaultBlurHash } from '../../util/Attachment.std.ts';
describe('computeBlurHashUrl', () => {
const BLUR_HASH = defaultBlurHash();
async function loadImage(
desiredWidth?: number,
desiredHeight?: number
): Promise<HTMLImageElement> {
const image = document.createElement('img');
image.src = computeBlurHashUrl(BLUR_HASH, desiredWidth, desiredHeight);
await image.decode();
return image;
}
it('produces a square image by default', async () => {
const image = await loadImage();
assert.strictEqual(image.naturalWidth, 32);
assert.strictEqual(image.naturalHeight, 32);
});
it('respects the requested aspect ratio', async () => {
const image = await loadImage(1600, 400);
assert.strictEqual(image.naturalWidth / image.naturalHeight, 4);
});
describe('bounds sender-provided dimensions', () => {
const DEGENERATE: ReadonlyArray<[number, number]> = [
[1, 25_000_000],
[25_000_000, 1],
[1, 0xffffffff],
[0xffffffff, 1],
[0, 0xffffffff],
[0xffffffff, 0xffffffff],
[-1, -25_000_000],
[Number.MAX_SAFE_INTEGER, 1],
[1, Number.MAX_SAFE_INTEGER],
];
for (const [desiredWidth, desiredHeight] of DEGENERATE) {
it(`stays small for ${desiredWidth}x${desiredHeight}`, async () => {
const image = await loadImage(desiredWidth, desiredHeight);
assert.isAtLeast(image.naturalWidth, 4);
assert.isAtLeast(image.naturalHeight, 4);
assert.isAtMost(image.naturalWidth, 256);
assert.isAtMost(image.naturalHeight, 256);
});
}
});
});
+14 -5
View File
@@ -46,6 +46,12 @@ const BITMAP_HEADER = new Uint8Array([
]);
const PIXEL_COUNT = 32 * 32;
const MIN_DIMENSION = 4;
// width * height = PIXEL_COUNT, so aspect_ratio = PIXEL_COUNT / (height * height), which is
// maximized when height is small
const MAX_ASPECT_RATIO = PIXEL_COUNT / (MIN_DIMENSION * MIN_DIMENSION); // 64
const MIN_ASPECT_RATIO = 1 / MAX_ASPECT_RATIO;
function writeUInt32LE(
bytes: Uint8Array<ArrayBuffer>,
@@ -68,9 +74,14 @@ export function computeBlurHashUrl(
desiredWidth = 1,
desiredHeight = 1
): string {
const invAspect =
const rawInvAspect =
(Math.abs(desiredHeight) + 1e-23) / (Math.abs(desiredWidth) + 1e-23);
const invAspect = Math.min(
MAX_ASPECT_RATIO,
Math.max(MIN_ASPECT_RATIO, rawInvAspect)
);
// Calculate width and height that roughly satisfy the desired PIXEL_COUNT
//
// height = invAspect * width
@@ -85,14 +96,12 @@ export function computeBlurHashUrl(
// oxlint-disable-next-line no-bitwise
width <<= 2;
// Give at least two pixels of width to show gradients
width = Math.max(2, width);
width = Math.max(MIN_DIMENSION, width);
let height = width * invAspect;
height = Math.round(height);
// Minimum two pixels of height for gradients
height = Math.max(2, height);
height = Math.max(MIN_DIMENSION, height);
const rgba = decode(blurHash, width, height);
const bgrSize = (rgba.byteLength / 4) * 3;