Uint8Array a cadena en Javascript

122

Tengo algunos datos codificados en UTF-8 que viven en un rango de elementos Uint8Array en Javascript. ¿Existe una forma eficiente de decodificarlos en una cadena javascript normal (creo que Javascript usa Unicode de 16 bits)? No quiero agregar un carácter a la vez ya que la concatenación de cadenas se volvería intensiva en la CPU.

Jack Wester
fuente
No estoy seguro de si funcionará, pero lo uso u8array.toString()cuando leo archivos de BrowserFS que exponen el objeto Uint8Array cuando llamas fs.readFile.
jcubic
1
@jcubic para mí, toStringen Uint8Arraydevuelve números separados por comas como "91,50,48,49,57,45"(Chrome 79)
kolen

Respuestas:

171

TextEncodery TextDecoderdel estándar Encoding , que está polyrellenado por la biblioteca stringencoding , convierte entre cadenas y ArrayBuffers:

var uint8array = new TextEncoder("utf-8").encode("¢");
var string = new TextDecoder("utf-8").decode(uint8array);
Vincent Scheib
fuente
39
Para cualquier persona perezosa como yo, npm install text-encoding, var textEncoding = require('text-encoding'); var TextDecoder = textEncoding.TextDecoder;. No, gracias.
Evan Hu
16
cuidado con la biblioteca de codificación de texto npm, el analizador de paquetes webpack muestra que la biblioteca es ENORME
wayofthefuture
3
@VincentScheib Browsers eliminó el soporte para cualquier otro formato excepto utf-8. Entonces, ¡el TextEncoderargumento es innecesario!
Tripulse
1
nodejs.org/api/string_decoder.html del ejemplo: const {StringDecoder} = require ('string_decoder'); const decoder = new StringDecoder ('utf8'); const ciento = Buffer.from ([0xC2, 0xA2]); console.log (decoder.write (cent));
curist
4
Tenga en cuenta que Node.js agregó las TextEncoder/ TextDecoderAPI en v11, por lo que no es necesario instalar ningún paquete adicional si solo apunta a las versiones actuales de Node.
Loilo
42

Esto debería funcionar:

// http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt

/* utf.js - UTF-8 <=> UTF-16 convertion
 *
 * Copyright (C) 1999 Masanao Izumo <[email protected]>
 * Version: 1.0
 * LastModified: Dec 25 1999
 * This library is free.  You can redistribute it and/or modify it.
 */

function Utf8ArrayToStr(array) {
    var out, i, len, c;
    var char2, char3;

    out = "";
    len = array.length;
    i = 0;
    while(i < len) {
    c = array[i++];
    switch(c >> 4)
    { 
      case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
        // 0xxxxxxx
        out += String.fromCharCode(c);
        break;
      case 12: case 13:
        // 110x xxxx   10xx xxxx
        char2 = array[i++];
        out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
        break;
      case 14:
        // 1110 xxxx  10xx xxxx  10xx xxxx
        char2 = array[i++];
        char3 = array[i++];
        out += String.fromCharCode(((c & 0x0F) << 12) |
                       ((char2 & 0x3F) << 6) |
                       ((char3 & 0x3F) << 0));
        break;
    }
    }

    return out;
}

Es algo más limpio que las otras soluciones porque no usa ningún truco ni depende de las funciones del navegador JS, por ejemplo, también funciona en otros entornos JS.

Consulte la demostración de JSFiddle .

Consulte también las preguntas relacionadas: aquí y aquí

