PDFビューワー for GROWIプラグインの紹介

[オープンソースのWikiであるGROWI](https://growi.org/ja/)にはプラグイン機能が用意されています。自社のデータを表示したり、表示をカスタマイズするのに利用できます。

今回は、GROWIプラグインとして作ったPDFビューワーを紹介します。Remarkプラグインとして作れば、手軽に拡張できます。

![FireShot Capture 358 - PDF - GROWI - localhost 358.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/ebdbdf6d-9d45-3e3f-f62a-da12ea7a4f5b.jpeg)


## コードについて

コードは[goofmint/growi-plugin-pdf-viewer: Viewing PDF File inside of GROWI](https://github.com/goofmint/growi-plugin-pdf-viewer)
にあります。見るべきファイルとしては、以下の2つになります。

- [client\-entry\.tsx](https://github.com/goofmint/growi-plugin-remark-youtube/blob/main/client-entry.tsx)
- [src/pdfviewer.ts](https://github.com/goofmint/growi-plugin-remark-youtube/blob/main/src/youtube.ts)

## プラグインを追加する

利用する際には、GROWIの管理画面の `プラグイン` にて追加してください。URLは `https://github.com/goofmint/growi-plugin-pdf-viewer` です。

![FireShot Capture 359 - プラグイン - GROWI - localhost 359.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f5b3e2d3-ccf3-d536-d59e-7301ea1b2f7e.jpeg)


## 使い方

プラグインは `$pdfviewer` という記法を使います。エディタにて、PDFファイルをアップロードし、 `$pdfviewer(ファイル名)` と記述すると、PDFビューワーが表示されます。

たとえば以下のような記述です。

```
$pdfviewer(/attachment/669c803453e575a188286be3)
```

ただし、そのままだと埋め込みサイズがうまくいかないので、 `width` と `height` のオプションで調整してください。

```
$pdfviewer(/attachment/669c803453e575a188286be3,width=75%,height=850px)
```

## 実装について

プラグインの登録部分 `client-entry.tsx` のコードです。

```tsx
import { plugin } from './src/pdfviewer';

const activate = (): void => {
  if (growiFacade == null || growiFacade.markdownRenderer == null) {
    return;
  }

  const { optionsGenerators } = growiFacade.markdownRenderer;

  optionsGenerators.customGenerateViewOptions = (...args) => {
    const options = optionsGenerators.generateViewOptions(...args);
		// プラグインを追加
    options.remarkPlugins.push(plugin as any);
    return options;
  };
};
```

## PDFビューワー部分

今回はPDF Viewerとして、PDF.jsをベースにした `ts-pdf` を利用しています。Webワーカーが必要なので、CloudfareのCDNから取得しています。また、PDFを表示するdivタグが必要なので一旦それをレンダリングし、その後（500ms後）にPDFを表示します。

何度かこのプラグインコード部分が呼ばれることがあったので、描画したdivタグの中にShadow DOMが存在しないかを確認しています。あればPDFビューワーを初期化しません。

divタグのidは、ファイルパスをsha256でハッシュ化しています。これにより、1ページの中で複数のPDFビューワーを表示しても問題ありません。

```tsx
import sha256 from 'crypto-js/sha256';
import { TsPdfViewer, TsPdfViewerOptions } from 'ts-pdf';
import type { Plugin } from 'unified';
import { visit } from 'unist-util-visit';

interface GrowiNode extends Node {
  name: string;
  type: string;
  attributes: {[key: string]: string}
  children: GrowiNode[];
  value: string;
}

export const plugin: Plugin = function() {
  return (tree) => {
    visit(tree, (node) => {
      const n = node as unknown as GrowiNode;
      try {
        if (n.type === 'leafGrowiPluginDirective' && n.name === 'pdfviewer') {
          const filePath = Object.keys(n.attributes)[0];
          const { width, height } = n.attributes;
          // ファイル名からユニークなIDを生成
          const key = sha256(filePath);
          const containerSelector = `#pdf-${key}`;
          const options: TsPdfViewerOptions = {
            containerSelector,
            workerSource: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js',
          };
          n.type = 'html';
          // PDFビューワーを表示するdivタグをレンダリング
          n.value = `<div id="pdf-${key}" style="width: ${width}; height: ${height};"></div>`;
          // HTML描画を待って、500ms後にPDFビューワーを初期化
          setTimeout(() => {
            if (!document.querySelector(containerSelector)?.shadowRoot) {
              const viewer = new TsPdfViewer(options);
              viewer.openPdfAsync(filePath);
            }
          }, 500);
        }
      }
      catch (e) {
        n.type = 'html';
        n.value = `<div style="color: red;">Error: ${(e as Error).message}</div>`;
      }
    });
  };
};
```

## 注意点

PDF.jsには表示サイズを調整してくれる機能があるのですが、ts-pdfにはないため、表示サイズを手動調整する必要があります。また、CMapファイルを指定するオプションが見当たらず、現状では日本語PDFの表示には対応していません（指定方法があれば教えてください！）。

## まとめ

GROWIのRemarkプラグインを使うと、Markdown記法を自由に拡張できます。足りない機能があれば、どんどん追加できます。ぜひ、自分のWikiをカスタマイズしましょう。

[OSS開発wikiツールのGROWI | 快適な情報共有を、全ての人へ](https://growi.org/ja/)