#javascript #bit-manipulation #arraybuffer #uint8array
Вопрос:
Мне нужно добавить сжатие в мой проект, и я решил использовать алгоритм LZJB, который является быстрым, а код небольшим. Нашел эту библиотеку https://github.com/copy/jslzjb-k
Но API не очень хорош, потому что для распаковки файла вам нужна длина входного буфера (поскольку Uint8Array не является динамическим, вам нужно выделить некоторые данные). Итак, я хочу сохранить длину входного буфера как первые несколько байтов Uint8Array, чтобы я мог извлечь это значение и создать выходной Uint8Array на основе этого целого значения.
Я хочу, чтобы функция, которая возвращает Uint8Array из integer, была общей, возможно, сохраните длину байтов в первом байте, чтобы вы знали, сколько данных вам нужно извлечь, чтобы прочитать целое число. Я думаю, мне нужно извлечь эти байты и использовать некоторый сдвиг битов, чтобы получить исходное число. Но я не совсем уверен, как это сделать.
Итак, как я могу написать универсальную функцию, которая преобразует целое число в Uint8Array, которое можно встроить в больший массив, а затем извлечь это число?
Ответ №1:
Общий ответ
Эти функции позволяют любому целому числу (оно использует BigInts внутри, но может принимать числовые аргументы) кодироваться и декодироваться из любой части a Uint8Array
. Это несколько излишне, но я хотел научиться работать с целыми числами произвольного размера в JS.
// n can be a bigint or a number
// bs is an optional Uint8Array of sufficient size
// if unspecified, a large-enough Uint8Array will be allocated
// start (optional) is the offset
// where the length-prefixed number will be written
// returns the resulting Uint8Array
function writePrefixedNum(n, bs, start) {
start = start || 0;
let len = start 2; // start, length, and 1 byte min
for (let i=0x100n; i<n; i<<=8n, len ) /* increment length */;
if (bs === undefined) {
bs = new Uint8Array(len);
} else if (bs.length < len) {
throw `byte array too small; ${bs.length} < ${len}`;
}
let r = BigInt(n);
for (let pos = start 1; pos < len; pos ) {
bs[pos] = Number(r amp; 0xffn);
r >>= 8n;
}
bs[start] = len-start-1; // write byte-count to start byte
return bs;
}
// bs must be a Uint8Array from where the number will be read
// start (optional, defaults to 0)
// is where the length-prefixed number can be found
// returns a bigint, which can be coerced to int using Number()
function readPrefixedNum(bs, start) {
start = start || 0;
let size = bs[start]; // read byte-count from start byte
let n = 0n;
if (bs.length < start size) {
throw `byte array too small; ${bs.length} < ${start size}`;
}
for (let pos = start size; pos >= start 1; pos --) {
n <<= 8n;
n |= BigInt(bs[pos])
}
return n;
}
function test(n) {
const array = undefined;
const offset = 2;
let bs = writePrefixedNum(n, undefined, offset);
console.log(bs);
let result = readPrefixedNum(bs, offset);
console.log(n, result, "correct?", n == result)
}
test(0)
test(0x1020304050607080n)
test(0x0807060504030201n)
Простой 4-байтовый ответ
Этот ответ кодирует 4-байтовые целые числа в и из Uint8Array
s.
function intToArray(i) {
return Uint8Array.of(
(iamp;0xff000000)>>24,
(iamp;0x00ff0000)>>16,
(iamp;0x0000ff00)>> 8,
(iamp;0x000000ff)>> 0);
}
function arrayToInt(bs, start) {
start = start || 0;
const bytes = bs.subarray(start, start 4);
let n = 0;
for (const byte of bytes.values()) {
n = (n<<8)|byte;
}
return n;
}
for (let v of [123, 123<<8, 123<<16, 123<<24]) {
let a = intToArray(v);
let r = arrayToInt(a, 0);
console.log(v, a, r);
}
Комментарии:
1. Почему
arrayToInt(new Uint8Array([255, 255, 255, 255]))
возвращает-1
, а не максимальное значение?2. @jcubic Это потому, что отрицательные значения представлены в двоичном формате. Прочитайте о дополнении two .
arrayToInt(new Uint8Array([127, 255, 255, 255]))
это максимальное 32-разрядное значение со знаком.3.
arrayToInt(new Uint8Array([127, 255, 255, 255]))
ВОЗВРАТ0
.4. Есть 2 аргумента для arrayToInt (потому что вы сказали, что хотите извлекать числа из больших байтовых массивов). Если вы укажете аргумент (или используете новую версию, в которой по умолчанию он равен 0), он работает — потому что, помимо порядка байтов, он выполняет точно то же самое, что и ваша функция.
5. хорошо, извините, не заметил аргумент, потому что, когда я тестировал ваше решение в первый раз, когда я использовал
function arrayToInt(bs, start = 0) {
, именно поэтому я прокомментировал, что оно вернуло -1, и вы прокомментировали тот же код и не исправили его вторым аргументом.
Ответ №2:
Вот рабочие функции (основанные на преобразовании целого числа javascript в массив байтов и обратно)
function numberToBytes(number) {
// you can use constant number of bytes by using 8 or 4
const len = Math.ceil(Math.log2(number) / 8);
const byteArray = new Uint8Array(len);
for (let index = 0; index < byteArray.length; index ) {
const byte = number amp; 0xff;
byteArray[index] = byte;
number = (number - byte) / 256;
}
return byteArray;
}
function bytesToNumber(byteArray) {
let result = 0;
for (let i = byteArray.length - 1; i >= 0; i--) {
result = (result * 256) byteArray[i];
}
return resu<
}
при использовании const len = Math.ceil(Math.log2(number) / 8);
массива нужны только байты. Если вам нужен фиксированный размер, вы можете использовать константу 8
или 4
.
В моем случае я просто сохранил длину байтов в первом байте.
Комментарии:
1. Вы скрыли дополнительную сложность сохранения / чтения 1-го байта, указывающего длину результирующего байтового массива. Также обратите внимание, что тип JS Number не может точно представлять числа более ~ 2 ^ 53, поэтому вам никогда не понадобится больше 7 байт или меньше 1. Мой ответ ограничен (общим) случаем чисел в диапазоне 0-2 ^ 31, избегая этой сложности, предполагая 4-байтовые целые числа.
2. @tucuxi Я хочу, чтобы это было сжатие файлов, если я буду использовать ваш подход, я смогу сохранять файлы только около 1,9 ГБ, также ваш код не работает для вашего примера в комментарии.
3. @tucuxi Я также предпочитаю свое решение, потому что оно действительно работает.
4. пожалуйста, объясните, как ваш ответ работает за пределами 2 ^ 53 — 1 (предел
number
передаваемого вами аргумента). Вы вызывалиarrayToInt
только с 1 аргументом. Я по умолчанию присвоил этому аргументу значение 0. Это работало раньше (при передаче всех аргументов), теперь это также работает, даже когда вы не смотрите на код или не читаете примеры.5. @tucuxi Я предпочитаю свое решение, потому что оно экономит до 7 байт (чтобы не потерять точность), и если кому-то нужно постоянное количество байтов, он может использовать константу для
len
8 или 4.