Использование aavb Javascript для декодирования base64 не правильно декодирует строки utf-8

Я использую Javascript window.atob() функция для декодирования строки в кодировке base64 (в частности, содержимого в кодировке base64 из GitHub API). Проблема в том, что я возвращаю символы в кодировке ASCII (например, ⢠вместо ). Как правильно обработать входящий поток в кодировке base64, чтобы он декодировался как utf-8?

15 ответов

Решение

Есть замечательная статья о MDN-документах Mozilla, которая описывает именно эту проблему:

"Проблема Юникода" с DOMString s - это 16-битные строки, в большинстве браузеров вызывающие window.btoa на строке Unicode вызовет Character Out Of Range exception если символ превышает диапазон 8-битного байта (0x00~0xFF). Есть два возможных способа решения этой проблемы:

  • первый - экранировать всю строку (с UTF-8 см. encodeURIComponent) а затем закодировать его;
  • второй конвертировать UTF-16 DOMString в массив символов UTF-8, а затем закодировать его.

Примечание о предыдущих решениях: в статье MDN изначально предлагалось использовать unescape а также escape решить Character Out Of Range проблема исключения, но с тех пор они устарели. Некоторые другие ответы здесь предложили обойти это с decodeURIComponent а также encodeURIComponent Это оказалось ненадежным и непредсказуемым. В последнем обновлении этого ответа используются современные функции JavaScript для повышения скорости и модернизации кода.

Если вы пытаетесь сэкономить время, вы также можете использовать библиотеку:

Кодировка UTF8 ⇢ base64

function b64EncodeUnicode(str) {
    // first we use encodeURIComponent to get percent-encoded UTF-8,
    // then we convert the percent encodings into raw bytes which
    // can be fed into btoa.
    return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
        function toSolidBytes(match, p1) {
            return String.fromCharCode('0x' + p1);
    }));
}

b64EncodeUnicode('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
b64EncodeUnicode('\n'); // "Cg=="

Декодирование base64 ⇢ UTF8

function b64DecodeUnicode(str) {
    // Going backwards: from bytestream, to percent-encoding, to original string.
    return decodeURIComponent(atob(str).split('').map(function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
}

b64DecodeUnicode('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"
b64DecodeUnicode('Cg=='); // "\n"

Решение до 2018 года (функционально и хотя, вероятно, лучше поддерживает старые браузеры, но не обновлено)

Вот текущая рекомендация, прямо из MDN, с некоторой дополнительной совместимостью с TypeScript через @MA-Maddin:

// Encoding UTF8 ⇢ base64

function b64EncodeUnicode(str) {
    return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
        return String.fromCharCode(parseInt(p1, 16))
    }))
}

b64EncodeUnicode('✓ à la mode') // "4pyTIMOgIGxhIG1vZGU="
b64EncodeUnicode('\n') // "Cg=="

// Decoding base64 ⇢ UTF8

function b64DecodeUnicode(str) {
    return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
    }).join(''))
}

b64DecodeUnicode('4pyTIMOgIGxhIG1vZGU=') // "✓ à la mode"
b64DecodeUnicode('Cg==') // "\n"

Исходное решение (устарело)

Это б escape а также unescape (которые сейчас устарели, хотя это все еще работает во всех современных браузерах):

function utf8_to_b64( str ) {
    return window.btoa(unescape(encodeURIComponent( str )));
}

function b64_to_utf8( str ) {
    return decodeURIComponent(escape(window.atob( str )));
}

