Highcharts SVG zu Base64 PNG konvertieren für Drag & Drop Funktion

Will man ein Chart aus dem Browser in eine Office-Anwendung ziehen, so muss es als Bild downloadbar auf einem Server liegen. Hier zeigen wir eine Lösung mit AngularJS und ASP .NET. 

highcharts-svg-dragmode-1

Highcharts hat zwar eine Exportfunktion integriert, über welche das Chart als Bild heruntergeladen werden kann, jedoch genügt das manchen Anforderungen nicht, z.B. wenn das Chart „einfach“ per Drag & Drop in eine Office-Anwendung gezogen werden soll. In diesem Fall muss aus dem SVG des Charts manuell ein Bild erstellt, als Base64 auf den Server übertragen und dort per Download-Link zur Verfügung gestellt werden.

Hier nun ein Beispiel für AngularJS / Typescript und ASP .NET.

Im Controller bauen wir zunächst einen einfachen Chart.

this.$scope.highchartsConfig = {
	options: {
		chart: {
			type: 'bar'
		}
	},
	series: [{
			data: [10, 15, 12, 8, 7]
		}],
	title: {
		text: 'bar chart'
	},
	loading: false
};

Im Html zeigen wir das Chart an, wenn wir nicht im DragMode sind. Highcharts erzeugt automatisch ein SVG.

<div ng-show="!isDragMode">
	<highchart id="highchartContainer" config="highchartsConfig"></highchart>
</div>

Per Button können wir in den DragMode schalten. Beim Klick wird eine Funktion aufgerufen, welche das SVG in ein Base64 PNG umwandelt.

<button class="btn btn-default" ng-click="vm.dragMode()">DragMode</button>

Im Controller suchen wir zunächst das SVG.

// find the svg
var svg = (<HTMLElement>document.getElementById('highchartsContainer').children[0]).innerHTML;

Dann erzeugen wir ein neues Image mit dem SVG als Quelle.

// svg as image
var base_image = new Image();
var svgString = "data:image/svg+xml," + svg;
base_image.src = svgString;

Nun erzeugen wir ein Canvas Objekt und rendern das Bild darauf.

var canvas = document.createElement('canvas');
canvas.width = base_image.width;
canvas.height = base_image.height;
var context = canvas.getContext('2d').drawImage(base_image, 0, 0, canvas.width, canvas.height);

Aus dem Canvas können wir nun ein Base64 erhalten und entfernen noch die Informationen.

var base64img = canvas.toDataURL("image/png").split("data:image/png;base64,")[1];

Jetzt können wir den Base64 String zum Server posten und erhalten bei Erfolg den Pfad zum Bild zurück.

this._service.getImageUrl(base64img)
    .success((response: any) => {
	this.$scope.imageUrl = this.$location.protocol() + "://" + this.$location.host() + ":" + this.$location.port() + response.url;
    })
    .error((error) => {
	this.$scope.isImageUrlError = true;
    });

Auf dem Server erzeugen wir einen Dateinamen und ein Bytearray aus dem Base64 String.

string filename = System.Guid.NewGuid() + ".png";
byte[] data = System.Convert.FromBase64String(request.Base64Image);

Jetzt können wir aus dem stream das bild speichern und den Pfad zurückliefern.

using (MemoryStream ms = new MemoryStream(data))
{
    var image = System.Drawing.Image.FromStream(ms);
    string path = _environment.WebRootPath + "/tmp";
                
    string fullname = Path.Combine(path, filename);
    image.Save(fullname);

    var response = new ExportImageResponse() { Url = "/tmp/"+filename};
    return Ok(response);
}

Und hier noch einmal alles zusammen.

<div class="container" ng-controller="ChartCtrl">
	<div class="row">
		<div class="col-lg-12">
			<div class="pull-right" style="margin: 3px;">
				<button class="btn btn-default" ng-click="vm.dragMode()">
					<span ng-show="!isDragMode">
						<i class="fa fa-arrows"></i> Dragmode on
					</span>
					<span ng-show="isDragMode">
						<i class="fa fa-ban"></i> DragModeOf
					</span>
				</button>
			</div>
			
			<div ng-show="!isDragMode">
				<highchart id="highchartContainer" config="highchartsConfig"></highchart>
			</div>

			<div class="thumbnail" ng-show="isDragMode">
				<img ng-src="{{imageUrl}}" draggable="true" />
			</div>
		</div>
	</div>
</div>

export interface ICtrlScope extends ng.IScope {
	vm: ChartCtrl;
	isDragMode: boolean;
	imageUrl: string;
	isImageUrlError: boolean;
	highchartsConfig: any;
}

export class ChartCtrl {
	public static $inject = ['$scope', 'ChartService', '$location', '$document', '$window'];
	constructor(
		private $scope: ICtrlScope,
		private _service: ChartService,
		private $location: ng.ILocationService,
		private $document: ng.IDocumentService,
		private $window: ng.IWindowService
	) {
		this.$scope.vm = this;
		this.$scope.isDragMode = false;
		
		this.$scope.highchartsConfig = {
			options: {
				chart: {
					type: 'bar'
				}
			},
			series: [{
					data: [10, 15, 12, 8, 7]
				}],
			title: {
				text: 'bar chart'
			},
			loading: false
		};
	}

