Tworzenie BLOBa z ciągu Base64 w JavaScript

447

Mam dane binarne zakodowane w standardzie Base64 w ciągu:

const contentType = 'image/png';
const b64Data = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';

Chciałbym utworzyć blob:adres URL zawierający te dane i wyświetlić go użytkownikowi:

const blob = new Blob(????, {type: contentType});
const blobUrl = URL.createObjectURL(blob);

window.location = blobUrl;

Nie byłem w stanie dowiedzieć się, jak utworzyć BLOB.

W niektórych przypadkach mogę tego uniknąć, używając data:adresu URL:

const dataUrl = `data:${contentType};base64,${b64Data}`;

window.location = dataUrl;

Jednak w większości przypadków data:adresy URL są zbyt duże.


Jak mogę zdekodować ciąg Base64 na obiekt BLOB w JavaScript?

Jeremy Banks
źródło

Odpowiedzi:

790

atobFunkcja dekodowania Base64 zakodowany ciąg do nowego napisu z charakterem każdego bajta danych binarnych.

const byteCharacters = atob(b64Data);

Punkt kodowy każdego znaku (charCode) będzie wartością bajtu. Możemy utworzyć tablicę wartości bajtów, stosując tę .charCodeAtmetodę za pomocą metody dla każdego znaku w ciągu.

const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
    byteNumbers[i] = byteCharacters.charCodeAt(i);
}

Możesz przekonwertować tablicę wartości bajtów na tablicę bajtów o typie rzeczywistym, przekazując ją do Uint8Arraykonstruktora.

const byteArray = new Uint8Array(byteNumbers);

To z kolei można przekonwertować na BLOB, zawijając go w tablicę i przekazując do Blobkonstruktora.

const blob = new Blob([byteArray], {type: contentType});

Powyższy kod działa. Jednak wydajność można nieco poprawić, przetwarzając byteCharactersmniejsze kawałki, a nie wszystkie naraz. W moich testach zgrubnych 512 bajtów wydaje się mieć dobry rozmiar wycinka. To daje nam następującą funkcję.

const b64toBlob = (b64Data, contentType='', sliceSize=512) => {
  const byteCharacters = atob(b64Data);
  const byteArrays = [];

  for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    const slice = byteCharacters.slice(offset, offset + sliceSize);

    const byteNumbers = new Array(slice.length);
    for (let i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }

    const byteArray = new Uint8Array(byteNumbers);
    byteArrays.push(byteArray);
  }

  const blob = new Blob(byteArrays, {type: contentType});
  return blob;
}
const blob = b64toBlob(b64Data, contentType);
const blobUrl = URL.createObjectURL(blob);

window.location = blobUrl;

Pełny przykład:

Jeremy Banks
źródło
6
Cześć Jeremy. Mamy ten kod w naszej aplikacji internetowej i nie powodował żadnych problemów, dopóki pobierane pliki nie miały większych rozmiarów. Powodowało to zawieszanie się i awarie na serwerze produkcyjnym, gdy użytkownicy używali przeglądarki Chrome lub IE do pobierania plików większych niż 100 MB. Odkryliśmy, że w następnej linii IE pojawił się wyjątek pamięci „var byteNumbers = new Array (slice.length)”. Jednak w przypadku Chrome była to pętla for powodująca ten sam problem. Nie mogliśmy znaleźć odpowiedniego rozwiązania tego problemu, a następnie przeszliśmy do bezpośredniego pobierania plików za pomocą window.open. Czy możesz tu pomóc?
Akshay Raut
Czy jest to jakaś metoda konwersji pliku wideo na base64 w reakcji native? Udało mi się to zrobić z plikiem obrazu, ale nie znalazłem rozwiązania tego samego dla filmów. Linki będą pomocne lub również rozwiązanie.
Diksha235
Więc nie ma problemów z przechowywaniem zer w ciągu zwracanym przez atob ()?
wcochran
to nie działało dla mnie dla niektórych obiektów blob w Chrome i Firefox, ale działało na krawędzi: /
Gragas Incoming
pracował dla mnie. zgłasza ** Błąd analizy JSON: Nierozpoznany toke „<” ** Sprawdziłem ciąg base64, uruchamiając przeglądarkę, która tworzy obraz. potrzebuję pomocy.
Aman Deep
273