// Usage:
utf8_to_b64('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
b64_to_utf8('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"

И последнее: я впервые столкнулся с этой проблемой при вызове GitHub API. Чтобы заставить это работать на (Mobile) Safari должным образом, мне фактически пришлось убрать все пустое пространство из источника base64, прежде чем я смог даже декодировать источник. Будет ли это актуально в 2017 году, я не знаю:

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

Декодирование base64 в строку UTF8

Ниже приведен текущий ответ, получивший наибольшее количество голосов от @brandonscript

function b64DecodeUnicode(str) {
    // Going backwards: from bytestream, to percent-encoding, to original string.
    return decodeURIComponent(atob(str).split('').map(function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
}

Приведенный выше код может работать, но он очень медленный. Если ваш ввод представляет собой очень большую строку base64, например 30000 символов для HTML-документа base64. Это потребует много вычислений.

Вот мой ответ: используйте встроенный TextDecoder, почти в 10 раз быстрее, чем код выше, для большого ввода.

function decodeBase64(base64) {
    const text = atob(base64);
    const length = text.length;
    const bytes = new Uint8Array(length);
    for (let i = 0; i < length; i++) {
        bytes[i] = text.charCodeAt(i);
    }
    const decoder = new TextDecoder(); // default is utf-8
    return decoder.decode(bytes);
}

Вещи меняются. Методы escape / unescape устарели.

Вы можете кодировать строку URI перед тем, как кодировать ее Base64. Обратите внимание, что это не производит кодирование Base64 UTF8, а скорее данные в кодировке Base64 URL-кодированные. Обе стороны должны согласовать одну и ту же кодировку.

Смотрите рабочий пример здесь: http://codepen.io/anon/pen/PZgbPW

// encode string
var base64 = window.btoa(encodeURIComponent('€ 你好 æøåÆØÅ'));
// decode string
var str = decodeURIComponent(window.atob(tmp));
// str is now === '€ 你好 æøåÆØÅ'

Для решения проблемы OP сторонняя библиотека, такая как js-base64, должна решить проблему.

Полная статья, которая мне подходит: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding

Часть, в которой мы кодируем из Unicode/UTF-8, это

function utf8_to_b64( str ) {
   return window.btoa(unescape(encodeURIComponent( str )));
}

function b64_to_utf8( str ) {
   return decodeURIComponent(escape(window.atob( str )));
}

// Usage:
utf8_to_b64('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
b64_to_utf8('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"

В настоящее время это один из наиболее часто используемых методов.

Если вам больше нравится обрабатывать строки как байты, вы можете использовать следующие функции

function u_atob(ascii) {
    return Uint8Array.from(atob(ascii), c => c.charCodeAt(0));
}

function u_btoa(buffer) {
    var binary = [];
    var bytes = new Uint8Array(buffer);
    for (var i = 0, il = bytes.byteLength; i < il; i++) {
        binary.push(String.fromCharCode(bytes[i]));
    }
    return btoa(binary.join(''));
}


// example, it works also with astral plane characters such as ''
var encodedString = new TextEncoder().encode('✓');
var base64String = u_btoa(encodedString);
console.log('✓' === new TextDecoder().decode(u_atob(base64String)))

Я хотел бы предположить, что можно было бы хотеть, чтобы это base64 было закодировано более стандартным способом, чем некоторые решения, перечисленные выше. Этот "стандартный" способ естественно интерпретируется браузером, так что вы можете использовать base64 в URI данных. Пожалуйста, посетите этот URI данных здесь, чтобы увидеть демонстрацию: data:text/plain;base64,4pi44pi54pi64pi74pi84pi+4pi/ (Скопируйте данные URI, откройте новую вкладку, вставьте URI данных в адресную строку, затем нажмите Enter, чтобы перейти на страницу). Как вы можете видеть, несмотря на тот факт, что это URL в кодировке base64, браузер по-прежнему может распознавать старшие кодовые точки и правильно их декодировать. Таким образом, эта демонстрация доказывает, что это лучший путь, потому что это самый стандарт W3C. Ниже приведен код, используемый для его генерации.

var fromCharCode = String.fromCharCode;
var btoaUTF8 = (function(btoa, replacer){"use strict";
    return function(inputString, BOMit){
        return btoa((BOMit ? "\xEF\xBB\xBF" : "") + inputString.replace(
            /[\x80-\uD7ff\uDC00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]?/g, replacer
        ));
    }
})(btoa, function(nonAsciiChars){"use strict";
    // make the UTF string into a binary UTF-8 encoded string
    var point = nonAsciiChars.charCodeAt(0);
    if (point >= 0xD800 && point <= 0xDBFF) {
        var nextcode = nonAsciiChars.charCodeAt(1);
        if (nextcode !== nextcode) // NaN because string is 1 code point long
            return fromCharCode(0xef/*11101111*/, 0xbf/*10111111*/, 0xbd/*10111101*/);
        // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
        if (nextcode >= 0xDC00 && nextcode <= 0xDFFF) {
            point = (point - 0xD800) * 0x400 + nextcode - 0xDC00 + 0x10000;
            if (point > 0xffff)
                return fromCharCode(
                    (0x1e/*0b11110*/<<3) | (point>>>18),
                    (0x2/*0b10*/<<6) | ((point>>>12)&0x3f/*0b00111111*/),
                    (0x2/*0b10*/<<6) | ((point>>>6)&0x3f/*0b00111111*/),
                    (0x2/*0b10*/<<6) | (point&0x3f/*0b00111111*/)
                );
        } else return fromCharCode(0xef, 0xbf, 0xbd);
    }
    if (point <= 0x007f) return inputString;
    else if (point <= 0x07ff) {
        return fromCharCode((0x6<<5)|(point>>>6), (0x2<<6)|(point&0x3f));
    } else return fromCharCode(
        (0xe/*0b1110*/<<4) | (point>>>12),
        (0x2/*0b10*/<<6) | ((point>>>6)&0x3f/*0b00111111*/),
        (0x2/*0b10*/<<6) | (point&0x3f/*0b00111111*/)
    );
});

Затем для декодирования данных base64 либо HTTP получает данные в виде URI данных, либо использует функцию ниже.

var clz32 = Math.clz32 || (function(log, LN2){"use strict";
    return function(x) {return 31 - log(x >>> 0) / LN2 | 0};
})(Math.log, Math.LN2);
var fromCharCode = String.fromCharCode;
var atobUTF8 = (function(atob, replacer){"use strict";
    return function(inputString, keepBOM){
        if (!keepBOM && inputString.substring(0,3) === "\xEF\xBB\xBF")
            inputString = inputString.substring(3); // eradicate UTF-8 BOM
        // 0xc0 => 0b11000000; 0xff => 0b11111111; 0xc0-0xff => 0b11xxxxxx
        // 0x80 => 0b10000000; 0xbf => 0b10111111; 0x80-0xbf => 0b10xxxxxx
        return atob(inputString).replace(/[\xc0-\xff][\x80-\xbf]*/g, replacer);
    }
})(atob, function(encoded){"use strict";
    var codePoint = encoded.charCodeAt(0) << 24;
    var leadingOnes = clz32(~codePoint);
    var endPos = 0, stringLen = encoded.length;
    var result = "";
    if (leadingOnes < 5 && stringLen >= leadingOnes) {
        codePoint = (codePoint<<leadingOnes)>>>(24+leadingOnes);
        for (endPos = 1; endPos < leadingOnes; ++endPos)
            codePoint = (codePoint<<6) | (encoded.charCodeAt(endPos)&0x3f/*0b00111111*/);
        if (codePoint <= 0xFFFF) { // BMP code point
          result += fromCharCode(codePoint);
        } else if (codePoint <= 0x10FFFF) {
          // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
          codePoint -= 0x10000;
          result += fromCharCode(
            (codePoint >> 10) + 0xD800,  // highSurrogate
            (codePoint & 0x3ff) + 0xDC00 // lowSurrogate
          );
        } else endPos = 0; // to fill it in with INVALIDs
    }
    for (; endPos < stringLen; ++endPos) result += "\ufffd"; // replacement character
    return result;
});

Преимущество более стандартного состоит в том, что этот кодер и этот декодер более широко применяются, поскольку они могут использоваться в качестве действительного URL-адреса, который отображается правильно. Обратите внимание.

(function(window){
    "use strict";
    var sourceEle = document.getElementById("source");
    var urlBarEle = document.getElementById("urlBar");
    var mainFrameEle = document.getElementById("mainframe");
    var gotoButton = document.getElementById("gotoButton");
    var parseInt = window.parseInt;
    var fromCodePoint = String.fromCodePoint;
    var parse = JSON.parse;
    
    function unescape(str){
        return str.replace(/\\u[\da-f]{0,4}|\\x[\da-f]{0,2}|\\u{[^}]*}|\\[bfnrtv"'\\]|\\0[0-7]{1,3}|\\\d{1,3}/g, function(match){
          try{
            if (match.startsWith("\\u{"))
              return fromCodePoint(parseInt(match.slice(2,-1),16));
            if (match.startsWith("\\u") || match.startsWith("\\x"))
              return fromCodePoint(parseInt(match.substring(2),16));
            if (match.startsWith("\\0") && match.length > 2)
              return fromCodePoint(parseInt(match.substring(2),8));
            if (/^\\\d/.test(match)) return fromCodePoint(+match.slice(1));
          }catch(e){return "\ufffd".repeat(match.length)}
          return parse('"' + match + '"');
        });
    }
    
    function whenChange(){
      try{ urlBarEle.value = "data:text/plain;base64," + btoaUTF8(unescape(sourceEle.value), true);
      } finally{ gotoURL(); }
    }
    sourceEle.addEventListener("change",whenChange,{passive:1});
    sourceEle.addEventListener("input",whenChange,{passive:1});
    
    // IFrame Setup:
    function gotoURL(){mainFrameEle.src = urlBarEle.value}
    gotoButton.addEventListener("click", gotoURL, {passive: 1});
    function urlChanged(){urlBarEle.value = mainFrameEle.src}
    mainFrameEle.addEventListener("load", urlChanged, {passive: 1});
    urlBarEle.addEventListener("keypress", function(evt){
      if (evt.key === "enter") evt.preventDefault(), urlChanged();
    }, {passive: 1});
    
        
    var fromCharCode = String.fromCharCode;
    var btoaUTF8 = (function(btoa, replacer){
      "use strict";
        return function(inputString, BOMit){
         return btoa((BOMit?"\xEF\xBB\xBF":"") + inputString.replace(
          /[\x80-\uD7ff\uDC00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]?/g, replacer
      ));
     }
    })(btoa, function(nonAsciiChars){
  "use strict";
     // make the UTF string into a binary UTF-8 encoded string
     var point = nonAsciiChars.charCodeAt(0);
     if (point >= 0xD800 && point <= 0xDBFF) {
      var nextcode = nonAsciiChars.charCodeAt(1);
      if (nextcode !== nextcode) { // NaN because string is 1code point long
       return fromCharCode(0xef/*11101111*/, 0xbf/*10111111*/, 0xbd/*10111101*/);
      }
      // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
      if (nextcode >= 0xDC00 && nextcode <= 0xDFFF) {
       point = (point - 0xD800) * 0x400 + nextcode - 0xDC00 + 0x10000;
       if (point > 0xffff) {
        return fromCharCode(
         (0x1e/*0b11110*/<<3) | (point>>>18),
         (0x2/*0b10*/<<6) | ((point>>>12)&0x3f/*0b00111111*/),
         (0x2/*0b10*/<<6) | ((point>>>6)&0x3f/*0b00111111*/),
         (0x2/*0b10*/<<6) | (point&0x3f/*0b00111111*/)
        );
       }
      } else {
       return fromCharCode(0xef, 0xbf, 0xbd);
      }
     }
     if (point <= 0x007f) { return inputString; }
     else if (point <= 0x07ff) {
      return fromCharCode((0x6<<5)|(point>>>6), (0x2<<6)|(point&0x3f/*00111111*/));
     } else {
      return fromCharCode(
       (0xe/*0b1110*/<<4) | (point>>>12),
       (0x2/*0b10*/<<6) | ((point>>>6)&0x3f/*0b00111111*/),
       (0x2/*0b10*/<<6) | (point&0x3f/*0b00111111*/)
      );
     }
    });
    setTimeout(whenChange, 0);
})(window);
img:active{opacity:0.8}
<center>
<textarea id="source" style="width:66.7vw">Hello \u1234 W\186\0256ld!
Enter text into the top box. Then the URL will update automatically.
</textarea><br />
<div style="width:66.7vw;display:inline-block;height:calc(25vw + 1em + 6px);border:2px solid;text-align:left;line-height:1em">
<input id="urlBar" style="width:calc(100% - 1em - 13px)" /><img id="gotoButton" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAeCAMAAADqx5XUAAAAclBMVEX///9NczZ8e32ko6fDxsU/fBoSQgdFtwA5pAHVxt+7vLzq5ex23y4SXABLiiTm0+/c2N6DhoQ6WSxSyweVlZVvdG/Uz9aF5kYlbwElkwAggACxs7Jl3hX07/cQbQCar5SU9lRntEWGum+C9zIDHwCGnH5IvZAOAAABmUlEQVQoz7WS25acIBBFkRLkIgKKtOCttbv//xdDmTGZzHv2S63ltuBQQP4rdRiRUP8UK4wh6nVddQwj/NtDQTvac8577zTQb72zj65/876qqt7wykU6/1U6vFEgjE1mt/5LRqrpu7oVsn0sjZejMfxR3W/yLikqAFcUx93YxLmZGOtElmEu6Ufd9xV3ZDTGcEvGLbMk0mHHlUSvS5svCwS+hVL8loQQyfpI1Ay8RF/xlNxcsTchGjGDIuBG3Ik7TMyNxn8m0TSnBAK6Z8UZfp3IbAonmJvmsEACum6aNv7B0CnvpezDcNhw9XWsuAr7qnRg6dABmeM4dTgn/DZdXWs3LMspZ1KDMt1kcPJ6S1icWNp2qaEmjq6myx7jbQK3VKItLJaW5FR+cuYlRhYNKzGa9vF4vM5roLW3OSVjkmiGJrPhUq301/16pVKZRGFYWjTP50spTxBN5Z4EKnSonruk+n4tUokv1aJSEl/MLZU90S3L6/U6o0J142iQVp3HcZxKSo8LfkNRCtJaKYFSRX7iaoAAUDty8wvWYR6HJEepdwAAAABJRU5ErkJggg==" style="width:calc(1em + 4px);line-height:1em;vertical-align:-40%;cursor:pointer" />
<iframe id="mainframe" style="width:66.7vw;height:25vw" frameBorder="0"></iframe>
</div>
</center>

Помимо того, что приведенные выше фрагменты кода очень стандартизированы, они также очень быстры. Вместо непрямой цепочки правопреемства, когда данные должны быть преобразованы несколько раз между различными формами (например, в ответе Риккардо Галли), приведенный выше фрагмент кода является настолько прямым, насколько это возможно с точки зрения производительности. Он использует только один простой пост String.prototype.replace вызов для обработки данных при кодировании и только один для декодирования данных при декодировании. Еще один плюс в том, что (особенно для больших струн), String.prototype.replace позволяет браузеру автоматически обрабатывать основное управление памятью путем изменения размера строки, что приводит к значительному увеличению производительности, особенно в вечнозеленых браузерах, таких как Chrome и Firefox, которые сильно оптимизируют String.prototype.replace, Наконец, изюминка в том, что для вас латинский скрипт исключает пользователей, строки, которые не содержат кодовых точек выше 0x7f, обрабатываются очень быстро, потому что строка остается неизменной по алгоритму замены.

Я создал хранилище github для этого решения по адресу https://github.com/anonyco/BestBase64EncoderDecoder/

Вот обновленное решение 2018 года, описанное в Ресурсах Mozilla Development.

КОДИРОВАТЬ ОТ ЮНИКОДА К B64

function b64EncodeUnicode(str) {
    // first we use encodeURIComponent to get percent-encoded UTF-8,
    // then we convert the percent encodings into raw bytes which
    // can be fed into btoa.
    return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
        function toSolidBytes(match, p1) {
            return String.fromCharCode('0x' + p1);
    }));
}

b64EncodeUnicode('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
b64EncodeUnicode('\n'); // "Cg=="

ДЕКОДИТЬ С B64 НА ЮНИКОД

function b64DecodeUnicode(str) {
    // Going backwards: from bytestream, to percent-encoding, to original string.
    return decodeURIComponent(atob(str).split('').map(function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
}

b64DecodeUnicode('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"
b64DecodeUnicode('Cg=='); // "\n"

TL;DR однострочное решение:

Если вы пытаетесь декодировать представление Base64 данных в кодировке utf8 в узле, вы можете использовать встроенный помощник Buffer .

      Buffer.from("4pyTIMOgIGxhIG1vZGU=", "base64").toString(); // '✓ à la mode'

The toStringметод Buffer по умолчанию использует utf8, но вы можете указать любую желаемую кодировку. Например, обратная операция будет выглядеть так

      Buffer.from('✓ à la mode', "utf8").toString("base64"); // "4pyTIMOgIGxhIG1vZGU="

Это мое однострочное решение, сочетающее ответ Джеки Ханса и некоторый код из другого вопроса :

      const utf8_encoded_text = new TextDecoder().decode(Uint8Array.from(window.atob(base_64_decoded_text).split("").map(x => x.charCodeAt(0))));

Вот код на будущее для браузеров, которые могут не иметь escape/unescape(), Обратите внимание, что IE 9 и старше не поддерживают atob/btoa(), так что вам нужно использовать пользовательские функции base64 для них.

// Polyfill for escape/unescape
if( !window.unescape ){
    window.unescape = function( s ){
        return s.replace( /%([0-9A-F]{2})/g, function( m, p ) {
            return String.fromCharCode( '0x' + p );
        } );
    };
}
if( !window.escape ){
    window.escape = function( s ){
        var chr, hex, i = 0, l = s.length, out = '';
        for( ; i < l; i ++ ){
            chr = s.charAt( i );
            if( chr.search( /[A-Za-z0-9\@\*\_\+\-\.\/]/ ) > -1 ){
                out += chr; continue; }
            hex = s.charCodeAt( i ).toString( 16 );
            out += '%' + ( hex.length % 2 != 0 ? '0' : '' ) + hex;
        }
        return out;
    };
}

// Base64 encoding of UTF-8 strings
var utf8ToB64 = function( s ){
    return btoa( unescape( encodeURIComponent( s ) ) );
};
var b64ToUtf8 = function( s ){
    return decodeURIComponent( escape( atob( s ) ) );
};

Более полный пример кодирования и декодирования UTF-8 можно найти здесь: http://jsfiddle.net/47zwb41o/

Небольшая коррекция, unescape и escape не рекомендуются, поэтому:

function utf8_to_b64( str ) {
    return window.btoa(decodeURIComponent(encodeURIComponent(str)));
}

function b64_to_utf8( str ) {
     return decodeURIComponent(encodeURIComponent(window.atob(str)));
}


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

2023: В браузерах по-прежнему нет встроенной поддержки кодирования и декодирования base64 в UTF8.

Если вы действительно не хотите изобретать велосипед и тестировать крайние случаи, как для браузеров, так и для Node используйте https://github.com/dankogai/js-base64.

Концепция двоичной строки

Проблема с функциями заключается в том, что они обе работают назначения, но содержимое этих строк отличается от того, что строки обычно должны содержать. Ожидается, что строки, полученные, например, будут отформатированы как двоичные строки , которые представляют собой последовательности, подобные массиву, в которых каждый 16-битный символ представляет 8-битное значение. Ожидается, что каждый элемент в строке будет содержать значение от 0 до 255, а значения символов за пределами этого диапазона считаются недействительными. Значения, возвращаемые с помощью, форматируются таким же образом. Было бы разумнее, если бы эти функции работали с байтовыми массивами, но обе они используют строки.

Строки Юникода в Javascript, напротив, хранятся как серия кодовых единиц UTF-16, где каждая кодовая единица имеет значение от 0 до 65535. Передача строки Unicode вбудет работать правильно, если все символы, содержащиеся в строке, лежат в диапазоне Latin1 (0–255), но в противном случае вызов завершится неудачно. Его аналог, с другой стороны, будет принимать строку в формате Base64 и возвращать двоичную строку независимо от того, представляет ли содержимое строку Latin1, строку UTF-8, строку UTF-16 или произвольные двоичные данные. Это сделано специально.

Применяя это к конкретному примеру, представленному в вопросе, рассмотрим представления UTF-8 и UTF-16 символа Unicode «Знак торговой марки», ™. Представление этого символа в формате UTF-8 — . Представление этой последовательности в Base64 — . Кормлениеквернет строку, состоящую из трех 16-битных значений, каждое из которых представляет один байт:,, и. Интерпретируемые как двоичная строка, эти значения представляют собой последовательность UTF-8.,,(исходный символ ™, как и ожидалось). Однако эта последовательность интерпретируется как обычная строка UTF-16 и представляет собой строку, что вы и получаете.

Кодирование и декодирование собственных строк

Двоичное кодирование

Прежде чем мы сможем преобразовать строку Unicode в Base64, нам нужно выбрать двоичную кодировку для этой строки. Это может быть UTF-8, UTF-16 или любая другая кодировка, способная представлять исходную строку. Мы можем написать несколько функций для преобразования собственных строк в двоичные строки для определенных кодировок:

Собственная строка для UTF-8

      function encodeAsUTF8(str) {
    const encoder = new TextEncoder();

    const utf8 = encoder.encode(str);

    var binaryString = '';
    for (let b = 0; b < utf8.length; ++b) {
        binaryString += String.fromCharCode(utf8[b]);
    }

    return binaryString;
}

Собственная строка для UTF-16

      function encodeAsUTF16(str) {
    var utf16 = new Uint16Array(str.length);

    for (let p = 0; p < utf16.length; ++p) {
        utf16[p] = str.charCodeAt(p);
    }

    const bytes = new Uint8Array(utf16.buffer);

    var binaryString = '';
    for (let b = 0; b < bytes.length; ++b) {
        binaryString += String.fromCharCode(bytes[b]);
    }

    return binaryString;        
}

Возможны и другие кодировки, но двух приведенных выше должно быть достаточно для иллюстрации концепции.

Декодирование

Преобразование из двоичной кодировки в собственную строку требует знания исходной кодировки, чтобы двоичные значения правильно интерпретировались. Снова взяв в качестве примера UTF-8 и UTF-16, мы можем написать функции для преобразования двоичных строк UTF-8 и UTF-16 в собственные строки:

UTF-8 в собственную строку

      function decodeUTF8(binary) {
    const bytes = new Uint8Array(binary.length);
    for (let b = 0; b < bytes.length; ++b) {
        bytes[b] = binary.charCodeAt(b);
    }

    const decoder = new TextDecoder('utf-8');

    return decoder.decode(bytes);
}

UTF-16 в собственную строку

      function decodeUTF16(binary) {
    const utf16 = new Uint8Array(binary.length);
    for (let b = 0; b < utf16.length; ++b) {
        utf16[b] = binary.charCodeAt(b);
    }

    const decoder = new TextDecoder('utf-16');

    return decoder.decode(utf16);
}

Собственная строка в Base64

Имея различные функции кодирования строк, мы можем закодировать строку как UTF-8 и, в свою очередь, преобразовать ее в Base64, вызвав:

      base64string = btoa(encodeAsUTF8('™'));

Мы также можем закодировать строку как UTF-16 и преобразовать ее в Base64, вызвав:

      base64string = btoa(encodeAsUTF16('™'));

Base64 в собственную строку

Чтобы преобразовать строку в кодировке UTF-8 из Base64 в собственную строку, вызовите:

      decodeUTF8(atob(base64string));

Чтобы преобразовать строку в кодировке UTF-16 из Base64 в собственную строку, вызовите:

      decodeUTF16(atob(base64string));

Если вы все еще сталкиваетесь с проблемой, включите вышеуказанное решение, попробуйте, как показано ниже, рассмотрите случай, когда escape не поддерживается для TS

blob = new Blob(["\ufeff", csv_content]); // this will make symbols to appears in excel 

для csv_content вы можете попробовать как ниже.

function b64DecodeUnicode(str: any) {        
        return decodeURIComponent(atob(str).split('').map((c: any) => {
            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        }).join(''));
    }
Другие вопросы по тегам