Spis treści:

Kategoria:HTMLJavaScriptCanvas


Konwersja obrazu canvas do zwykłego obrazka

ikona do artykułu o zapisywaniu obrazów z elementu canvas

Zapisywanie obrazu z elementu canvas

Samochód walnął w drzewo, a świadkiem całego zdarzenia był baca. Przesłuchuje go policjant:
- Baco jak to było?
Na to baca:
- Panocku, widzicie to drzewo?
- Widzę.
- A oni nie widzieli...

Płótno, powszechnie znane jako canvas, jeden z ciekawszych elementów HTML5 pozwala na wiele. Pokazałem już między innymi jak na tym płótnie wygenerować jakiś ciekawy obraz (Generowanie losowego terenu 2D) oraz jak rysować myszką (Rysowanie myszką po canvas w HTML5). Obie te, nazwijmy sobie, aplikacje, chętnie skorzystałyby z jednej dodatkowej funkcji - zapisu utworzonych za jej pomocą obrazków. Moglibyśmy wtedy zapisać gdzieś na serwerze wygenerowany teren lub genialny szkic wykonany myszką i pokazać znajomymPrzeglądarki pozwalają na zapisanie obrazu płótna w postaci pliku na lokalnym dysku.. Da się to oczywiście zrobić programowo i nie jest to tak trudne, jakby się mogło wydawać. Popatrzmy na jedno z przykładowych zastosowań tytułowej techniki.

Generowanie awatarów i zapisywanie w obrazkach

Łatwiej tłumaczyć teorię, gdy widoczne są jej praktyczne możliwości. To dlatego przygotowałem prostą aplikację. Pozwala ona wygenerować losowy awatar i dodać go do listy obrazków. Taki awatar mógłby symbolizować użytkownika forum, członka drużyny lub jakiejś innej grupy. Żeby nie przedłużać, popatrzmy na przykład:

Zasada działania jest prosta. Przycisk Generuj obrazek generuje i rysuje losową facjatę. Jeżeli uznamy, że ilustracja jest odpowiednia, naciskamy przycisk Dodaj do listy. Zrzut płótna jest pobierany do pamięci, a na podstawie tego zrzutu tworzony jest zwykły obrazek, to jest znacznik <img> z odpowiednią zawartością. Ten obrazek staje się integralną częścią strony HTML.

Kod aplikacji

Aplikacja oprócz pobierania zawartości płótna wykonuje parę innych rzeczy. Głównym jej zadaniem jest wygenerowanie przyzwoicie wyglądającego awatara. Co więcej, awatar ten jest losowy - oczywiście w ramach tak skromnego kodu ta losowość jest mocno ograniczona. Samo pobieranie stanu elementu canvas i tworzenie znacznika <img> znajduje się w funkcji getImage() i to na niej należy skupić swoją uwagę. Nie uprzedzajmy jednak faktów. Przyjrzyjmy się listingowi całej aplikacji:

<html>
<head>
    <title>Konwersja obrazu canvas do zwykłego obrazka</title>
</head>
<body>
    <div>
        <canvas id="paint" width="50" height="50"></canvas>
    </div>
    <input type="button" value="Dodaj do listy" onclick="getImage();" />
    <input type="button" value="Generuj obrazek" onclick="generateImage();" />
    <div id="generatedImageList"></div>
    <script>
        function getImage() {
            var canvas = document.getElementById("paint");
            var imgData = canvas.toDataURL("image/png");
            var list = document.getElementById("generatedImageList");
            var img = document.createElement("img");
            img.src = imgData;
            list.appendChild(img);
        }

        function generateImage() {
            var faceColors = ["#EDA", "#EBA", "#432"];
            var canvas = document.getElementById("paint");
            var ctx = canvas.getContext("2d");
            ctx.fillStyle = "#FFF";
            ctx.fillRect(0, 0, 50, 50);
            ctx.fillStyle = faceColors[Math.floor((Math.random() * faceColors.length))];
            fillEllipse(ctx, 25, 25, 10 + Math.random() * 8, 15 + Math.random() * 8);

            var eyeY = Math.random() * 4;
            ctx.fillStyle = "#FFF";
            fillEllipse(ctx, 20, 15 + eyeY, 4, 4);
            fillEllipse(ctx, 30, 15 + eyeY, 4, 4);
            ctx.fillStyle = "#88F";
            fillEllipse(ctx, 20, 15 + eyeY, 2, 2);
            fillEllipse(ctx, 30, 15 + eyeY, 2, 2);

            drawNose(ctx);

            drawSmile(ctx, 25, 30, 7, 2 + Math.random() * 5);
        }

        function fillEllipse(ctx, x, y, rx, ry) {
            ctx.save();
            ctx.scale(rx / ry, 1);
            ctx.beginPath();
            ctx.arc(x * ry / rx, y, ry, 0, Math.PI * 2);
            ctx.fill();
            ctx.closePath();
            ctx.restore();
        }

        function drawNose(ctx) {
            ctx.save();
            ctx.beginPath();
            ctx.lineWidth = 2;
            ctx.moveTo(25, 23);
            ctx.lineTo(25, 27);
            ctx.stroke();
            ctx.closePath();
            ctx.restore();
        }

        function drawSmile(ctx, x, y, rx, ry) {
            ctx.save();
            ctx.strokeStyle = "#F63";
            ctx.lineWidth = 1.5;
            ctx.scale(rx / ry, 1);
            ctx.beginPath();
            ctx.arc(x * ry / rx, y, ry, 0, Math.PI);
            ctx.stroke();
            ctx.closePath();
            ctx.restore();
        }

        generateImage();
    </script>