Oto bardziej minimalna metoda bez żadnych zależności i bibliotek.
Wymaga nowego interfejsu API pobierania. ( Czy mogę tego użyć? )

var url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="

fetch(url)
.then(res => res.blob())
.then(console.log)

Za pomocą tej metody można również łatwo uzyskać ReadableStream, ArrayBuffer, tekst i JSON.

Jako funkcja:

const b64toBlob = (base64, type = 'application/octet-stream') => 
  fetch(`data:${type};base64,${base64}`).then(res => res.blob())

Zrobiłem prosty test wydajności w stosunku do wersji synchronizacji ES6 Jeremy'ego.
Wersja synchronizacji blokuje interfejs użytkownika na chwilę. utrzymywanie devtoola otwartego może spowolnić działanie pobierania

Nieskończony
źródło
1
Czy to nadal zadziała, jeśli rozmiar łańcucha zakodowanego w base64 jest duży, powiedzmy większy niż 665536 znaków, co stanowi limit rozmiarów URI w Operze?
Daniel Kats
1
Nie wiem, wiem, że może to być ograniczenie paska adresu, ale robienie rzeczy z AJAX może być wyjątkiem, ponieważ nie musi być renderowane. Musisz to przetestować. Gdyby to było miejsce, w którym nigdy nie zdobyłbym łańcucha base64. Myślenie, że to zła praktyka, zajmuje więcej pamięci i czasu na dekodowanie i kodowanie. createObjectURLzamiast readAsDataURLjest na przykład znacznie lepszy. A jeśli przesyłasz pliki za pomocą ajax, wybierz FormDatazamiast JSONlub użyj canvas.toBlobzamiasttoDataURL
Endless
7
Jeszcze lepiej jak w linii:await (await fetch(imageDataURL)).blob()
icl7126,
3
jasne, jeśli celujesz w najnowszą przeglądarkę. Ale to wymaga, aby funkcja również znajdowała się w funkcji asynchronicznej. Mówiąc o ... await fetch(url).then(r=>r.blob())jest sorter
Endless
2
Bardzo fajne rozwiązanie, ale według mojej wiedzy nie będzie działać z IE (z polyfill ofc) z powodu Access is denied.błędu. Wydaje mi się, że fetchujawnia obiekt blob pod adresem URL obiektu blob - w ten sam sposób URL.createObjectUrl- który nie działa na ie11. odniesienie . Może jest jakieś obejście, aby użyć pobierania z IE11? Wygląda znacznie lepiej niż inne rozwiązania synchronizacji :)
Papi
72

Zoptymalizowana (ale mniej czytelna) implementacja:

function base64toBlob(base64Data, contentType) {
    contentType = contentType || '';
    var sliceSize = 1024;
    var byteCharacters = atob(base64Data);
    var bytesLength = byteCharacters.length;
    var slicesCount = Math.ceil(bytesLength / sliceSize);
    var byteArrays = new Array(slicesCount);

    for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
        var begin = sliceIndex * sliceSize;
        var end = Math.min(begin + sliceSize, bytesLength);

        var bytes = new Array(end - begin);
        for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
            bytes[i] = byteCharacters[offset].charCodeAt(0);
        }
        byteArrays[sliceIndex] = new Uint8Array(bytes);
    }
    return new Blob(byteArrays, { type: contentType });
}
Bacher
źródło
2
Czy istnieje jakiś powód, aby pokroić bajty na obiekty BLOB? Jeśli nie używam, czy jest jakaś wada lub ryzyko?
Alfred Huang
Działa świetnie na Androidzie z Ionic 1 / Angular 1. Kromka jest wymagana, w przeciwnym razie uruchomię OOM (Android 6.0.1).
Jürgen 'Kashban' Wahlmann
4
Jedyny przykład, który mogłem płynnie pracować z dowolnym typem dokumentu w środowisku korporacyjnym zarówno w IE 11, jak i Chrome.
Santos
To jest fantastyczne. Dziękuję Ci!
elliotwesoff
Wytłumaczenie byłoby w porządku. Np. Dlaczego ma wyższą wydajność?
Peter Mortensen
19