	dragMode() {
		this.$scope.isDragMode = !this.$scope.isDragMode;
		if (this.$scope.isDragMode) {
			// find the svg
			var svg = (<HTMLElement>document.getElementById('highchartsContainer').children[0]).innerHTML;
			
			// svg as image
			var base_image = new Image();
			var svgString = "data:image/svg+xml," + svg;
			base_image.src = svgString;

			// create new canvas with the image
			var canvas = document.createElement('canvas');
			canvas.width = base_image.width;
			canvas.height = base_image.height;
			var context = canvas.getContext('2d');
			context.drawImage(base_image, 0, 0, canvas.width, canvas.height);

			// base64
			var base64img = canvas.toDataURL("image/png").split("data:image/png;base64,")[1];
			
			// post
			var response = this._service.getImageUrl(base64img)
				.success((response: any) => {
					this.$scope.imageUrl = this.$location.protocol() + "://" + this.$location.host() + ":" + this.$location.port() + response.url;
				})
				.error((error) => {
					this.$scope.isImageUrlError = true;
				});
		} 
	}
}

module Services {
    'use strict';

    export class ChartService {
        static $inject = ['$http'];
        constructor(private _$http: ng.IHttpService) { }

        getImageUrl(base64: string) {
            var request = { base64Image: base64 };
            return this._$http.post('api/image', request);
        }
    }

    angular.module('services', [])
        .service('ChartService', ChartService);
}

using Microsoft.AspNetCore.Mvc;
using System.IO;
using Microsoft.AspNetCore.Hosting;

namespace WebUI.Controllers
{
    public class ImageRequest
    {
        public string Base64Image { get; set; }
    }

    public class ImageResponse
    {
        public string Url { get; set; }
    }


    [Route("api/[controller]")]
    public class ImageController : Controller
    {
        private IHostingEnvironment _environment;
		
        public ExportController(IHostingEnvironment environment)
        {
            _environment = environment;
        }

        [HttpPost]
        public IActionResult Image([FromBody]ExportImageRequest request)
        {
            string filename = System.Guid.NewGuid() + ".png";
            byte[] data = System.Convert.FromBase64String(request.Base64Image);

            using (MemoryStream ms = new MemoryStream(data))
            {
                var image = System.Drawing.Image.FromStream(ms);
                string path = _environment.WebRootPath + "/tmp";
                
                string fullname = Path.Combine(path, filename);
                image.Save(fullname);

                var response = new ExportImageResponse() { Url = "/tmp/"+filename};
                return Ok(response);
            }
        }
    }
}

Firefox und Internet Explorer

Jetzt hat vielleicht der ein oder andere bemerkt, dass diese Lösung nicht im Firefox und Internet Explorer funktioniert. Die Codierung verhält sich anders und das Zeichnen des SVG auf die Canvas scheitert. Warum das genau passiert, konnte ich nicht herausfinden. Nur die Lösung: die Bibliothek canvg. Diese muss manuell heruntergeladen werden, da man über Bower nur eine veraltete Version erhält. Und dann lässt man diese das Zeichnen machen.

Die Bibliothek binden wir im Html ein.

<script src="lib/canvg/rgbcolor.js" type="text/javascript"></script>
<script src="lib/canvg/StackBlur.js" type="text/javascript"></script>
<script src="lib/canvg/canvg.js" type="text/javascript"></script>

Jetzt holen wir uns zusätzlich zum Html des SVG den Container. Daraus können wir die Größe des Canvas setzen. Diese Variante funktioniert im Firefox besser, als die oben beschriebene.

var svgBox = <SVGSVGElement>((<HTMLElement>document.getElementById('highchartsContainer').children[0]).children[0]);

var canvas = document.createElement('canvas');
canvas.width = svgBox.width.baseVal.value;
canvas.height = svgBox.height.baseVal.value;

Und jetzt lassen wir canvg die schmutzige Arbeit machen.

canvg(canvas, svg, {
     scaleWidth: canvas.width,
     scaleHeight: canvas.height,
     ignoreDimensions: true
});

Hier noch einmal die dragMode-Funktion mit canvg im Gesamten.

dragMode() {
	this.$scope.isDragMode = !this.$scope.isDragMode;
	if (this.$scope.isDragMode) {
		// create Base64 PNG

		// find the svg
		var svgBox = <SVGSVGElement>((<HTMLElement>document.getElementById('highchartsContainer').children[0]).children[0]);
		var svg = (<HTMLElement>document.getElementById('highchartsContainer').children[0]).innerHTML;

		// create a canvas
		var canvas = document.createElement('canvas');
		canvas.width = svgBox.width.baseVal.value;
		canvas.height = svgBox.height.baseVal.value;

		// it only works with canvg in firefox and ie 
		canvg(canvas, svg, {
			scaleWidth: canvas.width,
			scaleHeight: canvas.height,
			ignoreDimensions: true
		});

		// get the base64
		var base64img = canvas.toDataURL("image/png").split("data:image/png;base64,")[1];
		//console.log(base64img, 'base64img');

		// post base64 to server to get a download link
		var response = this._dashboardService.getImageUrl(base64img)
			.success((response: any) => {
				this.$scope.imageUrl = this.$location.protocol() + "://" + this.$location.host() + ":" + this.$location.port() + response.url;
			})
			.error((error) => {
				console.log("Error");
				this.$scope.isImageUrlError = true;
			});

	} else {
		this.$scope.isImageUrlError = false;
	}
}

Quellen

Wir hoffen wie immer, diese erstbeste Lösung ist hilfreich.

Ähnliche Beiträge

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert