« 2016年03月 | メイン | 2016年07月 »

1  2  3

2016年04月 記事一覧

2016年04月03日

第01回 PDFファイルフォーマットの概要

PDFのファイル仕様はおおまかに以下の形式になっています。

%PDF1.xxx				//ファイルヘッダー
xxx 0 obj				//1つ目のオブジェクト開始
<<xxx>>					//1つ目のオブジェクトの属性値
endobj					//1つ目のオブジェクト終了
xxx 0 obj				//2つ目のオブジェクト開始
<<xxx /Length xxx>>		//2つ目のオブジェクトの属性値
stream					//2つ目のオブジェクトのstreamの開始
xxx						//2つ目のオブジェクトのstream内容(Lengthのバイト数分)
endstream				//2つ目のオブジェクトのstreamの終了
endobj					//2つ目のオブジェクト終了
.
.
.
xref					//クロスリファレンスの開始
0 xxx					//クロスリファレンスの数
0000000000 65535 f
xxxxxxxxxx 00000 n		//1つ目のオブジェクトの位置情報
xxxxxxxxxx 00000 n		//2つ目のオブジェクトの位置情報
xxxxxxxxxx 00000 n		//3つ目のオブジェクトの位置情報
xxxxxxxxxx 00000 n		//4つ目のオブジェクトの位置情報
trailer					//トレーラーの開始
<<xxx /Root xxx 0 R /Size xxx>>		//トレーラーの属性値
startxref
xxx						//クロスリファレンスの位置情報
%%EOF					//ファイルフッター


以下はおおまかな注意事項

・各情報の区切りには改行コード(「\r」、「\n」、「\r\n」の3種類どれか)が入る
・stream末尾(「endstream」の1~2バイト前)にも改行コードが入る
・オブジェクト情報がstreamとして格納されることもある
・stream情報は圧縮されることもある
・クロスリファレンス情報はstreamとしてどこかのオブジェクト内にバイナリ形式で格納されることもある(3.4.7 Cross-Reference Streams)
・クロスリファレンス情報はセクション分割されて表記されることもある(3.4.3 Cross-Reference Table example3.6)
・オブジェクト属性値の空白は省略や改行されることもある(例:「<</Type/Page>>」「<<\r/Type\r/Page\r>>」「<</Type /Page>>」)
・「%」から始まる行はコメント行(PDFヘッダー/フッターを除く)

・文字は「xyz」(そのまま書く)、「<78797A>」(16進数で文字コードを書く)、「\120\121\122」(10進数で文字コードを書く)のように複数の表現がある
・文字中の「(」「)」「\」はエスケープして「\(」「\)」「\\」のようにされるが、カッコはエスケープされないこともある
・文字中の改行コードの1バイト前に「\」がある場合は改行コードは無視する


簡単に要約すると「PDFの表記方法は複数あるため、PDFを読むのは大変」。
汎用的なPDF操作処理を行いたい場合は無理せずにAdobeのSDKやPDFのライブラリを使うのが無難です。


PDFの仕様書
Adobe Portable Document Format Version 1.7
http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/pdf_reference_1-7.pdf

第02回 ファイルから行単位で読み込む

PDFファイルはバイナリデータも含みますが、基本的にはテキストファイルです。
改行コードが\r、\n、\r\nの3種類あり得るため、実際にPDFファイルへアクセスする前に改行コードを処理できるファイル読み込みクラスを作成します。

今回はVisual Studio 2012を利用しました。
「ファイルメニュー」の「新規作成」にある「プロジェクトの作成」を選択し、新しいプロジェクトを作成します。

テンプレートは「Visual C#」の「Windows」にある「Windowsフォームアプリケーション」を採用し、プロジェクト名は「Pdf2Pdf」としました。

次に「プロジェクト」メニューかあら「クラスの追加」を選択し、「PDFRawReader.cs」というクラスを追加し、中身を以下のようにして終わり。
今回はクラスを作っただけなので動作は一切ありません。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Pdf2Pdf
{
	class PDFRawReader : BinaryReader
	{
		public PDFRawReader(Stream strm)
			: base(strm)
		{
		}

		public char cbCommentChar
		{
			get { return _cbCommentChar; }
			set { _cbCommentChar = value; }
		}
		char _cbCommentChar = '%';


		/// <summary>
		/// 1行ずつ読み込む
		/// bSkipCommentLine == trueならコメント行(%から始まる行)は読み取らない
		/// </summary>
		public string ReadLine(bool bSkipCommentLine)
		{
			return ReadLine(Encoding.ASCII, bSkipCommentLine);
		}


		/// <summary>
		/// 1行ずつ読み込む
		/// bSkipCommentLine == trueならコメント行(%から始まる行)は読み取らない
		/// </summary>
		public string ReadLine(Encoding enc, bool bSkipCommentLine)
		{
			do
			{
				byte tmp;
				byte[] data = new byte[0];
				int n = 0;

				try
				{
					while (true)
					{
						//改行コードチェック(\r、\r\n、\nの3通りあり得る)
						tmp = ReadByte();
						if (tmp == 0x0d)	//\rなら、\r\nか\rかをチェック
						{
							tmp = ReadByte();
							if (tmp == 0x0a)		//\r\nだった
								break;

							//\r\nではなく、\rだったので一文字戻してから抜ける
							BaseStream.Seek(-1, SeekOrigin.Current);
							break;
						}
						if (tmp == 0x0a)
							break;

						Array.Resize(ref data, data.Length + 1);
						data[n] = tmp;
						n++;
					}
				}
				catch (Exception)
				{
				}

				string ret = enc.GetString(data);

				if (bSkipCommentLine == false || string.IsNullOrEmpty(ret) || ret[0] != _cbCommentChar)
					return ret;
			}
			while (true);
		}


		/// <summary>
		/// データを参照する(シーク位置は変えない)
		/// </summary>
		byte ReferByte(int nOffset)
		{
			byte ret;
			long current = BaseStream.Position;

			BaseStream.Seek(nOffset, SeekOrigin.Current);
			ret  = ReadByte();

			BaseStream.Seek(current, SeekOrigin.Begin);

			return ret;
		}



		public string ReadBackLine(bool bSkipCommentLine)
		{
			return ReadBackLine(Encoding.ASCII, bSkipCommentLine);
		}

		/// <summary>
		/// 現在のシーク位置に対して前の1行を読み込む
		/// ※どんどんファイル先頭側へシークしていく
		/// 
		/// bSkipCommentLine == trueならコメント行(%から始まる行)は読み取らない
		/// </summary>
		public string ReadBackLine(Encoding enc, bool bSkipCommentLine)
		{
			do
			{
				byte tmp;
				int nBreakLine = 0;
				int nReferPos = 0;

				List<byte> listData = new List<byte>();

				try
				{
					//直前の改行コードは無視する
					{
						nReferPos--;
						tmp = ReferByte(nReferPos);

						//改行コードチェック(\r(0x0d)、\r\n(0x0d0a)、\n(0x0a)の3通りあり得る)
						{
							if (tmp == 0x0a)	//\nなら、\r\nか\nかをチェック
							{
								byte tmp2 = ReferByte(nReferPos - 1);
								if (tmp2 == 0x0d)
								{
									//\r\nだった
									nReferPos--;
								}
								else
								{
									//\r\nではなく、\nだった
								}
							}
							else if (tmp == 0x0d)
							{
								//\rだった
							}
						}
					}


					while (nBreakLine == 0)
					{
						nReferPos--;

						tmp = ReferByte(nReferPos);

						//改行コードチェック(\r(0x0d)、\r\n(0x0d0a)、\n(0x0a)の3通りあり得る)
						{
							if (tmp == 0x0a)	//\nなら、\r\nか\nかをチェック
							{
								byte tmp2 = ReferByte(nReferPos - 1);
								if (tmp2 == 0x0d)
								{
									//\r\nだった
									nBreakLine++;
									continue;
								}
								else
								{
									//\r\nではなく、\nだった
									nBreakLine++;
									continue;
								}
							}
							else if (tmp == 0x0d)
							{
								//\rだった
								nBreakLine++;
								continue;
							}
						}

						listData.Insert(0, tmp);
					}
				}
				catch (Exception)
				{
				}

				BaseStream.Seek(nReferPos + 1, SeekOrigin.Current);

				string ret = enc.GetString(listData.ToArray());

				if (bSkipCommentLine == false || string.IsNullOrEmpty(ret) || ret[0] != _cbCommentChar)
					return ret;

				if (BaseStream.Position == 0)
					return ret;
			}
			while (true);
		}
	}
}

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

第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ダウンロード元: http://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>>

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

第04回 PDFファイルからテキスト情報を抜き出す

今回はstreamの中身を読んで、そこに画面表示するテキストがあったらそれを抜き出す。

PDFでのテキスト描画は以下のような形になっている。
このほかにも何通りかテキスト描画の表記方法があるが、今回はこの形式のみを処理対象とした。
またテキストのエンコーディングもshift-jis決め打ちとして処理している。

