web-dev-qa-db-fra.com

Échec de l'exécution de 'btoa' sur 'Window': La chaîne à coder contient des caractères situés en dehors de la plage Latin1.

L'erreur dans le titre est levée uniquement dans Google Chrome, selon mes tests. Je code en base64 un gros fichier XML pour pouvoir le télécharger:

this.loader.src = "data:application/x-forcedownload;base64,"+
                  btoa("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
                  +"<"+this.gamesave.tagName+">"
                  +this.xml.firstChild.innerHTML
                  +"</"+this.gamesave.tagName+">");

this.loader est caché iframe.

Cette erreur est en fait un changement important car normalement, Google Chrome se bloquait lors de l'appel de btoa. Mozilla Firefox n'a pas de problèmes ici, donc le problème est lié au navigateur. Je ne suis au courant d'aucun caractère étrange dans le fichier. En fait, je pense qu’il n’ya pas de personnages non ascii.

Q: Comment trouver les caractères problématiques et les remplacer afin que Chrome cesse de se plaindre?

J'ai essayé d'utiliser Downloadify pour lancer le téléchargement, mais cela ne fonctionne pas. Il n'est pas fiable et ne génère aucune erreur pour permettre le débogage.

99
Tomáš Zato

Si vous avez UTF8, utilisez ceci (fonctionne réellement avec le source SVG), comme:

btoa(unescape(encodeURIComponent(str)))

exemple:

 var imgsrc = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(markup)));
 var img = new Image(1, 1); // width, height values are optional params 
 img.src = imgsrc;

Si vous avez besoin de décoder cette base64, utilisez ceci:

var str2 = decodeURIComponent(escape(window.atob(b64)));
console.log(str2);

Exemple:

var str = "äöüÄÖÜçéèñ";
var b64 = window.btoa(unescape(encodeURIComponent(str)))
console.log(b64);

var str2 = decodeURIComponent(escape(window.atob(b64)));
console.log(str2);

Remarque: Si vous souhaitez que cela fonctionne dans mobile-safari, vous devrez peut-être effacer tout l'espace blanc des données base64 ...

function b64_to_utf8( str ) {
    str = str.replace(/\s/g, '');    
    return decodeURIComponent(escape(window.atob( str )));
}

Mise à jour 2017

Ce problème m'a encore embêté.
En réalité, atob ne gère pas vraiment les chaînes UTF8 - c’est uniquement ASCII.
En outre, je n’utiliserais pas bloatware, comme js-base64.
Mais webtoolkit a une petite implémentation, agréable et très maintenable:

/**
*
*  Base64 encode / decode
*  http://www.webtoolkit.info
*
**/
var Base64 = {

    // private property
    _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="

    // public method for encoding
    , encode: function (input)
    {
        var output = "";
        var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
        var i = 0;

        input = Base64._utf8_encode(input);

        while (i < input.length)
        {
            chr1 = input.charCodeAt(i++);
            chr2 = input.charCodeAt(i++);
            chr3 = input.charCodeAt(i++);

            enc1 = chr1 >> 2;
            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
            enc4 = chr3 & 63;

            if (isNaN(chr2))
            {
                enc3 = enc4 = 64;
            }
            else if (isNaN(chr3))
            {
                enc4 = 64;
            }

            output = output +
                this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
                this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
        } // Whend 

        return output;
    } // End Function encode 


    // public method for decoding
    ,decode: function (input)
    {
        var output = "";
        var chr1, chr2, chr3;
        var enc1, enc2, enc3, enc4;
        var i = 0;

        input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
        while (i < input.length)
        {
            enc1 = this._keyStr.indexOf(input.charAt(i++));
            enc2 = this._keyStr.indexOf(input.charAt(i++));
            enc3 = this._keyStr.indexOf(input.charAt(i++));
            enc4 = this._keyStr.indexOf(input.charAt(i++));

            chr1 = (enc1 << 2) | (enc2 >> 4);
            chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
            chr3 = ((enc3 & 3) << 6) | enc4;

            output = output + String.fromCharCode(chr1);

            if (enc3 != 64)
            {
                output = output + String.fromCharCode(chr2);
            }

            if (enc4 != 64)
            {
                output = output + String.fromCharCode(chr3);
            }

        } // Whend 

        output = Base64._utf8_decode(output);

        return output;
    } // End Function decode 


    // private method for UTF-8 encoding
    ,_utf8_encode: function (string)
    {
        var utftext = "";
        string = string.replace(/\r\n/g, "\n");

        for (var n = 0; n < string.length; n++)
        {
            var c = string.charCodeAt(n);

            if (c < 128)
            {
                utftext += String.fromCharCode(c);
            }
            else if ((c > 127) && (c < 2048))
            {
                utftext += String.fromCharCode((c >> 6) | 192);
                utftext += String.fromCharCode((c & 63) | 128);
            }
            else
            {
                utftext += String.fromCharCode((c >> 12) | 224);
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                utftext += String.fromCharCode((c & 63) | 128);
            }

        } // Next n 

        return utftext;
    } // End Function _utf8_encode 

    // private method for UTF-8 decoding
    ,_utf8_decode: function (utftext)
    {
        var string = "";
        var i = 0;
        var c, c1, c2, c3;
        c = c1 = c2 = 0;

        while (i < utftext.length)
        {
            c = utftext.charCodeAt(i);

            if (c < 128)
            {
                string += String.fromCharCode(c);
                i++;
            }
            else if ((c > 191) && (c < 224))
            {
                c2 = utftext.charCodeAt(i + 1);
                string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
                i += 2;
            }
            else
            {
                c2 = utftext.charCodeAt(i + 1);
                c3 = utftext.charCodeAt(i + 2);
                string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
                i += 3;
            }

        } // Whend 

        return string;
    } // End Function _utf8_decode 

}

