Open11

Next.js react-view-pdf&fabric.js

KizitoraKizitora

のちに記事にしたいのでそのたたき台としてスクラップにまとめておく.


react-pdfとreact-pdf-viewerの二つがあるけど,お手軽なのは後者.ただし商用利用は有料.個人利用なら問題ないはず.

「next.jsでreact-pdfを使ってpdfを表示する方法を教えてください.」という質問に対するChatGPT 4oの回答

Next.jsでreact-pdfを使用してPDFを表示する方法をステップバイステップで説明します。

  1. プロジェクトのセットアップ
    まず、Next.jsプロジェクトを作成し、react-pdfライブラリをインストールします。

bash
コードをコピーする
npx create-next-app my-pdf-viewer
cd my-pdf-viewer
npm install @react-pdf-viewer/core @react-pdf-viewer/default-layout
npm install @react-pdf-viewer/toolbar pdfjs-dist
2. 必要なライブラリのインストール
Next.jsでPDFを表示するために必要なパッケージをインストールします。react-pdfは以下のパッケージを使用してPDFの表示を簡単に行うことができます。

bash
コードをコピーする
npm install @react-pdf-viewer/core @react-pdf-viewer/default-layout
npm install pdfjs-dist
3. PDF表示コンポーネントの作成
次に、PDFを表示するためのPdfViewerコンポーネントを作成します。

jsx
コードをコピーする
// src/components/PdfViewer.jsx
import React from 'react';
import { Worker, Viewer } from '@react-pdf-viewer/core';
import '@react-pdf-viewer/core/lib/styles/index.css';

