Vue 3で画像付きテーブルをExcelにエクスポートする方法

ExcelJSライブラリを使用すると、画像を含むテーブルデータをExcelファイルとして出力できます。以下では、画像がURLまたはBase64形式で提供される場合の実装例を紹介します。

必要なパッケージのインストール

npm install exceljs file-saver

URL形式の画像を1セル1枚で出力

この実装では、画像を並列でダウンロードし、必要に応じて圧縮してからExcelに埋め込みます。

<template>
  <div>
    <button @click="exportToExcel">Excel出力</button>
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>名前</th>
          <th>画像</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="record in dataset" :key="record.id">
          <td>{{ record.id }}</td>
          <td>{{ record.title }}</td>
          <td><img :src="record.imageUrl" width="80" height="80" /></td>
        </tr>
      </tbody>
    </table>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import ExcelJS from 'exceljs';
import { saveAs } from 'file-saver';

const dataset = ref([
  {
    id: 1,
    title: 'サンプルA',
    imageUrl: 'https://example.com/image1.jpg'
  },
  {
    id: 2,
    title: 'サンプルB',
    imageUrl: 'https://example.com/image2.png'
  }
]);

const exportToExcel = async () => {
  const workbook = new ExcelJS.Workbook();
  const sheet = workbook.addWorksheet('画像付きデータ');
  sheet.columns = [
    { header: 'ID', key: 'id', width: 10 },
    { header: '名前', key: 'title', width: 20 },
    { header: '画像', key: 'image', width: 30 }
  ];

  // 画像の並列取得
  const imagePromises = dataset.value.map(async (item, idx) => {
    try {
      const res = await fetch(item.imageUrl, { cache: 'force-cache' });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      
      const contentType = res.headers.get('content-type') || 'image/jpeg';
      const ext = contentType.split('/')[1] || 'jpeg';
      const blob = await res.blob();
      const optimized = await resizeImage(blob, ext);
      const buffer = await optimized.arrayBuffer();
      
      return { rowIndex: idx, data: buffer, format: ext };
    } catch (err) {
      console.warn(`画像取得失敗 (${item.id})`, err);
      return { rowIndex: idx, data: null, format: null };
    }
  });

  const results = await Promise.all(imagePromises);
  const sorted = results.sort((a, b) => a.rowIndex - b.rowIndex);

  // テキストデータの書き込み
  dataset.value.forEach((item, i) => {
    const row = i + 2;
    sheet.getCell(`A${row}`).value = item.id;
    sheet.getCell(`B${row}`).value = item.title;
  });

  // 画像の挿入
  sorted.forEach((img, i) => {
    const row = i + 2;
    if (!img.data) {
      sheet.getCell(`C${row}`).value = '画像なし';
      return;
    }

    const imgId = workbook.addImage({
      buffer: img.data,
      extension: img.format
    });

    sheet.addImage(imgId, {
      tl: { col: 2, row: row - 1 },
      ext: { width: 100, height: 100 }
    });
    sheet.getRow(row).height = 80;
  });

  const buf = await workbook.xlsx.writeBuffer();
  const blob = new Blob([buf], {
    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  });
  saveAs(blob, '画像付きデータ.xlsx');
};

// 画像リサイズ関数
const resizeImage = (blob, fmt, maxSize = 200) => {
  return new Promise((resolve) => {
    const img = new Image();
    img.src = URL.createObjectURL(blob);
    img.onload = () => {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');

      let { width, height } = img;
      if (width > height && width > maxSize) {
        height = (height * maxSize) / width;
        width = maxSize;
      } else if (height > maxSize) {
        width = (width * maxSize) / height;
        height = maxSize;
      }

      canvas.width = width;
      canvas.height = height;
      ctx.drawImage(img, 0, 0, width, height);

      canvas.toBlob(
        (newBlob) => {
          URL.revokeObjectURL(img.src);
          resolve(newBlob || blob);
        },
        `image/${fmt}`,
        0.8
      );
    };
    img.onerror = () => resolve(blob);
  });
};
</script>

1行に複数画像を配置する場合

各行に複数の画像がある場合、横方向に並べて表示できます。

const dataset = ref([
  {
    id: 1,
    title: '複数画像',
    images: [
      { url: 'https://example.com/img1.jpg' },
      { url: 'https://example.com/img2.jpg' }
    ]
  }
]);

// 出力処理内では、各画像に対して:
// - 所属行(rowIndex)
// - 行内での順序(imgIndex)
// を記録し、Excel上で横並びに配置

const colOffset = (imgIndex * (imgWidth + spacing)) / 64;
worksheet.addImage(imgId, {
  tl: { col: 2 + colOffset, row: actualRow - 1 },
  ext: { width: imgWidth, height: 200 }
});

Base64形式の画像を扱う場合

Base64文字列を直接ArrayBufferに変換して使用します。ただし、データ量が増加するため小規模な用途に限定すべきです。

const processBase64Image = (base64Str) => {
  const parts = base64Str.split(',');
  const data = parts.length > 1 ? parts[1] : base64Str;
  const binary = atob(data);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
};

const mimeType = parts[0]?.match(/:(.*?);/)?.[1] || 'image/png';
const ext = mimeType.split('/')[1];

方式の比較と選択指針

  • URL形式:ネットワーク環境が良好で画像数が多い場合に最適。ブラウザキャッシュや並列ダウンロードにより効率的。
  • Base64形式:オフライン対応や小規模データ向け。信頼性は高いがデータサイズが約33%増加。
  • サーバーからのArrayBuffer直送:理論的には高速だが、実際にはBase64エンコードやストリーム処理が必要で、大容量時にメモリ・ネットワーク負荷が高まる。画像が少なく、かつ一時URLなどキャッシュ不可なケースに限定して検討。

タグ: Vue3 ExcelJS FileSaver.js javascript Canvas

9月7日 08:15 投稿