Raspberry PiのCUI画面に直接JPEG画像を表示する（libjpeg）

# 概要
Raspberry Pi ZeroでGUIを使わずにCUIで色々やりたいです（非力なRaspberry Pi ZeroにGUIは重すぎる）。
たまに画像を表示したいのですが、そんなときは `fbi` を使います。
でも、自力で画像を表示するプログラムを作ってみたい。
ということで作ってみました。

# 作るのは面倒なので画像を見たいだけなら
こんな感じで `fbi` を使うとCUIでも簡単に画像見れます。
`fbi` の使い方はググってください。

```shell
$ sudo apt install fbi
$ fbi hogehoge.jpg
```

# ここでやること
まず以下の動作確認をしたのは Raspberry Pi Zero WH です。
他の機種でも大きくは変わらないと思いますが、もしかするとうまくいかないかもしれません。

で、以下でやることは、、

* jpeg を読み込む（C言語／libjpeg使用）
* 読み込んだ画像をフレームバッファ用に変換
* フレームバッファに書き出す

です。

# フレームバッファの話
Raspberry Pi のフレームバッファは `/dev/fb0` なので、ここに所定のフォーマットで値を書き込んでいくとCUIの画面を書き換えることができます。

## フレームバッファのフォーマット
0RGB0RGB0RGB... と1ピクセル4バイト（0:1バイト（ダミー？）、R:1バイト、G:1バイト、B:1バイト）とフレームバッファのサイズ分だけ並んでいるようです。

## フレームバッファのサイズ
フレームバッファのサイズは `fbset` で確認できます。私の環境では 1824x984 でした。

```shell
$ fbset

mode "1824x984"
    geometry 1824 984 1824 984 32
    timings 0 0 0 0 0 0 0
    rgba 8/16,8/8,8/0,8/24
endmode
```

## フレームバッファのデータを読み込んでみる（CUI画面のスクリーンショット）

フレームバッファのデータをPPMという形式に直すプログラム。
ヘッダとして `P6\n画像の幅 画像の高さ\n最大輝度` を足して、1ピクセル4バイトのデータを3バイト（0RGB0RGB... → RGBRGB...）に直してます。

```c:fb2ppm.c
#include <stdio.h>
#include <stdint.h>

#define WIDTH  1824
#define HEIGHT  984
typedef uint32_t pixel;

int main()
{
    pixel p;
    printf("P6\n%d %d\n%d\n", WIDTH, HEIGHT, 255);
    for(int i=0; i < WIDTH * HEIGHT; i++){
        fread(&p, sizeof(pixel), 1, stdin);
        fputc((p >> 16) & 0xFF, stdout);
        fputc((p >>  8) & 0xFF, stdout);
        fputc((p >>  0) & 0xFF, stdout);
    }
}
```

これで以下のようにすると、CUI画面のスクリーンショットを取ることができます。
`WIDTH` と `HEIGHT` の定義は自分の環境に合わせないと画像が崩れますのでご注意を。
（以下同様）

```shell
$ gcc fb2ppm.c -o fb2ppm
$ ./fb2ppm < /dev/fb0 > snapshot.ppm
```

