こんにちは!開発本部のレーホアントゥです。
先日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とFastly Image Optimizerを利用します。この機能は既に実装されているので、そちらを利用しました。詳しく知りたい方は柳さんの記事をご覧ください。

最後に
サムネイル画像をPDFの1ページ目から自動生成する機能が実装できました。この機能を開発するときにはPdfjsライブラリの使用を勉強したり、GoとFastlyとAWS(S3, Lambda, …)に若干触ったりするチャンスがありました。
それに今回は私がPR TIMESのプロジェクトでやったことについて共有するのは初めてのブログです。このブログが皆さんに何か役に立てば嬉しいです。今後、このようなブログ記事をたくさん書きたいと思います。