サムネイル画像をPDFの1ページ目から自動生成する

こんにちは!開発本部のレーホアントゥです。

先日PR TIMESがサムネイル画像をPDFの1ページ目から自動生成し、その画像が資料とガイドラインのサムネイルとして使用されるという新機能をリリースました。なので、今回PDFの1ページ目からサムネイル画像を作成する方法を紹介したいと思います。

目次

やりたいこと

現在、プレスキットの編集画面ではロゴ、画像、資料などのプレスキットの素材となるファイルをアップロードできる機能があります。ロゴ、画像はそれぞれの画像ファイルを変換したサムネイルが表示されていますが、資料とガイドラインファイルはPDFファイルであるため、画像ファイルと同様の変換処理が行えず、デフォルトのサムネイルが使われています。

今回、それぞれの素材をひと目で識別しやすくするために、PDFファイルの1ページ目からサムネイル画像を作成しようと思います。ロゴのタブと資料タブを見比べて、資料の方にサムネイル画像が無いことがひと目で伝わる画像があると良さそうです。

実装

仕様

サムネイルを作成する実装は2段階を分けます。

  • 1段階目: PDFファイルのアップロード

PDFファイルのアップロードが成功だったら、それらをフロントエンド側Queueに保存しておきます。

  • 2段階目: サムネイル画像の作成処理とアップロード

Queueに保存されたPDFファイルを取って、それぞれの1ページ目を画像化してから、その画像ファイルをまたS3に保存します。

使用ライブラリ

PDFファイルを読み込んで、PDFファイルから画像化するため、Pdfjsライブラリ(Mozillaに開発されている)を使います。このライブラリはPDFファイルをcanvasにレンダリングすることができます。

https://github.com/mozilla/pdf.js/

実装

if (uploadedFile.file.type === 'application/pdf') {
  setIsProcessingThumbnail(true);
  setThumbnailFileQueue((prev) => [
    ...prev,
    {
      file: uploadedFile,
      canvas: canvas,
    },
  ]);
}

return (
<li>
	<canvas ref={canvas} css={css`
	  position: absolute;
    opacity: 0;
    z-index: -1;
	`}></canvas>
	...
</li>)

ここで、PDFファイルのアップロードに成功した後、次の段階(画像の処理)のためにそのファイルをQueueに追加します。

