htermで画像のインライン表示・ダウンロード

f:id:w_haibara:20210402001643p:plain

はじめに

ChromeからSSH接続ができる、Secure Shellという拡張機能をよく使っています。 このSecure ShellはGoogleが開発しているもので、内部でhtermというターミナルエミュレータが使われています。このhtermはiTerm2におけるOSC 1337コマンドを一部実装しており、これを使うことで画像のインライン表示・ファイルのダウンロードが可能です。このエントリではGoでこれらの挙動を検証します。
作成したデモは下記のレポジトリにあります。
github.com

OSC 1337

Mac向けのターミナルエミュレータであるiTerm2は、独自のエスケープシーケンスを定義しています。OSC 1337はその一つです。
Proprietary Escape Codes - Documentation - iTerm2 - macOS Terminal Replacement

OSC 1337は様々な仕様を含みますが、htermでは「画像の表示」「ファイル転送」の2つが実装されています。
hterm and Secure Shell - hterm Control Sequences

画像のインライン表示

ターミナルに画像がインライン表示されます。htermで表示できるのはChromeが解釈できるメディアに限られるようです (それはそう)。 f:id:w_haibara:20210402010005p:plain

ファイル転送(ダウンロード)

SSH接続先のファイルがクライアントにダウンロードされます。画像以外のファイルもダウンロードできます。 f:id:w_haibara:20210402010055p:plain

仕様

iTerm2におけるOSC 1337の画像表示・ファイル転送の仕様は以下に書かれています。
Images - Documentation - iTerm2 - macOS Terminal Replacement

また、htermにおける仕様は以下に書かれています。
hterm and Secure Shell - hterm Control Sequences

画像表示・ファイル転送共に、下記のエスケープシーケンスを含む文字列を出力します。

ESC]1337;File=[arguments]:[base64 data]BEL

ここでargumentsは下記のものです。

  • name: The base64 encoded name of the file or other human readable text.
  • size: How many bytes in the base64 data (for transfer progress).
  • width: The display width specification (see below). Defaults to auto.
  • height: The display height specification (see below). Defaults to auto.
  • preserveAspectRatio: If 0, scale/stretch the display to fit the space. If 1 (the default), fill the display as much as possible without stretching.
  • inline: If 0 (the default), download the file instead of displaying it. If 1, display the file in the terminal.
  • align: Set the display alignment with left (the default), right, or center.
  • type: Set the MIME type of the content. Auto-detected otherwise.
    hterm and Secure Shell - hterm Control Sequences

例えば、ファイル名がsample.png(Base64エンコードするとc2FtcGxlLnBuZw==)で、ダウンロードではなくインライン表示をする場合、argumentsは以下のようになります。

name=c2FtcGxlLnBuZw==;inline=1;

また、base64 dataは画像やその他のファイルのデータをBase64エンコードしたものです。

アプリケーション例

OSC 1337の画像表示を使ったアプリケーションをいくつか挙げます。

Goでの実装

コードは下記レポジトリにあります。Goで書きました。 github.com

  • osc1337パッケージ
package osc1337

import (
    "encoding/base64"
    "fmt"
    "io"
)

type Encoder struct {
    W                   io.Writer
    Name                []byte
    Size                int
    Width               string
    Height              string
    PreserveAspectRatio bool
    Inline              bool
    Align               string
    Type                string
}

func NewEncoder() Encoder {
    return Encoder{
        Width:               "auto",
        Height:              "auto",
        PreserveAspectRatio: true,
        Align:               "left",
        Inline:              false,
    }
}

func (e Encoder) Encode(img []byte) {
    if e.W == nil || img == nil {
        return
    }

    fmt.Fprintf(e.W, "\x1b]1337;File=name=%s",
        base64.StdEncoding.EncodeToString(e.Name))

    if e.Size != 0 {
        fmt.Fprintf(e.W, ";size=%d", e.Size)
    }

    inline := 0
    if e.Inline {
        inline = 1
    }

    preserveAspectRatio := 0
    if e.PreserveAspectRatio {
        preserveAspectRatio = 1
    }

    fmt.Fprintf(e.W,
        ";width=%s;height=%s;preserveAspectRatio=%d;inline=%d;align=%s;type=%s:%s\a\n",
        e.Width, e.Height, preserveAspectRatio, inline, e.Align, e.Type,
        base64.StdEncoding.EncodeToString(img))
}
  • mainパッケージ
package main

import (
    "bufio"
    "bytes"
    "io"
    "os"

    "sample/osc1337"
)

func main() {
    fname := "image.png"
    body, err := importFile(fname)
    if err != nil {
        panic(err.Error())
    }

    var buf bytes.Buffer
    e := osc1337.NewEncoder() // get default values
    e.W = &buf
    e.Name = []byte(fname)
    e.Width = "50%"
    e.Inline = true
    e.Type = "image/png"

    e.Encode(body)

    os.Stdout.Write(buf.Bytes())
    os.Stdout.Sync()
}

func importFile(fname string) ([]byte, error) {
    f, err := os.Open(fname)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    var r io.Reader = bufio.NewReader(f)

    body, err := io.ReadAll(r)
    if err != nil {
        return nil, err
    }

    return body, nil
}

これをビルドして実行すると、実行ファイルと同じディレクトリにあるiamge.pngがインライン表示されます (冒頭で紹介した画像です)。
f:id:w_haibara:20210402010005p:plain
main.goの以下の部分で設定を書き換えることができます。

e := osc1337.NewEncoder() // get default values
e.W = &buf
e.Name = []byte(fname)
e.Width = "50%"
e.Inline = true
e.Type = "image/png"

各種設定による挙動を見てみましょう。

インライン表示・大きさ指定

  • width = 100%
e := osc1337.NewEncoder() // get default values
e.W = &buf
e.Name = []byte(fname)
e.Width = "100%"
e.Inline = true
e.Type = "image/png"

f:id:w_haibara:20210402010246p:plain

インライン表示・Align

  • align = center
e := osc1337.NewEncoder() // get default values
e.W = &buf
e.Name = []byte(fname)
e.Width = "50%"
e.Inline = true
e.Align = "center"
e.Type = "image/png"

f:id:w_haibara:20210402010453p:plain

インライン表示・アスペクト比

  • preserveAspectRatio = 0
e := osc1337.NewEncoder()
e.W = &buf
e.Name = []byte(fname)
e.Width = "50%"
e.Height = "10%"
e.PreserveAspectRatio = false
e.Inline = true
e.Type = "image/png"

f:id:w_haibara:20210402010724p:plain

ダウンロード

  • pngのダウンロード
e := osc1337.NewEncoder()
e.W = &buf
e.Name = []byte(fname)
e.Type = "image/png"

f:id:w_haibara:20210402011120p:plain

  • main.goのダウンロード
fname := "main.go" 
<中略>
e := osc1337.NewEncoder()
e.W = &buf
e.Name = []byte(fname)
e.Type = "image/png"

f:id:w_haibara:20210402011316p:plain

おわりに

htermでインライン表示・ダウンロードの検証ができました。GoのCLIツールで使っていきたいですね。