Albert
fuente
6
Esto parece un poco lento. Pero el único fragmento del universo que encontré que funciona. Buen hallazgo + adopción!
Redsandro
6
No entiendo por qué esto no tiene más votos a favor. Parece eminentemente sensato pasar por la convención UTF-8 para pequeños fragmentos. Async Blob + Filereader funciona muy bien para textos grandes, como han indicado otros.
DanHorner
2
La pregunta era cómo hacer esto sin la concatenación de cadenas
Jack Wester
5
Funciona muy bien, excepto que no maneja secuencias de 4+ bytes, por ejemplo, fromUTF8Array([240,159,154,133])resulta vacío (mientras fromUTF8Array([226,152,131])→"☃")
unhammer
1
¿Por qué se excluyen los casos 8, 9, 10 y 11? ¿Alguna razón en particular? Y el caso 15 también es posible, ¿verdad? 15 (1111) indicará que se utilizan 4 bytes, ¿no es así?
RaR
31

Esto es lo que uso:

var str = String.fromCharCode.apply(null, uint8Arr);
dlchambers
fuente
7
Desde el documento , esto no parece decodificar UTF8.
Albert
29
Esto arrojará RangeErrortextos más grandes. "Se superó el tamaño máximo de la pila de llamadas"
Redsandro
1
Si está convirtiendo Uint8Arrays grandes en cadenas binarias y obtiene RangeError, consulte la función Uint8ToString de stackoverflow.com/a/12713326/471341 .
yonran
IE 11 lanza SCRIPT28: Out of stack spacecuando le doy 300 + k caracteres, o RangeErrorpara Chrome 39. Firefox 33 está bien. 100 + k funciona bien con los tres.
Sheepy
Esto no produce el resultado correcto del ejemplo de caracteres Unicode en en.wikipedia.org/wiki/UTF-8 . por ejemplo, String.fromCharCode.apply (null, new Uint8Array ([0xc2, 0xa2])) no produce ¢.
Vincent Scheib
16

Se encuentra en una de las aplicaciones de muestra de Chrome, aunque está destinado a bloques de datos más grandes en los que está de acuerdo con una conversión asincrónica.

/**
 * Converts an array buffer to a string
 *
 * @private
 * @param {ArrayBuffer} buf The buffer to convert
 * @param {Function} callback The function to call when conversion is complete
 */
function _arrayBufferToString(buf, callback) {
  var bb = new Blob([new Uint8Array(buf)]);
  var f = new FileReader();
  f.onload = function(e) {
    callback(e.target.result);
  };
  f.readAsText(bb);
}
Will Scott
fuente
2
Como dijiste, esto funcionaría terriblemente a menos que el búfer para convertir sea realmente enorme. La conversión sincrónica de UTF-8 a wchar de una cadena simple (digamos 10-40 bytes) implementada en, digamos, V8 debería ser mucho menor que un microsegundo, mientras que supongo que su código requeriría cientos de veces eso. Gracias de todos modos.
Jack Wester
15

En Nodo, las " Bufferinstancias también son Uint8Arrayinstancias ", por lo que buf.toString()funciona en este caso.

kpowz
fuente
Funciona muy bien para mi. ¡Y tan simple! Pero, de hecho, Uint8Array tiene el método toString ().
Doom
Simple y elegante, no sabía Bufferque también es Uint8Array. ¡Gracias!
LeOn - Han Li
1
@doom En el lado del navegador, Uint8Array.toString () no compilará una cadena utf-8, enumerará los valores numéricos en la matriz. Entonces, si lo que tiene es un Uint8Array de otra fuente que no es también un Buffer, deberá crear uno para hacer la magia:Buffer.from(uint8array).toString('utf-8')
Joachim Lous
12

La solución proporcionada por Albert funciona bien siempre y cuando la función proporcionada se invoque con poca frecuencia y solo se use para matrices de tamaño modesto; de lo contrario, es extremadamente ineficiente. Aquí hay una solución de JavaScript vainilla mejorada que funciona tanto para Node como para navegadores y tiene las siguientes ventajas:

• Funciona de manera eficiente para todos los tamaños de matriz de octetos

• No genera cadenas intermedias desechables

• Admite caracteres de 4 bytes en motores JS modernos (de lo contrario, se sustituye "?")

