JavaScriptでPDFファイルのDelfate圧縮をデコードする

Author:

PDFファイル内のベクター形式のデータを見る

PDFファイル内のベクター形式のグラフィックデータは、Contents streamに直接記述することで描画することができます。
既存のPDFファイルもベクター形式のグラフィックデータは、Contents streamに直接記述されています。しかし、圧縮されることでテキストデータからバイナリーデータに変換されてしまっているため、テキストエディターで確認することができません。
圧縮されたデータをJavaScriptで解凍すると、元のベクター形式のグラフィックデータをテキストエディターで確認することができます。
ベクター形式のグラフィックデータが取得できるる様になると、SVGを作成するなど活用することができるようになります。

完成例

入力: PDFファイル


使い方

Deflate圧縮をデコードしたいPDFファイル(.pdf)を開きます。
PDFファイルを開くイベントをきっかけにJavaScriptが動き、テキストファイルが保存されます🤗。
テキストファイルをテキストエディターで開き、デコードされていることを確認します。
うまくデコードできない場合は、PDFファイルがパスワードによる暗号化(編集制限)されていることが考えられます。Web上に転がっているパスワード解除ツールを使用して暗号化を解除して再度試してください。

ソース

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>PDFのDeflate圧縮をデコードするJavaScript</title>
  </head>
  <body>
    入力: PDFファイル<br />
    <input type="file" id="input" /><br />
    <script>
      'use strict';

      // @ts-check

      /**
        * 標準組み込みオブジェクト
        * @typeof {object} ArrayBuffer
        * @typeof {object} DataView
        * @typeof {object} RegExpExecArray
        * Web API
        * @typeof {object} Blob
        * @typeof {object} BlobPart
        * @typeof {object} FileList
        * @typeof {object} HTMLAnchorElement
        * @typeof {object} HTMLInputElement
        */

      //------------------------------------------------------------
      // ライブラリー
      //------------------------------------------------------------

      /**
        * バイナリーデータ(ArrayBufferオブジェクト)を16進数テキストに変換
        * @param {ArrayBuffer} arrayBuffer 16進数テキストに変換するArrayBufferオブジェクトによるバイナリーデータ
        * @returns {string} 変換された16進数テキスト
        */
      function arrayBufferToHexText(arrayBuffer) {
        // 出力用テキスト
        let hexText = '';
        /** @type {DataView} */
        const dataView = new DataView(arrayBuffer);
        for (let i = 0; i < arrayBuffer.byteLength; i ++) {
          hexText += dataView.getUint8(i).toString(16).padStart(2, '0');
        }
        return hexText;
      }

      /**
        * 16進数テキストをバイナリーデータ(ArrayBufferオブジェクト)に変換
        * @param {string} hexText ArrayBufferオブジェクトによるバイナリーデータに変換する16進数テキスト
        * @returns {ArrayBuffer} 変換されたArrayBufferオブジェクトによるバイナリーデータ
        */
      function hexTextToArrayBuffer(hexText) {
        /** @type {ArrayBuffer} */
        const arrayBuffer = new ArrayBuffer(hexText.length / 2);
        /** @type {DataView} */
        const dataView = new DataView(arrayBuffer);
        for (let i = 0; i < arrayBuffer.byteLength; i ++) {
          dataView.setUint8(i, parseInt(hexText.slice(i * 2, (i + 1) * 2), 16));
        }
        return arrayBuffer;
      }

      /**
        * ファイルに保存
        * @param {(BlobPart[])} blobParts Arrayオブジェクトなどの反復可能オブジェクト
        * @param {string} type blobに格納されるデータのMIMEタイプ
        * @param {string} fileName ファイル名
        */
        function saveFile(blobParts, type, fileName) {
        /** @type {Blob} */
        const blob = new Blob(blobParts, {
          type: type
        });
        const blobUrl = URL.createObjectURL(blob);
        /** @type {HTMLAnchorElement} */
        const a = document.createElement('a');
        a.download = fileName;
        a.href = blobUrl;
        a.click();
        URL.revokeObjectURL(blobUrl);
      }

      //------------------------------------------------------------
      // イベント  type="file"のインプット要素でファイルを選択時
      //------------------------------------------------------------
      // ts-check エラー対策 変数を用意し、nullを排除し、HTMLElementではなくHTMLInputElementとする
      const htmlInputElement = /** @type {!HTMLInputElement} */ (document.getElementById('input'));
      /** @this HTMLInputElement */
      htmlInputElement.addEventListener('change', async function() {
        //------------------------------------------------------------
        // ローカル変数の設定 変更不要
        //------------------------------------------------------------
        // ts-check エラー対策 変数を用意し、nullを排除し、FileListとする
        const fileList = /** @type {!FileList} */ (this.files);
        // Fileオブジェクトは、Blobオブジェクトからメソッドを継承
        // Blob.prototype.arrayBuffer()
        // Fileをストリームに変換し、最後まで読み込む
        /** @type {ArrayBuffer} */
        const arrayBuffer = await fileList[0].arrayBuffer();
        // バイナリーデータ(ArrayBufferオブジェクト)を16進数テキストに変換
        const inputHexText = arrayBufferToHexText(arrayBuffer);
        // 出力用テキスト
        let outputText = '';
        // ts-check エラー対策 変数を用意し、nullを排除し、RegExpMatchArrayとする
        const FILE_NAME = /** @type {RegExpMatchArray} */ (fileList[0].name.match(new RegExp('^(.+)\..+$')))[1];

        // 656e646f626a = endobj
        const inputHexTextArray = inputHexText.split('656e646f626a');

        for (let i = 0; i < inputHexTextArray.length; i ++) {
          // 2f46696c746572(?:20)*2f466c6174654465636f6465 = /Filter/FlateDecode
          if (new RegExp('2f46696c746572(?:20)*(?:5b)*(?:20)*2f466c6174654465636f6465(?:20)*(?:5d)*', 'g').test(inputHexTextArray[i])) {
            // 73747265616 = stream
            // 656e6473747265616d = endstream
            // [0-9A-Fa-f]+?の?は、貪欲(最長)モードから怠惰(最短)モードにする
            // ts-checkエラー対策 nullを排除し、RegExecArrayとする
            const regExpExecArray = /** @type {!RegExpExecArray} */ (new RegExp('^([0-9A-Fa-f]*73747265616d(?:0d0a|0d|0a){1})([0-9A-Fa-f]+?)((?:0d0a|0d|0a){1}656e6473747265616d[0-9A-Fa-f]*)$').exec(inputHexTextArray[i]));
            try {
              const streamArrayBuffer = await new Response(new Blob([hexTextToArrayBuffer(regExpExecArray[2])]).stream().pipeThrough( new DecompressionStream('deflate'))).arrayBuffer();
              outputText += regExpExecArray[1] + arrayBufferToHexText(streamArrayBuffer) + regExpExecArray[3] + '656e646f626a';
            } catch {
              console.log(i, (new TextDecoder()).decode(hexTextToArrayBuffer(inputHexTextArray[i])));
            }
          } else {
            outputText += i !== inputHexTextArray.length - 1 ? inputHexTextArray[i] + '656e646f626a' : inputHexTextArray[i];
          }
        }

        saveFile([hexTextToArrayBuffer(outputText)], 'application/octet-stream', FILE_NAME + '(解凍).txt');

      });
    </script>
  </body>
</html>

関連情報


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です