JavaScriptでPDFからテキストを抽出し、PDFをテキストに変換する

近年、Webアプリケーションの開発において、クライアント側でPDFファイルを処理するニーズが増加しています。特に、PDFからテキストを抽出し、表示・分析・さらなる処理を行う場面が多く見られます。Reactフレームワーク上でJavaScriptを使用してPDFのテキストを抽出することで、ユーザーエクスペリエンスを向上させるだけでなく、サーバー側の負荷を軽減し、データ処理の即時性と効率を高めることができます。この方法により、開発者はより動的で応答性の高いアプリケーションを構築でき、ユーザーはサーバーからの応答を待つことなく、必要な情報を迅速に取得できます。

本記事では、**JavaScriptを使用してReactでPDF文書のテキストを抽出する方法**を紹介します。

### 主な内容
- **[JavaScriptでPDFのテキストを抽出する基本手順](#javascriptでpdfのテキストを抽出する基本手順)**
- **[レイアウトを保持したPDFテキストの抽出](#レイアウトを保持したpdfテキストの抽出)**
- **[空白を削除したPDFテキストの抽出](#空白を削除したpdfテキストの抽出)**
- **[指定領域のPDFテキストの抽出](#指定領域のpdfテキストの抽出)**
- **[強調表示されたPDFテキストの抽出](#強調表示されたpdfテキストの抽出)**

本記事で紹介する方法では、[Spire.PDF for JavaScript](https://www.e-iceblue.com/Introduce/pdf-for-javascript.html)を使用します。
NPMインストールコマンド：`npm install spire.pdf`

## JavaScriptでPDFのテキストを抽出する基本手順

Spire.PDF for JavaScriptには、PDF文書を処理するWebAssemblyモジュールが提供されています。このモジュールの`PdfTextExtractor`を使用すると、簡単にPDFのテキストを抽出できます。以下は、基本的な操作手順です。

1. **WebAssembly環境の初期化**  
   `Spire.Pdf.Base.js`を読み込み、WebAssemblyモジュールを初期化します。

2. **PDFファイルの読み込み**  
   `wasmModule.FetchFileToVFS()`を使用して、PDFファイルを仮想ファイルシステム（VFS）に保存します。

3. **PDFドキュメントオブジェクトの作成**  
   `wasmModule.PdfDocument.Create()`で`PdfDocument`インスタンスを作成し、`PdfDocument.LoadFromFile()`でVFSからPDFを読み込みます。

4. **テキスト抽出オプションの設定**  
   `wasmModule.PdfTextExtractOptions.Create()`を使用して`PdfTextExtractOptions`インスタンスを作成し、抽出オプションを設定します。

5. **PDFページの取得**  
   `PdfDocument.Pages.get_Item()`または`Pages`コレクションをループ処理して、必要なページを取得します。

6. **テキスト抽出器の作成**  
   `wasmModule.PdfTextExtractor.Create()`を使用して、取得したページオブジェクトから`PdfTextExtractor`インスタンスを作成します。

7. **テキストの抽出**  
   `PdfTextExtractor.ExtractText()`を呼び出し、PDFページからテキストを取得します。

8. **テキストの処理・エクスポート**  
   抽出したテキストを必要に応じて処理し、ローカルに保存します。

`PdfTextExtractOptions`には、`IsSimpleExtraction`、`IsExtractAllText`、`ExtractArea`、`IsShowHiddenText`などのプロパティがあり、多様なカスタム抽出が可能です。

## レイアウトを保持したPDFテキストの抽出

`PdfTextExtractor.ExtractText()`をデフォルトオプションで使用すると、ページ全体のテキストを抽出し、空白を保持することでレイアウトが維持されます。

**コード例：**
```js
import React, { useState, useEffect } from 'react';

function App() {

  // ロードされたWASMモジュールの状態を保存するためのステート
  const [wasmModule, setWasmModule] = useState(null);

  // useEffectフックを使用して、コンポーネントがマウントされたときにWASMモジュールをロード
  useEffect(() => {
    const loadWasm = async () => {
      try {
        // グローバルwindowオブジェクトからModuleとspirepdfを取得
        const { Module, spirepdf } = window;

        // 実行時に初期化された際にwasmModuleステートを設定
        Module.onRuntimeInitialized = () => {
          setWasmModule(spirepdf);
        };
      } catch (err) {
        // モジュールのロード中に発生したエラーを記録
        console.error('WASMモジュールのロードに失敗しました:', err);
      }
    };

    // WASM JavaScriptファイルをロードするためのスクリプト要素を作成
    const script = document.createElement('script');
    script.src = `${process.env.PUBLIC_URL}/Spire.Pdf.Base.js`;
    script.onload = loadWasm;

    // スクリプトをドキュメントのbodyに追加
    document.body.appendChild(script);

    // クリーンアップ関数：コンポーネントがアンマウントされた際にスクリプトを削除
    return () => {
      document.body.removeChild(script);
    };
  }, []);

  // PDFドキュメントからすべてのテキストを抽出する関数
  const ExtractPDFText = async () => {
    if (wasmModule) {
      // 入力ファイル名と出力ファイル名を指定
      const inputFileName = 'Sample.pdf';
      const outputFileName = 'PDFTextWithLayout.txt';

      // 入力ファイルを取得してVFSに追加
      await wasmModule.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/`);

      // PdfDocumentクラスのインスタンスを作成
      const pdf = wasmModule.PdfDocument.Create();

      // VFSからPDFドキュメントをロード
      pdf.LoadFromFile(inputFileName);

      // 抽出したテキストを保存するための文字列オブジェクトを作成
      let text = '';

      // PdfTextExtractOptionsクラスのインスタンスを作成
      const extractOptions = wasmModule.PdfTextExtractOptions.Create();

      // PDFドキュメントの各ページをループ
      for (let i = 0; i < pdf.Pages.Count; i++) {
        // 現在のページを取得
        const page = pdf.Pages.get_Item(i);
        // PdfTextExtractorクラスのインスタンスを作成
        const textExtractor = wasmModule.PdfTextExtractor.Create(page);
        // 現在のページからテキストを抽出して文字列に追加
        text += textExtractor.ExtractText(extractOptions);
      }

      // テキスト文字列からBlobオブジェクトを作成し、ダウンロード
      const blob = new Blob([text], { type: 'text/plain' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `${outputFileName}`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }
  };

  return (
      <div style={{ textAlign: 'center', height: '300px' }}>
        <h1>ReactでJavaScriptを使用してPDFからテキストを抽出</h1>
        <button onClick={ExtractPDFText} disabled={!wasmModule}>
          抽出してダウンロード
        </button>
      </div>
  );
}

export default App;
```

**抽出結果：**  
![レイアウトを保持したテキストが取得](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278784/603fb13f-1b38-4078-bdb0-bffac1ddd7d7.png)

## 空白を削除したPDFテキストの抽出

`PdfTextExtractOptions`の`IsSimpleExtraction`を`true`に設定すると、不要な空白を削除し、テキストのレイアウトを保持せずに抽出できます。この方法は、抽出したテキストをさらに処理する場合に適しています。

**コード例：**
```js
import React, { useState, useEffect } from 'react';

function App() {

  // ロードされたWASMモジュールの状態を保存するためのステート
  const [wasmModule, setWasmModule] = useState(null);

  // useEffectフックを使用して、コンポーネントがマウントされたときにWASMモジュールをロード
  useEffect(() => {
    const loadWasm = async () => {
      try {
        // グローバルwindowオブジェクトからModuleとspirepdfを取得
        const { Module, spirepdf } = window;

        // 実行時に初期化された際にwasmModuleステートを設定
        Module.onRuntimeInitialized = () => {
          setWasmModule(spirepdf);
        };
      } catch (err) {
        // モジュールのロード中に発生したエラーを記録
        console.error('WASMモジュールのロードに失敗しました:', err);
      }
    };

    // WASM JavaScriptファイルをロードするためのスクリプト要素を作成
    const script = document.createElement('script');
    script.src = `${process.env.PUBLIC_URL}/Spire.Pdf.Base.js`;
    script.onload = loadWasm;

    // スクリプトをドキュメントのbodyに追加
    document.body.appendChild(script);

    // クリーンアップ関数：コンポーネントがアンマウントされた際にスクリプトを削除
    return () => {
      document.body.removeChild(script);
    };
  }, []);

  // PDFドキュメントからテキストを抽出（レイアウトを保持しない）の関数
  const ExtractPDFText = async () => {
    if (wasmModule) {
      // 入力ファイル名と出力ファイル名を指定
      const inputFileName = 'Sample.pdf';
      const outputFileName = 'PDFTextWithoutLayout.txt';

      // 入力ファイルを取得してVFSに追加
      await wasmModule.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/`);

      // PdfDocumentクラスのインスタンスを作成
      const pdf = wasmModule.PdfDocument.Create();

      // VFSからPDFドキュメントをロード
      pdf.LoadFromFile(inputFileName);

      // 抽出したテキストを保存するための文字列オブジェクトを作成
      let text = '';

      // PdfTextExtractOptionsクラスのインスタンスを作成
      const extractOptions = wasmModule.PdfTextExtractOptions.Create();

      // シンプルなテキスト抽出モードを有効化し、テキストのレイアウトを保持しない
      extractOptions.IsSimpleExtraction = true;

      // PDFドキュメントの各ページをループ
      for (let i = 0; i < pdf.Pages.Count; i++) {
        // 現在のページを取得
        const page = pdf.Pages.get_Item(i);
        // PdfTextExtractorクラスのインスタンスを作成
        const textExtractor = wasmModule.PdfTextExtractor.Create(page);
        // 現在のページからテキストを抽出して文字列に追加
        text += textExtractor.ExtractText(extractOptions);
      }

      // テキスト文字列からBlobオブジェクトを作成し、ダウンロード
      const blob = new Blob([text], { type: 'text/plain' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `${outputFileName}`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }
  };

  return (
      <div style={{ textAlign: 'center', height: '300px' }}>
        <h1>ReactでJavaScriptを使用してPDFからテキストを抽出（レイアウトを保持しない）</h1>
        <button onClick={ExtractPDFText} disabled={!wasmModule}>
          抽出してダウンロード
        </button>
      </div>
  );
}

export default App;
```

**抽出結果：**  
![空白が削除されたテキストが取得](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278784/de5c52b6-37e4-49b8-aec8-67222d598475.png)

## 指定領域のPDFテキストの抽出

`PdfTextExtractOptions`の`ExtractArea`プロパティに`RectangleF`オブジェクトを設定することで、指定領域のテキストのみを抽出できます。これにより、ページ内の不要な固定コンテンツを避けることができます。

**コード例：**
```js
import React, { useState, useEffect } from 'react';

function App() {

  // ロードされたWASMモジュールの状態を保存するためのステート
  const [wasmModule, setWasmModule] = useState(null);

  // useEffectフックを使用して、コンポーネントがマウントされたときにWASMモジュールをロード
  useEffect(() => {
    const loadWasm = async () => {
      try {
        // グローバルwindowオブジェクトからModuleとspirepdfを取得
        const { Module, spirepdf } = window;

        // 実行時に初期化された際にwasmModuleステートを設定
        Module.onRuntimeInitialized = () => {
          setWasmModule(spirepdf);
        };
      } catch (err) {
        // モジュールのロード中に発生したエラーを記録
        console.error('WASMモジュールのロードに失敗しました:', err);
      }
    };

    // WASM JavaScriptファイルをロードするためのスクリプト要素を作成
    const script = document.createElement('script');
    script.src = `${process.env.PUBLIC_URL}/Spire.Pdf.Base.js`;
    script.onload = loadWasm;

    // スクリプトをドキュメントのbodyに追加
    document.body.appendChild(script);

    // クリーンアップ関数：コンポーネントがアンマウントされた際にスクリプトを削除
    return () => {
      document.body.removeChild(script);
    };
  }, []);

  // PDFページの特定のエリアからテキストを抽出する関数
  const ExtractPDFText = async () => {
    if (wasmModule) {
      // 入力ファイル名と出力ファイル名を指定
      const inputFileName = 'Sample.pdf';
      const outputFileName = 'PDFTextPageArea.txt';

      // 入力ファイルを取得してVFSに追加
      await wasmModule.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/`);

      // PdfDocumentクラスのインスタンスを作成
      const pdf = wasmModule.PdfDocument.Create();

      // VFSからPDFドキュメントをロード
      pdf.LoadFromFile(inputFileName);

      // 抽出したテキストを保存するための文字列オブジェクトを作成
      let text = '';

      // PDFドキュメントの特定のページを取得
      const page = pdf.Pages.get_Item(0);

      // PdfTextExtractOptionsクラスのインスタンスを作成
      const extractOptions = wasmModule.PdfTextExtractOptions.Create();

      // 抽出するテキストのページ領域を指定（RectangleFオブジェクトを使用）
      extractOptions.ExtractArea = wasmModule.RectangleF.Create({ x: 0, y: 500, width: page.Size.Width, height: 200 });

      // PdfTextExtractorクラスのインスタンスを作成
      const textExtractor = wasmModule.PdfTextExtractor.Create(page);

      // 指定された領域からテキストを抽出
      text = textExtractor.ExtractText(extractOptions);

      // テキスト文字列からBlobオブジェクトを作成し、ダウンロード
      const blob = new Blob([text], { type: 'text/plain' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `${outputFileName}`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }
  };

  return (
      <div style={{ textAlign: 'center', height: '300px' }}>
        <h1>ReactでJavaScriptを使用してPDFページの特定のエリアからテキストを抽出</h1>
        <button onClick={ExtractPDFText} disabled={!wasmModule}>
          抽出してダウンロード
        </button>
      </div>
  );
}

export default App;
```

**抽出結果：**  
![指定領域のテキストのみが取得](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278784/6e005583-d93a-4295-84e6-69ea4484f0f4.png)

## 強調表示されたPDFテキストの抽出

PDF内の強調表示テキストは、通常、注釈として追加されています。そのため、`PdfPageBase.Annotations`を使用してPDFページのすべての注釈を取得し、`PdfTextMarkupAnnotationWidget`インスタンスを識別することで、強調表示されたテキストを特定できます。その後、`Bounds`プロパティを使用して強調表示された範囲を取得し、抽出領域として設定することで、強調表示テキストを抽出できます。

**コード例：**
```js
import React, { useState, useEffect } from 'react';

function App() {

  // ロードされたWASMモジュールの状態を保存するためのステート
  const [wasmModule, setWasmModule] = useState(null);

  // useEffectフックを使用して、コンポーネントがマウントされたときにWASMモジュールをロード
  useEffect(() => {
    const loadWasm = async () => {
      try {
        // グローバルwindowオブジェクトからModuleとspirepdfを取得
        const { Module, spirepdf } = window;

        // 実行時に初期化された際にwasmModuleステートを設定
        Module.onRuntimeInitialized = () => {
          setWasmModule(spirepdf);
        };
      } catch (err) {
        // モジュールのロード中に発生したエラーを記録
        console.error('WASMモジュールのロードに失敗しました:', err);
      }
    };

    // WASM JavaScriptファイルをロードするためのスクリプト要素を作成
    const script = document.createElement('script');
    script.src = `${process.env.PUBLIC_URL}/Spire.Pdf.Base.js`;
    script.onload = loadWasm;

    // スクリプトをドキュメントのbodyに追加
    document.body.appendChild(script);

    // クリーンアップ関数：コンポーネントがアンマウントされた際にスクリプトを削除
    return () => {
      document.body.removeChild(script);
    };
  }, []);

  // PDFからハイライトされたテキストを抽出する関数
  const ExtractPDFText = async () => {
    if (wasmModule) {
      // 入力ファイル名と出力ファイル名を指定
      const inputFileName = 'Sample.pdf';
      const outputFileName = 'PDFTextHighlighted.txt';

      // 入力ファイルを取得してVFSに追加
      await wasmModule.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/`);

      // PdfDocumentクラスのインスタンスを作成
      const pdf = wasmModule.PdfDocument.Create();

      // VFSからPDFドキュメントをロード
      pdf.LoadFromFile(inputFileName);

      // 抽出したテキストを保存するための文字列オブジェクトを作成
      let text = '';

      // PDFドキュメントの各ページをループ
      for (const page of pdf.Pages) {
        // 現在のページのすべての注釈をループ
        for (let i = 0; i < page.Annotations.Count; i++) {
          // 現在の注釈を取得
          const annotation = page.Annotations.get_Item(i);
          // 注釈がPdfTextMarkupAnnotationのインスタンス（ハイライト）であるかをチェック
          if (annotation instanceof wasmModule.PdfTextMarkupAnnotationWidget) {
            // ハイライト注釈の境界を取得
            const bounds = annotation.Bounds;
            // PdfTextExtractOptionsのインスタンスを作成
            const extractOptions = wasmModule.PdfTextExtractOptions.Create();
            // ハイライト注釈の境界をテキスト抽出領域として設定
            extractOptions.ExtractArea = bounds;
            // PdfTextExtractorクラスのインスタンスを作成
            const textExtractor = wasmModule.PdfTextExtractor.Create(page);
            // ハイライトテキストを抽出し、text文字列に追加
            text += textExtractor.ExtractText(extractOptions);
          }
        }
      }

      // テキスト文字列からBlobオブジェクトを作成し、ダウンロード
      const blob = new Blob([text], { type: 'text/plain' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `${outputFileName}`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }
  };

  return (
      <div style={{ textAlign: 'center', height: '300px' }}>
        <h1>ReactでJavaScriptを使用してPDF内のハイライトされたテキストを抽出</h1>
        <button onClick={ExtractPDFText} disabled={!wasmModule}>
          抽出してダウンロード
        </button>
      </div>
  );
}

export default App;
```

**抽出結果：**  
![強調表示されたテキストが取得](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278784/613c3a37-e756-4831-9291-0858a74cae67.png)

## まとめ

本記事では、JavaScriptを使用してReact環境でPDFテキストを抽出する方法を紹介しました。また、レイアウトを保持した抽出、空白を削除した抽出、指定領域の抽出、強調表示テキストの抽出など、さまざまなカスタム抽出方法について解説しました。