PPM形式の画像は最初に紹介した `fbi` でも見れますし、Windowsでは[IrfanView](https://forest.watch.impress.co.jp/library/software/irfanview/)などのソフトでも見ることができます。
ヘッダを部分を外して4バイト（0x00RRGGBB）に直して `/dev/fb0` に書き戻してやるとCUI画面に表示することもできます。

## フレームバッファを塗りつぶす

```c:fillFb.c
#include <stdio.h>
#include <stdint.h>

#define WIDTH 1824
#define HEIGHT 984
typedef uint32_t pixel;

int main()
{
    pixel p = 0x00000000;
    for(int i=0; i < WIDTH * HEIGHT; i++){
        fwrite(&p, sizeof(pixel), 1, stdout);
    }
}
```

からの

```shell
$ gcc fillFb.c -o fillFb
$ fillFb > /dev/fb0
```

で、画面が真っ黒に。
`p` の値は 1バイトずつ 0x00RRGGBB な感じで値を変えてやると好きな色で塗りつぶせます。

いちいち標準出力を `/dev/fb0` に渡してやるのも面倒なので、

```c:fillFb2.c
#include <stdio.h>
#include <stdint.h>

#define WIDTH 1824
#define HEIGHT 984

typedef uint32_t pixel;

int main()
{
    pixel p = 0x00000088;
    FILE *fp;

    fp = fopen("/dev/fb0", "w");
    for(int i=0; i < WIDTH * HEIGHT; i++){
        fwrite(&p, sizeof(pixel), 1, stdout);
    }
    fclose(fp);
}
``` 

みたいなのももちろんありです。

# いよいよJPEGをフレームバッファに書き出してみる
その前に `libjpeg` のインストール

```shell
$ sudo apt install libjpeg-dev
```

で、、（ここに全部貼るにはちょっと長いですが、、）

```c:viewJpeg.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/stat.h>
#include <jpeglib.h>

#define WIDTH 1824
#define HEIGHT 984

typedef uint32_t pixel;

int main(int argc, char * const argv[])
{
    // 第1引数は JPEGファイル名（入力）
    if(argc < 2){
        printf("err: arg cnt\n");
        return -1;
    }
    char *jpgFileName = argv[1];
    FILE* fpJpg = fopen(jpgFileName, "rb");
    if (fpJpg == NULL) {
        printf("err: jpeg filename: %s\n", jpgFileName);
        return -1;
    }

    // JPEGファイル読み込み
    struct jpeg_decompress_struct jpgInfo;
    jpeg_create_decompress(&jpgInfo);

    struct jpeg_error_mgr jpgErr;
    jpgInfo.err = jpeg_std_error(&jpgErr); // 既定値

    jpeg_stdio_src(&jpgInfo, fpJpg);
    jpeg_read_header(&jpgInfo, TRUE);

    jpeg_start_decompress(&jpgInfo);

    // 読み込みメモリ領域確保
    JSAMPARRAY jpgData = (JSAMPARRAY)malloc(sizeof(JSAMPROW) * jpgInfo.output_height);
    for(int i = 0; i < jpgInfo.output_height; i++){
        jpgData[i] = (JSAMPROW)calloc(sizeof(JSAMPLE), jpgInfo.output_width * jpgInfo.output_components);
    }

    // 画像データ
    while(jpgInfo.output_scanline < jpgInfo.output_height){
        jpeg_read_scanlines(&jpgInfo, jpgData + jpgInfo.output_scanline, jpgInfo.output_height - jpgInfo.output_scanline);
    }

    // JPEGライブラリの後片付け
    jpeg_finish_decompress(&jpgInfo);
    jpeg_destroy_decompress(&jpgInfo);
    fclose(fpJpg);

    // フレームバッファ用のメモリ領域確保
    pixel *fb = (pixel*)malloc(WIDTH * HEIGHT * sizeof(pixel));
    pixel *cfb;
    for(int y=0; y<HEIGHT && y<jpgInfo.output_height; y++){
        cfb = fb + y * WIDTH;
        for(int x=0; x<WIDTH && x<jpgInfo.output_width; x++){
            // JPEGデータは1ピクセル3バイト、フレームバッファは4バイト
            *cfb = (pixel)((jpgData[y][x*3] << 16) + (jpgData[y][x*3+1] << 8) + jpgData[y][x*3+2]);
            cfb++;
        }
    }
    
    // フレームバッファに書き出し
    FILE* fp = fopen("/dev/fb0", "w");
    fwrite(fb, sizeof(pixel), WIDTH*HEIGHT, fp);
    fclose(fp);

    // メモリ解放
    free(fb);
    for(int i = 0; i < jpgInfo.output_height; ++i){
        free(jpgData[i]);
    }
    free(jpgData);

    return 0;
}
```

エラー処理とかすっ飛ばしてます。
大きい画像は画面からはみ出ます。
`libjpeg` についても書くと長くなりすぎるのでまた今度。

要するに JPEG 画像を読み込んで、1ピクセルごとにフレームバッファの 0x00RRGGBB 形式に直して、`/dev/fb0` に書き込んでやるという処理になっています。

ビルド時は -ljpeg オプションを付けるのを忘れずに、、

```shell
$ gcc viewJpeg.c -o viewJpeg -ljpeg
$ viewJpeg hogehoge.jpg
```

hogehoge.jpg がCUI画面上に表示されれば成功です。