https://www.fileformat.info/info/unicode/utf8.htm

  • Pour tout caractère égal ou inférieur à 127 (hex 0x7F), la représentation UTF-8 est un octet. Ce ne sont que les 7 bits les plus bas de la valeur Unicode complète. C'est également la même chose que la valeur ASCII.

  • Pour les caractères inférieurs ou égaux à 2047 (hex 0x07FF), la représentation UTF-8 est répartie sur deux octets. Les deux bits de poids fort sont définis sur le premier octet et le troisième est effacé (c'est-à-dire 0xC2 à 0xDF). Le deuxième octet aura le bit de haut défini et le second effacé (c'est-à-dire 0x80 à 0xBF).

  • Pour tous les caractères égaux ou supérieurs à 2048 mais inférieurs à 65535 (0xFFFF), la représentation UTF-8 est répartie sur trois octets.

159
Stefan Steiger

Utiliser btoa avec unescape et encodeURIComponent ne fonctionnait pas pour moi. Remplacer tous les caractères spéciaux par des entités XML/HTML, puis convertir en représentation base64 était le seul moyen de résoudre ce problème pour moi. Un code:

base64 = btoa(str.replace(/[\u00A0-\u2666]/g, function(c) {
    return '&#' + c.charCodeAt(0) + ';';
}));
11
Italo Borssatto

J'ai juste pensé que je devrais partager comment j'ai résolu le problème et pourquoi je pense que c'est la bonne solution (à condition que vous n'optimisiez pas avec l'ancien navigateur) .

Conversion de données en dataURL (data: ...)

var blob = new Blob(
              // I'm using page innerHTML as data
              // note that you can use the array
              // to concatenate many long strings EFFICIENTLY
              [document.body.innerHTML],
              // Mime type is important for data url
              {type : 'text/html'}
); 
// This FileReader works asynchronously, so it doesn't lag
// the web application
var a = new FileReader();
a.onload = function(e) {
     // Capture result here
     console.log(e.target.result);
};
a.readAsDataURL(blob);

Permettre à l'utilisateur de sauvegarder des données

Outre la solution évidente - en ouvrant une nouvelle fenêtre avec votre URL de données comme URL, vous pouvez faire deux autres choses.

1. Utilisez fileSaver.js

L'économiseur de fichier peut créer un dialogue fichier réel avec un nom de fichier prédéfini. Il peut également revenir à une approche normale dataURL.

2. Utilisez (expérimental) URL.createObjectURL

C'est parfait pour la réutilisation de données encodées en base64. Il crée une URL courte pour votre dataURL:

console.log(URL.createObjectURL(blob));
//Prints: blob:http://stackoverflow.com/7c18953f-f5f8-41d2-abf5-e9cbced9bc42

N'oubliez pas d'utiliser l'URL, y compris le préfixe blob qui précède. J'ai utilisé document.body encore:

image description

Vous pouvez utiliser cette courte URL comme AJAX cible, <script> source ou <a>href. Vous êtes responsable de la destruction de l'URL si:

URL.revokeObjectURL('blob:http://stackoverflow.com/7c18953f-f5f8-41d2-abf5-e9cbced9bc42')
8
Tomáš Zato

Utilisez une bibliothèque à la place

