今回はPDFファイルを一気に読み込みます。
一般的なPDFの読み込み方としては、以下の流れになります。
1. PDFヘッダー/フッターをチェック
2. クロスリファレンス情報を読み込む
3. クロスリファレンスに沿ってオブジェクトを読み込む
4. オブジェクト内容に沿って表示
しかしクロスリファレンスは表記形式が複数あり、きちんと作るとこれだけで日が暮れそうなので省略。
以下の流れで読むことにします。
1. PDFヘッダーチェック
2. PDFフッターチェック
3. クロスリファレンスがあることをチェック(内容は見ない)
4. PDFヘッダー直後のファイル位置にseek
5. seek位置から「xxx 0 obj」という行を順々に探し、あったらオブジェクトとして読み込み開始
6. オブジェクトにstreamがあった場合はそれを読み込む
7. endobjeまで読み込む
8. 5に戻る
オブジェクトは例えば以下のような形式で表現され、「<<」が複数あったり、streamとして付加データが存在することもあります。
streamの「/FlateDecode」指定は、圧縮されたstreamデータを示し、C#の場合はDeflateStreamクラスで解凍できます。
2 0 obj << /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] /Font << /F1 14 0 R /F2 16 0 R>> /XObject << >> >> endobj 12 0 obj <</Type /Catalog /Pages 1 0 R /OpenAction [3 0 R /FitH null] /PageLayout /OneColumn>> endobj 20 0 obj <</Filter /FlateDecode /Length 701>> stream endstream xxxxx 圧縮されたバイナリデータ701bytes xxxxx endobj
最初に読み込むPDFファイルを準備します。
小説家になろうにある以下の小説をピックアップしました。
「その者。のちに・・・」
PDFダウンロード元: https://pdfnovels.net/n9442cw/
ダウンロードしたファイルはCドライブのルートに保存しておきます。
ちょうどこの文章を作成しているときに読んでいたものです。
小説家になろうで典型的な俺TUEEEファンタジーハーレム物です。珍しく異世界転生属性はなし。
ある意味展開がありふれているので安心して?読めました。
順番が逆ですが、form1.cs内に後で作るPDF読み込みクラスを呼び出す処理を追加。
■Form1.csusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Pdf2Pdf { public partial class Form1 : Form { public Form1() { InitializeComponent(); //以下を追加 PDFTextReader pr = new PDFTextReader(); bool ret = pr.Read(@"c:\N9442CW.pdf"); } } }
そしてPDFにアクセスするクラスを作成します。
「プロジェクト」メニューかあら「クラスの追加」を選択し、「PDFTextReader.cs」というクラスを追加、以下のようにすれば終わりです。
難しい処理は特になく、ごりごり読むだけです。
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Pdf2Pdf { class PDFTextReader { void LogOut(string strText) { Debug.WriteLine(strText); } /// <summary> /// PDFヘッダー /// </summary> public string strPDFHeader { get { return _strPDFHeader; } } string _strPDFHeader = ""; public bool Read(string strFile) { using (FileStream strm = new FileStream(strFile, FileMode.Open, FileAccess.Read)) { return Read(strm); } } public bool Read(Stream strm) { using (PDFRawReader br = new PDFRawReader(strm)) { long nAfterHeaderPos; //PDFヘッダーチェック { _strPDFHeader = br.ReadLine(false); LogOut(_strPDFHeader); if (_strPDFHeader.Substring(0, 7) != "%PDF-1.") return false; nAfterHeaderPos = br.BaseStream.Position; //PDFヘッダー直後の位置を保存 } //PDFフッターチェック { strm.Seek(0, SeekOrigin.End); string strHooter = br.ReadBackLine(false); if (strHooter != "%%EOF") return false; } //クロスリファレンス取得・・・はしない。startxref存在チェックだけ { //クロスリファレンス位置取得 string strXrefPos = br.ReadBackLine(true); string strXrefHeader = br.ReadBackLine(true); if (strXrefHeader != "startxref") return false; long nXrefPos = long.Parse(strXrefPos); //クロスリファレンスまでシーク strm.Seek(nXrefPos, SeekOrigin.Begin); string strXrefStart = br.ReadLine(true); if (strXrefStart == "xref") { //通常のクロスリファレンス } else if (strXrefStart.IndexOf(" 0 obj") > 0) { //オブジェクトに入ったクロスリファレンス byte[] data; string strAttribute; bool ret = ReadObject(br, strXrefStart, out strAttribute, out data); } else { //未知のクロスリファレンス形式 return false; } } //PDFヘッダー直後にシークしておく br.BaseStream.Seek(nAfterHeaderPos, SeekOrigin.Begin); while (true) { LogOut(""); if (strm.Position == strm.Length) break; //obj開始 string strObj; { strObj = br.ReadLine(true); LogOut(strObj); //「endobj」に続いて、次のobjの前にスペースだけの行があるpdfもある→xrefを参照せずに読もうとするのが悪い。本当はxref参照しないと駄目 if (strObj.IndexOf("obj") < 0) continue; //クロスリファレンス if (strObj == "xref") { //セクションのあるxrefに未対応。この方法だとxref読み込み失敗することある int nCount = 0; string strSubsection = br.ReadLine(true); { Regex re = new Regex(@"\d+ (\d+)", RegexOptions.IgnoreCase); Match m = re.Match(strSubsection); if (m.Success) nCount = int.Parse(m.Groups[1].Value); } for (int i = 0; i < nCount; i++) { string strXref = br.ReadLine(true); } continue; } //トレーラー if (strObj == "trailer") { //トレーラーの情報は読み取らない //トレーラーはPDF末尾に配置されるから、これで読み込み終わり break; } } //オブジェクトの読み込み { byte[] data; string strAttribute; bool ret = ReadObject(br, strObj, out strAttribute, out data); if (ret && data != null && data.Length > 0) { //オブジェクトの処理 } if (ret == false) { Debug.Assert(false); } } } } return true; } /// <summary> /// PDFのオブジェクト情報読み取り /// "123 0 obj" ~ "endobj" まで読み取る /// /// strObjectHeaderは "123 0 obj" というようなオブジェクト開始文字列 /// stream/endstream内の情報は解凍してpcbStreamで返す /// 「<< >>」で囲まれていた属性部分のテキストはstrAttributeですべて返す /// </summary> bool ReadObject(PDFRawReader br, string strObjectHeader, out string strAttribute, out byte[] pcbStream) { string strLine = strObjectHeader; pcbStream = null; strAttribute = ""; LogOut(strLine); if (strLine.IndexOf(" 0 obj") < 0) return false; long nStartPosition = br.BaseStream.Position; //オブジェクトの属性読み取り int nLength = 0; bool bFlateDecode = false; int nTagCount = 0; while (true) { strLine = br.ReadLine(true); LogOut(strLine); int nFind = strLine.IndexOf("/Length "); if (nFind >= 0) { Regex re = new Regex(@"/Length (\d+)", RegexOptions.IgnoreCase); Match m = re.Match(strLine); if (m.Success) nLength = int.Parse(m.Groups[1].Value); } nFind = strLine.IndexOf("/FlateDecode"); if (nFind >= 0) bFlateDecode = true; //属性情報の始点(<<)をカウント nFind = 0; while (nFind >= 0) { nFind = strLine.IndexOf("<<", nFind); if (nFind >= 0) { nTagCount++; nFind += 2; } } //属性情報の終点(>>)をカウント nFind = 0; while (true) { nFind = strLine.IndexOf(">>", nFind); if (nFind < 0) break; nTagCount--; nFind += 2; //属性情報がまだあるか if (nTagCount > 0) continue; //「>>」につづいて改行を挟まずに「stream」が開始することあるから、その場合はseek if (nFind < strLine.Length) br.BaseStream.Seek(-(strLine.Length - nFind + 2), SeekOrigin.Current); //属性情報が終わった break; } //属性情報が終わったかどうか if (nTagCount == 0) break; } // << >>で囲まれていた部分をすべて抜き出す { long nEndPosition = br.BaseStream.Position; br.BaseStream.Seek(nStartPosition, SeekOrigin.Begin); byte[] data = br.ReadBytes((int)(nEndPosition - nStartPosition)); strAttribute = Encoding.ASCII.GetString(data); } //コンテンツがあるなら読み込む if (nLength > 0) { string strStart = br.ReadLine(true); if (strStart != "stream") { Debug.Assert(false); return false; } if (bFlateDecode) { //FlateDecodeヘッダー2バイト br.ReadByte(); br.ReadByte(); nLength -= 2; //FlateDecodeのコンテンツをすべて読み取る byte[] data = br.ReadBytes(nLength); if (data.Length != nLength) return false; //解凍 pcbStream = DecodeDeflate(data); } else { //streamコンテンツ読み込み pcbStream = br.ReadBytes(nLength); } string strEnd; strEnd = br.ReadLine(false); //streamデータ末尾の改行分を読む(改行コードは1or2bytes) if (strEnd == "endstream") { //ここに入ることは普通はないはず。入ったらstream末尾に改行コードなくendstreamしていたpdf } else { //endstreamの読み込み strEnd = br.ReadLine(true); if (strEnd != "endstream") { Debug.Assert(false); return false; } } } { string strObjEnd = br.ReadLine(true); if (strObjEnd != "endobj") { Debug.Assert(false); return false; } } return true; } /// <summary> /// Deflateをデコードする /// </summary> byte[] DecodeDeflate(byte[] data) { byte[] ret = new byte[0]; byte[] buffer = new byte[1024]; using (MemoryStream ms = new MemoryStream(data)) using (DeflateStream ds = new DeflateStream(ms, CompressionMode.Decompress)) { while (true) { int nRead = ds.Read(buffer, 0, buffer.Length); if (nRead <= 0) break; int nSize = ret.Length; Array.Resize(ref ret, nSize + nRead); Buffer.BlockCopy(buffer, 0, ret, nSize, nRead); } } return ret; } } }
実行するとVisual Studioの出力ウインドウに以下のような形式でPDFの内容が出力されます。
%PDF-1.3 3 0 obj 3 0 obj <</Type /Page /Parent 1 0 R /Resources 2 0 R /Contents 4 0 R>> 4 0 obj 4 0 obj <</Filter /FlateDecode /Length 236>> 5 0 obj 5 0 obj <</Type /Page /Parent 1 0 R /Resources 2 0 R /Contents 6 0 R>>