// PDFを表示するコンポーネント
const PdfViewer = ({ pdfUrl }) => {
return (
<div style={{ height: '750px' }}>
<Worker workerUrl={https://unpkg.com/pdfjs-dist@2.10.377/build/pdf.worker.min.js}>
<Viewer fileUrl={pdfUrl} />
</Worker>
</div>
);
};

export default PdfViewer;
4. ページにPDFViewerコンポーネントを追加
pages/index.jsまたは任意のページで、作成したPdfViewerコンポーネントを使ってPDFを表示します。Next.jsのページで動作するようにするために、import dynamicを使ってサーバーサイドレンダリングなしで動的にコンポーネントをインポートします。

jsx
コードをコピーする
// pages/index.js
import dynamic from 'next/dynamic';
import React from 'react';

// クライアントサイドでのみ動作させるためにdynamicを使用
const PdfViewer = dynamic(() => import('../src/components/PdfViewer'), {
ssr: false,
});

const Home = () => {
return (
<div>
<h1>PDF Viewer Example</h1>
<PdfViewer pdfUrl="/sample.pdf" />
</div>
);
};

export default Home;
5. PDFファイルの準備
Next.jsのpublicディレクトリにPDFファイルを置きます。例えば、sample.pdfというファイルをpublicフォルダに置きます。

ディレクトリ構造は次のようになります。

java
コードをコピーする
my-pdf-viewer/
├── public/
│ └── sample.pdf
├── src/
│ └── components/
│ └── PdfViewer.jsx
├── pages/
│ └── index.js
└── package.json
6. アプリの起動
次に、アプリケーションを起動してPDFが正しく表示されるか確認します。

bash
コードをコピーする
npm run dev
ブラウザでhttp://localhost:3000にアクセスすると、sample.pdfが表示されるはずです。

まとめ
@react-pdf-viewer/coreライブラリを使用して、PDFファイルを簡単に表示できるコンポーネントを作成しました。
Next.jsではサーバーサイドレンダリングがデフォルトなので、PDF表示コンポーネントはクライアントサイドでのみ実行されるようにdynamicを使用しています。
PDFファイルはpublicフォルダに配置して、ブラウザから直接アクセス可能なパスを指定します。
これで、Next.jsアプリケーション内でreact-pdfを使用してPDFを表示する準備が整いました。

KizitoraKizitora

react-pdf-viewerはかなり直感的でお手軽.以下手順

  1. npm install pdfjs-dist@3.4.120
  2. npm install @react-pdf-viewer/core@3.12.0
  3. page.jsを記述
page.js
"use client"
import dynamic from 'next/dynamic';
import React from 'react';
import { Worker, Viewer } from '@react-pdf-viewer/core';

const Home = () => {
  return (
    <div>
      <h1>PDF Viewer Example</h1>
        <Worker workerUrl={`https://unpkg.com/pdfjs-dist@3.4.120/build/pdf.worker.min.js`}>
          <Viewer fileUrl={"/a.pdf"} />
        </Worker>
    </div>
  );
};

export default Home;

https://react-pdf-viewer.dev/docs/getting-started/

KizitoraKizitora

fabric.jsをNext.jsで使おうとするとかなり難しい.Next.jsはSSRなのでfabric.jsがうまく動作してくれない.Next.jsでfabric.jsを実装する記事は見つけたけど,結局

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

というエラーが出てうまくいかない.

"use client";

import { useState, useEffect } from "react";
import { fabric } from "fabric";

const Component = () => {
  const [fabricCanvas, setFabricCanvas] = useState()

  useEffect(() => {
    // キャンバスの初期化処理
    const c = new fabric.Canvas("canvas", { // <- ここでエラー
      isDrawingMode: true, // 手書きモード
      width: 800,
      height: 300,
      backgroundColor: "#80beaf",
    });
    setFabricCanvas(c);
  }, []);

  // canvas要素に紐づけ
  return (
    <div>
      <canvas id="canvas" />
    </div>
  );
};

export default Component

https://dev.to/ziqinyeow/step-by-step-on-how-to-setup-fabricjs-in-the-nextjs-app-3hi3

https://zenn.dev/ynstakeru/articles/434b47afdde1ac

KizitoraKizitora

できた!!!
next.config.jsonの設定はいらない.インポートの仕方の問題と,デフォルトの手書きの色が白だったから背景と同化してうまくいっているのかわからなくなっていたみたい.

"use client"

import { useEffect, useState } from 'react'
// import { fabric } from 'fabric' 
// TypeError: Cannot read properties of undefined (reading 'Canvas') というエラーがでる
// https://github.com/fabricjs/fabric.js/issues/8299
import * as fabric from 'fabric'

function App() {
  const [fabricCanvas, setFabricCanvas] = useState()

  // キャンバスの初期化処理
  // ref. https://github.com/citrus-candy/handwritten-app
  useEffect(() => {
    const canvas = new fabric.Canvas('fabric', {
      isDrawingMode: true,
      width: 500,
      height: 500,
    })

    canvas.freeDrawingBrush = new fabric.PencilBrush(canvas); // 明示的にPencilBrushを設定
    canvas.freeDrawingBrush.color = "red"; // 色を赤に設定

    setFabricCanvas(canvas)

    return () => {
        canvas.dispose();
      };  
  }, [])

  return (
    <div>
      <canvas id={'fabric'} />
    </div>
  )
}

export default App
KizitoraKizitora

インポート関連でエラーが出る原因は,fabric.jsがfabricをエクスポートしていないから.だからfabric.Canvasを使うと,そもそもないよって言われる.モジュールの全エクスポートをまとめてインポートし、名前空間fabricとしてアクセスするようにするとうまくいく.

とりあえずエラーの原因とそのgpt 4oの回答をメモしておく

「import { fabric } from 'fabric'」と「import * as fabric from 'fabric'」の違いを教えててください.

import { fabric } from 'fabric' と import * as fabric from 'fabric' は、JavaScript ES6モジュールのインポート方法であり、インポートの仕方に違いがあります。これにより、取得されるオブジェクトやモジュールの使い方に微妙な違いがあります。

  1. import { fabric } from 'fabric'
    この形式は、ES6の名前付きエクスポートを使って、モジュールから特定のエクスポートをインポートする方法です。この場合、fabric.jsが「名前付きエクスポート」としてfabricをエクスポートしている場合にのみ、この形式でインポートできます。

使い方: この形式は、特定の関数やオブジェクトだけを取り込む際に使います。モジュール全体ではなく、一部の要素だけをインポートします。

条件: モジュール側で明示的にexport const fabricまたはexport { fabric }のように名前付きエクスポートを定義している必要があります。

javascript
コードをコピーする
// 例: 名前付きエクスポートが存在する場合
import { fabric } from 'fabric';

const canvas = new fabric.Canvas(...);
エラーの可能性: fabric.jsがデフォルトエクスポートか、名前付きエクスポートのfabricを持っていない場合、この形式でのインポートは失敗します。Fabric.jsは通常、名前付きではなく、すべての機能をエクスポートしているため、こちらは正しい形式ではない可能性があります。
2. import * as fabric from 'fabric'
この形式は、ES6モジュールの全エクスポートを一つの名前空間にまとめてインポートする方法です。この場合、モジュールのすべてのエクスポート(関数やクラス、オブジェクトなど)がfabricという名前空間にまとめられます。

使い方: モジュールの全体、つまり名前付きエクスポートやその他のエクスポートされた機能をすべてインポートする際に使用します。fabric.jsのようにモジュールが複数のエクスポートを持つ場合に適しています。
javascript
コードをコピーする
// 例: 全エクスポートをインポートする場合
import * as fabric from 'fabric';

const canvas = new fabric.Canvas(...);
汎用性: fabric.jsは名前付きエクスポートではなく、通常全エクスポートを扱うため、この形式の方が安全で一般的です。多くのユーティリティやクラスが一度に必要な場合、この方法が適しています。
違いのまとめ
import { fabric } from 'fabric' は、fabric.jsが名前付きエクスポートを持っている場合にのみ機能し、特定のエクスポートをインポートする形式です。fabric.jsの場合、一般的には適さない可能性があります。
import * as fabric from 'fabric' は、モジュールの全エクスポートをまとめてインポートし、名前空間fabricとしてアクセスします。この形式はfabric.jsのような大規模ライブラリでは一般的に使われます。
fabric.jsの場合、通常は import * as fabric from 'fabric' を使用するのが正しい方法です。

import { fabric } from 'fabric'だとTypeError: Cannot read properties of undefined (reading 'Canvas') というエラーがでる のはなぜ?

ChatGPT
import { fabric } from 'fabric'を使用してエラー「TypeError: Cannot read properties of undefined (reading 'Canvas')」が発生するのは、Fabric.jsがデフォルトでモジュール全体をエクスポートしているためです。

import { fabric } from 'fabric'は名前付きエクスポートを想定しており、モジュール内で特定の名前でエクスポートされたオブジェクトや関数に対してのみ有効です。しかし、Fabric.jsはデフォルトではモジュール全体をエクスポートしているため、名前付きエクスポートの形式では正しくインポートできません。

Fabric.jsでは次のように、モジュール全体をインポートする方法が正しいです:

import * as fabric from 'fabric';

なぜ import { fabric } from 'fabric' だとエラーが出るのか
fabric.jsは名前付きエクスポートを使用していないため、{ fabric }とインポートしようとすると、そのfabricがモジュール内で見つからないため、undefinedが返されます。
その結果、fabric.Canvasにアクセスしようとすると、fabricがundefinedであるため、undefinedのプロパティを読み取ろうとするエラーが発生します。
正しいインポート方法
Fabric.jsを使用する際には、以下のようにインポートするのが標準的です:

import * as fabric from 'fabric';

または、場合によっては動的インポートを使用してクライアントサイドでのみFabric.jsを読み込むこともあります(特にNext.jsなどサーバーサイドレンダリングのフレームワークを使う場合):

これにより、fabric.Canvasのようなプロパティに正しくアクセスできるようになります。

KizitoraKizitora

react-view-pdfはページごとの表示方法が分からなかったので一旦やめてreact-pdfに戻った.
そして日本語の記事を調べ続けてもわけわからなかった.けど公式ドキュメント読んだたら一発だった!ちゃんと公式ドキュメントを読もう.renderTextLayerとrenderAnnotationLayerで下部に出てくるテキストや注釈情報をなくせる.

https://github.com/wojtekmaj/react-pdf

"use client"
import React from 'react';
import { Document, Page, pdfjs} from 'react-pdf';

pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;

const App = () => (
  <div>
    <Document file="/a.pdf">
      <Page pageNumber={4} renderTextLayer={false} renderAnnotationLayer={false}/>
    </Document>
  </div>
);

export default App
KizitoraKizitora

pdfとフリーハンドでの書き込みができた!やった!

"use client"

import React, { useEffect, useState } from 'react';
import * as fabric from 'fabric';
import { Document, Page, pdfjs} from 'react-pdf';

pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;

const Home = () => {
  const [fabricCanvas, setFabricCanvas] = useState();

  // キャンバスの初期化処理
  useEffect(() => {
    const canvas = new fabric.Canvas('fabric', {
      isDrawingMode: true,
      width: 500, // 必要に応じてPDFのサイズに合わせて調整
      height: 500, // 必要に応じてPDFのサイズに合わせて調整
    });

    canvas.freeDrawingBrush = new fabric.PencilBrush(canvas); // 明示的にPencilBrushを設定
    canvas.freeDrawingBrush.color = "red"; // 色を赤に設定

    setFabricCanvas(canvas);

    // この処理入れないと,きちんと終了されなくて,既に初期化されたキャンバスをもう一度初期化しちゃってエラーになる
    return () => {
      canvas.dispose();
    };
  }, []);

  return (
    <div style={{ position: 'relative' }}>
      <div style={{ position: 'absolute'}}>
        <Document file="/a.pdf">
          <Page pageNumber={4} renderTextLayer={false} renderAnnotationLayer={false}/>
        </Document>
      </div>
      <canvas id="fabric" style={{ position: 'absolute'}}/>
    </div>
  );
};

export default Home;

KizitoraKizitora

というかそもそもpdfをレンダリングした上にさらにfabric.jsの書き込みをレンダリングするのはよくない.なぜなら二回レンダリングしなくちゃいけないうえにpdfのサイズの変化にfabricが対応できない.fabricの背景画像としてpdfを画像化したものを設定する方法をしている人がいるから,これを参考にこちらも対応したほうがいい.

https://stackoverflow.com/questions/59199931/fabricjs-scale-canvas-view

KizitoraKizitora

背景画像を設定するためのsetBackgroundImage関数がfabric.jsから削除されていることに気が付いた...ともったけれど公式ドキュメントには書いてあって頭が混乱してきた.

http://fabricjs.com/docs/fabric.Canvas.html#setBackgroundImage

バージョン違いかでこうなっているのか,それともsetBackgroundImageメソッドが削除されていると勘違いしているだけなのか.でもgrep関数でnode_modulesのfabricディレクトリを「setBackgroundImage」で検索しても何も引っかからなかった.一応直近でも9ヶ月前にこのメソッドを使った方法を回答している人がいるけれど,よくわからん.

https://stackoverflow.com/questions/77586483/fabric-js-background-image-sizing-issue

KizitoraKizitora

できた~どうやらsetBackgroundImageはなくなったけど,canvasのメンバー変数にbackgroundImageというのがあって,そこに画像データを格納すればいいみたい.

↓ 参考になったDiscussions

https://github.com/fabricjs/fabric.js/discussions/10004

ただし,上記のやつだと動かないので,ちゃんと動くコードを下に乗っける

"use client";

import React, { useEffect, useState } from "react";
// npm instal fabric
import * as fabric from "fabric";
// npm install react-pdf
import { Document, Page, pdfjs } from "react-pdf";

pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;

const Home = () => {
  const [fabricCanvas, setFabricCanvas] = useState();

  // キャンバスの初期化処理
  useEffect(() => {
    const canvas = new fabric.Canvas("fabric", {
      isDrawingMode: true,
      width: 500, // 必要に応じてPDFのサイズに合わせて調整
      height: 500, // 必要に応じてPDFのサイズに合わせて調整
      /*
      backgroundImage: fabric.Image.fromURL(
        "http://localhost:3000/app_service.png",
        (img) => {
          img.scaleX = (canv.width * 0.5) / img.width;
          img.scaleY = (canv.height * 0.5) / img.height;

          canvas.backgroundImage = img;
          canv.renderAll();
        }
      ),
      */
    });

    canvas.freeDrawingBrush = new fabric.PencilBrush(canvas); // 明示的にPencilBrushを設定
    canvas.freeDrawingBrush.color = "red"; // 色を赤に設定

    // 画像を設定
    fabric.FabricImage.fromURL("http://localhost:3000/app_service.png").then(
      function (img) {
        canvas.backgroundImage = img;
        img.canvas = canvas;

        console.log(canvas.backgroundImage);
        canvas.renderAll();
      }
    );

    setFabricCanvas(canvas);

    return () => {
      canvas.dispose();
    };
  }, []);

  return (
    <>
      <div style={{ position: "relative" }}>
        <canvas id="fabric" style={{ position: "absolute" }} />
      </div>
    </>
  );
};

export default Home;
KizitoraKizitora

強引だけど,画像の大きさに合わせてcanvasの大きさを設定できるようにした

"use client";

import React, { useEffect, useState } from "react";
// npm instal fabric
import * as fabric from "fabric";
// npm install react-pdf
import { Document, Page, pdfjs } from "react-pdf";

pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;

const Home = () => {
  const [fabricCanvas, setFabricCanvas] = useState();
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  // キャンバスの初期化処理
  useEffect(() => {
    const img = new Image();
    img.src = "http://localhost:3000/app_service.png";

    const canvas = new fabric.Canvas("fabric", {
      isDrawingMode: true,
      width: img.width, // 必要に応じてPDFのサイズに合わせて調整
      height: img.height, // 必要に応じてPDFのサイズに合わせて調整
    });

    canvas.freeDrawingBrush = new fabric.PencilBrush(canvas); // 明示的にPencilBrushを設定
    canvas.freeDrawingBrush.color = "red"; // 色を赤に設定

    // 画像を設定
    fabric.FabricImage.fromURL("http://localhost:3000/app_service.png").then(
      function (img) {
        canvas.backgroundImage = img;
        img.canvas = canvas;
        canvas.width = img.width;
        canvas.height = img.height;
        canvas.renderAll();
      }
    );

    setFabricCanvas(canvas);

    return () => {
      canvas.dispose();
    };
  }, []);

  return (
    <>
      <div style={{ position: "relative" }}>
        <canvas id="fabric" style={{ position: "absolute" }} />
      </div>
    </>
  );
};

export default Home;