4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React+Vite】Gemini 2.0 Flashをブラウザだけで動かす校務DXアプリを作ったら、PDF生成の「日本語フォント」で沼った話

4
Posted at

はじめに

teshiokayumi_teacher_facing_a_pile_of_papers_--ar_169_--niji__d9d5f81b-f265-4f15-88d9-2150a5aee216_1.png

「学校の先生が使うツールを作りたい。でも、Pythonの環境構築は(利用する先生の)ハードルが高すぎるし、サーバーを立てる予算もない……」

そんな制約の中で、「ブラウザだけで完結する(Serverless)」かつ「AI(Gemini 2.0 Flash)」を活用した議事録・タスク管理アプリ『Gakuen AI』をプロトタイプ開発しました。

本記事では、Vibe Coding(AIペアプログラミング)で爆速開発する中で直面した、「jsPDFで日本語が豆腐化・エラー落ちする問題」の解決策を中心に、技術スタックと実装のポイントを共有します。

開発のゴールと技術スタック

今回の要件は「インストール不要、URLを開くだけで動く」こと。そのため、バックエンドを持たず、すべての処理をクライアントサイド(ブラウザ)で行う構成にしました。

Tech Stack

  • Framework: React + Vite
  • AI Model: Google Gemini 2.0 Flash (Client-side execution)
  • SDK: @google/generative-ai
  • Voice Input: Web Speech API (Browser Native)
  • PDF Generation: jspdf
  • UI/UX: Vanilla CSS (Glassmorphism design)

1. Gemini APIをクライアントサイドで叩く

通常、APIキーの隠蔽などの観点からサーバー経由で叩くのがセオリーですが、今回はプロトタイプかつ「設定画面でユーザー自身のキーを入れてもらう」運用を想定し、フロントエンドから直接 GoogleGenerativeAI を呼び出しています。

実装コード(抜粋)

Gemini 2.0 Flashは非常に高速なので、ローディング表示が一瞬で終わるのがUX的に最高でした。

import { GoogleGenerativeAI } from "@google/generative-ai";

const generateTasks = async (apiKey, textLog) => {
  const genAI = new GoogleGenerativeAI(apiKey);
  // Gemini 2.0 Flashを指定
  const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });

  const prompt = `
    あなたは学校運営の専門家です。以下の会議ログからTODOリストを作成してください。
    【重要】昨年の同時期の動きも考慮し、文脈を補完して提案してください。
    ---
    ${textLog}
  `;

  const result = await model.generateContent(prompt);
  return result.response.text();
};


2. 【本題】jsPDFの日本語フォント対応と「widths」エラーの怪

今回の開発で最も時間を溶かしたのが、「AIが作ったタスクリストをPDFで出力する」機能です。
ご存知の通り、海外製ライブラリである jsPDF は標準で日本語フォントを持っていません。そのまま出力すると文字がすべて化けます。

解決策1:フォントをaddFileToVFSする

一般的に知られている手法は、.ttf ファイルを用意してBase64エンコードし、jsPDFのVFS(仮想ファイルシステム)に登録する方法です。

// 一般的な手法(概念コード)
doc.addFileToVFS("MyFont.ttf", fontBase64);
doc.addFont("MyFont.ttf", "MyFont", "normal");
doc.setFont("MyFont");

これで解決……と思いきや、以下のエラーが発生してアプリがクラッシュしました。

発生したエラー

TypeError: Cannot read properties of undefined (reading 'widths')

「widths(文字幅)が読み込めない」というエラーです。

原因:Variable Font(可変フォント)は使えない

Google Fontsからダウンロードした NotoSansJP-VariableFont_wght.ttf をそのまま使っていたのが原因でした。
最近主流の Variable Font(可変フォント) は、データ構造が特殊なため、jsPDFのような少し枯れたライブラリではパースできず、文字幅情報の取得に失敗します。

解決策2:Staticフォント + FileReader

解決策は2点ありました。

  1. Staticフォントを使う: Google Fontsのダウンロードフォルダ内にある static/NotoSansJP-Regular.ttf を使用する
  2. FileReaderで確実に読み込む: 数MBあるフォントファイルを扱うため、fetch で取ってきたBlobを FileReader でBase64に変換する堅牢な関数を実装する

以下が、最終的に動作した「日本語PDF生成」の完全なコードです。

// src/utils/pdfGenerator.js
import { jsPDF } from "jspdf";

export const generatePDF = async (title, content) => {
  const doc = new jsPDF();
  
  // public/fonts/NotoSansJP-Regular.ttf を配置しておく
  const fontUrl = "/fonts/NotoSansJP-Regular.ttf"; 
  const fileName = "custom-font.ttf";

  try {
    // 1. フォントをBase64として読み込む
    const fontBase64 = await loadFontAsBase64(fontUrl);

    // 2. jsPDFに登録(ここが重要)
    doc.addFileToVFS(fileName, fontBase64);
    doc.addFont(fileName, "MyFont", "normal");
    doc.setFont("MyFont");

    // 3. 描画
    doc.text(title, 20, 20);
    const splitText = doc.splitTextToSize(content, 170); // 折り返し処理
    doc.text(splitText, 20, 40);

    doc.save("report.pdf");
  } catch (error) {
    console.error("PDF Error:", error);
  }
};

// 安全なBase64変換ヘルパー
async function loadFontAsBase64(url) {
  const response = await fetch(url);
  const blob = await response.blob();
  
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onloadend = () => {
      // "data:font/ttf;base64,..." のヘッダーを除去
      const base64data = reader.result.split(',')[1];
      resolve(base64data);
    };
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  });
}

これで無事、日本語が綺麗に表示されたPDFが出力されました。


3. UI/UX: Glassmorphismの実装

教育現場のツールは無機質になりがちですが、「使っていてテンションが上がる」こともDXには重要です。
今回はCSSだけで実装できる Glassmorphism(すりガラス風デザイン) を採用しました。
school-support-app-01-26-2026_02_51_AM.png

/* Glassmorphism Panel */
.glass-panel {
  background: rgba(255, 255, 255, 0.1); /* 半透明の白 */
  backdrop-filter: blur(12px);          /* 背景をぼかす */
  -webkit-backdrop-filter: blur(12px);  /* Safari対応 */
  border: 1px solid rgba(255, 255, 255, 0.2);
  box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
  border-radius: 16px;
}

backdrop-filter プロパティを使うだけで、モダンな管理画面が作れます。


まとめ

今回の開発で得られた知見は以下の通りです。

  1. 校務DXはブラウザ完結が正義: React + Viteなら環境構築不要で配布できる。
  2. Gemini 2.0 Flashは爆速: クライアントサイドAIとしての体験が良い。
  3. jsPDFにはStaticフォントを食わせろ: Variable Fontはエラーの元。

「黒い画面」を使わずに、ここまでの機能実装ができる現代のWeb標準とAIの進化に感謝です。
今回はプロトタイプでしたが、今後はLocal Storageを使ったデータ永続化や、さらなるプロンプトエンジニアリングの改善を進めていく予定です。

参考リンク

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?