LoginSignup
18
23

More than 5 years have passed since last update.

JavaScriptでJPEGを出力する際にEXIFやXMPのメタデータを付与する

Last updated at Posted at 2016-01-27

はじめに

Thetaの360°画像にぼかしを入れるWebアプリSphereBlur.com作った際に、元のJPEG画像から取り出した姿勢情報を、編集後に保存する際に一緒に書き込む必要があった。(JPEGから姿勢情報を取り出す方法についてはこちら参照)

画像からJPEGデータへの変換方法

ブラウザに組み込まれたAPIとして、Canvasエレメントの

var data = canvas.toDataURL('image/jpeg');

を呼び出す方法がある。しかし、この方法だと当然メタデータは保存できない。もちろん、このデータの中身にメタデータを追加するということは可能だが、いろいろ調べてみると、toDataURL()に頼らずに、JavaScriptだけでJPEGデータに変換するJPEGEncoderというライブラリを見つけたので、そちらを使ってみることにした。

JPEGEncoderの改造

JPEGEncoder自体にはメタデータを付与する機能はないので、少し改造する。JPEGEncoder.encode()メソッドを読んでいくと、

jpeg_encoder_basic.js
        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には対応してない。

jpeg_encoder_basic.js
        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);
  },
18
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
23