</body>
</html>

Zamieściłem pełny kod. Można go sobie skopiować i poeksperymentować z różnymi ustawieniami. Funkcja generateImage służy do narysowania losowej twarzyczki i wspomaga się w tym procesie trzema innymi funkcjami: fillEllipse, drawNose i drawSmile. To co ważne, jak wcześniej wspomniałem, znajduje się w funkcji getImage.

Jak to działa?

Kod funkcji getImage jest na tyle krótki, że można go opisać linijka po linijce. Pierwsza linijka to nic nowego - pobieramy element canvas, czyli nasze płótno. Druga linijka jest najważniejsza - to ona pobiera dane reprezentujące obrazek. Parametr metody toDataURL definiuje format, w jakim zwrócone będą dane. Standard wymaga obsługi tylko jednej wartości: image/png. To, jak nietrudno się domyślić, obrazek PNG. Teoretycznie można tu przekazać dowolny typ mime, ale należy mieć na uwadze ograniczone, i ile jakiekolwiek jest, wsparcie niestandardowych formatów. Dane zwrócone są w postaci łańcucha znaków Base64. Przykładowy obrazek będzie miał wobec tego następującą postać:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAADrUlEQ...(inne znaki)

Gdybyśmy zechcieli skonwertować ten łańcuch na zwykły, binarny plik PNG, odczytalibyśmy znaki po przecinku i skonwertowali taki łańcuch na tablicę bajtów. Warto wiedzieć, że słowo base64 nie jest obowiązkowe i wcale nie musi się w takim łańcuchu znajdowaćWięcej informacji można znaleźć w specyfikacji RFC2397.. Dalsza część nie ma już nic wspólnego z elementem canvas. Pobierany jest kontener na wygenerowane obrazki (linia 3), generowany nowy znacznik img (linia 4), do atrybutu src przypisywana jest wartość pobrana przez funkcję toDataURL (linia 5), a na końcu, nowy obrazek wstawiany jest do kodu HTML na koniec listy (linia 6).

Z pozostałych fragmentów kodu warto zwrócić uwagę na zapamiętywanie i przywracanie stanu kontekstu przez wszystkie metody pomocnicze, wykorzystanie skalowania do rysowania elips i wycinków elips, a także wykorzystanie tablicy predefiniowanych kolorów w połączeniu z losowością.

Pobieranie wygenerowanych obrazków

W pierwszej wersji aplikacji obrazki były dodawane do listy. Trzeba wiedzieć, że dane reprezentujące zawartość płótna można dowolnie przetwarzać. Sam format data:image;base64,iVBORw0KGgoA... jest pełnoprawnym adresem URL i tak jak może być wpisany do atrybuty scr obrazka, tak samo może być przypisany do atrybutu href hiperłącza. Popatrzmy na nieco inną implementację funkcji generateImage pokazaną poniżej:

function getImage() {
    var canvas = document.getElementById("paint"),
        imgData = canvas.toDataURL("image/png")
        list = document.getElementById("generatedImageList"),
        a = document.createElement("a"),
        img = document.createElement("img");
    a.href = imgData;
    a.download = "obrazek "+new Date().toLocaleTimeString()+".png";
    img.src = imgData;
    a.appendChild(img);
    //automatycznie pobierz zawartość hiperłącza
    //a.click();
    list.appendChild(a);
}

W tym przypadku wygenerowany obrazek otoczony jest hiperłączem z atrybutem download. Ale nie ten atrybut jest najważniejszy. Zwróćmy uwagę, że ustawiony jest też atrybut href, a jego zawartość jest właśnie naszym ciągiem data. Dla małych obrazków takie rozwiązanie jest wystarczające. Jeżeli jednak obrazki są duże lub jest ich bardzo dużo, warto się zastanowić nad innym rozwiązaniem. Wygenerowany HTML zawiera przecież dwie kopie tekstowej reprezentacji obrazka. Co zrobić?

