画像を別の形式に変換するクソアプリ

[クソアプリ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/kuso-app) 1日目の記事です。

アドベントカレンダーの記事投稿を終わらせた俺にかわせない攻撃は無い

# 前置き
おはようございます。DE-TEIUです。
冬の季語でおなじみのクソアプリアドベントカレンダーの時間です。

## 過去にアドベントカレンダー用に作ったクソアプリ
- [p5.jsで作ったダメなソリティア](https://qiita.com/de_teiu_tkg/items/c938fbb52218185659e7)
- [絶対に作業できない作業用BGMサイト](https://qiita.com/de_teiu_tkg/items/6ebcef3550507258edaf)
- [社会に一石を投じるクソアプリ開発](https://qiita.com/de_teiu_tkg/items/e82870eca1f265807dbe)
- [鼻毛が生えるカメラアプリ](https://qiita.com/de_teiu_tkg/items/213aa2ade2ef840a33b5)
- [映画館のスクリーンでYouTubeが見られるクソアプリ](https://qiita.com/de_teiu_tkg/items/94259a0f90d1db861237)
- [電卓の成長を体験できるクソアプリ](https://qiita.com/de_teiu_tkg/items/5d878f01207f408ee919)
- [JamstackだけどJamstackじゃないクソアプリ](https://qiita.com/de_teiu_tkg/items/cdb6ad2972a23663dbde)

## 

画像ファイルを別の形式に変換するアプリって色々あるじゃないですか。
何か適当な画像ファイルを渡すと、以下の形式に変換して出力する、みたいなやつ。
- jpg
- png
- gif
- webp
- tif
- etc...

ちょっと軽く調べただけでも、わりと色んな形式の変換に対応したアプリが色々見つかります。
しかし、恐らく画像をとある形式に変換するアプリは無さそうだったので、今回はそれに対応した画像変換アプリを開発してみました。

# 成果物

[Image to Excel](https://imagetoexcel.vercel.app/)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173272/bc7e6b32-d58c-7487-d675-b9f1d01dada7.png)

## 画像をExcelファイル形式に
変換できたと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173272/4b28db2e-1042-297a-e451-24aa610af73e.png)

## Excelファイルの中身どうなってんの

極小のセル1つ1つに背景色を指定することで画像を表現しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173272/5957b478-93ce-6486-801d-b67c7419a027.png)

# 構成
これだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173272/7f379d8a-df72-dac2-537f-d0e31c4cf8f1.png)

- フロントエンドはSvelteKitで実装してVercelで実行
- サーバサイド(画像→Excel変換)はJavaで実装してAWS Lambdaで実装

# 解説

- [AWS SAMでLambdaの開発環境構築](#aws-samでlambdaの開発環境構築)
- [Apache POIでExcelファイル生成](#apache-poiでexcelファイル生成)
- [JH Labs Java Image Filtersで画像の減色](#jh-labs-java-image-filtersで画像の減色)

## AWS SAMでLambdaの開発環境構築

ローカル環境にLambdaの開発環境を構築する際は、**AWS SAM（AWS Serverless Application Model）** を使うと楽です。


> AWS Serverless Application Model (AWS SAM) は、AWS 上でサーバーレスアプリケーションを構築および実行するデベロッパーのエクスペリエンスを改善するツールキットです。AWS SAM は次の 2 つの主要な部分で構成されます。
> 1. AWS SAM テンプレート仕様 – AWS でサーバーレスアプリケーションインフラストラクチャを定義するために使用できるオープンソースフレームワーク。
> 2. AWS SAM コマンドラインインターフェイス (AWS SAM CLI) – AWS SAM テンプレートやサポートされているサードパーティーの統合と併用することで、サーバーレスアプリケーションを構築し、実行できるコマンドラインツール。

（[AWS公式のデベロッパーガイド](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html)から引用）

要するに、AWSでサーバーレスアプリケーションを構築する際に使えるお助けツール的なものです。

### 準備

1. ローカル環境にAWS SAM CLIをインストール

    [公式ドキュメント](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/install-sam-cli.html)の手順に従ってインストールし、samコマンドを使えるようにします。

2. ローカル環境にDockerの実行環境を用意
  Lambdaの実行環境をローカルに作成する際、コンテナを作成することになるのでDockerをインストールしておきます。
  （私は普段Windowsで開発しているため、WSL2とDocker Desktop for Windowsをインストールしました）

### プロジェクト作成

Lambdaプロジェクトを作成したいディレクトリで、以下のコマンドを実行します。

```bash
$ sam init
```

あとは案内に従って数字を入力していくだけです。
（この記事では、JavaでのLambda開発環境を作る想定で入力します）


- プロジェクトに使用するテンプレートの種類を選択
```
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1
```

- 使用するテンプレートを選択

```
Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Data processing
        3 - Hello World Example with Powertools for AWS Lambda
        4 - Multi-step workflow
        5 - Scheduled task
        6 - Standalone function
        7 - Serverless API
        8 - Infrastructure event management
        9 - Lambda Response Streaming
        10 - Serverless Connector Hello World Example
        11 - Multi-step workflow with Connectors
        12 - GraphQLApi Hello World Example
        13 - Full Stack
        14 - Lambda EFS example
        15 - Hello World Example With Powertools for AWS Lambda
        16 - DynamoDB Example
        17 - Machine Learning
Template: 1
```

- 最も一般的なランタイムとパッケージ（Pythonとzip）を使うか
（今回はJavaを使うのでNo）
```
Use the most popular runtime and package type? (Python and zip) [y/N]: N
```

- 開発に使用したい言語とバージョンを選択
```
Which runtime would you like to use?
        1 - aot.dotnet7 (provided.al2)
        2 - dotnet6
        3 - go1.x
        4 - go (provided.al2)
        5 - go (provided.al2023)
        6 - graalvm.java11 (provided.al2)
        7 - graalvm.java17 (provided.al2)
        8 - java21
        9 - java17
        10 - java11
        11 - java8.al2
        12 - java8
        13 - nodejs20.x
        14 - nodejs18.x
        15 - nodejs16.x
        16 - nodejs14.x
        17 - python3.9
        18 - python3.8
        19 - python3.7
        20 - python3.12
        21 - python3.11
        22 - python3.10
        23 - ruby3.2
        24 - ruby2.7
        25 - rust (provided.al2)
        26 - rust (provided.al2023)
Runtime: 9
```


- パッケージタイプの選択
```
What package type would you like to use?
        1 - Zip
        2 - Image
Package type: 2
```

- GradleとMavenのどちらを使うか
```
Which dependency manager would you like to use?
        1 - gradle
        2 - maven
Dependency manager: 2
```
- X-Ray（アプリケーションへのリクエストに関するデータを収集するやつ）を使うか
```
Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: N
```

- CloudWatch Application Insights（アプリケーションのモニタリングをするやつ）を使うか
```
Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: N
```

- 関数内にJSON形式の構造化ログを設定するか
```
Would you like to set Structured Logging in JSON format on your Lambda functions?  [y/N]: N
```

- プロジェクト名を入力
```
Project name [sam-app]: test
```

最後にこんなメッセージが表示されて完了。
```
    -----------------------
    Generating application:
    -----------------------
    Name: test
    Base Image: amazon/java17-base
    Architectures: x86_64
    Dependency Manager: maven
    Output Directory: .
    Configuration file: test\samconfig.toml

    Next steps can be found in the README file at test\README.md


Commands you can use next
=========================
[*] Create pipeline: cd test && sam pipeline init --bootstrap
[*] Validate SAM template: cd test && sam validate
[*] Test Function in the Cloud: cd test && sam sync --stack-name {stack-name} --watch
```

作成されたディレクトリの中を見ると、アプリのソースコードやら諸々の設定ファイルやらが入っています。
これで開発の準備は完了。

#### 余談
※Lambdaでは、アプリのデプロイを以下のどちらかの方法で行えます。
-  必要なファイルをまとめたzipファイルをアップロードする
-  コンテナイメージをアップロードする

個人的にはコンテナの方がやりやすかったのでそっちにしてます。
あとzipファイルでやる場合は、
- アップロードするzipファイルのサイズは最大50MB
- zip解凍後のディレクトリ内のファイルサイズは最大250MB

という制限があるのでご注意ください。（コンテナだと最大10GB）

### ビルドしてみる
作成されたディレクトリの直下(template.yamlがあるところ)に移動し、以下のコマンドを実行。

```bash
$ sam build
```
これでlambdaの実行環境が入ったDockerのコンテナイメージが作成されます。
MavenかGradleのビルドが走ったログなどが出てくるはず。

### ローカルで実行してみる
以下のコマンドを実行してローカルサーバー起動。
```
$ sam local start-lambda
```

こんなメッセージが表示されたら起動完了。
```
Initializing the lambda functions containers.
Building image.................
Using local image: helloworldfunction:rapid-x86_64.

Containers Initialization is done.
Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template through the endpoint.
2023-12-03 20:51:42 WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:3001
2023-12-03 20:51:42 Press CTRL+C to quit
```
http://127.0.0.1:3001 にリクエストを投げるとLambda関数が実行されます。
あとはひたすら実装していきましょう。

## Apache POIでExcelファイル生成

JavaでExcelファイルを編集する際は、[Apache POI](https://poi.apache.org/)を使うのがおすすめです。

(Mavenを使ってる場合は)pom.xmlに以下のように追記して導入。

```xml:pom.xml
<dependency>
  <groupId>org.apache.poi</groupId>
  <artifactId>poi-ooxml</artifactId>
  <version>5.2.4</version>
</dependency>
<dependency>
  <groupId>org.apache.poi</groupId>
  <artifactId>poi</artifactId>
  <version>5.2.4</version>
</dependency>
```

基本的な使い方は[公式ドキュメント](https://poi.apache.org/components/spreadsheet/quick-guide.html)を見ておけば良いので割愛。

### セルのスタイル変更

セルの背景に任意の色を設定するには、
1. 背景色の設定を含むスタイル情報を作成
2. スタイル情報をセルに適用する

という手順を踏む必要があります。

```java:セルのスタイル作成
private static Map<String, CellStyle> createCellStyles(List<Color> colors, Workbook workbook) {
    // colorsには、画像の各座標の色情報が入っている想定
    
    Map<String, CellStyle> result = new HashMap<String, CellStyle>();
	for (int i = 0; i < colors.size(); i++) {
		Color color = colors.get(i);
		int red = color.getRed();
		int green = color.getGreen();
		int blue = color.getBlue();
		int alpha = color.getAlpha();
		String hex = String.format("#%02x%02x%02x%02x", red, green, blue, alpha);
		if (!result.containsKey(hex)) {
			XSSFCellStyle cellStyle = (XSSFCellStyle) workbook.createCellStyle();
			XSSFColor xssfColor = new XSSFColor(color, null);
			cellStyle.setFillForegroundColor(xssfColor);
			cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
			result.put(hex, cellStyle);
		}
	}
	return result;
}
```

```java:セルにスタイル適用
//sheet.getRowまたはcreateRowを実行して行情報(row)を取得した後、
//row.getCellまたはrow.createCellでセルを取得しておく。

//あとはこれでスタイルを設定するだけ。
cell.setCellStyle(cellStyle);
```

**※スタイル情報を、カラーコードをキーとするMapに格納している理由について**
実はExcelファイル(.xlsx)には、**1ファイル内に設定できるスタイルの数は64,000個まで**、という制限があります。

参考：[Excel で「異なるセル形式が多すぎます」というエラー メッセージが表示される](https://learn.microsoft.com/ja-JP/office/troubleshoot/excel/too-many-different-cell-formats-in-excel)

例えば、サイズが1000×1000の画像からExcelファイルを作ろうとすると、背景色が設定されたスタイルが単純計算で1,000,000個必要になりますが、その場合この制限に引っかかって以下のような例外が出力されます。

```
java.lang.IllegalStateException: The maximum number of Cell Styles was exceeded. You can define up to 64000 style in a .xlsx Workbook
```

それの対策として、背景色が同じ色のスタイルが重複して生成されないように上記のような実装にしています。


しかしこれだけでは、そもそも画像に使用されている色が64,000色以上あるケースには対応できません。
ということで今回は、Excelファイルを作成する前に画像の減色処理をやったりしています。

## JH Labs Java Image Filtersで画像の減色

[JH LabsのJava Image Filters](http://www.jhlabs.com/ip/filters/)を使用して画像の減色処理を行っています。このライブラリを使うと、減色以外にも様々な画像処理ができるようです。

```xml:pom.xml
<dependency>
  <groupId>com.jhlabs</groupId>
  <artifactId>filters</artifactId>
  <version>2.0.235-1</version>
</dependency>
```

実装はこんな感じ。読み込んだ画像を256色pngに変換する。

```java:減色処理
public static byte[] convert(byte[] imageData) throws IOException {
    try (ByteArrayInputStream inputStream = new ByteArrayInputStream(imageData);
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			BufferedOutputStream os = new BufferedOutputStream(bos);) {
   
		BufferedImage img = ImageIO.read(inputStream);
		QuantizeFilter qf = new QuantizeFilter();
        //256色に減色
        qf.setNumColors(256);
        //ディザリング処理あり
		qf.setDither(true);
        //サーペンタインパターンを使用
		qf.setSerpentine(true);
		BufferedImage convertedImage = qf.filter(img, null);
		convertedImage.flush();
		ImageIO.write(convertedImage, "png", os);
		byte[] result = bos.toByteArray();
		return result;
	} catch (IOException e) {
		e.printStackTrace();
		throw e;
	}
}
```

参考：[画像処理についてあれこれ: Java2DとJava Image Filters(pixels)を使用して画像を減色する](https://kyle-in-jp.blogspot.com/2008/09/java2djava-image-filterspixels_28.html)

# 所感

AWS SAMがかなり便利だった。コンテナ作るのもローカル実行も楽。