BT /F2 20.00 Tf ET					//フォント名とフォントサイズ
BT 320.88 354.34 Td (その者。のちに・・・) Tj ET		//描画位置xyとテキスト


まずは抜き出したテキストを保存するするクラスを作成します。
「プロジェクト」メニューかあら「クラスの追加」を選択し、「PDFPage.cs」というクラスを追加し、以下のようにします。
テキスト情報(フォント名、フォントサイズ、表示座標、テキスト)をリスト化して"ページ"として保持するだけのクラスです。

次にPDFTextReaderにstreamが来たらそれを読んで、情報を抜き出す処理を追加します。
本来であればオブジェクトを1つずつ読み解いてアクセスするべきものですが、何も気にせずに読み込んでいます。

実行すると、List Pages に情報が蓄積されます。
かなりいい加減ですがこれでPDFからの情報読み込み処理は完了です。

■PDFPage.cs
using System.Collections.Generic;

namespace Pdf2Pdf
{
	class PDFPosition
	{
		public float fX
		{
			get { return _fX; }
			set { _fX = value; }
		}
		float _fX = 0;

		public float fY
		{
			get { return _fY; }
			set { _fY = value; }
		}
		float _fY = 0;

		public PDFPosition()
		{
		}

		public PDFPosition(float x, float y)
		{
			_fX = x;
			_fY = y;
		}
	}


	class PDFText : PDFPosition
	{
		public string strText
		{
			get { return _strText; }
			set { _strText = value; }
		}
		string _strText = "";

		public string strFont
		{
			get { return _strFont; }
			set { _strFont = value; }
		}
		string _strFont = "";

		public float fPoint
		{
			get { return _fPoint; }
			set { _fPoint = value; }
		}
		float _fPoint = 0;


		public PDFText()
		{
		}

		public PDFText(float x, float y, string strText)
			: base(x, y)
		{
			_strText = strText;
		}

		public PDFText(string strFont, float fPoint, float x, float y, string strText)
			: base(x, y)
		{
			_strFont = strFont;
			_fPoint = fPoint;
			_strText = strText;
		}

	}


	class PDFPage
	{
		public List<object> Items
		{
			get { return _Items; }
			set { _Items = value; }
		}
		List<object> _Items = new List<object>();


		public PDFPage()
		{
		}
	}
}
■PDFTextReader.cs
						//トレーラー
						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)
						{
							//オブジェクトの処理

							PDFPage page = new PDFPage();

							//コンテンツの中身はテキストだと決めてかかってる。本当は駄目。
							//フォントが埋め込まれてたらそのフォントのバイナリに対してまでこれが呼ばれてしまう
							AnalyzeContents(data, page);

							//情報があったら、"ページ"として追加する
							if (page.Items.Count > 0)
								_listPages.Add(page);

						}
						if (ret == false)
						{
							Debug.Assert(false);
						}
					}


				}
			}
			return true;
		}



		public List<PDFPage> Pages
		{
			get { return _listPages; }
		}
		List<PDFPage> _listPages = new List<PDFPage>();





		/// <summary>
		/// テキスト情報の取り出し
		/// 
		/// 取り出したテキスト情報はPDFPageへ格納する
		///
		/// shift-jisのみ対応
		/// ( ) で囲まれたテキストのみを処理し、¥表記や、<>表記のテキストには対応しない
		/// </summary>
		void AnalyzeContents(byte[] pcbContents, PDFPage page)
		{
			byte[] data;

			//エスケープシーケンスを外す
			//「文章」以外の部分も処理することになるけど気にしない
			using (MemoryStream ms1 = new MemoryStream(pcbContents))
			using (BinaryReader br = new BinaryReader(ms1))
			using (MemoryStream ms2 = new MemoryStream())
			{
				while (ms1.Position != ms1.Length)
				{
					byte tmp = br.ReadByte();
					if (tmp != 0x5c)		//「\\」
					{
						ms2.WriteByte(tmp);
						continue;
					}
					tmp = br.ReadByte();
					ms2.WriteByte(tmp);
				}
				ms2.Flush();

				data = ms2.ToArray();
			}

			using (MemoryStream ms = new MemoryStream(data))
			using (PDFRawReader pr = new PDFRawReader(ms))
			{
				//"小説家になろう"の縦書きPDFはshift-jis
				Encoding enc = Encoding.GetEncoding("shift_jis");

				string strFontObj = "";
				float fFontPoint = 0;

				while (true)
				{
					if (ms.Position == ms.Length)
						break;

					string strLine = pr.ReadLine(enc, false);
					LogOut(strLine);

					//フォントobject名とフォントサイズの取得
					{
						Regex re = new Regex(@"BT /(.+) ([\d\.]+) Tf", RegexOptions.IgnoreCase);
						Match m = re.Match(strLine);
						while (m.Success)
						{
							try
							{
								strFontObj = m.Groups[1].Value;

								string strSize = m.Groups[2].Value;
								fFontPoint = float.Parse(strSize);
							}
							catch (Exception)
							{
								strFontObj = "";
								fFontPoint = 0;
							}

							m = m.NextMatch();
						}
					}


					//テキストの取得
					{
						//表示座標とテキストだけ抜き出す
						// ( ) で囲まれたテキストのみを処理し、¥表記や、<>表記のテキストには対応しない
						Regex re = new Regex(@"BT ([\d\.]+) ([\d\.]+) Td \((.+)\) Tj ET", RegexOptions.IgnoreCase);
						Match m = re.Match(strLine);
						while (m.Success)
						{
							try
							{
								string strX = m.Groups[1].Value;
								string strY = m.Groups[2].Value;
								string strText = m.Groups[3].Value;

								float x = float.Parse(strX);
								float y = float.Parse(strY);


								PDFText text = new PDFText(strFontObj, fFontPoint, x, y, strText);
								page.Items.Add(text);
							}
							catch (Exception)
							{
							}

							m = m.NextMatch();
						}
					}

				}
			}
		}

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

第05回 PDFファイルの内容を表示する

今回の処理は作ろうとしているツールとは無関係ですが、
前回までに抜き出したPDFファイル内のテキスト情報を画面表示します。

フォーム上にPictureBoxを配置して、そこにGraphicsとして描く形で表示しました。
マウスクリックでページを進む/戻るが可能です。

■Form1.cs
using System.Drawing;
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");


			PictureBox pictureBox1 = new PictureBox();
			pictureBox1.Parent = this;
			pictureBox1.Dock = DockStyle.Fill;


			int nPage = 0;

			Bitmap bmp = new Bitmap(Width, Height);
			DrawPage(pr, nPage, bmp);
			pictureBox1.Image = bmp;


			pictureBox1.MouseUp += (sender, e) =>
			{
				if (e.Button == MouseButtons.Left)
					nPage++;
				else if (e.Button == MouseButtons.Right)
					nPage--;
				else
					return;

				bmp = new Bitmap(Width, Height);

				while (true)
				{
					ret = DrawPage(pr, nPage, bmp);
					if (ret)
						break;

					nPage++;
					if (nPage >= pr.Pages.Count)
						break;
				}

				pictureBox1.Image.Dispose();
				pictureBox1.Image = bmp;
			};
		}

		//横書きに変換表示
		bool DrawPage(PDFTextReader pr, int nPage, Bitmap bmp)
		{
			if (nPage < 0 || nPage >= pr.Pages.Count)
				return false;

			PDFPage page = pr.Pages[nPage];

			using (Graphics g = Graphics.FromImage(bmp))
			using (Font font = new Font("MS ゴシック", 10))
			{
				foreach (object obj in page.Items)
				{
					if (obj.GetType() != typeof(PDFText))
						continue;

					//(0,0)はページ左下
					//A4縦は(595,842)がページ右上
					//A4横は(842,595)がページ右上

					float x = (obj as PDFText).fX;
					float y = (obj as PDFText).fY;

					//ページ番号は描画しない
					//ページ番号のy座標は56.7?
					if ((int)(y) == 56)
						continue;


					x = 842 - x;
					y = 595 - y;
					g.DrawString((obj as PDFText).strText, font, System.Drawing.Brushes.Black, y, x);
				}
			}
			return true;
		}

	}
}

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


第06回 簡単なPDFファイルを作成する


今回はPDFを作成します。
PDFは1ページのみで、内容は半角英数字のテキストのみ、フォントの埋め込みもなしです。

構成内容がこれだけの簡単なPDFです。

・PDFヘッダー
・フォントオブジェクト
・カタログオブジェクト(/Type /Catalog)
・ページインデックスオブジェクト(/Type /Pages)
・ページコンテンツインデックスオブジェクト(/Type /Page)
・ページコンテンツオブジェクト(stream情報)
・クロスリファレンス
・トレーラー
・PDFヘッダー

各所でオブジェクトのインデックスが参照されるので、nObjIndexとして管理。
クロスリファレンス作成用に_listnXrefを用意して、各オブジェクトを書き込む前にそのseekポジションを保存しました。
コンテンツ内容のstreamは圧縮する必要はないのですが、今後のことも考えてDeflateStreamで圧縮しています。

