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>