クロスリファレンステーブルの役割
PDFファイルは、内部でオブジェクト(ページ、テキスト、画像、フォントや色空間など)として構造化された状態でデータを保持しています。
クロスリファレンステーブル(xrefテーブル)は、PDFファイル内のオブジェクトの位置情報を管理する役割を持っています。
これにより、PDFビューアーは素早く必要なデータを読み取ることができ、効率的に表示・処理できます。
ファイルトレーラーの役割
PDFビューアーは、PDFファイルを開く際にファイルトレーラーを見て、どこにクロスリファレンステーブルがあるかを確認します。
またPDFファイルの構造はツリーのようになっており、最上位に「ルートオブジェクト」がありますが、ファイルトレーラーを見てどのオブジェクトが「ルートオブジェクト」であるかを確認し、ルートオブジェクトを起点にページのレイアウトを決定します。
タイトル、作成者、作成日時などのメタデータをファイルトレーラーに保存することも可能です。
テキスト形式で作成したPDFファイルのCross-Reference TableおよびFile Trailerを作成するJavaScript
オブジェクトの位置情報を一つ一つ調べて入力するのは非常に手間がかかるので、JavaScriptを使用してCross-Reference Tableを作成します。ついでにFile Trailerも作成します。
オブジェクトストリームは、バイナリーによるクロスリファレンスストリームを必要としますので、テキスト形式でPDFファイルを作成するという方針と異なりますので、対応していません。
JavaScriptを作成する際の方針は以下の通りとなります。
① PDFの文字コードはUTF-8。
② 改行コードはCR+LF。
③ Rootの間接オブジェクト番号は 1。
④ オブジェクトストリームおよびクロスリファレンスストリームには対応しない。
⑤ File Trailerのstartxrefを出力するため、適当なダミーのCross-Reference TableをPDFファイル内に記述しておく。
完成例
入力: テキスト形式で作成したPDFファイル出力: テキスト (xrefおよびtrailer)
使い方
① Cross-Reference TableおよびFile Trailerを作成したいPDFファイル(.pdf)を開きます。
② PDFファイルを開くイベントをきっかけにJavaScriptが動き、テキストエリアにCross-Reference TableおよびFile Trailerが表示されます🤗。
③ PDFファイルのソーステキストに作成したCross-Reference TableおよびFile Trailerをコピー&ペーストで挿入します。
ソース
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>テキスト形式で作成したPDFファイルのCross-Reference TableおよびFile Trailerを作成する</title> </head> <body> 入力: テキスト形式で作成したPDFファイル <input type="file" id="input" accept="application/pdf" /><br /> <br /> 出力: テキスト (xrefおよびtrailer)<br /> <br /> <div id="output"></div> <script>
'use strict'; // @ts-check /** * 標準組み込みオブジェクト * @typeof {object} ArrayBuffer * @typeof {object} DataView * @typeof {object} RegExpExecArray * Web API * @typeof {object} FileList * @typeof {object} HTMLElement * @typeof {object} HTMLInputElement * @typeof {object} HTMLTextAreaElement */ //------------------------------------------------------------ // ライブラリー //------------------------------------------------------------ /** * バイナリーデータ(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; } //------------------------------------------------------------ // イベント 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オブジェクト)をUTF-8でデコードしてテキストに変換 const inputUtf8Text = (new TextDecoder()).decode(arrayBuffer); // バイナリーデータ(ArrayBufferオブジェクト)を16進数テキストに変換 const inputHexText = arrayBufferToHexText(arrayBuffer); // PDFファイル内のオブジェクトのバイト位置と世代番号を管理。 // オブジェクト番号をインデックスとして使用 let pdfObjectArray = []; pdfObjectArray[0] = [{ bytePosition: 0, generationNumber: 0 }] let maxObjectNumber = 0; // 出力用テキスト let outputText = 'xref\u000d\u000a'; let numberOfEntriesFlag = false; let numberOfEntries = 0; // Cross-Reference Tableのバイト位置 let xrefBytePosition = 0; //------------------------------------------------------------ // Cross-Reference Table //------------------------------------------------------------ const regExpExecArrays = [...inputUtf8Text.matchAll(new RegExp('(?:\\u000d\\u000a|\\u000d|\\000a)\\s*(([0-9]+)\\s*([0-9]+)\\s*obj)', 'g'))]; for (const regExpExecArray1 of regExpExecArrays) { let objectNumber = Number(regExpExecArray1[2]); let generationNumber = Number(regExpExecArray1[3]); // ts-checkエラー対策 nullを排除し、RegExecArrayとする const regExpExecArray2 = /** @type {!RegExpExecArray} */ (new RegExp('((?:0d0a|0d|0a){1}(?:20)*)' + arrayBufferToHexText((new TextEncoder()).encode(regExpExecArray1[1]).buffer), 'g').exec(inputHexText)); let bytePosition = (regExpExecArray2.index + regExpExecArray2[1].length) / 2; if (!pdfObjectArray[objectNumber]) { pdfObjectArray[objectNumber] = []; } pdfObjectArray[objectNumber].push({ bytePosition, generationNumber }); maxObjectNumber = objectNumber > maxObjectNumber ? objectNumber : maxObjectNumber; } for (let i = 0; i <= maxObjectNumber; i ++) { if (pdfObjectArray[i]) { // Entryの数を決定する if (!numberOfEntriesFlag) { for (let j = i; j <= maxObjectNumber; j ++) { if (pdfObjectArray[j + 1]) { numberOfEntries ++; } else { numberOfEntriesFlag = true; outputText += `${ i } ${ numberOfEntries + 1 }\u000d\u000a`; break; } } } if (i === 0) { outputText += '0000000000 65535 f\u000d\u000a'; } else { outputText += `${ pdfObjectArray[i][pdfObjectArray[i].length - 1].bytePosition.toString().padStart(10, '0') } ${ pdfObjectArray[i][pdfObjectArray[i].length - 1].generationNumber.toString().padStart(5, '0') } n\u000d\u000a`; } } else { numberOfEntriesFlag = false; numberOfEntries = 0; } } //------------------------------------------------------------ // File Trailer //------------------------------------------------------------ if (new RegExp('(?:0d0a|0d|0a){1}(?:20)*78726566(?:20)*(?:0d0a|0d|0a){1}', 'g').test(inputHexText)){ // ts-checkエラー対策 nullを排除し、RegExecArrayとする const regExpExecArray = /** @type {RegExpExecArray} */ (new RegExp('((?:0d0a|0d|0a){1}(?:20)*)78726566(?:20)*(?:0d0a|0d|0a){1}', 'g').exec(inputHexText)); xrefBytePosition = (regExpExecArray.index + regExpExecArray[1].length) / 2; } if (xrefBytePosition !== 0) { outputText += `\u000d\u000atrailer\u000a<< /Root 1 ${ pdfObjectArray[1][pdfObjectArray[1].length - 1].generationNumber } R /Size ${ maxObjectNumber + 1 } >>\u000d\u000astartxref\u000d\u000a${ xrefBytePosition }\u000d\u000a%%EOF`; } else { outputText += `\u000d\u000atrailer\u000a<< /Root 1 ${ pdfObjectArray[1][pdfObjectArray[1].length - 1].generationNumber } R /Size ${ maxObjectNumber + 1 } >>\u000d\u000astartxref\u000d\u000a${ arrayBuffer.byteLength }\u000d\u000a%%EOF`; } //------------------------------------------------------------ // コンテンツ区分要素内に出力 //------------------------------------------------------------ // ts-check エラー対策 変数を用意し、nullを排除し、HTMLElementとする const htmlElement = /** @type {!HTMLElement} */ (document.getElementById('output')); /** @type {HTMLTextAreaElement} */ const textareaElement = document.createElement('textarea'); htmlElement.appendChild(textareaElement); textareaElement.setAttribute('cols', '80'); textareaElement.setAttribute('rows', '20'); textareaElement.appendChild(document.createTextNode(outputText)); });
</script> </body> </html>