ExcelJSでxlsxファイルから画像を読み込む

## はじめに
初投稿です。
Reactを使用したアプリ開発で、次のような .xlsx ファイルに挿入された画像を、全件取得してデータベースに保存する機能を実装しました。（xlsxはスプレッドシートで作成)
![スクリーンショット 2024-09-29 060017.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3897363/f0711779-29c5-7429-daa3-b6b947d77cf4.png)
思いのほか難しく、少し手間取ったのでそのメモを投稿します。
## 目次
- [用いる技術](#用いる技術)
- [セットアップ](#セットアップ)
- [xlsxファイルのアップロード](#xlsxファイルのアップロード)
- [ExcelJsによる画像読み込み](#exceljsによる画像読み込み)
- [コードまとめ](#コードまとめ)
- [最後に](#最後に)

## 用いる技術
- **exceljs**: 4.4.0
- **Node.js**: 20.12.2
- **TypeScript**: 4.9.5
- **@types/node**: 16.18.108
- **React**: 18.3.1
## セットアップ
node.jsをインストール後

reactアプリを作成
```
npx create-react-app "アプリ名"
```
最後に、ExcelJsライブラリをインストールします
```
npm install exceljs
```
[ExcelJS GitHub リポジトリ](https://github.com/exceljs/exceljs)
## .xlsxファイルのアップロード
.xlsxファイルを入力するためのフォームを作成します。
```FileInput.tsx
import React, { useState } from 'react';

export const FileInput = () => {
  const [selectedFile, setSelectedFile] = useState<File | null>(null);

  // ファイルが選択されたときに呼ばれる関数
  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (file && file.name.endsWith('.xlsx')) {
      setSelectedFile(file);
    } else {
      console.log('xlsxファイルを選択してください');
    }
  };
  // ボタンを押下したときに呼ばれる関数
  const handleFileUpload = () => {
    if (!selectedFile) {
      console.log('ファイルが選択されていません');
      return;
    }
    // 画像を読み込む処理を記述
  };

  return (
    <div>
      <input 
        type="file" 
        id="fileInput" 
        name="fileInput" 
        accept=".xlsx" 
        onChange={handleFileChange} />
      <button onClick={handleFileUpload}>選択したファイルから画像を読み込み</button>
    </div>
  );
};
```
## ExcelJsによる画像読み込み
次に、.xlsxファイルを読み込み画像を取得する関数を作成します。

必要なモジュールをインポートし関数を作成
```getImages.ts
import ExcelJS from "exceljs";
import { useState } from "react";

export const getImages = async(file:File) => {
    
}
```
.xlsxファイルをarrayBuffer形式に変換後、シートのデータを読み込む。
```getImages.ts
const exelData = await file.arrayBuffer(); // Arrray Buffer(jsでバイナリデータを扱う為の形式）にxlsxを変換
const workbookImg = new ExcelJS.Workbook(); // 画像データを読み込むためにexel jsライブラリでworkbookを作成
await workbookImg.xlsx.load(exelData); //  xlsxデータを作成したworkBookにロード
let imgSheet = workbookImg.worksheets[0]; // シートの読み込み
```
シートから画像を全件取得し、取得した画像の数だけブックから画像を取得
```getImages.ts
const images = imgSheet.getImages(); // シートから画像を全件取得
    let result = []; // 読み込んだ画像データを格納する配列
    for (let i = 0; i < images.length; i++){
        let imageData = workbookImg.getImage(i); // getimage(引数はimageId)を用いて画像データを取得
        result.push(imageData);
    }
}
```
この方法で画像は正常に取得できましたが、getImage() の引数である imageId は、どうやら .xlsx ファイルに画像が登録された順序に基づいて付与されているようです。
そのため、.xlsx ファイルの1行目にある画像の imageId が 1、2行目の画像の imageId が 2 になると期待していても、実際には1行目の画像の imageId が 10 になってしまう場合もあります。
つまり、.xlsxファイルの画像の順番と、読み込んだ画像の順番が一致しない可能性があります。

よって、.xlsxのセルの位置情報を基に読み込む画像を指定していきます。
```getImages.ts
let result = [];
for (let i = 0; i < images.length; i++){
    const targetCell = 'A' + String(i + 2); // セルの指定(A2など)
    const cell = imgSheet.getCell(targetCell); // セルの情報を取得
    const rowIndex = Number(cell.row); // セルの行位置
    const colIndex = Number(cell.col); // セルの列位置
    // 取得したい画像を含むセルの位置情報でimagesをフィルタリングし、特定のセルに含まれる画像を全件取得
    const cellImages = images.filter(image => {
        const { range } = image; // 画像の範囲情報を取得
        return range.tl.col === colIndex - 1 && range.tl.row === rowIndex - 1; // 条件に一致する画像のみを返す
    });

    let imageData = workbookImg.getImage(Number(cellImages[0].imageId)); // フィルタリング画像の先頭のみを取得
    result.push(imageData); // 配列に加える
}
```
これで、A列の画像を上から順に全件取得できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3897363/d3d02522-fa2d-526e-4f2e-0e3eccd087af.png)

最後に、ここまでで取得した画像はUint8Array形式 等ですので、file形式に変換するコードを加えて完成です。
```getImages.ts
if (imageData && imageData.buffer) {
    const blob = new Blob([imageData.buffer], { type: 'image/png'}); // Blobに変換
    const file = new File([blob], `preItemFile`, { type: 'image/png'}); // Fileに変換
    result.push(file);
}
```
## コードまとめ
### .xlsxファイルの入力フォーム
```FileInput.tsx
import React, { useState } from 'react';
import { getImages } from './getImages';

export const FileInput = () => {
  const [selectedFile, setSelectedFile] = useState<File | null>(null);

  // ファイルが選択されたときに呼ばれる関数
  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (file && file.name.endsWith('.xlsx')) {
      setSelectedFile(file);
    } else {
      console.log('xlsxファイルを選択してください');
    }
  };
  // ボタンを押下したときに呼ばれる関数
  const handleFileUpload = () => {
    if (!selectedFile) {
      console.log('ファイルが選択されていません');
      return;
    }
    getImages(selectedFile); // 作成した関数を呼び出し
  };

  return (
    <div>
      <input 
        type="file" 
        id="fileInput" 
        name="fileInput" 
        accept=".xlsx" 
        onChange={handleFileChange} />
      <button onClick={handleFileUpload}>選択したファイルから画像を読み込み</button>
    </div>
  );
};
```
### .xlsxファイルから画像を取得する関数
```getImages.ts
import ExcelJS from "exceljs";

export const getImages = async(file:File) => {
    const exelData = await file.arrayBuffer(); // Arrray Buffer(jsでバイナリデータを扱う為の形式）にxlsxを変換
    const workbookImg = new ExcelJS.Workbook(); // 画像データを読み込むためにexel jsライブラリでworkbookを作成
    await workbookImg.xlsx.load(exelData); //  xlsxデータを作成したworkBookにロード
    const imgSheet = workbookImg.worksheets[0]; // シートの読み込み
    const images = imgSheet.getImages(); // シートから画像を全件取得
    let result = [];
    for (let i = 0; i < images.length; i++){
        const targetCell = 'A' + String(i + 2); // セルの指定(A2など)
        const cell = imgSheet.getCell(targetCell); // セルの情報を取得
        const rowIndex = Number(cell.row); // セルの行位置
        const colIndex = Number(cell.col); // セルの列位置
        // 取得したい画像を含むセルの位置情報でimagesをフィルタリングし、特定のセルに含まれる画像を全件取得
        const cellImages = images.filter(image => {
            const { range } = image; // 画像の範囲情報を取得
            return range.tl.col === colIndex - 1 && range.tl.row === rowIndex - 1; // 条件に一致する画像のみを返す
        });
        let imageData = workbookImg.getImage(Number(cellImages[0].imageId)); // フィルタリング画像先頭のみを取得
        if (imageData && imageData.buffer) {
            const blob = new Blob([imageData.buffer], { type: 'image/png'}); // Blobに変換
            const file = new File([blob], `preItemFile`, { type: 'image/png'}); // Fileに変換
            result.push(file);
        }
        result.push(imageData); // 配列に加える
    }
    console.log(result);
}
```


## 最後に
本記事では、[ExcelJs](https://github.com/exceljs/exceljs)を用いて.xlsxファイルから画像ファイルを読み込みました。
初心者エンジニアですのでいたらない点が多いかと思いますがご容赦いただけますと幸いです。

## 参考
[ExcelJS GitHub リポジトリ](https://github.com/exceljs/exceljs)