---
まずは別に作らなくていいのですが、PDFRawReaderを作ってしまったので、それに合わせて、PDFRawWriterクラスを作ります。
「プロジェクト」メニューから「クラスの追加」を選択し、「PDFRawWriter.cs」というクラスを追加。

同様にPDFTextReaderに合わせて、PDFTextWriterクラスを作ります。
「プロジェクト」メニューから「クラスの追加」を選択し、「PDFTextWriter.cs」というクラスを追加。

最後にForm1.csからPDFTextWriter.CreatePDFFile()を呼び出すようにすれば完成です。

■PDFRawWriter.cs
using System.IO;
using System.Text;

namespace Pdf2Pdf
{
	class PDFRawWriter : BinaryWriter
	{
		public PDFRawWriter(Stream strm)
			: base(strm)
		{
		}


		/// <summary>
		/// asciiエンコーディングで書き込む!
		/// </summary>
		public void WriteLine(string strText)
		{
			byte[] data = Encoding.ASCII.GetBytes(strText);
			Write(data);
			Write0x0a();
		}


		public void Write0x0a()
		{
			Write('\r');
		}
	}
}

■PDFTextWrite.cs
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Text;

namespace Pdf2Pdf
{
	class PDFTextWriter
	{
		enum FONT_TYPE
		{
			ASCII,
		}



		//クロスリファレンス生成用データ(各オブジェクトの位置を保存)
		List<long> _listnXref = new List<long>();

		//フォントの種類保存用
		IDictionary<string, FONT_TYPE> _mapFontType = new Dictionary<string, FONT_TYPE>();



		public void Close()
		{
			_listnXref.Clear();
			_mapFontType.Clear();
		}


		public bool CreatePDFFile(string strFile)
		{
			Close();

			int nObjIndex = 1;

			using (FileStream fs = new FileStream(strFile, FileMode.Create, FileAccess.Write))
			using (PDFRawWriter bw = new PDFRawWriter(fs))
			{
				//ヘッダー出力
				bw.WriteLine("%PDF-1.7");

				//フォント出力
				int nResourceIndex;
				{
					nResourceIndex = nObjIndex;
					WriteFont_Ascii(bw, ref nObjIndex, "Times-Italic", "F0");
				}

				//カタログ出力
				int nRoot = nObjIndex;
				{
					WriteCatalog(bw, ref nObjIndex, nObjIndex + 1);
				}

				//ページ出力
				{
					List<int> listPage = new List<int>();

					//全ページのインデックスを出力
					int nPagesReferenceIndex = nObjIndex;
					{
						listPage.Add(nObjIndex + 1);				//1ページ目のインデックスを渡す
						WritePages(bw, ref nObjIndex, listPage);
					}


					//1ページ出力
					{
						WritePageContentsIndex(bw, ref nObjIndex, nPagesReferenceIndex, nResourceIndex, nObjIndex + 1, 595, 842);

						//ページテキスト出力
						{
							List<PDFText> listTexts = new List<PDFText>();

							listTexts.Add(new PDFText("F0", 40, 50, 540, @"abc123"));


							//コンテンツの書き出し
							using (MemoryStream ms = new MemoryStream())
							using (BinaryWriter bwms = new BinaryWriter(ms))
							{
								//文字データをPDF出力用に準備
								PrepareTextContents(ms, listTexts);

								ms.Flush();

								byte[] data = ms.ToArray();

								//ページコンテンツの出力
								WriteFlateData(bw, ref nObjIndex, data);
							}
						}
					}
				}

				//クロスリファレンス/トレーラー出力
				WriteXrefTrailer(bw, nRoot);
			}

			return true;
		}



		/// <summary>
		/// カタログの書き込み
		/// 
		/// nPagesObjIndex は、WritePages()のnObjIndexを指定
		/// </summary>
		void WriteCatalog(PDFRawWriter bw, ref int nObjIndex, int nPagesObjIndex)
		{
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Type /Catalog");
				bw.WriteLine("/Pages " + nPagesObjIndex + " 0 R");
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;
		}


		/// <summary>
		/// ページ情報の書き込み
		/// 
		/// listnKidsObjIndex は、WritePageContentsIndex()へのインデックス。すべてのページ分を用意する
		/// </summary>
		void WritePages(PDFRawWriter bw, ref int nObjIndex, List<int> listnKidsObjIndex)
		{
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Type /Pages");

				string strKidLine = "/Kids [";
				foreach (int nKidObj in listnKidsObjIndex)
				{
					strKidLine += "" + nKidObj + " 0 R ";
				}
				strKidLine += "]";
				bw.WriteLine(strKidLine);

				bw.WriteLine("/Count " + listnKidsObjIndex.Count);
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;
		}



		/// <summary>
		/// 欧文フォントの指定
		/// 
		/// デフォルトで用意されているフォントを利用する場合はこれで指定、
		/// 使えるフォント名(strFont)は↓
		/// Times-Roman
		/// Helvetica
		/// Courier
		/// Symbol
		/// Times-Bold
		/// Helvetica-Bold
		/// Courier-Bold
		/// ZapfDingbats
		/// Times-Italic
		/// Helvetica-Oblique
		/// Courier-Oblique
		/// Times-BoldItalic
		/// Helvetica-BoldOblique
		/// Courier-BoldOblique
		/// </summary>
		void WriteFont_Ascii(PDFRawWriter bw, ref int nObjIndex, string strFont, string strFontObjName)
		{
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Font");
				bw.WriteLine("<</" + strFontObjName);
					bw.WriteLine("<<");
						bw.WriteLine("/Type /Font");
						bw.WriteLine("/BaseFont /" + strFont);
						bw.WriteLine("/Subtype /Type1");
					bw.WriteLine(">>");
				bw.WriteLine(">>");
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;

			//フォント情報の保存
			_mapFontType.Add(strFontObjName, FONT_TYPE.ASCII);
		}



		void WritePageContentsIndex(PDFRawWriter bw, ref int nObjIndex, int nParentObjIndex, int nResourcesObjIndex, int nContentsObjIndex, float fWidth, float fHeight)
		{
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Type /Page");
				bw.WriteLine("/Parent " + nParentObjIndex + " 0 R");
				bw.WriteLine("/Resources " + nResourcesObjIndex + " 0 R");
				bw.WriteLine("/MediaBox [0 0 " + fWidth + " " + fHeight + "]");
				bw.WriteLine("/Contents " + nContentsObjIndex + " 0 R");
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;
		}