useEffect(() => {
  if (thumbnailFileQueue.length > 0) {
    const copyQueue = [...thumbnailFileQueue];
    setThumbnailFileQueue([]);
    const convertFirstPagePDFToImage = async () => {
      for (const item of copyQueue) {
        const newImageFile = await convertPDFToImage(
          item.file.file,
          item.canvas,
        );

				if(newImageFile) {
				    // upload to S3
						// get resourceUri and save into Database
						...
				}
				else {
						...
				}
			}
    };

    convertFirstPagePDFToImage();
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [thumbnailFileQueue]);

2段階目にQueueから全てのアイテムを取って、順番にPDFファイルの1ページ目を画像化する処理を実装します。その後、S3に保存します。上記にconvertPDFToImageを呼び出していますが、convertPDFToImageの実装は以下の通りです。

const convertPDFToImage = async (
  file: File,
  canvas: RefObject<HTMLCanvasElement>,
) => {
	pdfjs.getDocument({
	  url: URL.createObjectURL(file),
	  cMapUrl: `/common/js/pdfjs-dist/cmaps/`,
	  cMapPacked: true,
	})
	.promise.then(async (doc) => {
	  return await doc.getPage(1).then(async (page) => {
      const DEFAULT_SCALE = 1;
	    const MAX_CANVAS_WIDTH = 1280;
			const scaleReguired = 
					MAX_CANVAS_WIDTH / page.getViewport({ scale: DEFAULT_SCALE }).width
			const viewPort = page.getViewport({
	      scale: scaleReguired,

			canvas.current &&
      ((canvas.current?.width = viewPort.width),
      (canvas.current?.height = viewPort.height));

			const context = 
					canvas.current?.getContext('2d',}) as CanvasRenderingContext2D;
	    const renderContext = {
	      canvasContext: context,
	      viewport: viewPort,
	    };

			const thumbnailFile = await page
	      .render(renderContext)
	      .promise.then(async () => {
	        const url = canvas.current ? canvas.current.toDataURL('image/png') : '';
	
	        if (url === '') return;
	
	        const res = await fetch(url);
				  const buf = await res.arrayBuffer();
	
				  return new File([buf], 'filename', { type: 'png' });
	      });
			canvas.currrent.remove();
	    return thumbnailFile;
		  });
	}).catch(err){
			...
	}
}

12行目から28行目までは単純にpageViewportによって、canvasの幅と高さとrenderContextを初期化します。pageViewportのスケールを調整できますが、大きいスケールで調整したら、canvasの幅と高さも上がりますので、レンダリングする時にメモリが足りなかったり、ファイルのサイズが大きくなってアップロードできなくなったりします。そうならないようにcanvasの幅を固定します。そして、pageViewportスケールとcanvasの高さはcanvasの幅に沿って、初期化します。残りのコードはレンダリングとファイル化になります。

レンダリングした上で、canvasで表示された画像を取得します。そのcanvasを通じてbase64(image/png)データに変化できます。良い画質の画像が取得できるためimage/pngを指定します。そんなbase64データからファイルオブジェクトに変換して、S3などストレージにアップロードできます。その後、canvasは不要になるので、UIに影響をかけないように削除します。

実装結果はビデオでデモします。

注意点

  • UIに影響しないように

canvasにレンダリングする時はUIが変わってしまうので、そのレンダリング結果をCSSで隠します。

css`
  position: absolute;
  opacity: 0;
  z-index: -1;
`,
  • パスワード付きPDFファイルを対応する

今回、パスワードが設定されていないPDFファイルだけを処理します。パスワード付きPDFファイルの場合にキャッチして、スキップします。

.then((res) => {
	...
})
.catch((err) => {
  if (err.name === 'PasswordException') 
	  console.log('パスワード付きPDFはサムネイルを作成することができません')
}
  • 1つのcanvasに複数のPDFファイルをレンダリングしないように

1つのcanvasに1つのPDFファイルだけをレンダリングしたほうがいいです。なぜならば、1つのcanvasに複数のPDFファイルをレンダリングすれば、コンフリクトして、イメージの結果が壊れて、期待通りになりません。

参考

  • サムネイル画像をS3へアップロードする

サムネイル画像をS3へアップロードするのを実行するのは江間さんの記事を参考してもらいました。気になる人は下記のリンクをご参考してください。

あわせて読みたい
S3 を活用して工数を削減させた、ファイルアップロード機能の設計と実装
S3 を活用して工数を削減させた、ファイルアップロード機能の設計と実装こんにちは、開発本部・バックエンドエンジニアの江間です。先日、 PR TIMES の新規機能としてプレスキット機能の提供が開始されました。プレスキット機能では、画像コ...
  • アップロードしたサムネイル画像を使用して表示する

画像を表示するためにS3とFastly Image Optimizerを利用します。この機能は既に実装されているので、そちらを利用しました。詳しく知りたい方はさんの記事をご覧ください。

あわせて読みたい
新卒エンジニアがプレスリリース画像の画質改善に取り組んだ話
新卒エンジニアがプレスリリース画像の画質改善に取り組んだ話こんにちは、21新卒エンジニアの柳です。この度、プレスリリースのサムネイル画像とプレスリリース詳細ページ内で掲載されている画像の画質改善を行いました。今回行っ...

最後に

サムネイル画像をPDFの1ページ目から自動生成する機能が実装できました。この機能を開発するときにはPdfjsライブラリの使用を勉強したり、GoとFastlyとAWS(S3, Lambda, …)に若干触ったりするチャンスがありました。

それに今回は私がPR TIMESのプロジェクトでやったことについて共有するのは初めてのブログです。このブログが皆さんに何か役に立てば嬉しいです。今後、このようなブログ記事をたくさん書きたいと思います。

この記事を書いた人

ベトナムエンジニアとしてPR TIMESの開発の本部でフロントエンドの仕事をしています。

目次
閉じる