#はじめに
Thetaの360°画像にぼかしを入れるWebアプリSphereBlur.comを作った際に、元のJPEG画像から取り出した姿勢情報を、編集後に保存する際に一緒に書き込む必要があった。(JPEGから姿勢情報を取り出す方法についてはこちら参照)
#画像からJPEGデータへの変換方法
ブラウザに組み込まれたAPIとして、Canvasエレメントの
var data = canvas.toDataURL('image/jpeg');
を呼び出す方法がある。しかし、この方法だと当然メタデータは保存できない。もちろん、このデータの中身にメタデータを追加するということは可能だが、いろいろ調べてみると、toDataURL()に頼らずに、JavaScriptだけでJPEGデータに変換するJPEGEncoderというライブラリを見つけたので、そちらを使ってみることにした。
#JPEGEncoderの改造
JPEGEncoder自体にはメタデータを付与する機能はないので、少し改造する。JPEGEncoder.encode()メソッドを読んでいくと、
this.encode = function(image,quality,toRaw) // image data object
{
var time_start = new Date().getTime();
if(quality) setQuality(quality);
// Initialize bit writer
byteout = new Array();
bytenew=0;
bytepos=7;
// Add JPEG headers
writeWord(0xFFD8); // SOI
writeAPP0();
writeDQT();
writeSOF0(image.width,image.height);
writeDHT();
writeSOS();
// 後略
}
と、いかにもJPEGのヘッダーを書き込んでる部分があったので、下記のように、image.xmpがある場合はXMPのAPP1ヘッダーを書き込むように変更する。EXIFに関しても同様の改造をすれば対応できるが、今回はEXIFには対応してない。
this.encode = function(image,quality,toRaw) // image data object
{
var time_start = new Date().getTime();
if(quality) setQuality(quality);
// Initialize bit writer
byteout = new Array();
bytenew=0;
bytepos=7;
// Add JPEG headers
writeWord(0xFFD8); // SOI
writeAPP0();
if (image.xmp) {
var xap = "http://ns.adobe.com/xap/1.0/\0";
writeWord(0xFFE1); // marker of APP1
writeWord(image.xmp.length + xap.length + 3);
for (var i=0; i<xap.length; i++) {
writeByte(xap.charCodeAt(i));
}
for (var i=0; i<image.xmp.length; i++) {
writeByte(image.xmp.charCodeAt(i));
}
writeByte(0);
}
writeDQT();
writeSOF0(image.width,image.height);
writeDHT();
writeSOS();
// 後略
}
#WebGLのフレームバッファーからピクセルデータの取得
WebGLのフレームバッファーからピクセルデータの取得するには、gl.readPixels()を使う。
_getBufferPixelsForSave: function() {
var pixels = new Uint8Array(this._frameBufferForEdit0.width * this._frameBufferForEdit0.height * 4);
this._gl.bindFramebuffer(this._gl.FRAMEBUFFER, this._frameBufferForEdit0.f);
this._gl.readPixels(0, 0, this._frameBufferForEdit0.width, this._frameBufferForEdit0.height, this._gl.RGBA, this._gl.UNSIGNED_BYTE, pixels);
this._gl.bindFramebuffer(this._gl.FRAMEBUFFER, null);
return pixels;
},
#XMPメタデータの作成
XMPの仕様および、Photo Sphere XMP Metadataの仕様に従って下記のようなXMLの文字列を作成する。
_createXMP: function() {
var w = this._frameBufferForEdit0.width;
var h = this._frameBufferForEdit0.height;
var heading = 0;
var pitch = 0;
var roll = 0;
if (this._poseInfo) {
heading = this._poseInfo.heading;
pitch = this._poseInfo.pitch;
roll = this._poseInfo.roll;
}
var xmp =
'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>' +
'<x:xmpmeta xmlns:x="adobe:ns:meta/" xmptk="Sphere Blur">' +
'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' +
'<rdf:Description rdf:about="" xmlns:GPano="http://ns.google.com/photos/1.0/panorama/">' +
'<GPano:ProjectionType>equirectangular</GPano:ProjectionType>' +
'<GPano:UsePanoramaViewer>True</GPano:UsePanoramaViewer>' +
'<GPano:CroppedAreaImageWidthPixels>' + w + '</GPano:CroppedAreaImageWidthPixels>' +
'<GPano:CroppedAreaImageHeightPixels>' + h + '</GPano:CroppedAreaImageHeightPixels>' +
'<GPano:FullPanoWidthPixels>' + w + '</GPano:FullPanoWidthPixels>' +
'<GPano:FullPanoHeightPixels>' + h + '</GPano:FullPanoHeightPixels>' +
'<GPano:CroppedAreaLeftPixels>0</GPano:CroppedAreaLeftPixels>' +
'<GPano:CroppedAreaTopPixels>0</GPano:CroppedAreaTopPixels>' +
'<GPano:PoseHeadingDegrees>' + heading + '</GPano:PoseHeadingDegrees>' +
'<GPano:PosePitchDegrees>' + pitch + '</GPano:PosePitchDegrees>' +
'<GPano:PoseRollDegrees>' + roll + '</GPano:PoseRollDegrees>' +
'</rdf:Description>' +
'</rdf:RDF>' +
'</x:xmpmeta>' +
'<?xpacket end="r"?>';
return xmp;
},
#JPEGファイルの保存
あとは、JPEGEncoderを使って実際にピクセルデータからXMP付きのJPEGデータのデータURLを作って、Aタグエレメントを作ってa.downloadを指定してa.click()を呼べばダウンロードすることができる。
save: function() {
var pixels = this._getBufferPixelsForSave();
var jpegEnc = new JPEGEncoder();
var jpegUri = jpegEnc.encode({
width: this._frameBufferForEdit0.width,
height: this._frameBufferForEdit0.height,
data: pixels,
xmp: this._createXMP()
}, 70);
var a = document.createElement('a');
a.href = jpegUri;
document.body.appendChild(a);
a.download = "image.jpg";
a.click();
document.body.removeChild(a);
},