W przypadku wszystkich przeglądarek, zwłaszcza na Androidzie, możesz dodać:

try{
    blob = new Blob(byteArrays, {type : contentType});
}
catch(e){
    // TypeError old Google Chrome and Firefox
    window.BlobBuilder = window.BlobBuilder ||
                         window.WebKitBlobBuilder ||
                         window.MozBlobBuilder ||
                         window.MSBlobBuilder;
    if(e.name == 'TypeError' && window.BlobBuilder){
        var bb = new BlobBuilder();
        bb.append(byteArrays);
        blob = bb.getBlob(contentType);
    }
    else if(e.name == "InvalidStateError"){
        // InvalidStateError (tested on FF13 WinXP)
        blob = new Blob(byteArrays, {type : contentType});
    }
    else{
        // We're screwed, blob constructor unsupported entirely
    }
}
Jayce Lin
źródło
Dzięki, ale we fragmencie kodu, który napisałeś powyżej, są dwa problemy, jeśli poprawnie go przeczytałem: (1) Kod w catch () w ostatnim przypadku - jeśli jest taki sam jak oryginalny kod w try (): "blob = nowy obiekt Blob (byteArrays, {type: contentType}) „... Nie wiem, dlaczego sugerujesz powtarzanie tego samego kodu po oryginalnym wyjątku? ... (2) BlobBuilder.append () NIE akceptuje tablic bajtów, ale ArrayBuffer. Tak więc wejściowe tablice bajtów muszą zostać przekształcone dalej w ArrayBuffer przed użyciem tego interfejsu API.
ODNIESIENIE
14

W przypadku danych obrazu korzystanie z niego jest prostsze canvas.toBlob(asynchroniczne)

function b64toBlob(b64, onsuccess, onerror) {
    var img = new Image();

    img.onerror = onerror;

    img.onload = function onload() {
        var canvas = document.createElement('canvas');
        canvas.width = img.width;
        canvas.height = img.height;

        var ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

        canvas.toBlob(onsuccess);
    };

    img.src = b64;
}

var base64Data = 'data:image/jpg;base64,/9j/4AAQSkZJRgABAQA...';
b64toBlob(base64Data,
    function(blob) {
        var url = window.URL.createObjectURL(blob);
        // do something with url
    }, function(error) {
        // handle error
    });
amirnissim
źródło
1
Myślę, że tracisz z tym trochę informacji ... jak meta info to jak konwersja dowolnego obrazu do formatu png, więc nie jest to ten sam wynik, to także działa tylko dla obrazów
Endless
Myślę, że można to poprawić, wyodrębniając typ obrazu image/jpgz ciągu base64, a następnie przekazując go jako drugi parametr do toBlobfunkcji, aby wynik był tego samego typu. Poza tym uważam, że jest to idealne - pozwala zaoszczędzić 30% ruchu i miejsca na dysku na serwerze (w porównaniu do base64) i działa dobrze nawet z przezroczystym PNG.
icl7126
1
Funkcja ulega awarii z obrazami większymi niż 2 MB ... w Androidzie otrzymuję wyjątek: android.os.TransactionTooLarge
Ruben
14

Zobacz ten przykład: https://jsfiddle.net/pqhdce2L/

function b64toBlob(b64Data, contentType, sliceSize) {
  contentType = contentType || '';
  sliceSize = sliceSize || 512;

  var byteCharacters = atob(b64Data);
  var byteArrays = [];

  for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    var slice = byteCharacters.slice(offset, offset + sliceSize);

    var byteNumbers = new Array(slice.length);
    for (var i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }

    var byteArray = new Uint8Array(byteNumbers);

    byteArrays.push(byteArray);
  }
    
  var blob = new Blob(byteArrays, {type: contentType});
  return blob;
}


var contentType = 'image/png';
var b64Data = Your Base64 encode;

var blob = b64toBlob(b64Data, contentType);
var blobUrl = URL.createObjectURL(blob);

var img = document.createElement('img');
img.src = blobUrl;
document.body.appendChild(img);

