📁

PDFから画像を取得する

2025/02/24に公開

PDF.js でPDFファイルから内部画像を取得するための備忘録です。

環境

  • pdfjs-dist v4.10.38
  • React 19.0.0
  • Typescript 5.7.2

読み込み

今回はブラウザで動作するようにします。
Viteを使用するため、こちらを参考にworkerの設定をします。

import {
  getDocument,
  GlobalWorkerOptions
} from 'pdfjs-dist'
GlobalWorkerOptions.workerSrc = new URL(
  "pdfjs-dist/build/pdf.worker.min.mjs",
  import.meta.url,
).toString();

PDFファイル読み込み

export const loadPdf = async (file: File): Promise<PDFDocumentProxy> => {
  if (!file || file.type !== 'application/pdf') {
    throw new Error('Invalid file type');
  }

  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = async (event) => {
      const buffer = event.target?.result;
      if (!buffer) {
        return reject('Failed to read file');
      }

      const loadingTask = getDocument({ data: buffer, isOffscreenCanvasSupported: false  });
      const pdf = await loadingTask.promise;
      return resolve(pdf);
    };
    reader.onerror = () => reject('Failed to read file');
    reader.readAsArrayBuffer(file);
  });
}

isOffscreenCanvasSupported: false

後に画像データを取得する際にOffscreenCanvasが使用されているとデータが取得できないため、設定が必要です。(v3.4.120以上)

ページオブジェクトから画像データ取得

const addAlphaChannel = (imgData: Uint8ClampedArray, width: number, height: number) => {
  const newImageData = new Uint8ClampedArray(width * height * 4);

  for (let j = 0, k = 0, jj = width * height * 4; j < jj;) {
    newImageData[j++] = imgData[k++];
    newImageData[j++] = imgData[k++];
    newImageData[j++] = imgData[k++];
    newImageData[j++] = 255;
  }

  return newImageData;
}

export const extractPageImages = async (page: PDFPageProxy) => {
  const { fnArray, argsArray } = await page.getOperatorList();

  const targets = [OPS.paintImageXObject, OPS.paintInlineImageXObject];
  const imageIndexArray = fnArray.reduce((acc, cur, index) => {
    if (targets.includes(cur)) {
      acc.push(index);
    }
    return acc;
  }, [] as number[]);

  const images = [];
  for (let i = 0; i < imageIndexArray.length; i++) {
    const imgName = argsArray[imageIndexArray[i]][0];

    const imgObj = await page.objs.get(imgName);

    const imageWithAlpha = addAlphaChannel(imgObj.data, imgObj.width, imgObj.height);
    const imageData = new ImageData(imageWithAlpha, imgObj.width, imgObj.height);

    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    if (!context) {
      throw new Error('Failed to get 2d context');
    }

    canvas.height = imgObj.height;
    canvas.width = imgObj.width;
    context.putImageData(imageData, 0, 0);
    images.push(canvas.toDataURL());
  }

  return images;
}

PDFOperatorListから画像のインデックスをfnArrayから取得し、argsArrayから画像データを取得します。
取得したデータをCanvasに書き出し、dataURLに変換し保持します。
PDFから取得した画像データにはalphaチャンネルがないことがあるようなので、追加してImageDataを作成します。(画像の種類による?)

全ページに適用する

あとはページ分、上記処理を実行し全てのページの画像を取得するようにします。

export const extractImagesFromPdf = async (file: File) => {
  const pdf = await loadPdf(file);
  const images = [];
  for (let i = 1; i <= pdf.numPages; i++) {
    const page = await pdf.getPage(i);
    const pageImages = await extractPageImages(page);
    images.push(...pageImages);
  }
  return images;
}

まとめ

PDF.jsは設定の周りなど調べることが結構あるイメージなので参考になれば幸いです。
https://github.com/SakaiBorder/pdf-image-extract-sample

参考

https://zenn.dev/watty/articles/77657c0ac4838d
https://github.com/mozilla/pdf.js/issues/16282
https://codepen.io/allandiego/pen/RwVGbyj
https://github.com/mozilla/pdf.js/blob/master/examples/image_decoders/jpeg_viewer.mjs

Discussion