第03回 PDFファイルを読み込む

今回は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.cs
using 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」というクラスを追加、以下のようにすれば終わりです。
難しい処理は特になく、ごりごり読むだけです。

■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>>

プロジェクトファイルをダウンロード


カテゴリー「PDFを処理する(C#)」 のエントリー