var utf8ArrayToStr = (function () {
    var charCache = new Array(128);  // Preallocate the cache for the common single byte chars
    var charFromCodePt = String.fromCodePoint || String.fromCharCode;
    var result = [];

    return function (array) {
        var codePt, byte1;
        var buffLen = array.length;

        result.length = 0;

        for (var i = 0; i < buffLen;) {
            byte1 = array[i++];

            if (byte1 <= 0x7F) {
                codePt = byte1;
            } else if (byte1 <= 0xDF) {
                codePt = ((byte1 & 0x1F) << 6) | (array[i++] & 0x3F);
            } else if (byte1 <= 0xEF) {
                codePt = ((byte1 & 0x0F) << 12) | ((array[i++] & 0x3F) << 6) | (array[i++] & 0x3F);
            } else if (String.fromCodePoint) {
                codePt = ((byte1 & 0x07) << 18) | ((array[i++] & 0x3F) << 12) | ((array[i++] & 0x3F) << 6) | (array[i++] & 0x3F);
            } else {
                codePt = 63;    // Cannot convert four byte code points, so use "?" instead
                i += 3;
            }

            result.push(charCache[codePt] || (charCache[codePt] = charFromCodePt(codePt)));
        }

        return result.join('');
    };
})();
Bob Arlof
fuente
2
La mejor solución aquí, ya que también maneja caracteres de 4 bytes (por ejemplo, emojis) ¡Gracias!
fiffy
1
y ¿cuál es la inversa de esto?
simbo1905
6

Haga lo que dijo @Sudhir, y luego para obtener una cadena de la lista de números separados por comas, use:

for (var i=0; i<unitArr.byteLength; i++) {
            myString += String.fromCharCode(unitArr[i])
        }

Esto le dará la cadena que desea, si aún es relevante

shuki
fuente
Lo siento, no he notado la última frase en la que dijiste que no querías agregar un carácter a la vez. Espero que esto ayude a otros que no tienen problemas con el uso de la CPU.
shuki
14
Esto no realiza la decodificación UTF8.
Albert
Aún más corto: String.fromCharCode.apply(null, unitArr);. Como se mencionó, no maneja la codificación UTF8, pero a veces esto es lo suficientemente simple si solo necesita soporte ASCII pero no tiene acceso a TextEncoder / TextDecoder.
Ravenstine
La respuesta menciona un @Sudhir pero busqué en la página y encontré esa respuesta. Así que sería mejor incluir en línea todo lo que dijo
Joakim
Esto tendrá un rendimiento terrible en cadenas más largas. No use el operador + en cadenas.
Max
3

Si no puede usar la API TextDecoder porque no es compatible con IE :

  1. Puede utilizar el polyfill FastestSmallestTextEncoderDecoder recomendado por el sitio web de Mozilla Developer Network ;
  2. Puede utilizar esta función que también se proporciona en el sitio web de MDN :