Arcaela
źródło
Wytłumaczenie byłoby w porządku.
Peter Mortensen
9

Zauważyłem, że Internet Explorer 11 staje się niesamowicie wolny podczas krojenia danych, jak sugerował Jeremy. Dotyczy to przeglądarki Chrome, ale wydaje się, że Internet Explorer ma problem z przesyłaniem podzielonych danych do Konstruktora obiektów blob. Na moim komputerze przekazanie 5 MB danych powoduje awarię programu Internet Explorer i zużycie pamięci spada. Chrome tworzy kroplę w mgnieniu oka.

Uruchom ten kod, aby porównać:

var byteArrays = [],
    megaBytes = 2,
    byteArray = new Uint8Array(megaBytes*1024*1024),
    block,
    blobSlowOnIE, blobFastOnIE,
    i;

for (i = 0; i < (megaBytes*1024); i++) {
    block = new Uint8Array(1024);
    byteArrays.push(block);
}

//debugger;

console.profile("No Slices");
blobSlowOnIE = new Blob(byteArrays, { type: 'text/plain'});
console.profileEnd();

console.profile("Slices");
blobFastOnIE = new Blob([byteArray], { type: 'text/plain'});
console.profileEnd();

Dlatego postanowiłem zawrzeć obie metody opisane przez Jeremy'ego w jednej funkcji. Podziękowania należą się mu za to.

function base64toBlob(base64Data, contentType, sliceSize) {

    var byteCharacters,
        byteArray,
        byteNumbers,
        blobData,
        blob;

    contentType = contentType || '';

    byteCharacters = atob(base64Data);

    // Get BLOB data sliced or not
    blobData = sliceSize ? getBlobDataSliced() : getBlobDataAtOnce();

    blob = new Blob(blobData, { type: contentType });

    return blob;


    /*
     * Get BLOB data in one slice.
     * => Fast in Internet Explorer on new Blob(...)
     */
    function getBlobDataAtOnce() {
        byteNumbers = new Array(byteCharacters.length);

        for (var i = 0; i < byteCharacters.length; i++) {
            byteNumbers[i] = byteCharacters.charCodeAt(i);
        }

        byteArray = new Uint8Array(byteNumbers);

        return [byteArray];
    }

    /*
     * Get BLOB data in multiple slices.
     * => Slow in Internet Explorer on new Blob(...)
     */
    function getBlobDataSliced() {

        var slice,
            byteArrays = [];

        for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
            slice = byteCharacters.slice(offset, offset + sliceSize);

            byteNumbers = new Array(slice.length);

            for (var i = 0; i < slice.length; i++) {
                byteNumbers[i] = slice.charCodeAt(i);
            }

            byteArray = new Uint8Array(byteNumbers);

            // Add slice
            byteArrays.push(byteArray);
        }

        return byteArrays;
    }
}
martinoss
źródło
Dziękujemy za uwzględnienie tego. Wraz z ostatnią aktualizacją IE11 (między 5/2016 a 8/2016), generowanie obiektów blob z tablic zaczęło przyjmować o wiele większą ilość pamięci RAM. Wysyłając pojedynczy Uint8Array do konstruktora blogów, prawie nie używał pamięci RAM i faktycznie zakończył proces.
Andrew Vogel,
Zwiększenie rozmiaru wycinka w próbce testowej z 1K do 8..16K znacznie skraca czas w IE. Na moim komputerze oryginalny kod trwał od 5 do 8 sekund, kod z blokami 8K zajął tylko 356ms, a 225ms dla bloków 16K
Victor
6

Dla wszystkich miłośników kopiowania i wklejania, takich jak ja, oto gotowana funkcja pobierania, która działa w Chrome, Firefox i Edge:

window.saveFile = function (bytesBase64, mimeType, fileName) {
var fileUrl = "data:" + mimeType + ";base64," + bytesBase64;
fetch(fileUrl)
    .then(response => response.blob())
    .then(blob => {
        var link = window.document.createElement("a");
        link.href = window.URL.createObjectURL(blob, { type: mimeType });
        link.download = fileName;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    });
}
Eyup Yusein
źródło
createObjectURLnie akceptują 2nd argumentu ...
Kompletne
5