Nous n'avons pas à réinventer la roue. Utilisez simplement une bibliothèque pour économiser du temps et des maux de tête.

js-base64

https://github.com/dankogai/js-base64 est bon et je confirme qu'il prend très bien en charge le format Unicode.

Base64.encode('dankogai');  // ZGFua29nYWk=
Base64.encode('小飼弾');    // 5bCP6aO85by+
Base64.encodeURI('小飼弾'); // 5bCP6aO85by-

Base64.decode('ZGFua29nYWk=');  // dankogai
Base64.decode('5bCP6aO85by+');  // 小飼弾
// note .decodeURI() is unnecessary since it accepts both flavors
Base64.decode('5bCP6aO85by-');  // 小飼弾
7
Tyler Long

btoa () ne prend en charge que les caractères de String.fromCodePoint (0) jusqu'à String.fromCodePoint (255). Pour les caractères Base64 avec un point de code égal ou supérieur à 256, vous devez les encoder/décoder avant et après.

Et là, ça devient délicat ...

Tous les signes possibles sont disposés dans une table Unicode. La table Unicode est divisée en différents plans (langues, symboles mathématiques, etc.). Chaque signe dans un avion a un numéro de code unique. Théoriquement, le nombre peut devenir arbitrairement grand.

Un ordinateur stocke les données en octets (8 bits, hexadécimal 0x00 - 0xff, binaire 00000000 - 11111111, décimal 0 - 255). Cette plage sert normalement à sauvegarder les caractères de base (plage Latin1).

Pour les caractères avec un point de code supérieur à 255, il existe différents codages. JavaScript utilise 16 bits par signe (UTF-16), la chaîne appelée DOMString. Unicode peut gérer des points de code jusqu'à 0x10fffff. Cela signifie qu’il doit exister une méthode pour stocker plusieurs bits sur plusieurs cellules.

String.fromCodePoint(0x10000).length == 2

UTF-16 utilise des paires de substitution pour stocker 20 bits dans deux cellules 16 bits. La première mère de substitution supérieure commence par 110110xxxxxxxxxxx , la seconde inférieure par 110111xxxxxxxxxx . Unicode a réservé ses propres plans pour cela: https://unicode-table.com/de/#high-surrogates

Pour stocker des caractères dans des octets (plage Latin1), utilisez les procédures normalisées TF-8 .

Désolé de le dire, mais je pense qu’il n’ya pas d’autre moyen d’implémenter cette fonction soi-même.

function stringToUTF8(str)
{
    let bytes = [];

    for(let character of str)
    {
        let code = character.codePointAt(0);

        if(code <= 127)
        {
            let byte1 = code;

            bytes.Push(byte1);
        }
        else if(code <= 2047)
        {
            let byte1 = 0xC0 | (code >> 6);
            let byte2 = 0x80 | (code & 0x3F);

            bytes.Push(byte1, byte2);
        }
        else if(code <= 65535)
        {
            let byte1 = 0xE0 | (code >> 12);
            let byte2 = 0x80 | ((code >> 6) & 0x3F);
            let byte3 = 0x80 | (code & 0x3F);

            bytes.Push(byte1, byte2, byte3);
        }
        else if(code <= 2097151)
        {
            let byte1 = 0xF0 | (code >> 18);
            let byte2 = 0x80 | ((code >> 12) & 0x3F);
            let byte3 = 0x80 | ((code >> 6) & 0x3F);
            let byte4 = 0x80 | (code & 0x3F);

            bytes.Push(byte1, byte2, byte3, byte4);
        }
    }

    return bytes;
}