		/// <summary>
		/// 与えられたテキストデータをストリームに以下の形式で書き込む
		/// "BT /F0 10 Tf 150 200 Td (hello) Tj ET\r";
		/// </summary>
		void PrepareTextContents(Stream strm, List<PDFText> listTexts)
		{
			using (MemoryStream ms = new MemoryStream())
			using (BinaryWriter bwms = new BinaryWriter(ms))
			{
				foreach (PDFText text in listTexts)
				{
					bool ret;
					FONT_TYPE type;

					ret = _mapFontType.TryGetValue(text.strFont, out type);
					if (ret == false)
					{
						//フォントの種類特定失敗
						Debug.Assert(false);
						continue;
					}


					string strText = text.strText;

					byte[] raw2 = null;

					if (type == FONT_TYPE.ASCII)
					{
						//カッコと¥をエスケープ
						strText = strText.Replace(@"\", @"\\");
						strText = strText.Replace(@"(", @"\(");
						strText = strText.Replace(@")", @"\)");

						raw2 = Encoding.ASCII.GetBytes(strText);
					}
					else
					{
						Debug.Assert(false);
					}

					string strData = "BT /" + text.strFont + " " + text.fPoint + " Tf " + text.fX + " " + text.fY + " Td (";
					byte[] raw1 = Encoding.ASCII.GetBytes(strData);
					byte[] raw3 = Encoding.ASCII.GetBytes(") Tj ET");

					bwms.Write(raw1);
					bwms.Write(raw2);
					bwms.Write(raw3);
					bwms.Write('\r');
				}
				ms.Flush();

				byte[] data = ms.ToArray();
				strm.Write(data, 0, data.Length);
			}
		}


		/// <summary>
		/// pcbRawData をCompressDeflate()にかけてstreamとして書き込む
		/// (pcbRawDataは圧縮せずに生データを渡すこと!)
		/// </summary>
		void WriteFlateData(PDFRawWriter bw, ref int nObjIndex, byte[] pcbRawData)
		{
			_listnXref.Add(bw.BaseStream.Position);
			{
				int nLength;

				byte[] data = CompressDeflate(pcbRawData);
				nLength = data.Length + 2;

				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Filter /FlateDecode /Length " + nLength + ">>");
				bw.WriteLine("stream");
				{
					byte[] header = new byte[2];
					header[0] = 0x78;
					header[1] = 0x9c;
					bw.Write(header);
				}
				bw.Write(data);
				bw.Write0x0a();
				bw.WriteLine("endstream");
				bw.WriteLine("endobj");
			}
			nObjIndex++;
		}



		/// <summary>
		/// クロスリファレンス/トレーラー/PDFフッターの書き出し
		/// </summary>
		void WriteXrefTrailer(PDFRawWriter bw, int nRootObjIndex)
		{
			//Trailerは1行のバイト数が重要

			long nXrefPos = bw.BaseStream.Position;

			bw.WriteLine("xref");
			bw.WriteLine("0 " + (_listnXref.Count + 1));
			bw.WriteLine("0000000000 65535 f ");

			foreach (long n in _listnXref)
			{
				bw.WriteLine(string.Format("{0:0000000000}", n) + " 00000 n ");
			}

			bw.WriteLine("trailer");
			bw.WriteLine("<<");
			bw.WriteLine("/Root " + nRootObjIndex + " 0 R");
			bw.WriteLine("/Size " + (_listnXref.Count + 1));
			bw.WriteLine(">>");

			bw.WriteLine("startxref");
			bw.WriteLine("" + nXrefPos);
	
			bw.WriteLine("%%EOF");
		}


		/// <summary>
		/// Deflateを圧縮する
		/// </summary>
		byte[] CompressDeflate(byte[] data)
		{
			byte[] ret = new byte[0];

			using (MemoryStream ms = new MemoryStream())
			{
				DeflateStream ds = new DeflateStream(ms, CompressionMode.Compress);

				ds.Write(data, 0, data.Length);
				ds.Flush();
				ds.Close();

				ret = ms.ToArray();
				ds.Dispose();
			}

			return ret;
		}
	}
}
■Form1.cs
		public Form1()
		{
			InitializeComponent();

			PDFTextReader pr = new PDFTextReader();
			bool ret = pr.Read(@"c:\N9442CW.pdf");

			//以下を追加
			PDFTextWriter pw = new PDFTextWriter();
			pw.CreatePDFFile("test.pdf");

			PictureBox pictureBox1 = new PictureBox();
			pictureBox1.Parent = this;
			pictureBox1.Dock = DockStyle.Fill;

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

第07回 PDFに日本語を書き出す

今回は書き出すPDFのフォントに日本語を指定します。

shift-jis指定での書き込みもできるのですが、shift-jisを使うのも今更なのでユニコード(ビッグエンディアン)にしました。
文字列をユニコードで書き込むので日本語も何も必要ないのでは?
とも思うのですが日本語限定です。

フォント埋め込みは次回以降です。

■PDFTextWriter.cs
	class PDFTextWriter
	{
		enum FONT_TYPE
		{
			ASCII,
			ADOBE_JPN1_6,
		}


		//クロスリファレンス生成用データ(各オブジェクトの位置を保存)
		List<long> _listnXref = new List<long>();

		//フォントの種類保存用
		IDictionary<string, FONT_TYPE> _mapFontType = new Dictionary<string, FONT_TYPE>();


		public void Close()
		{
			_listnXref.Clear();
			_mapFontType.Clear();
		}


		public bool CreatePDFFile(string strFile)
		{
			Close();

			int nObjIndex = 1;

			using (FileStream fs = new FileStream(strFile, FileMode.Create, FileAccess.Write))
			using (PDFRawWriter bw = new PDFRawWriter(fs))
			{
				//ヘッダー出力
				bw.WriteLine("%PDF-1.7");

				//フォント出力
				int nResourceIndex;
				{
					nResourceIndex = nObjIndex;

					//WriteFont_Ascii(bw, ref nObjIndex, "Times-Italic", "F0");						//欧文フォント指定
					WriteFont_UnicodeJapanese(bw, ref nObjIndex, "KozMinPr6N-Regular", "F0");		//日本語フォント指定(フォント埋め込みなし)
				}

				//カタログ出力
				int nRoot = nObjIndex;
				{
					WriteCatalog(bw, ref nObjIndex, nObjIndex + 1);
				}

				//ページ出力
				{
					List<int> listPage = new List<int>();

					//全ページのインデックスを出力
					int nPagesReferenceIndex = nObjIndex;
					{
						listPage.Add(nObjIndex + 1);				//1ページ目のインデックスを渡す
						WritePages(bw, ref nObjIndex, listPage);
					}


					//1ページ出力
					{
						WritePageContentsIndex(bw, ref nObjIndex, nPagesReferenceIndex, nResourceIndex, nObjIndex + 1, 595, 842);

						//ページテキスト出力
						{
							List<PDFText> listTexts = new List<PDFText>();

							listTexts.Add(new PDFText("F0", 40, 50, 540, @"abcあいうえお漢字123"));			//変更


							//コンテンツの書き出し
							using (MemoryStream ms = new MemoryStream())
							using (BinaryWriter bwms = new BinaryWriter(ms))
							{
								//文字データをPDF出力用に準備
								PrepareTextContents(ms, listTexts);

								ms.Flush();

								byte[] data = ms.ToArray();

								//ページコンテンツの出力
								WriteFlateData(bw, ref nObjIndex, data);
							}
						}
					}
				}

				//クロスリファレンス/トレーラー出力
				WriteXrefTrailer(bw, nRoot);
			}

			return true;
		}
		/// <summary>
		/// 日本語フォントの指定(フォント埋め込みなし)
		/// 
		/// フォント名は↓
		/// https://www.adobe.com/jp/support/type/aj1-6.html
		/// 小塚明朝 Std Rは「KozMinStd-Regular」
		/// 小塚明朝 Pro Rは「KozMinPro-Regular」
		/// 小塚明朝 Pr6N Rは「KozMinPr6N-Regular」
		/// りょう Text PlusN Rは「RyoTextPlusN-Regular」
		/// </summary>
		void WriteFont_UnicodeJapanese(PDFRawWriter bw, ref int nObjIndex, string strFont, string strFontObjName)
		{
			_listnXref.Add(bw.BaseStream.Position);

			bw.WriteLine("" + nObjIndex + " 0 obj");
			bw.WriteLine("<</Font");
			bw.WriteLine("<</" + strFontObjName);
			bw.WriteLine("<<");
			bw.WriteLine("/Type /Font");
			bw.WriteLine("/BaseFont /" + strFont);
			bw.WriteLine("/Subtype /Type0");
			bw.WriteLine("/Encoding /UniJIS-UTF16-H");			//ユニコード(big endian)
			bw.WriteLine("/DescendantFonts [" + (nObjIndex + 1) + " 0 R]");
			bw.WriteLine(">>");
			bw.WriteLine(">>");
			bw.WriteLine(">>");
			bw.WriteLine("endobj");

			nObjIndex++;

			_listnXref.Add(bw.BaseStream.Position);

			bw.WriteLine("" + nObjIndex + " 0 obj");
			bw.WriteLine("<</Type /Font");
			bw.WriteLine("/Subtype /CIDFontType0");
			bw.WriteLine("/BaseFont /" + strFont);
			bw.WriteLine("/CIDSystemInfo <<");
			bw.WriteLine("/Registry (Adobe)");
			//bw.WriteLine("/Ordering (UCS)");
			bw.WriteLine("/Ordering (Japan1)");
			bw.WriteLine("/Supplement 6");
			bw.WriteLine(">>");
			bw.WriteLine("/FontDescriptor " + (nObjIndex + 1) + " 0 R");
			bw.WriteLine(">>");
			bw.WriteLine("endobj");

			nObjIndex++;

			_listnXref.Add(bw.BaseStream.Position);

			bw.WriteLine("" + nObjIndex + " 0 obj");
			bw.WriteLine("<<");
			bw.WriteLine("/Type /FontDescriptor");
			bw.WriteLine("/FontName /" + strFont);
			bw.WriteLine("/Flags 4");
			//bw.WriteLine("/FontBBox [-437 -340 1147 1317]");
			bw.WriteLine("/FontBBox [0 0 0 0]");			//↓この辺どう取得したらいいのか不明
			bw.WriteLine("/ItalicAngle 0");
			bw.WriteLine("/Ascent 1317");
			bw.WriteLine("/Descent -349");
			bw.WriteLine("/CapHeight 742");
			bw.WriteLine("/StemV 80");
			bw.WriteLine(">>");
			bw.WriteLine("endobj");

			nObjIndex++;


			//フォント情報の保存
			_mapFontType.Add(strFontObjName, FONT_TYPE.ADOBE_JPN1_6);
		}


		void WritePageContentsIndex(PDFRawWriter bw, ref int nObjIndex, int nParentObjIndex, int nResourcesObjIndex, int nContentsObjIndex, float fWidth, float fHeight)
		{
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Type /Page");
				bw.WriteLine("/Parent " + nParentObjIndex + " 0 R");
				bw.WriteLine("/Resources " + nResourcesObjIndex + " 0 R");
				bw.WriteLine("/MediaBox [0 0 " + fWidth + " " + fHeight + "]");
				bw.WriteLine("/Contents " + nContentsObjIndex + " 0 R");
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;
		}


		/// <summary>
		/// 与えられたテキストデータをストリームに以下の形式で書き込む
		/// "BT /F0 10 Tf 150 200 Td (hello) Tj ET\r";
		/// </summary>
		void PrepareTextContents(Stream strm, List<PDFText> listTexts)
		{
			using (MemoryStream ms = new MemoryStream())
			using (BinaryWriter bwms = new BinaryWriter(ms))
			{
				foreach (PDFText text in listTexts)
				{
					bool ret;
					FONT_TYPE type;

					ret = _mapFontType.TryGetValue(text.strFont, out type);
					if (ret == false)
					{
						//フォントの種類特定失敗
						Debug.Assert(false);
						continue;
					}


					string strText = text.strText;

					byte[] raw2 = null;

					{
						byte[] tmp = null;

						//フォントに応じてエンコーディング
						if (type == FONT_TYPE.ASCII)
							tmp = Encoding.ASCII.GetBytes(strText);
						if (type == FONT_TYPE.ADOBE_JPN1_6)
							tmp = Encoding.BigEndianUnicode.GetBytes(strText);
						else
						{
							Debug.Assert(false);
						}

						// 「()\」をエスケープする
						using (MemoryStream ms_esc = new MemoryStream())
						{
							foreach (byte cb in tmp)
							{
								if (cb == 0x28		//'('
									|| cb == 0x29	//')'
									|| cb == 0x5c)	//'\'
								{
									ms_esc.WriteByte(0x5c);		//'\'
								}
								ms_esc.WriteByte(cb);
							}
							ms_esc.Flush();

							raw2 = ms_esc.ToArray();
						}
					}

					string strData = "BT /" + text.strFont + " " + text.fPoint + " Tf " + text.fX + " " + text.fY + " Td (";
					byte[] raw1 = Encoding.ASCII.GetBytes(strData);
					byte[] raw3 = Encoding.ASCII.GetBytes(") Tj ET");

					bwms.Write(raw1);
					bwms.Write(raw2);
					bwms.Write(raw3);
					bwms.Write('\r');
				}
				ms.Flush();

				byte[] data = ms.ToArray();
				strm.Write(data, 0, data.Length);
			}
		}

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

第08回 PDF埋め込みフォントの概要

今回はPDFへのフォント埋め込みの概要~というかフォントについてです。

フォントファイルはいわゆるtrue type fontと、open type fontがあります。
true type fontは少し複雑なので、ここではopen type fontのみを扱うことにします。



フォントファイルというのは文字の「形」を収録したファイルです。
フォントファイルの中には複数の文字の「形」が収録されています。
あまりに当たり前な話ですが、これが埋め込みに際して重要になります。


「A」という文字は1番目の位置に収録されている
「B」という文字は2番目の位置に収録されている
「C」という文字は3番目の位置に収録されている
 ・
 ・
 ・
「を」という文字は999番目の位置に収録されている
「ん」という文字は1000番目の位置に収録されている

フォントを埋め込む際には、
このような"文字"とそれが何番目に収録されているかという対応表が必要になり、

埋め込むテキストはユニコードやshift-jisといった一般的な形式でPDF内に出力するのではなく、
この対応表に基づいた変換結果を入れる必要があります。
(例えば「ABC」という文字列を書き込む場合は「0x0001 0x0002 0x0003」という変換結果のバイナリを書き込みます)




実際に「対応表」を見てみることにします。

まずはフォントファイルの準備。
"C:Windows\Fonts\"フォルダーの中にある「MS ゴシック 標準」(ファイル名:msgothic.ttc)を適当なフォルダにコピーします。

このファイルはtrue type fontのため、open type fontに変換します。
オンラインで変換してくれるこのサイトで、output formatを「otf - OpenType font」として変換します。
http://www.fontconverter.org/
そして変換した結果できたopen type fontファイル「msgothic.otf」をダウンロード/保存します。

次に対応表を見るためのツールをダウンロード。
Microsoftが用意しているttfdumpというツールを利用します。
以下にある「fonttools.exe」をダウンロードし、この中にある「TTFDump.zip」を解凍すると「ttfdump.exe」があるので保存します。
https://www.microsoft.com/en-us/typography/tools.aspx
http://download.microsoft.com/download/f/f/a/ffae9ec6-3bf6-488a-843d-b96d552fd815/FontTools.exe

ついでにopen type fontの情報を見るためのツールもダウンロード。
以下にある「lcdf-typetools-w32」をダウンロードし、「otfinfo.exe」を解凍/保存します。
https://www.lcdf.org/type/index.html
ftp://akagi.ms.u-tokyo.ac.jp/pub/TeX/win32/lcdf-typetools-w32.tar.xz

これらの作業で集まった以下のファイルを同じフォルダに入れます。
・msgothic.otf
・ttfdump.exe
・otfinfo.exe

そして以下のようにしたbatファイルを実行。

otfinfo -i msgothic.otf > otf_info.txt
ttfdump.exe msgothic.otf -tcmap -nx  > otf_cmap.txt
pause


するとファイルが出力されます。
otf_info.txt を見ると、フォントの名前情報が。
otf_cmap.txt には変換表があります。
どちらもPDF埋め込みの際に必要になる情報です。

ユニコード以外からもフォントを扱えるように、変換表は複数入っているのが普通です。
この中から、Platform ID == 3、Specific ID == 1のものを抜き出します。

■otf_info.txt
Family:              MS Gothic				//フォントファミリー名
Subfamily:           Regular
Full name:           MS Gothic
PostScript name:     MS-Gothic				//フォント名
Version:             Version 5.01
Unique ID:           Microsoft:MS Gothic:2009
Trademark:           MS Gothic is a registered trademark of the Microsoft Corporation.
Copyright:           (C)2009 data:RICOH Co.,Ltd. typeface:RYOBI IMAGIX CO.
Vendor ID:           RICO
■otf_cmap.txt
		     Char 253 -> Index 451
		     Char 254 -> Index 449
		     Char 255 -> Index 440
  
Subtable  4.   Platform ID:   3				//Platform ID == 3 はWindows、Specific ID == 1 はユニコードを示す
               Specific ID:   1				//つまりこのセクションにあるのが対応表
               'cmap' Offset: 0x0000002C
	      ->Format:	4 : Segment mapping to delta values
		Length:		34880
		Version:	0
		segCount:	4358  (X2 = 8716)
		searchRange:	8192
		entrySelector:	12
		rangeShift:	524
		Seg   1 : St = 0000, En = 0000, D =      1, RO =     0, gId# = N/A
		Seg   2 : St = 000D, En = 000D, D =    -11, RO =     0, gId# = N/A
		Seg   3 : St = 0020, En = 007E, D =    -29, RO =     0, gId# = N/A
		Seg   4 : St = 00A0, En = 017F, D =    -62, RO =     0, gId# = N/A
		Seg   5 : St = 0192, En = 0193, D =    -80, RO =     0, gId# = N/A

		Seg 4353 : St = FDFC, En = FDFC, D =  16088, RO =     0, gId# = N/A
		Seg 4354 : St = FE45, En = FE46, D =  16016, RO =     0, gId# = N/A
		Seg 4355 : St = FF01, En = FF9F, D =  15830, RO =     0, gId# = N/A
		Seg 4356 : St = FFE0, En = FFE6, D =  15766, RO =     0, gId# = N/A
		Seg 4357 : St = FFE8, En = FFEE, D =  15765, RO =     0, gId# = N/A
		Seg 4358 : St = FFFF, En = FFFF, D =      1, RO =     0, gId# = N/A

		Which Means:
		   1. Char 0000 -> Index 1		//対応表開始
		   2. Char 000D -> Index 2		//ユニコードで0x000Dの文字は、2番目にあるという意味
		   3. Char 0020 -> Index 3		//ユニコードで0x0020の文字は、3番目にあるという意味
		      Char 0021 -> Index 4		//ユニコードで0x0021の文字は、4番目にあるという意味
		      Char 0022 -> Index 5
		      Char 0023 -> Index 6
		      Char 0024 -> Index 7
		      Char 0025 -> Index 8
		      Char 0026 -> Index 9
		      Char 0027 -> Index 10
		      Char 0028 -> Index 11
		      Char 0029 -> Index 12
		      Char 002A -> Index 13
		      Char 002B -> Index 14
		      Char 002C -> Index 15
		      Char 002D -> Index 16
		      Char 002E -> Index 17

		4356. Char FFE0 -> Index 15734
		      Char FFE1 -> Index 15735
		      Char FFE2 -> Index 15736
		      Char FFE3 -> Index 15737
		      Char FFE4 -> Index 15738
		      Char FFE5 -> Index 15739
		      Char FFE6 -> Index 15740
		4357. Char FFE8 -> Index 15741
		      Char FFE9 -> Index 15742
		      Char FFEA -> Index 15743
		      Char FFEB -> Index 15744
		      Char FFEC -> Index 15745
		      Char FFED -> Index 15746
		      Char FFEE -> Index 15747		//対応表終了
  
Subtable  5.   Platform ID:   3
               Specific ID:   10
               'cmap' Offset: 0x0000886C
	      ->Format:	12 : Segmented coverage (32 bit)
		Length:		55732
		Version:	0
		Seg   1 : startCharCode = 00000000, endCharCode = 00000000, startGlyphCode = 1
		Seg   2 : startCharCode = 0000000D, endCharCode = 0000000D, startGlyphCode = 2
		Seg   3 : startCharCode = 00000020, endCharCode = 0000007E, startGlyphCode = 3

ここまで情報が揃ったらフォントの埋め込み処理はできたも同然です。

第09回 PDFファイルにフォントを埋め込む

今回は前回取得したopen type fontに関する情報を利用してPDFファイルにフォントを埋め込みます。

まず、前回の方法で変換表だけを抜き出したテキストファイル「cmap_msgothic.txt」を作成します。
Windows7に含まれていたMSゴシックでは1万5747行になりました。

■cmap_msgothic.txt
		   1. Char 0000 -> Index 1
		   2. Char 000D -> Index 2
		   3. Char 0020 -> Index 3
		      Char 0021 -> Index 4
		      Char 0022 -> Index 5
		      Char 0023 -> Index 6
		      Char 0024 -> Index 7
		      Char 0025 -> Index 8
		      Char 0026 -> Index 9
		      Char 0027 -> Index 10
		      Char 0028 -> Index 11
		      Char 0029 -> Index 12
		      Char 002A -> Index 13

		      Char FFE6 -> Index 15740
		4357. Char FFE8 -> Index 15741
		      Char FFE9 -> Index 15742
		      Char FFEA -> Index 15743
		      Char FFEB -> Index 15744
		      Char FFEC -> Index 15745
		      Char FFED -> Index 15746
		      Char FFEE -> Index 15747

作成したcmap_msgothic.txtと、フォントファイルであるmsgothic.otfの2ファイルを、プロジェクトの実行フォルダ(debugフォルダ)へコピーしておきます。


そしてフォント埋め込みのコード作成。

本来であれば、PDF内で使われている文字分だけのフォントを埋め込むべきですが、
それをするのが面倒なため、msgothic.otfを丸ごとオブジェクトとして埋め込みました。
そして表示する文章はcmap_msgothic.txtを参照して、ユニコードからインデックス値に変換して書き出しています。

これでフォントを埋め込んだPDFが作成できました。
PDF内で文字を選択してクリップボードにコピー、そしてメモ帳にペーストしようとすると文字化けします。
これはインデックス値からユニコードへと変換する表を埋め込んでいないためです。これは次回以降へ持ち越し。

また、今は変換表をcmap_msgothic.txtという形で手動で用意しています。
これをフォントファイルから直接読み取る処理も次回以降です。

■PdfTextWriter.cs
using System;									//追加
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Text.RegularExpressions;			//追加

namespace Pdf2Pdf
{
	class PDFTextWriter
	{
		enum FONT_TYPE
		{
			ASCII,
			ADOBE_JPN1_6,
			EMBEDDED
		}



		//クロスリファレンス生成用データ(各オブジェクトの位置を保存)
		List<long> _listnXref = new List<long>();

		//フォントの種類保存用
		IDictionary<string, FONT_TYPE> _mapFontType = new Dictionary<string, FONT_TYPE>();



		public void Close()
		{
			_listnXref.Clear();
			_mapFontType.Clear();
		}


		public bool CreatePDFFile(string strFile)
		{
			Close();

			int nObjIndex = 1;

			using (FileStream fs = new FileStream(strFile, FileMode.Create, FileAccess.Write))
			using (PDFRawWriter bw = new PDFRawWriter(fs))
			{
				//ヘッダー出力
				bw.WriteLine("%PDF-1.7");

				//フォント出力
				int nResourceIndex;
				{
					nResourceIndex = nObjIndex;

					//WriteFont_Ascii(bw, ref nObjIndex, "Times-Italic", "F0");						//欧文フォント指定
					//WriteFont_UnicodeJapanese(bw, ref nObjIndex, "KozMinPr6N-Regular", "F0");		//日本語フォント指定(フォント埋め込みなし)


					//フォント埋め込み
					{
						List<KeyValuePair<string, int>> listFonts = new List<KeyValuePair<string, int>>();

						{
							string strFontObjName = "F0";
							listFonts.Add(new KeyValuePair<string, int>(strFontObjName, nObjIndex));
							WriteFont_EmbeddedUnicode(bw, ref nObjIndex, "F0", "MS-Gothic", "MS Gothic", @"msgothic.otf");
						}

						//埋め込みフォント一覧のみのリソース
						nResourceIndex = nObjIndex;
						_listnXref.Add(bw.BaseStream.Position);
						{
							bw.WriteLine("" + nObjIndex + " 0 obj");
							bw.WriteLine("<</Font");
							bw.WriteLine("<<");
							foreach (KeyValuePair<string, int> pair in listFonts)
							{
								bw.WriteLine("/" + pair.Key + " " + pair.Value + " 0 R");
							}
							bw.WriteLine(">>");
							bw.WriteLine(">>");
							bw.WriteLine("endobj");
						}
						nObjIndex++;
					}
				}

				//カタログ出力
				int nRoot = nObjIndex;
				{
					WriteCatalog(bw, ref nObjIndex, nObjIndex + 1);
				}

				//ページ出力
				{
					List<int> listPage = new List<int>();

					//全ページのインデックスを出力
					int nPagesReferenceIndex = nObjIndex;
					{
						listPage.Add(nObjIndex + 1);				//1ページ目のインデックスを渡す
						WritePages(bw, ref nObjIndex, listPage);
					}


					//1ページ出力
					{
						WritePageContentsIndex(bw, ref nObjIndex, nPagesReferenceIndex, nResourceIndex, nObjIndex + 1, 595, 842);

						//ページテキスト出力
						{
							List<PDFText> listTexts = new List<PDFText>();

							listTexts.Add(new PDFText("F0", 40, 50, 540, @"abcあいうえお漢字123"));


							//コンテンツの書き出し
							using (MemoryStream ms = new MemoryStream())
							using (BinaryWriter bwms = new BinaryWriter(ms))
							{
								//文字データをPDF出力用に準備
								PrepareTextContents(ms, listTexts);

								ms.Flush();

								byte[] data = ms.ToArray();

								//ページコンテンツの出力
								WriteFlateData(bw, ref nObjIndex, data);
							}
						}
					}
				}

				//クロスリファレンス/トレーラー出力
				WriteXrefTrailer(bw, nRoot);
			}

			return true;
		}

		//フォントごとのcmap変換表を保管
		IDictionary<string, IDictionary<ushort, byte[]>> _cmapFonts = new Dictionary<string, IDictionary<ushort, byte[]>>();



		/// <summary>
		/// フォントの埋め込み
		/// </summary>
		void WriteFont_EmbeddedUnicode(PDFRawWriter bw, ref int nObjIndex, string strFontObjName, string strFont, string strFontFamily, string strFontFile)
		{
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Type /Font");
				bw.WriteLine("/BaseFont /" + strFont);
				bw.WriteLine("/Subtype /Type0");
				bw.WriteLine("/Encoding /Identity-H");			//PDF独自のエンコード
				bw.WriteLine("/DescendantFonts [" + (nObjIndex + 1) + " 0 R]");
				//bw.WriteLine("/ToUnicode " + (nObjIndex + 4) + " 0 R");		//ToUnicode変換表
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;


			int nDescendantFontsObjIndex = nObjIndex;
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Type /Font");
				bw.WriteLine("/Subtype /CIDFontType0");
				bw.WriteLine("/BaseFont /" + strFont);
				//bw.WriteLine("/CIDToGIDMap/Identity");
				bw.WriteLine("/CIDSystemInfo <<");
				bw.WriteLine("/Registry (Adobe)");
				bw.WriteLine("/Ordering (Identity)");		//Japan1にはしない
				bw.WriteLine("/Supplement 0");				//6にした方がいい?
				bw.WriteLine(">>");
				bw.WriteLine("/FontDescriptor " + (nObjIndex + 1) + " 0 R");
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;



			ushort nRangeMin = 0xFFFF;
			ushort nRangeMax = 0;



			//CMAPの準備
			{
				string strFontCMapFile = @"cmap_msgothic.txt";

				//
				//CMAPの読み込み
				//
				//以下を実行してcmap.txtを取得、その中から「Char 30D6 -> Index 2121」というようなunicode用のcmapテーブルを抜き出してcmap_msgothic.txtに保存
				// ttfdump.exe HuiFont29.ttf -tcmap -nx >cmap.txt
				//
				// ttfdump.exeは以下からダウンロード(fonttools.exeに含まれる)
				// http://www.microsoft.com/typography/tools/tools.aspx
				// http://download.microsoft.com/download/f/f/a/ffae9ec6-3bf6-488a-843d-b96d552fd815/FontTools.exe
				//
				//
				//	//本当なら↓のコードで簡単にフォントからcmapを取得できるはずだけど、
				//	//きちんとした対応にならない???
				//	{
				//		//「PresentationCore」への参照追加
				//		GlyphTypeface gtf = new GlyphTypeface(new Uri(strFontFile));
				//		var cmap = gtf.CharacterToGlyphMap;
				//	}
				//

				IDictionary<ushort, byte[]> cmap = new Dictionary<ushort, byte[]>();
				_cmapFonts.Add(strFontObjName, cmap);

				using (FileStream fs = new FileStream(strFontCMapFile, FileMode.Open, FileAccess.Read))
				using (StreamReader sr = new StreamReader(fs))
				{
					string strCMap = sr.ReadToEnd();

					Regex re = new Regex(@"Char ([ABCDEFabcdef\d]+) -> Index (\d+)", RegexOptions.IgnoreCase);
					Match m = re.Match(strCMap);
					while (m.Success)
					{
						try
						{
							string strChar = m.Groups[1].Value;
							string strIndex = m.Groups[2].Value;

							ushort nChar = Convert.ToUInt16(strChar, 16);
							ushort nIndex = ushort.Parse(strIndex);

							//ビッグエンディアン変換
							byte tmp;
							byte[] bytes = BitConverter.GetBytes(nIndex);
							tmp = bytes[1];
							bytes[1] = bytes[0];
							bytes[0] = tmp;

							cmap.Add(nChar, bytes);

							//indexの最小値最大値を保存しておく
							if (nIndex < nRangeMin)
								nRangeMin = nIndex;
							if (nIndex > nRangeMax)
								nRangeMax = nIndex;
						}
						catch (Exception)
						{
						}

						m = m.NextMatch();
					}
				}
			}



			int nFontDescriptorObjIndex = nObjIndex;
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Type /FontDescriptor");
				bw.WriteLine("/FontName /" + strFont);
				bw.WriteLine("/FontFamily(" + strFontFamily + ")");
				//bw.WriteLine(@"/Style<</Panose <0801020B0609070205080204> >>");		//The font family class and subclass ID bytes, given in the sFamilyClass field of the “OS/2” table in a TrueType font. This field is documented in Microsoft’s TrueType 1.0 Font Files Technical Specification

				//bw.WriteLine("/CIDSet 15 0 R");				//CID表
				bw.WriteLine("/FontFile2 " + (nObjIndex + 1) + " 0 R");
				bw.WriteLine("/Flags 6");									//Font uses the Adobe standard Latin character set or a subset of it
				bw.WriteLine("/FontBBox [0 0 0 0]");																		//だから0 0 0 0で自動にする
				//bw.WriteLine("/FontBBox [-437 -340 1147 1317]");															//指定例
				bw.WriteLine("/ItalicAngle 0");			//PostScriptHeaaderの値?面倒だからスルー
				//bw.WriteLine("/Lang/ja");				//日本語指定しておく?
				bw.WriteLine("/Ascent 1317");
				bw.WriteLine("/Descent -349");
				bw.WriteLine("/CapHeight 742");			//取得方法不明
				bw.WriteLine("/StemV 80");				//取得方法不明
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;




			int nFontFileObjIndex = nObjIndex;
			_listnXref.Add(bw.BaseStream.Position);
			{
				long nEncodedLength;
				long nDecodedLength;
				byte[] data;

				using (FileStream fs = new FileStream(strFontFile, FileMode.Open, FileAccess.Read))
				using (BinaryReader br = new BinaryReader(fs))
				{
					nDecodedLength = fs.Length;
					data = br.ReadBytes((int)nDecodedLength);
				}

				byte[] compress = CompressDeflate(data);

				nEncodedLength = compress.Length;

				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Filter /FlateDecode /Length " + nEncodedLength + " /Length1 " + nDecodedLength + ">>");
				bw.WriteLine("stream");
				{
					byte[] header = new byte[2];
					header[0] = 0x78;
					header[1] = 0x9c;
					bw.Write(header);
				}
				bw.Write(compress);
				bw.Write0x0a();
				bw.WriteLine("endstream");
				bw.WriteLine("endobj");
			}
			nObjIndex++;


			//フォント情報の保存
			_mapFontType.Add(strFontObjName, FONT_TYPE.EMBEDDED);
		}








		void WritePageContentsIndex(PDFRawWriter bw, ref int nObjIndex, int nParentObjIndex, int nResourcesObjIndex, int nContentsObjIndex, float fWidth, float fHeight)
		{
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Type /Page");
				bw.WriteLine("/Parent " + nParentObjIndex + " 0 R");
				bw.WriteLine("/Resources " + nResourcesObjIndex + " 0 R");
				bw.WriteLine("/MediaBox [0 0 " + fWidth + " " + fHeight + "]");
				bw.WriteLine("/Contents " + nContentsObjIndex + " 0 R");
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;
		}




		/// <summary>
		/// 与えられたテキストデータをストリームに以下の形式で書き込む
		/// "BT /F0 10 Tf 150 200 Td (hello) Tj ET\r";
		/// </summary>
		void PrepareTextContents(Stream strm, List<PDFText> listTexts)
		{
			using (MemoryStream ms = new MemoryStream())
			using (BinaryWriter bwms = new BinaryWriter(ms))
			{
				foreach (PDFText text in listTexts)
				{
					bool ret;
					FONT_TYPE type;

					ret = _mapFontType.TryGetValue(text.strFont, out type);
					if (ret == false)
					{
						//フォントの種類特定失敗
						Debug.Assert(false);
						continue;
					}

					IDictionary<ushort, byte[]> cmap = _cmapFonts[text.strFont];
					if (cmap == null)
					{
						//cmap取得失敗
						Debug.Assert(false);
						continue;
					}


					string strText = text.strText;

					byte[] raw2 = null;

					{
						byte[] tmp = null;

						//フォントに応じてエンコーディング
						if (type == FONT_TYPE.ASCII)
							tmp = Encoding.ASCII.GetBytes(strText);
						if (type == FONT_TYPE.ADOBE_JPN1_6)
							tmp = Encoding.BigEndianUnicode.GetBytes(strText);
						else if (type == FONT_TYPE.EMBEDDED)
							tmp = ConvertStringByCMap(strText, cmap);
						else
						{
							Debug.Assert(false);
						}

						// 「()\」をエスケープする
						using (MemoryStream ms_esc = new MemoryStream())
						{
							foreach (byte cb in tmp)
							{
								if (cb == 0x28		//'('
									|| cb == 0x29	//')'
									|| cb == 0x5c)	//'\'
								{
									ms_esc.WriteByte(0x5c);		//'\'
								}
								ms_esc.WriteByte(cb);
							}
							ms_esc.Flush();

							raw2 = ms_esc.ToArray();
						}
					}


					string strData = "BT /" + text.strFont + " " + text.fPoint + " Tf " + text.fX + " " + text.fY + " Td (";
					byte[] raw1 = Encoding.ASCII.GetBytes(strData);
					byte[] raw3 = Encoding.ASCII.GetBytes(") Tj ET");

					bwms.Write(raw1);
					bwms.Write(raw2);
					bwms.Write(raw3);
					bwms.Write('\r');
				}
				ms.Flush();

				byte[] data = ms.ToArray();
				strm.Write(data, 0, data.Length);
			}
		}



		/// <summary>
		/// 文字列をcmapにもとづいて変換する
		/// </summary>
		byte[] ConvertStringByCMap(string strText, IDictionary<ushort, byte[]> cmap)
		{
			byte[] data;

			using (MemoryStream ms = new MemoryStream())
			using (BinaryWriter bw = new BinaryWriter(ms))
			{
				foreach (ushort c in strText)
				{
					bool bFind = false;

					byte[] value;
					bFind = cmap.TryGetValue(c, out value);
					if (bFind)
						bw.Write(value);

					//文字が見つからなかったら「.」を代わりに出力
					if (bFind == false)
					{
						bFind = cmap.TryGetValue('.', out value);
						if (bFind)
							bw.Write(value);
					}
				}
				bw.Flush();

				return ms.ToArray();
			}
		}


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

第10回 PDF内の文字をコピペできるようにする

前回作成したPDFでは、文字を選択してクリップボードにコピー、そしてメモ帳にペーストしようとすると文字化けします。
これはユニコードへの「変換表」をPDF内に埋め込んでいないためです。

今回はこの変換表の埋め込み処理を追加します。
と言っても、オブジェクトを1つ用意するだけです。

■PDFTextWriter.cs
		/// <summary>
		/// フォントの埋め込み
		/// </summary>
		void WriteFont_EmbeddedUnicode(PDFRawWriter bw, ref int nObjIndex, string strFontObjName, string strFont, string strFontFamily, string strFontFile)
		{
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Type /Font");
				bw.WriteLine("/BaseFont /" + strFont);
				bw.WriteLine("/Subtype /Type0");
				bw.WriteLine("/Encoding /Identity-H");			//PDF独自のエンコード
				bw.WriteLine("/DescendantFonts [" + (nObjIndex + 1) + " 0 R]");
				bw.WriteLine("/ToUnicode " + (nObjIndex + 4) + " 0 R");		//ToUnicode変換表
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;


			int nDescendantFontsObjIndex = nObjIndex;
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Type /Font");
				bw.WriteLine("/Subtype /CIDFontType0");
				bw.WriteLine("/BaseFont /" + strFont);
				//bw.WriteLine("/CIDToGIDMap/Identity");
				bw.WriteLine("/CIDSystemInfo <<");
				bw.WriteLine("/Registry (Adobe)");
				bw.WriteLine("/Ordering (Identity)");		//Japan1にはしない
				bw.WriteLine("/Supplement 0");				//6にした方がいい?
				bw.WriteLine(">>");
				bw.WriteLine("/FontDescriptor " + (nObjIndex + 1) + " 0 R");
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;



			ushort nRangeMin = 0xFFFF;
			ushort nRangeMax = 0;



			//CMAPの準備
			//{
				string strFontCMapFile = @"cmap_msgothic.txt";

				//
				//CMAPの読み込み
				//
				//以下を実行してcmap.txtを取得、その中から「Char 30D6 -> Index 2121」というようなunicode用のcmapテーブルを抜き出してcmap_msgothic.txtに保存
				// ttfdump.exe HuiFont29.ttf -tcmap -nx >cmap.txt
				//
				// ttfdump.exeは以下からダウンロード(fonttools.exeに含まれる)
				// http://www.microsoft.com/typography/tools/tools.aspx
				// http://download.microsoft.com/download/f/f/a/ffae9ec6-3bf6-488a-843d-b96d552fd815/FontTools.exe
				//
				//
				//	//本当なら↓のコードで簡単にフォントからcmapを取得できるはずだけど、
				//	//きちんとした対応にならない???
				//	{
				//		//「PresentationCore」への参照追加
				//		GlyphTypeface gtf = new GlyphTypeface(new Uri(strFontFile));
				//		var cmap = gtf.CharacterToGlyphMap;
				//	}
				//

				IDictionary<ushort, byte[]> cmap = new Dictionary<ushort, byte[]>();
				_cmapFonts.Add(strFontObjName, cmap);

				using (FileStream fs = new FileStream(strFontCMapFile, FileMode.Open, FileAccess.Read))
				using (StreamReader sr = new StreamReader(fs))
				{
					string strCMap = sr.ReadToEnd();

					Regex re = new Regex(@"Char ([ABCDEFabcdef\d]+) -> Index (\d+)", RegexOptions.IgnoreCase);
					Match m = re.Match(strCMap);
					while (m.Success)
					{
						try
						{
							string strChar = m.Groups[1].Value;
							string strIndex = m.Groups[2].Value;

							ushort nChar = Convert.ToUInt16(strChar, 16);
							ushort nIndex = ushort.Parse(strIndex);

							//ビッグエンディアン変換
							byte tmp;
							byte[] bytes = BitConverter.GetBytes(nIndex);
							tmp = bytes[1];
							bytes[1] = bytes[0];
							bytes[0] = tmp;

							cmap.Add(nChar, bytes);

							//indexの最小値最大値を保存しておく
							if (nIndex < nRangeMin)
								nRangeMin = nIndex;
							if (nIndex > nRangeMax)
								nRangeMax = nIndex;
						}
						catch (Exception)
						{
						}

						m = m.NextMatch();
					}
				}
			//}



			int nFontDescriptorObjIndex = nObjIndex;
			_listnXref.Add(bw.BaseStream.Position);
			{
				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Type /FontDescriptor");
				bw.WriteLine("/FontName /" + strFont);
				bw.WriteLine("/FontFamily(" + strFontFamily + ")");
				//bw.WriteLine(@"/Style<</Panose <0801020B0609070205080204> >>");		//The font family class and subclass ID bytes, given in the sFamilyClass field of the “OS/2” table in a TrueType font. This field is documented in Microsoft’s TrueType 1.0 Font Files Technical Specification

				//bw.WriteLine("/CIDSet 15 0 R");				//CID表
				bw.WriteLine("/FontFile2 " + (nObjIndex + 1) + " 0 R");
				bw.WriteLine("/Flags 6");									//Font uses the Adobe standard Latin character set or a subset of it
				bw.WriteLine("/FontBBox [0 0 0 0]");																		//だから0 0 0 0で自動にする
				//bw.WriteLine("/FontBBox [-437 -340 1147 1317]");															//指定例
				bw.WriteLine("/ItalicAngle 0");			//PostScriptHeaaderの値?面倒だからスルー
				//bw.WriteLine("/Lang/ja");				//日本語指定しておく?
				bw.WriteLine("/Ascent 1317");
				bw.WriteLine("/Descent -349");
				bw.WriteLine("/CapHeight 742");			//取得方法不明
				bw.WriteLine("/StemV 80");				//取得方法不明
				bw.WriteLine(">>");
				bw.WriteLine("endobj");
			}
			nObjIndex++;




			int nFontFileObjIndex = nObjIndex;
			_listnXref.Add(bw.BaseStream.Position);
			{
				long nEncodedLength;
				long nDecodedLength;
				byte[] data;

				using (FileStream fs = new FileStream(strFontFile, FileMode.Open, FileAccess.Read))
				using (BinaryReader br = new BinaryReader(fs))
				{
					nDecodedLength = fs.Length;
					data = br.ReadBytes((int)nDecodedLength);
				}

				byte[] compress = CompressDeflate(data);

				nEncodedLength = compress.Length;

				bw.WriteLine("" + nObjIndex + " 0 obj");
				bw.WriteLine("<</Filter /FlateDecode /Length " + nEncodedLength + " /Length1 " + nDecodedLength + ">>");
				bw.WriteLine("stream");
				{
					byte[] header = new byte[2];
					header[0] = 0x78;
					header[1] = 0x9c;
					bw.Write(header);
				}
				bw.Write(compress);
				bw.Write0x0a();
				bw.WriteLine("endstream");
				bw.WriteLine("endobj");
			}
			nObjIndex++;




			//ToUnicode変換表
			if (nRangeMin <= nRangeMax)
			{
				byte[] data;

				using (MemoryStream ms = new MemoryStream())
				using (StreamWriter bwms = new StreamWriter(ms, Encoding.ASCII))
				{
					//	/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CIDSystemInfo <<
					//	/Registry (TT1+0) /Ordering (T42UV) /Supplement 0 >> def
					//	/CMapName /TT1+0 def
					//	/CMapType 2 def
					//	1 begincodespacerange <0003> <0836> endcodespacerange		//<index>の最小最大値
					//	6 beginbfchar				//続く行数
					//	<0003> <0020>				//<index> <unicode>
					//	<001d> <003A>
					//	<0044> <0061>
					//	<0057> <0074>
					//	<005b> <0078>
					//	<0836> <3042>
					//	endbfchar
					//	1 beginbfrange				//続く行数
					//	<0010> <001a> <002D>		//いっぱいあるとき用。<0010> <001a> が<002D>に対応
					//	endbfrange
					//	endcmap CMapName currentdict /CMap defineresource pop end end

					bwms.Write("/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CIDSystemInfo <<\r");
					bwms.Write("/Registry (" + strFontObjName + "+0) /Ordering (T42UV) /Supplement 0 >> def\r");
					bwms.Write("/CMapName /" + strFontObjName + "+0 def\r");
					bwms.Write("/CMapType 2 def\r");
					bwms.Write("1 begincodespacerange <" + ConvertToHex_BE(nRangeMin) + "> <" + ConvertToHex_BE(nRangeMax) + "> endcodespacerange\r");

					bwms.Write("" + cmap.Count + " beginbfchar\r");
					foreach (KeyValuePair<ushort, byte[]> pair in cmap)
					{
						string value = String.Format("{0:X2}", pair.Value[0]) + String.Format("{0:X2}", pair.Value[1]);
						bwms.Write("<" + value + "> <" + ConvertToHex_BE(pair.Key) + ">\r");
					}
					bwms.Write("endbfchar\r");
					bwms.Write("endcmap CMapName currentdict /CMap defineresource pop end end\r");
					bwms.Flush();

					data = ms.ToArray();
				}

				if (data.Length > 0)
				{
					//17 0 obj
					//<</Filter/FlateDecode/Length 269>>stream
					_listnXref.Add(bw.BaseStream.Position);
					{
						long nEncodedLength;

						byte[] compress = CompressDeflate(data);

						nEncodedLength = compress.Length;

						bw.WriteLine("" + nObjIndex + " 0 obj");
						bw.WriteLine("<</Filter /FlateDecode /Length " + nEncodedLength + ">>");
						bw.WriteLine("stream");
						{
							byte[] header = new byte[2];
							header[0] = 0x78;
							header[1] = 0x9c;
							bw.Write(header);
						}
						bw.Write(compress);
						bw.Write0x0a();
						bw.WriteLine("endstream");
						bw.WriteLine("endobj");
					}
					nObjIndex++;
				}
			}

			//フォント情報の保存
			_mapFontType.Add(strFontObjName, FONT_TYPE.EMBEDDED);
		}


		string ConvertToHex_BE(ushort value)
		{
			byte[] bytes = BitConverter.GetBytes(value);

			return String.Format("{0:X2}", bytes[1]) + String.Format("{0:X2}", bytes[0]);
		}

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

1  2  3





usefullcode@gmail.com

About 2016年04月

2016年04月にブログ「UsefullCode.net」に投稿されたすべてのエントリーです。

前の記事は2016年03月です。

次の記事は2016年07月です。

他にも多くのエントリーがあります。メインページ記事一覧も見てください。