Jeśli możesz znieść dodanie jednej zależności do swojego projektu, istnieje świetny blob-utilpakiet npm, który zapewnia przydatną base64StringToBlobfunkcję. Po dodaniu package.jsonmożesz użyć go w następujący sposób:

import { base64StringToBlob } from 'blob-util';

const contentType = 'image/png';
const b64Data = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';

const blob = base64StringToBlob(b64Data, contentType);

// Do whatever you need with your blob...
gabriele.genta
źródło
3

Publikuję bardziej deklaratywny sposób synchronizacji konwersji Base64. Chociaż async fetch().blob()jest bardzo schludny i bardzo podoba mi się to rozwiązanie, nie działa on w przeglądarce Internet Explorer 11 (i prawdopodobnie Edge - nie testowałem tego), nawet z polifillem - spójrz na mój komentarz do Endless ' opublikuj po więcej szczegółów.

const blobPdfFromBase64String = base64String => {
   const byteArray = Uint8Array.from(
     atob(base64String)
       .split('')
       .map(char => char.charCodeAt(0))
   );
  return new Blob([byteArray], { type: 'application/pdf' });
};

Premia

Jeśli chcesz go wydrukować, możesz zrobić coś takiego:

const isIE11 = !!(window.navigator && window.navigator.msSaveOrOpenBlob); // Or however you want to check it
const printPDF = blob => {
   try {
     isIE11
       ? window.navigator.msSaveOrOpenBlob(blob, 'documents.pdf')
       : printJS(URL.createObjectURL(blob)); // http://printjs.crabbly.com/
   } catch (e) {
     throw PDFError;
   }
};

Bonus x 2 - Otwarcie pliku BLOB w nowej karcie dla Internet Explorera 11

Jeśli możesz wykonać wstępne przetwarzanie ciągu Base64 na serwerze, możesz udostępnić go pod jakimś adresem URL i użyć linku printJS:)

Papi
źródło
2

Poniżej znajduje się mój kod TypeScript, który można łatwo przekonwertować na JavaScript i można z niego korzystać

/**
 * Convert BASE64 to BLOB
 * @param Base64Image Pass Base64 image data to convert into the BLOB
 */
private convertBase64ToBlob(Base64Image: any) {
    // Split into two parts
    const parts = Base64Image.split(';base64,');

    // Hold the content type
    const imageType = parts[0].split(':')[1];

    // Decode Base64 string
    const decodedData = window.atob(parts[1]);

    // Create UNIT8ARRAY of size same as row data length
    const uInt8Array = new Uint8Array(decodedData.length);

    // Insert all character code into uInt8Array
    for (let i = 0; i < decodedData.length; ++i) {
        uInt8Array[i] = decodedData.charCodeAt(i);
    }

    // Return BLOB image after conversion
    return new Blob([uInt8Array], { type: imageType });
}
KAUSHIK PARMAR
źródło
4
Ten fragment kodu może być rozwiązaniem, ale wyjaśnienie naprawdę pomaga poprawić jakość posta. Pamiętaj, że w przyszłości odpowiadasz na pytanie czytelników, a ci ludzie mogą nie znać przyczyn Twojej sugestii kodu.
Johan
2
A także dlaczego krzyczycie na komentarze?
canbax,
4
Twój Typescript codekod ma tylko POJEDYNCZY typ, a ten typ to any. Na przykład, dlaczego w ogóle przeszkadza
zoran404
0

Metoda z pobieraniem jest najlepszym rozwiązaniem, ale jeśli ktoś musi użyć metody bez pobierania, oto ona, ponieważ wspomniane wcześniej nie działały dla mnie:

function makeblob(dataURL) {
    const BASE64_MARKER = ';base64,';
    const parts = dataURL.split(BASE64_MARKER);
    const contentType = parts[0].split(':')[1];
    const raw = window.atob(parts[1]);
    const rawLength = raw.length;
    const uInt8Array = new Uint8Array(rawLength);

    for (let i = 0; i < rawLength; ++i) {
        uInt8Array[i] = raw.charCodeAt(i);
    }

    return new Blob([uInt8Array], { type: contentType });
}
akshay
źródło