function utf8ArrayToString(aBytes) {
    var sView = "";
    
    for (var nPart, nLen = aBytes.length, nIdx = 0; nIdx < nLen; nIdx++) {
        nPart = aBytes[nIdx];
        
        sView += String.fromCharCode(
            nPart > 251 && nPart < 254 && nIdx + 5 < nLen ? /* six bytes */
                /* (nPart - 252 << 30) may be not so safe in ECMAScript! So...: */
                (nPart - 252) * 1073741824 + (aBytes[++nIdx] - 128 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
            : nPart > 247 && nPart < 252 && nIdx + 4 < nLen ? /* five bytes */
                (nPart - 248 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
            : nPart > 239 && nPart < 248 && nIdx + 3 < nLen ? /* four bytes */
                (nPart - 240 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
            : nPart > 223 && nPart < 240 && nIdx + 2 < nLen ? /* three bytes */
                (nPart - 224 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
            : nPart > 191 && nPart < 224 && nIdx + 1 < nLen ? /* two bytes */
                (nPart - 192 << 6) + aBytes[++nIdx] - 128
            : /* nPart < 127 ? */ /* one byte */
                nPart
        );
    }
    
    return sView;
}

let str = utf8ArrayToString([50,72,226,130,130,32,43,32,79,226,130,130,32,226,135,140,32,50,72,226,130,130,79]);

// Must show 2H₂ + O₂ ⇌ 2H₂O
console.log(str);

Rosberg Linhares
fuente
2

Prueba estas funciones,

var JsonToArray = function(json)
{
    var str = JSON.stringify(json, null, 0);
    var ret = new Uint8Array(str.length);
    for (var i = 0; i < str.length; i++) {
        ret[i] = str.charCodeAt(i);
    }
    return ret
};

var binArrayToJson = function(binArray)
{
    var str = "";
    for (var i = 0; i < binArray.length; i++) {
        str += String.fromCharCode(parseInt(binArray[i]));
    }
    return JSON.parse(str)
}

fuente: https://gist.github.com/tomfa/706d10fed78c497731ac , felicitaciones a Tomfa

serdarsenay
fuente
2

Me frustró ver que la gente no mostraba cómo ir en ambos sentidos o mostraba que las cosas no funcionan en cadenas UTF8 triviales. Encontré una publicación en codereview.stackexchange.com que tiene un código que funciona bien. Lo usé para convertir runas antiguas en bytes, para probar algunas criptomonedas en los bytes y luego convertir las cosas en una cadena. El código de trabajo está en github aquí . Cambié el nombre de los métodos para mayor claridad:

// https://codereview.stackexchange.com/a/3589/75693
function bytesToSring(bytes) {
    var chars = [];
    for(var i = 0, n = bytes.length; i < n;) {
        chars.push(((bytes[i++] & 0xff) << 8) | (bytes[i++] & 0xff));
    }
    return String.fromCharCode.apply(null, chars);
}

// https://codereview.stackexchange.com/a/3589/75693
function stringToBytes(str) {
    var bytes = [];
    for(var i = 0, n = str.length; i < n; i++) {
        var char = str.charCodeAt(i);
        bytes.push(char >>> 8, char & 0xFF);
    }
    return bytes;
}

La prueba unitaria utiliza esta cadena UTF-8:

    // http://kermitproject.org/utf8.html
    // From the Anglo-Saxon Rune Poem (Rune version) 
    const secretUtf8 = `ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ
ᛋᚳᛖᚪᛚ᛫ᚦᛖᚪᚻ᛫ᛗᚪᚾᚾᚪ᛫ᚷᛖᚻᚹᛦᛚᚳ᛫ᛗᛁᚳᛚᚢᚾ᛫ᚻᛦᛏ᛫ᛞᚫᛚᚪᚾ
ᚷᛁᚠ᛫ᚻᛖ᛫ᚹᛁᛚᛖ᛫ᚠᚩᚱ᛫ᛞᚱᛁᚻᛏᚾᛖ᛫ᛞᚩᛗᛖᛋ᛫ᚻᛚᛇᛏᚪᚾ᛬`;

Tenga en cuenta que la longitud de la cadena es de solo 117 caracteres, pero la longitud del byte, cuando se codifica, es de 234.

Si elimino el comentario de las líneas de console.log, puedo ver que la cadena que se decodifica es la misma cadena que se codificó (¡con los bytes pasados ​​a través del algoritmo de intercambio secreto de Shamir!):

prueba unitaria que muestra la codificación y decodificación

simbo1905
fuente
String.fromCharCode.apply(null, chars)Error si charses demasiado grande.
Marc J. Schmidt
1

En NodeJS, tenemos búferes disponibles y la conversión de cadenas con ellos es realmente fácil. Mejor aún, es fácil convertir un Uint8Array en un búfer. Pruebe este código, me funcionó en Node básicamente para cualquier conversión que involucre Uint8Arrays:

let str = Buffer.from(uint8arr.buffer).toString();

Solo estamos extrayendo el ArrayBuffer de Uint8Array y luego convirtiéndolo en un NodeJS Buffer adecuado. Luego, convertimos el búfer en una cadena (puede incluir una codificación hexadecimal o base64 si lo desea).

Si queremos volver a convertir a Uint8Array desde una cadena, haríamos esto:

let uint8arr = new Uint8Array(Buffer.from(str));

Tenga en cuenta que si declaró una codificación como base64 al convertir a una cadena, entonces tendría que usar Buffer.from(str, "base64") si usó base64, o cualquier otra codificación que haya usado.

¡Esto no funcionará en el navegador sin un módulo! Los búferes de NodeJS simplemente no existen en el navegador, por lo que este método no funcionará a menos que agregue la funcionalidad de búfer al navegador. Sin embargo, eso es bastante fácil de hacer, solo use un módulo como este , que es pequeño y rápido.

Arctic_Hen7
fuente
0
class UTF8{
static encode(str:string){return new UTF8().encode(str)}
static decode(data:Uint8Array){return new UTF8().decode(data)}

private EOF_byte:number = -1;
private EOF_code_point:number = -1;
private encoderError(code_point) {
    console.error("UTF8 encoderError",code_point)
}
private decoderError(fatal, opt_code_point?):number {
    if (fatal) console.error("UTF8 decoderError",opt_code_point)
    return opt_code_point || 0xFFFD;
}
private inRange(a:number, min:number, max:number) {
    return min <= a && a <= max;
}
private div(n:number, d:number) {
    return Math.floor(n / d);
}
private stringToCodePoints(string:string) {
    /** @type {Array.<number>} */
    let cps = [];
    // Based on http://www.w3.org/TR/WebIDL/#idl-DOMString
    let i = 0, n = string.length;
    while (i < string.length) {
        let c = string.charCodeAt(i);
        if (!this.inRange(c, 0xD800, 0xDFFF)) {
            cps.push(c);
        } else if (this.inRange(c, 0xDC00, 0xDFFF)) {
            cps.push(0xFFFD);
        } else { // (inRange(c, 0xD800, 0xDBFF))
            if (i == n - 1) {
                cps.push(0xFFFD);
            } else {
                let d = string.charCodeAt(i + 1);
                if (this.inRange(d, 0xDC00, 0xDFFF)) {
                    let a = c & 0x3FF;
                    let b = d & 0x3FF;
                    i += 1;
                    cps.push(0x10000 + (a << 10) + b);
                } else {
                    cps.push(0xFFFD);
                }
            }
        }
        i += 1;
    }
    return cps;
}

private encode(str:string):Uint8Array {
    let pos:number = 0;
    let codePoints = this.stringToCodePoints(str);
    let outputBytes = [];

    while (codePoints.length > pos) {
        let code_point:number = codePoints[pos++];

        if (this.inRange(code_point, 0xD800, 0xDFFF)) {
            this.encoderError(code_point);
        }
        else if (this.inRange(code_point, 0x0000, 0x007f)) {
            outputBytes.push(code_point);
        } else {
            let count = 0, offset = 0;
            if (this.inRange(code_point, 0x0080, 0x07FF)) {
                count = 1;
                offset = 0xC0;
            } else if (this.inRange(code_point, 0x0800, 0xFFFF)) {
                count = 2;
                offset = 0xE0;
            } else if (this.inRange(code_point, 0x10000, 0x10FFFF)) {
                count = 3;
                offset = 0xF0;
            }

            outputBytes.push(this.div(code_point, Math.pow(64, count)) + offset);

            while (count > 0) {
                let temp = this.div(code_point, Math.pow(64, count - 1));
                outputBytes.push(0x80 + (temp % 64));
                count -= 1;
            }
        }
    }
    return new Uint8Array(outputBytes);
}

private decode(data:Uint8Array):string {
    let fatal:boolean = false;
    let pos:number = 0;
    let result:string = "";
    let code_point:number;
    let utf8_code_point = 0;
    let utf8_bytes_needed = 0;
    let utf8_bytes_seen = 0;
    let utf8_lower_boundary = 0;

    while (data.length > pos) {
        let _byte = data[pos++];

        if (_byte == this.EOF_byte) {
            if (utf8_bytes_needed != 0) {
                code_point = this.decoderError(fatal);
            } else {
                code_point = this.EOF_code_point;
            }
        } else {
            if (utf8_bytes_needed == 0) {
                if (this.inRange(_byte, 0x00, 0x7F)) {
                    code_point = _byte;
                } else {
                    if (this.inRange(_byte, 0xC2, 0xDF)) {
                        utf8_bytes_needed = 1;
                        utf8_lower_boundary = 0x80;
                        utf8_code_point = _byte - 0xC0;
                    } else if (this.inRange(_byte, 0xE0, 0xEF)) {
                        utf8_bytes_needed = 2;
                        utf8_lower_boundary = 0x800;
                        utf8_code_point = _byte - 0xE0;
                    } else if (this.inRange(_byte, 0xF0, 0xF4)) {
                        utf8_bytes_needed = 3;
                        utf8_lower_boundary = 0x10000;
                        utf8_code_point = _byte - 0xF0;
                    } else {
                        this.decoderError(fatal);
                    }
                    utf8_code_point = utf8_code_point * Math.pow(64, utf8_bytes_needed);
                    code_point = null;
                }
            } else if (!this.inRange(_byte, 0x80, 0xBF)) {
                utf8_code_point = 0;
                utf8_bytes_needed = 0;
                utf8_bytes_seen = 0;
                utf8_lower_boundary = 0;
                pos--;
                code_point = this.decoderError(fatal, _byte);
            } else {
                utf8_bytes_seen += 1;
                utf8_code_point = utf8_code_point + (_byte - 0x80) * Math.pow(64, utf8_bytes_needed - utf8_bytes_seen);

                if (utf8_bytes_seen !== utf8_bytes_needed) {
                    code_point = null;
                } else {
                    let cp = utf8_code_point;
                    let lower_boundary = utf8_lower_boundary;
                    utf8_code_point = 0;
                    utf8_bytes_needed = 0;
                    utf8_bytes_seen = 0;
                    utf8_lower_boundary = 0;
                    if (this.inRange(cp, lower_boundary, 0x10FFFF) && !this.inRange(cp, 0xD800, 0xDFFF)) {
                        code_point = cp;
                    } else {
                        code_point = this.decoderError(fatal, _byte);
                    }
                }

            }
        }
        //Decode string
        if (code_point !== null && code_point !== this.EOF_code_point) {
            if (code_point <= 0xFFFF) {
                if (code_point > 0)result += String.fromCharCode(code_point);
            } else {
                code_point -= 0x10000;
                result += String.fromCharCode(0xD800 + ((code_point >> 10) & 0x3ff));
                result += String.fromCharCode(0xDC00 + (code_point & 0x3ff));
            }
        }
    }
    return result;
}

'

terran
fuente
Agrega una descripción para responder. @terran
Rohit Poudel
-3

Estoy usando este fragmento de Typecript:

function UInt8ArrayToString(uInt8Array: Uint8Array): string
{
    var s: string = "[";
    for(var i: number = 0; i < uInt8Array.byteLength; i++)
    {
        if( i > 0 )
            s += ", ";
        s += uInt8Array[i];
    }
    s += "]";
    return s;
}

Elimine las anotaciones de tipo si necesita la versión de JavaScript. ¡Espero que esto ayude!

Bernd Paradies
fuente
3
El OP pidió no agregar un carácter a la vez. Además, no quiere mostrarlo como una representación de cadena de una lista, sino simplemente como una cadena. Además, esto no convierte los caracteres en una cadena, pero muestra su número.
Albert