function utf8ToString(bytes, fallback)
{
    let valid = undefined;
    let codePoint = undefined;
    let codeBlocks = [0, 0, 0, 0];

    let result = "";

    for(let offset = 0; offset < bytes.length; offset++)
    {
        let byte = bytes[offset];

        if((byte & 0x80) == 0x00)
        {
            codeBlocks[0] = byte & 0x7F;

            codePoint = codeBlocks[0];
        }
        else if((byte & 0xE0) == 0xC0)
        {
            codeBlocks[0] = byte & 0x1F;

            byte = bytes[++offset];
            if(offset >= bytes.length || (byte & 0xC0) != 0x80) { valid = false; break; }

            codeBlocks[1] = byte & 0x3F;

            codePoint = (codeBlocks[0] << 6) + codeBlocks[1];
        }
        else if((byte & 0xF0) == 0xE0)
        {
            codeBlocks[0] = byte & 0xF;

            for(let blockIndex = 1; blockIndex <= 2; blockIndex++)
            {
                byte = bytes[++offset];
                if(offset >= bytes.length || (byte & 0xC0) != 0x80) { valid = false; break; }

                codeBlocks[blockIndex] = byte & 0x3F;
            }
            if(valid === false) { break; }

            codePoint = (codeBlocks[0] << 12) + (codeBlocks[1] << 6) + codeBlocks[2];
        }
        else if((byte & 0xF8) == 0xF0)
        {
            codeBlocks[0] = byte & 0x7;

            for(let blockIndex = 1; blockIndex <= 3; blockIndex++)
            {
                byte = bytes[++offset];
                if(offset >= bytes.length || (byte & 0xC0) != 0x80) { valid = false; break; }

                codeBlocks[blockIndex] = byte & 0x3F;
            }
            if(valid === false) { break; }

            codePoint = (codeBlocks[0] << 18) + (codeBlocks[1] << 12) + (codeBlocks[2] << 6) + (codeBlocks[3]);
        }
        else
        {
            valid = false; break;
        }

        result += String.fromCodePoint(codePoint);
    }

    if(valid === false)
    {
        if(!fallback)
        {
            throw new TypeError("Malformed utf-8 encoding.");
        }

        result = "";

        for(let offset = 0; offset != bytes.length; offset++)
        {
            result += String.fromCharCode(bytes[offset] & 0xFF);
        }
    }

    return result;
}

function decodeBase64(text, binary)
{
    if(/[^0-9a-zA-Z\+\/\=]/.test(text)) { throw new TypeError("The string to be decoded contains characters outside of the valid base64 range."); }

    let codePointA = 'A'.codePointAt(0);
    let codePointZ = 'Z'.codePointAt(0);
    let codePointa = 'a'.codePointAt(0);
    let codePointz = 'z'.codePointAt(0);
    let codePointZero = '0'.codePointAt(0);
    let codePointNine = '9'.codePointAt(0);
    let codePointPlus = '+'.codePointAt(0);
    let codePointSlash = '/'.codePointAt(0);

    function getCodeFromKey(key)
    {
        let keyCode = key.codePointAt(0);

        if(keyCode >= codePointA && keyCode <= codePointZ)
        {
            return keyCode - codePointA;
        }
        else if(keyCode >= codePointa && keyCode <= codePointz)
        {
            return keyCode + 26 - codePointa;
        }
        else if(keyCode >= codePointZero && keyCode <= codePointNine)
        {
            return keyCode + 52 - codePointZero;
        }
        else if(keyCode == codePointPlus)
        {
            return 62;
        }
        else if(keyCode == codePointSlash)
        {
            return 63;
        }

        return undefined;
    }

    let codes = Array.from(text).map(character => getCodeFromKey(character));

    let bytesLength = Math.ceil(codes.length / 4) * 3;

    if(codes[codes.length - 2] == undefined) { bytesLength = bytesLength - 2; } else if(codes[codes.length - 1] == undefined) { bytesLength--; }

    let bytes = new Uint8Array(bytesLength);

    for(let offset = 0, index = 0; offset < bytes.length;)
    {
        let code1 = codes[index++];
        let code2 = codes[index++];
        let code3 = codes[index++];
        let code4 = codes[index++];

        let byte1 = (code1 << 2) | (code2 >> 4);
        let byte2 = ((code2 & 0xf) << 4) | (code3 >> 2);
        let byte3 = ((code3 & 0x3) << 6) | code4;

        bytes[offset++] = byte1;
        bytes[offset++] = byte2;
        bytes[offset++] = byte3;
    }

    if(binary) { return bytes; }

    return utf8ToString(bytes, true);
}