Blob - duży obiekt binarny

Blob (ang. Binary Large Object, duży obiekt binarny) jest przewidziany do przechowywania dowolnych danych binarnych. W tym również obiektów reprezentujących obrazki. Popatrzmy na przykładową implementację:

function getImageAsBlob(){
    var canvas = document.getElementById("paint"),
        imgData = canvas.toDataURL("image/png"),
        list = document.getElementById("generatedImageList"),
        a = document.createElement("a"),
        img = document.createElement("img"),
        base64Pattern = "base64,",
        base64StartIndex = imgData.indexOf(base64Pattern),
        base64Value = imgData.substr(base64StartIndex+base64Pattern.length),
        binary = atob(base64Value),
        uInt8Array = new Uint8Array(binary.length);

    for (var i = 0; i < binary.length; ++i) {
        uInt8Array[i] = binary.charCodeAt(i);
    }
    var blob = new Blob([uInt8Array]),
        blobUrl = URL.createObjectURL(blob);
        
    a.href = blobUrl;
    a.download = "obrazek "+new Date().toLocaleTimeString()+".png";
    img.src = blobUrl;
    a.appendChild(img);
    list.appendChild(a);
}

Początek jest niemal identyczny. To, co nowe, zaczyna się od zmiennych base*. Najpierw w ciągu data:image;base64,iVBORw0KGgoA... wyszukiwany jest podciąg base64,, bo to po nim znajduje się zawartość pliku w formacie Base64. Taki tekstowy ciąg konwertowany jest na ciąg binarny. I tu pojawia się pułapka. Wartością funkcji atob (ang. Ascii to Binary) jest tekst. Nie jest to tradycyjna tablica bajtów w rozumieniu języków rodziny C. To dlatego trzeba ten tekst skonwertować na coś bardziej konkretnego - na typ Uint8ArraySkrót od angielskiego unsigned integer 8 array, co dosłownie można przetłumaczyć jako tablicę ośmiobitowych (jednobajtowych) liczb całkowitych bez znaku. reprezentujący tablicę bajtów. Potem już jest łatwo. Tworzymy obiekt Blob zgodnie ze składnią, a następnie wykorzystujemy funkcję URL.createObjectURL, która dla tego obiektu binarnego zwróci nam URL. URL do obiektu binarnego należy rozumieć jako URL do lokalnego zasobu przeglądarki. Wszędzie tam, gdzie można podać adres (obrazek, hiperłącze), można również podać URL do obiektu binarnego.

Metoda nie jest może napisana elegancko, ale chciałem dobrze pokazać składowe całego rozwiązania. Obsługę błędów i adresów w postaci data bez kodowania Base64 pozostawiam niedokończoną.

Na koniec muszę jeszcze wspomnieć o pewnej metodzie, która teoretycznie powinna wszystko uprościć: zgodnie z rekomendacją HTML5 obiekt canvas powinien posiadać metodę toBlob. Gdyby tak było, cały kod uprościłby się dość znacznie. Niestety, wsparcie przeglądarek jest na tyle małe, że postanowiłem zrobić to drogą okrężną.

Dopiski

W związku z pytaniem w komentarzu postanowiłem nieco rozszerzyć temat i mam nadzieję, że artykuł jest teraz pełniejszy. Gdyby pojawiły się kolejne pytania lub wątpliwości, zachęcam do pisania. W miarę możliwości postaram się odpowiedzieć na wszystkie pytania.

Kategoria:HTMLJavaScriptCanvas

, 2014-01-29

Komentarze:

vyq (2015-06-01 23:01:34)
a jak można zapisać wygenerowany obrazek do pliku?
PD (2015-06-11 12:33:15)
Uzupełniłem wpis o dwie metody pozwalające pobrać wygenerowany obrazek: hiperłącze z adresem typu "data" oraz poprzez obiekty Blob.
Bugi (2015-11-21 19:10:10)
Witam, jak to zapisać w pliku na serwerze z przykładową nazwą obrazek? Np. w funkcji getImageAsBlob na końcu mamy już obraz w zmiennych i jak te zmienne zapisać? Przesłać je do php i tam jako base64 ?
Bugi (2015-11-22 01:13:08)
Już znalazłem sposób, tworząc nową funkcje i pobierając dane z canvas przesyłamy je używając ajax do pliku z php metodą post. Tam zachodzi zapis do formatu png. Zapewne jest łatwiejszy sposób.
Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za rozjasnienie zagadnienia upsert. wlasnie sie ucze programowania :).
Co się stanie gdy spróbuję wyszukać:
SELECT * FROM NV_Airport WHERE Code='SVO'
SELECT * FROM V_Airport WHERE Code=N'SVO'
(odwrotnie są te N-ki)
Będzie konwersja czy nie znajdzie żadnego rekordu?