function encodeBase64(bytes) {
    if (bytes === undefined || bytes === null) {
        return '';
    }
    if (bytes instanceof Array) {
        bytes = bytes.filter(item => {
            return Number.isFinite(item) && item >= 0 && item <= 255;
        });
    }

    if (
        !(
            bytes instanceof Uint8Array ||
            bytes instanceof Uint8ClampedArray ||
            bytes instanceof Array
        )
    ) {
        if (typeof bytes === 'string') {
            const str = bytes;
            bytes = Array.from(unescape(encodeURIComponent(str))).map(ch =>
                ch.codePointAt(0)
            );
        } else {
            throw new TypeError('bytes must be of type Uint8Array or String.');
        }
    }

    const keys = [
        'A',
        'B',
        'C',
        'D',
        'E',
        'F',
        'G',
        'H',
        'I',
        'J',
        'K',
        'L',
        'M',
        'N',
        'O',
        'P',
        'Q',
        'R',
        'S',
        'T',
        'U',
        'V',
        'W',
        'X',
        'Y',
        'Z',
        'a',
        'b',
        'c',
        'd',
        'e',
        'f',
        'g',
        'h',
        'i',
        'j',
        'k',
        'l',
        'm',
        'n',
        'o',
        'p',
        'q',
        'r',
        's',
        't',
        'u',
        'v',
        'w',
        'x',
        'y',
        'z',
        '0',
        '1',
        '2',
        '3',
        '4',
        '5',
        '6',
        '7',
        '8',
        '9',
        '+',
        '/'
    ];
    const fillKey = '=';

    let byte1;
    let byte2;
    let byte3;
    let sign1 = ' ';
    let sign2 = ' ';
    let sign3 = ' ';
    let sign4 = ' ';

    let result = '';

    for (let index = 0; index < bytes.length; ) {
        let fillUpAt = 0;

        // tslint:disable:no-increment-decrement
        byte1 = bytes[index++];
        byte2 = bytes[index++];
        byte3 = bytes[index++];

        if (byte2 === undefined) {
            byte2 = 0;
            fillUpAt = 2;
        }

        if (byte3 === undefined) {
            byte3 = 0;
            if (!fillUpAt) {
                fillUpAt = 3;
            }
        }

        // tslint:disable:no-bitwise
        sign1 = keys[byte1 >> 2];
        sign2 = keys[((byte1 & 0x3) << 4) + (byte2 >> 4)];
        sign3 = keys[((byte2 & 0xf) << 2) + (byte3 >> 6)];
        sign4 = keys[byte3 & 0x3f];

        if (fillUpAt > 0) {
            if (fillUpAt <= 2) {
                sign3 = fillKey;
            }
            if (fillUpAt <= 3) {
                sign4 = fillKey;
            }
        }

        result += sign1 + sign2 + sign3 + sign4;

        if (fillUpAt) {
            break;
        }
    }

    return result;
}

let base64 = encodeBase64("\u{1F604}"); // unicode code point escapes for smiley
let str = decodeBase64(base64);

console.log("base64", base64);
console.log("str", str);

document.body.innerText = str;

comment l'utiliser: decodeBase64(encodeBase64("\u{1F604}"))

démo: https://jsfiddle.net/qrLadeb8/

2
Martin Wantke

En complément de Stefan Steiger, réponse: (comme ce n’est pas beau comme commentaire)

Prototype de chaîne extensible:

String.prototype.b64encode = function() { 
    return btoa(unescape(encodeURIComponent(this))); 
};
String.prototype.b64decode = function() { 
    return decodeURIComponent(escape(atob(this))); 
};

Utilisation:

var str = "äöüÄÖÜçéèñ";
var encoded = str.b64encode();
console.log( encoded.b64decode() );

NOTE:

Comme indiqué dans les commentaires, l'utilisation de unescape n'est pas recommandée car elle pourrait être supprimée à l'avenir:

Avertissement : Bien que unescape () ne soit pas strictement déconseillé (comme dans "supprimé des normes Web"), il est défini à l'annexe B du code ECMA- 262 standard, dont l'introduction est la suivante:… Toutes les caractéristiques et tous les comportements de langage spécifiés dans la présente annexe ont une ou plusieurs caractéristiques indésirables et, en l'absence d'usage hérité du passé, ils seraient supprimés de cette spécification.

Remarque: N'utilisez pas unescape pour décoder les URI, utilisez plutôt decodeURI ou decodeURIComponent .

2
lepe

Je viens de rencontrer ce problème moi-même.

Tout d’abord, modifiez légèrement votre code:

var download = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
                  +"<"+this.gamesave.tagName+">"
                  +this.xml.firstChild.innerHTML
                  +"</"+this.gamesave.tagName+">";

this.loader.src = "data:application/x-forcedownload;base64,"+
                  btoa(download);

Ensuite, utilisez votre inspecteur Web préféré, placez un point d'arrêt sur la ligne de code qui attribue this.loader.src, puis exécutez ce code:

for (var i = 0; i < download.length; i++) {
  if (download[i].charCodeAt(0) > 255) {
    console.warn('found character ' + download[i].charCodeAt(0) + ' "' + download[i] + '" at position ' + i);
  }
}

En fonction de votre application, le remplacement des caractères en dehors des limites peut ou peut ne pas fonctionner, car vous allez modifier les données. Voir la note sur le MDN concernant les caractères unicode avec la méthode btoa:

https://developer.mozilla.org/en-US/docs/Web/API/window.btoa

1
Mark Salisbury