1  2  3  4  5  6  7  8  9  10  11

記事一覧

2016年07月11日

第19回 ユニコードテキストを読み込む

2016/7/7に小説家になろう機能の不具合修正ということで、ダウンロードできるPDFの仕様が変更されました。

これまではPDF内のテキストはシフトJISでした。これがUniJIS-UTF16-V(ユニコード)になり、それに伴いツールが正常に動作しません。
今回はこの変更への対応を行います。

これまではPDFRawReader.ReadLine()でPDF内の情報をstringとして読み取っていました。
今回の仕様変更で1行の中にアスキーとユニコードの2つのエンコードが混在することになったため、byte[]を取得できるように変更します。
シフトJISのときは文字列のエスケープのことは気にしなくとも実用上は問題ありませんでした。しかしユニコードでは文字コード中に0x0dなどが頻繁に出てくるためエスケープ処理を入れておきます。
通常であれば単純にエスケープ処理するだけで済むのですが、PDFテキスト(BT~ET)の丸カッコ内ではエスケープしていない0x0aなどが入っていることがあるため、BT~ETだけ別処理で改行検出しています。

あとはテキスト情報の取得部分でそのbyte[]の情報を抜き出してユニコードとして処理すれば対応終了です。

■PDFRawReader.cs
		/// <summary>
		/// 1行ずつ読み込む
		/// bSkipCommentLine == trueならコメント行(%から始まる行)は読み取らない
		/// </summary>
		public string ReadLine(Encoding enc, bool bSkipCommentLine, bool bDescape = false)
		{
			byte[] pcbRawData;
			return ReadLine(enc, bSkipCommentLine, out pcbRawData, bDescape);
		}


		/// <summary>
		/// 1行ずつ読み込む
		/// bSkipCommentLine == trueならコメント行(%から始まる行)は読み取らない
		/// bDescape == trueならエスケープを外す
		/// </summary>
		public string ReadLine(Encoding enc, bool bSkipCommentLine, out byte[] pcbRawData, bool bDescape = false)
		{
			do
			{
				byte tmp;
				pcbRawData = new byte[0];
				int n = 0;

				try
				{
					while (true)
					{
						tmp = ReadByte();

						if (bDescape && tmp == 0x5c)	//'\\'(0x5c)ならエスケープチェック
						{
							tmp = ReadByte();
							if (tmp == 0x6e)		//'n'
								tmp = 0x0a;
							else if (tmp == 0x72)	//'r'
								tmp = 0x0d;
							else if (tmp == 0x74)	//'t'
								tmp = 0x09;
							else if (tmp == 0x62)	//'b'
								tmp = 0x08;
							else if (tmp == 0x66)	//'f'
								tmp = 0x0c;
							else if (tmp == 0x28)	//'('
							{
							}
							else if (tmp == 0x29)	//')'
							{
							}
							else if (tmp == 0x5c)	//'\\'
							{
							}

							Array.Resize(ref pcbRawData, pcbRawData.Length + 1);
							pcbRawData[n] = tmp;
							n++;
							continue;
						}

						if (tmp == 0x0a || tmp == 0x0d)
						{
							if (pcbRawData.Length < 6		//6文字以下なら処理
								|| (pcbRawData[0] != 0x42 || pcbRawData[1] != 0x54 || pcbRawData[2] != 0x20)	//「BT 」から始まらないなら処理
								|| (pcbRawData[pcbRawData.Length - 3] == 0x20 && pcbRawData[pcbRawData.Length - 2] == 0x45 && pcbRawData[pcbRawData.Length - 1] == 0x54))	//~「 ET」となるテキストエリアなら処理
							{
								//改行コードチェック(\r、\r\n、\nの3通りあり得る)
								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 pcbRawData, pcbRawData.Length + 1);
						pcbRawData[n] = tmp;
						n++;
					}
				}
				catch (Exception)
				{
				}

				string ret = enc.GetString(pcbRawData);

				if (bSkipCommentLine == false || string.IsNullOrEmpty(ret) || ret[0] != _cbCommentChar)
					return ret;
			}
			while (true);
		}
■PDFTextReader.cs
		/// <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(pcbContents))
			using (PDFRawReader pr = new PDFRawReader(ms))
			{
				//"小説家になろう"の縦書きPDFはshift-jis → UniJIS-UTF16-Vに変更(2016/7/7)
				Encoding enc = Encoding.ASCII;

				string strFontObj = "";
				float fFontPoint = 0;

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

					byte[] pcbRawData;

					string strLine = pr.ReadLine(enc, false, out pcbRawData, true);
					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 | RegexOptions.Singleline);
						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);

								//F9以外のフォントはbigendian unicode。F9はascii
								if (strFontObj != "F9")
								{
									int nStart = -1;
									int nEnd = -1;

									//byte[]から()内のテキストデータ部分を抜き出して変換
									for (int i = 0; i < pcbRawData.Length; i++)
									{
										if (pcbRawData[i] != '(')
											continue;
										nStart = i;
										break;
									}
									for (int i = pcbRawData.Length - 1; i >= 0; i--)
									{
										if (pcbRawData[i] != ')')
											continue;
										nEnd = i;
										break;
									}

									if (nStart > 0 && nEnd > 0)
									{
										byte[] pcbText = new byte[nEnd - nStart - 1];

										int j = 0;
										for (int i = nStart + 1; i < nEnd; i++, j++)
										{
											pcbText[j] = pcbRawData[i];		//ここでエスケープ解除するべき
										}
										strText = System.Text.Encoding.BigEndianUnicode.GetString(pcbText);
									}
								}

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

							m = m.NextMatch();
						}
					}

				}
			}
		}


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

2016年04月13日

第04回 府県天気予報XMLを整形する

気象庁防災情報XMLでは天気予報関連の配信は
・府県天気概況
・府県天気予報
・府県週間天気予報
・地方週間天気予報
の4種類があります。

今回はこのうち「府県天気予報」のXMLを読み込み/変換して出力します。


府県天気予報XMLには、
例えば宮崎地方気象台発表のものであれば、

・南部平野部
・北部平野部
・南部山沿い
・北部山沿い
の4地域の今日/明日/明後日の天気や風などの予報

・宮崎
・延岡
・都城
・高千穂
の4箇所の今後24時間までの3時間ごとの気温/風予報

・宮崎
・延岡
・都城
・高千穂
・油津
の5箇所の今日/明日の最高最低気温の予報

などが記載されています。

情報量が多くていいのですが、大きく2つの問題があります。
・どの都道府県の予報なのか分からない(地域名や○○気象台の名前などから判断するしかない)
・「宮崎」「延岡」などがどの地域(「南部山沿い」などの地域)に属するか分からない(別途対応表の用意が必要)

「宮崎県」だけであればたいした問題ではないのですが、47都道府県の処理を考えると少し面倒です。
さらにデータ構造に癖のあるXMLで扱いにくいため、気象庁のXMLから欲しい情報だけを抜き出して、別構造のXMLへと変換してしまいます。

データの階層構造は、地域の下にタイムシリーズ(「今日」「明日」などの時間区分)、その下に天気予報を配置する形にしました。
本来ならある程度汎用的な様式でのXMLに変換するべきなのでしょうが、そこまで拘る必要もないので特化したXMLです。

最終的な出力は、
・変換した結果の独自形式XML forecast○○.xml
・気象庁から配信された生XML forecast○○_raw.xml
の2ファイル×47都道府県=合計94ファイルとしました。




地点名、地域、都道府県の対応表は気象庁が技術資料として用意しているExcelファイルをCSV変換/修正したものを用意して対応しました。

以下の2ファイルをソースコード内の_strFolderDataで指定したフォルダへ保存しておきます。
・20160301_AreaInformationCity-AreaForecastLocalM.csv
・20160314_PointAmedas_mod.csv

気象庁防災情報XMLフォーマット 技術資料
http://xml.kishou.go.jp/tec_material.html




プロジェクトを作成します。
前回のソリューションを開き、ソリューションウインドウの「JmaForecast」ソリューションを右クリックして現れたメニューから「追加」にある「新しいプロジェクト」を選択し、
「Visual C#」の「コンソールアプリケーション」を追加します。
ここでプロジェクト名は「jmacnvforecast」、.NETのバージョンは「.NET Framework 4」としました。
さらに「プロジェクト」メニューにある「jmacnvforecastのプロパティ」を選択し、構成「Release」にたいして、デバッグ」タブにある「Visual Studioホスティングプロセスを有効にする」のチェックをはずしておきます。

今回はクライアント側でアクセスするためのXMLを整形/出力する処理があります。
その出力用XMLのクラス類を保存するためにプロジェクト内にソースファイルを1つ追加しておきます。
ソリューションウインドウの「jmacnvforecast」を右クリックして現れたメニューから「追加」にある「新しい項目」を選択し、「コードファイル」を追加します。
ファイル名は「SerializeData.cs」としました。

そしてソースコードを作成。
最後にjmadownload.exe内でjmacnvforecast.exeを実行するように修正したら完成です。

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

2016年04月12日

第03回 subscribeをダウンロードする

今回はsubscriberへ配信された内容を元に記事をダウンロードします。

気象庁防災情報XMLではsubscriberへ以下のようなfeedが配信されます。
entryの数はfeedによって異なり、実際の記事内容はlinkとしてURLが記載されています。
今回はこのlinkをダウンロードします。

なお、linkのURLは配信から数時間だけ有効で、それを過ぎると404エラーでダウンロードできなくなります。

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja">
<title>JMAXML publishing feed</title>
<subtitle>this feed is published by JMA</subtitle>
<updated>2016-04-06T17:45:01+09:00</updated>
<id>urn:uuid:d38e0e80-12ba-3236-b10f-256b78a08995</id>
<link href="http://www.jma.go.jp/" rel="related"/>
<link href="http://xml.kishou.go.jp/feed/other.xml" rel="self"/>
<rights>Published by Japan Meteorological Agency</rights>

<entry>
<title>地方海上警報</title>
<id>urn:uuid:e89f7227-becb-3cdf-b7f8-c665434602d4</id>
<updated>2016-04-06T08:44:37Z</updated>
<author><name>鹿児島地方気象台</name></author>
<link href="http://xml.kishou.go.jp/data/e89f7227-becb-3cdf-b7f8-c665434602d4.xml" type="application/xml"/>
<content type="text">【鹿児島海上気象】</content>
</entry>
</feed>
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja">
<title>JMAXML publishing feed</title>
<subtitle>this feed is published by JMA</subtitle>
<updated>2016-04-06T17:46:01+09:00</updated>
<id>urn:uuid:d38e0e80-12ba-3236-b10f-256b78a08995</id>
<link href="http://www.jma.go.jp/" rel="related"/>
<link href="http://xml.kishou.go.jp/feed/other.xml" rel="self"/>
<rights>Published by Japan Meteorological Agency</rights>

<entry>
<title>地方海上警報</title>
<id>urn:uuid:37eb566f-9585-36d6-8972-d9473d134bf0</id>
<updated>2016-04-06T08:45:25Z</updated>
<author><name>沖縄気象台</name></author>
<link href="http://xml.kishou.go.jp/data/37eb566f-9585-36d6-8972-d9473d134bf0.xml" type="application/xml"/>
<content type="text">【沖縄海上気象】</content>
</entry>
<entry>
<title>生物季節観測</title>
<id>urn:uuid:a43b264a-cefc-3495-9f81-1b315359cbb7</id>
<updated>2016-04-06T08:45:32Z</updated>
<author><name>甲府地方気象台</name></author>
<link href="http://xml.kishou.go.jp/data/a43b264a-cefc-3495-9f81-1b315359cbb7.xml" type="application/xml"/>
<content type="text">【生物季節観測】</content>
</entry>
</feed>



前回は↑のファイルが保存されるようにしました。
今回はこのファイルを読み込んで、linkを抽出、それをファイルへダウンロードします。

まずはプロジェクトを作成します。
前回のソリューションを開き、ソリューションウインドウの「JmaForecast」ソリューションを右クリックして現れたメニューから「追加」にある「新しいプロジェクト」を選択し、
「Visual C#」の「コンソールアプリケーション」を追加します。
ここでプロジェクト名は「jmadownload」、.NETのバージョンは「.NET Framework 4」としました。
さらに「プロジェクト」メニューにある「jmadownloadのプロパティ」を選択し、構成「Release」にたいして、デバッグ」タブにある「Visual Studioホスティングプロセスを有効にする」のチェックをはずしておきます。

これでダウンロード処理を用意し、さらにsubscriber側にjmadownload.exeを呼ぶ処理を追加します。
そして出来上がったexeを2つともサーバーへアップロードすればokです。

■Program.cs(jmadownload.exe)
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Xml;

namespace jmadownload
{
	/// <summary>
	/// 気象庁からpush配信されたxmlデータに記載されているリンクをダウンロードする
	/// 
	/// 呼び出すときにxmlへのファイルパスを渡すこと
	/// </summary>
	class Program
	{
		/// <summary>
		/// argsにファイル名を渡すこと!
		/// </summary>
		static void Main(string[] args)
		{
			if (args.Length == 0)
			{
#if DEBUG
				//デバッグ時のテスト用
				args = new string[1];
				//args[0] = @"..\..\635955615261813750.txt";		//サンプルxml
				args[0] = @"..\..\635955616026657500.txt";			//サンプルxml
#else
				return;
#endif
			}


			try
			{
				List<Entry> listEntry = new List<Entry>();

				//xmlファイルからエントリーリストを取得
				ListUpEntry(args[0], listEntry);

				bool bDownloaded = true;

				foreach (Entry entry in listEntry)
				{
					if (entry.strLink == "" || entry.strTitle == "")
						continue;
					if (entry.strLink.IndexOf(@"http://xml.kishou.go.jp/data/") != 0)		//気象庁のURL以外はダウンロードしない
						continue;

					string strFile;
					string strRootFolder;

					strRootFolder = Path.GetDirectoryName(args[0]) + @"\";

					PrepareFolerFile(entry, strRootFolder, out strFile);					//保存ファイル名の決定/保存先フォルダ作成

					if (strFile == "")
						continue;


					//すでにファイルがあるならダウンロードしない
					if (File.Exists(strFile))
						continue;

					//ブロッキングでダウンロード
					try
					{
						bool bSuccess = false;
						for (int i = 0; i < 10; i++)		//ダウンロード試行回数は10回
						{
							using (MemoryStream ms = new MemoryStream())
							{
								bool ret;
								Stream strm = (Stream)ms;

								//メモリストリームへダウンロード
								ret = DownloadStream(entry.strLink, ref strm);
								if (ret == false)
									continue;

								//ファイルへ保存
								using (FileStream fs = new FileStream(strFile, FileMode.Create, FileAccess.Write))
								{
									ms.CopyTo(fs);
									fs.Flush();
								}
								bSuccess = true;
								break;		//ダウンロード成功!
							}
						}
						if (bSuccess == false)
							bDownloaded = false;
					}
					catch (Exception)
					{
					}
				}

				if (bDownloaded)
					File.Delete(args[0]);
			}
			catch (Exception)
			{
			}
		}



		/// <summary>
		/// URLをStreamへダウンロードする
		/// </summary>
		static bool DownloadStream(string strURL, ref Stream outStream)
		{
			if (outStream == null)
				outStream = new MemoryStream();

			try
			{
				WebRequest request = WebRequest.Create(strURL);

				request.Method = "GET";

				using (WebResponse response = request.GetResponse())
				using (Stream dataStream = response.GetResponseStream())
				{
					dataStream.CopyTo(outStream);
				}
				if (outStream.CanSeek)
					outStream.Seek(0, SeekOrigin.Begin);
				return true;
			}
			catch (Exception)
			{
			}
			return false;
		}




		class Entry
		{
			public string strTitle
			{
				get { return _strTitle; }
				set { _strTitle = value; }
			}
			string _strTitle = "";

			public string strID
			{
				get { return _strID; }
				set { _strID = value; }
			}
			string _strID = "";

			public DateTime dtUpdated
			{
				get { return _dtUpdated; }
				set { _dtUpdated = value; }
			}
			DateTime _dtUpdated = DateTime.MinValue;

			public string strAuthorName
			{
				get { return _strAuthorName; }
				set { _strAuthorName = value; }
			}
			string _strAuthorName = "";

			public string strLink
			{
				get { return _strLink; }
				set { _strLink = value; }
			}
			string _strLink = "";

			public string strContent
			{
				get { return _strContent; }
				set { _strContent = value; }
			}
			string _strContent = "";



			public bool SetUpdated(string strDate)
			{
				bool ret;
				DateTime dtDate;

				ret = StringToDateTime(strDate, out dtDate);
				if (ret)
					_dtUpdated = dtDate;

				return ret;
			}

		}


		/// <summary>
		/// エントリーの列挙
		/// 
		/// 指定されたファイルパスのサブスクライブxmlを読み込み、中にあるエントリー情報をlistEntryへ追加する
		/// 失敗したらマイナス1
		/// 成功したら取得できたエントリー数を返す
		/// </summary>
		static int ListUpEntry(string strSubscribeXMLFile, List<Entry> listEntry)
		{
			//XML例。取得するのは/feed/entry
			//
			//<?xml version="1.0" encoding="utf-8"?>
			//<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja">
			//<title>JMAXML publishing feed</title>
			//<subtitle>this feed is published by JMA</subtitle>
			//<updated>2016-04-07T19:01:01+09:00</updated>
			//<id>urn:uuid:f57b5866-0c8c-3c92-9aff-10a715cdf48b</id>
			//<link href="http://www.jma.go.jp/" rel="related"/>
			//<link href="http://xml.kishou.go.jp/feed/extra.xml" rel="self"/>
			//<rights>Published by Japan Meteorological Agency</rights>
			//
			//<entry>
			//<title>気象特別警報・警報・注意報</title>
			//<id>urn:uuid:0e212e5b-fb6f-3409-a8e9-d96854a4346c</id>
			//<updated>2016-04-07T10:00:44Z</updated>
			//<author><name>岡山地方気象台</name></author>
			//<link href="http://xml.kishou.go.jp/data/0e212e5b-fb6f-3409-a8e9-d96854a4346c.xml" type="application/xml"/>
			//<content type="text">【岡山県気象警報・注意報】注意報を解除します。</content>
			//</entry>
			//<entry>
			//<title>気象警報・注意報</title>
			//<id>urn:uuid:1c90ac60-8ab6-3d80-b619-a1b7d9be6b71</id>
			//<updated>2016-04-07T10:00:44Z</updated>
			//<author><name>岡山地方気象台</name></author>
			//<link href="http://xml.kishou.go.jp/data/1c90ac60-8ab6-3d80-b619-a1b7d9be6b71.xml" type="application/xml"/>
			//<content type="text">【岡山県気象警報・注意報】注意報を解除します。</content>
			//</entry>
			//</feed>
			//

			try
			{
				int nCount = 0;

				using (XmlTextReader reader = new XmlTextReader(strSubscribeXMLFile))
				{
					//ネームスペースを無視
					reader.Namespaces = false;

					XmlDocument xml = new XmlDocument();
					xml.Load(reader);


					//エントリーの選択
					XmlNodeList entries = xml.DocumentElement.SelectNodes(@"/feed/entry");

					foreach (XmlNode entry in entries)
					{
						Entry item = new Entry();

						XmlNode node;

						node = entry.SelectSingleNode("title");
						if (node != null)
							item.strTitle = node.InnerText;

						node = entry.SelectSingleNode("id");
						if (node != null)
							item.strID = node.InnerText;

						node = entry.SelectSingleNode("author/name");
						if (node != null)
							item.strAuthorName = node.InnerText;

						node = entry.SelectSingleNode("updated");
						if (node != null)
							item.SetUpdated(node.InnerText);


						node = entry.SelectSingleNode("link");
						if (node != null)
						{
							XmlAttribute type = node.Attributes["type"];
							if (type != null && type.Value == @"application/xml")
							{
								XmlAttribute href = node.Attributes["href"];
								if (href != null)
									item.strLink = href.Value;
							}
						}

						node = entry.SelectSingleNode("content");
						if (node != null)
							item.strContent = node.InnerText;

						//タイトルがなかったら無視
						if (item.strTitle == "")
							continue;

						nCount++;
						listEntry.Add(item);
					}
				}

				return nCount;
			}
			catch (Exception)
			{
				return -1;
			}
		}



		/// <summary>
		/// ファイル名として不適な文字を置き換える
		/// </summary>
		static string ReplaceInvalidChar(string strText)
		{
			char[] pcbInvalid = Path.GetInvalidFileNameChars();


			//IDをファイル名にする
			// →ファイル名に使えない文字を除去
			{
				//ありがちな文字は決め打ちで全角に
				strText = strText.Replace('*', '*');
				strText = strText.Replace('\\', '¥');
				strText = strText.Replace(':', ':');
				strText = strText.Replace('<', '<');
				strText = strText.Replace('>', '>');
				strText = strText.Replace('?', '?');
				strText = strText.Replace('|', '|');

				//使えない文字除去
				foreach (char c in pcbInvalid)
				{
					strText = strText.Replace(c.ToString(), "");
				}
			}

			return strText;
		}



		/// <summary>
		/// フォルダーを作成して、保存すべきファイルパスを返す
		/// 
		/// 保存先はrootfolderの下にyyyymmddフォルダの下、
		/// ファイル名は「タイトル_ID.txt」
		/// </summary>
		static bool PrepareFolerFile(Entry entry, string strRootFolder, out string strFilePath)
		{
			strFilePath = "";
			if (entry == null || entry.strTitle == "" || entry.dtUpdated == DateTime.MinValue || strRootFolder == "")
				return false;

			try
			{
				strFilePath = ReplaceInvalidChar(entry.strTitle) + "_" + ReplaceInvalidChar(entry.strID) + ".txt";

				string strDate = string.Format("{0:0000}{1:00}{2:00}", entry.dtUpdated.Year, entry.dtUpdated.Month, entry.dtUpdated.Day);

				if (strRootFolder.Substring(strRootFolder.Length - 1) != @"\")
					strRootFolder += @"\";

				string strFolder = strRootFolder + strDate + @"\";
				Directory.CreateDirectory(strFolder);

				strFilePath = strFolder + strFilePath;

				return true;
			}
			catch (Exception)
			{
			}
			return false;
		}




		/// <summary>
		/// 「yyyy/mm/dd」「yyyymmdd」「yyyy/mm/dd hh:mm」「yyyy/mm/dd hh:mm:ss」をDateTimeにする
		/// 
		///「2016-04-07T19:01:01+09:00」の形式対応(時差がプラス9時間でなければそれに合わせて変換後返す)
		///「2016-04-07T10:00:44Z」(UTC日時)の形式対応(プラス9時間して返す)
		/// 
		/// yyyymmddの区切りは「年月日」「/」「:」「-」に対応
		/// hhmmssの区切りは「時分秒」「:」に対応
		/// カッコで囲まれた曜日表記に対応 ex. 「(日)」「(水)」「(Wed)」「(sat.)」
		/// </summary>
		public static bool StringToDateTime(string strDate, out DateTime dtDate)
		{
			//「yyyy年mm月dd日」				→「yyyy/mm/dd」
			//「yyyy年mm月dd日hh時mm分」		→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日hh時mm分」		→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日 hh時mm分」		→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日hh時mm分ss秒」	→「yyyy/mm/dd hh:mm:ss」
			//「yyyy年mm月dd日 hh時mm分ss秒」	→「yyyy/mm/dd hh:mm:ss」
			//「yyyy年mm月dd日hh:mm」			→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日 hh:mm」			→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日hh:mm:ss」		→「yyyy/mm/dd hh:mm:ss」
			//「yyyy年mm月dd日 hh:mm:ss」		→「yyyy/mm/dd hh:mm:ss」
			{
				//カッコで囲まれた曜日の除去
				{
					//カッコを全角に統一
					strDate = strDate.Replace("(", "(");
					strDate = strDate.Replace(")", ")");

					//カッコを1つのスペースに変換
					Regex regex = new Regex(@"((.*))");
					MatchCollection matchCol = regex.Matches(strDate);
					if (matchCol.Count > 0)
						strDate = strDate.Replace(matchCol[0].Groups[1].Value, " ");
				}

				strDate = strDate.Replace(" ", " ");	//全角スペースの半角化
				strDate = strDate.Replace("年", "/");
				strDate = strDate.Replace("月", "/");
				strDate = strDate.Replace("日 ", " ");	//後ろにスペースのある「日」はそのまま除去
				strDate = strDate.Replace("時", ":");
				strDate = strDate.Replace("分", ":");
				strDate = strDate.Replace("秒", "");

				//2つ以上のスペースを1つに変換
				while (strDate.IndexOf("  ") >= 0)
				{
					strDate = strDate.Replace("  ", " ");
				}
				//前後のスペースを除去
				{
					strDate = strDate.TrimStart();
					strDate = strDate.TrimEnd();
				}

				//以下の4パターンを考慮して「日」を除去する
				//「yyyy/mm/dd日」→「yyyy/mm/dd」
				//「yyyy/mm/dd日hh:mm」→「yyyy/mm/dd hh:mm」
				//「yyyy/mm/dd日hh:mm:ss」→「yyyy/mm/dd hh:mm:ss」
				//「yyyy/mm/dd日 hh:mm:ss」→「yyyy/mm/dd hh:mm:ss」
				if (strDate.IndexOf("日") > 0)
				{
					strDate = strDate.Replace("日 ", "");			//スペース除去
					Regex regex = new Regex(@"(\d+)日(\d+)");
					MatchCollection matchCol = regex.Matches(strDate);
					if (matchCol.Count > 0)
						strDate = strDate.Replace("日", " ");
					else
						strDate = strDate.Replace("日", "");
				}
			}



			//「yyyy/mm/dd」「yyyy/mm/d」「yyyy/m/dd」「yyyy/m/d」
			if (strDate.Length == 10 || strDate.Length == 9 || strDate.Length == 8)
			{
				Regex regex = new Regex(@"(\d{4})[-/:](\d+)[-/:](\d+)");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 4)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value));
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}

			//「yyyymmdd」
			if (strDate.Length == 8)
			{
				Regex regex = new Regex(@"(\d{4})(\d{2})(\d{2})");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 4)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value));
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}

			//「yyyy/mm/dd hh:mm」「yyyy/mm/d hh:mm」「yyyy/m/dd hh:mm」「yyyy/m/d hh:mm」「yyyy/m/d h:mm」
			if (strDate.Length == 16 || strDate.Length == 15 || strDate.Length == 14 || strDate.Length == 13)
			{
				Regex regex = new Regex(@"(\d{4})[-/:](\d+)[-/:](\d+) (\d+):(\d{2})");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 6)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value)
									, Int32.Parse(matchCol[i].Groups[4].Value), Int32.Parse(matchCol[i].Groups[5].Value), 0);
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}

			//「yyyy/mm/dd hh:mm:ss」「yyyy/m/dd hh:mm:ss」「yyyy/mm/d hh:mm:ss」「yyyy/m/d hh:mm:ss」「yyyy/m/d h:mm:ss」
			if (strDate.Length == 19 || strDate.Length == 18 || strDate.Length == 17 || strDate.Length == 16)
			{
				Regex regex = new Regex(@"(\d{4})[-/:](\d+)[-/:](\d+) (\d+):(\d{2}):(\d{2})");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 7)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value)
									, Int32.Parse(matchCol[i].Groups[4].Value), Int32.Parse(matchCol[i].Groups[5].Value), Int32.Parse(matchCol[i].Groups[6].Value));
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}


			//「2016-04-07T19:01:01+09:00」の形式
			//
			//「2016-04-07T19:01:01」がオフセット+09:00(日本時間)という意味。戻り値は「2016-04-07 19:01:01」
			//「2016-04-07T19:01:01+00:00」の場合、オフセット+00:00(UTC)という意味。戻り値はJST変換して「2016-04-08 04:01:01」
			if (strDate.Length == 25)
			{
				//Parseですませちゃう
				try
				{
					dtDate = DateTime.Parse(strDate);
					return true;
				}
				catch (Exception)
				{
				}
			}

			//「2016-04-07T10:00:44Z」の形式
			//
			//↑はタイムオフセットゼロ=UTCという意味。日本時間にするため時差9時間プラスして返す
			if (strDate.Length == 20)
			{
				//Parseですませちゃう
				try
				{
					dtDate = DateTime.Parse(strDate);
					return true;
				}
				catch (Exception)
				{
				}
			}

			//Parseしてみる
			try
			{
				dtDate = DateTime.Parse(strDate);
				return true;
			}
			catch (Exception)
			{
			}


			dtDate = DateTime.MinValue;
			return false;
		}

	}
}

■Program.cs(jmasubscriber.exe)
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;

namespace jmasubscriber
{
	class Program
	{
		const string _strVerifyToken = "test_token";
		const string _strFolerOut = "jma/";
		const string _strFolerJmaDonwload = "";				//"jmadownload.exe"のあるフォルダ


		static void Main(string[] args)
		{
			//POSTかGETか、というアクセス種別を取得する
			string strMethod = Environment.GetEnvironmentVariable("REQUEST_METHOD");

			//GETの場合は、subscriberチェック→チャレンジコードを返す
			if (strMethod == "GET")
			{
				try
				{
					//GETで↓のような文字列が渡される
					// 「hub.topic=http://www.example.com/test.txt&hub.challenge=9567222552380101910&hub.verify_token=test_token&hub.mode=subscribe&hub.lease_seconds=432000」
					string strQuery = Environment.GetEnvironmentVariable("QUERY_STRING");

					NameValueCollection col = QueryToNameValueCollection(strQuery);

					string strHubMode = col["hub.mode"];
					string strHubChallenge = col["hub.challenge"];
					string strHubVerifyToken = col["hub.verify_token"];

					if (strHubMode == "subscribe" || strHubMode == "unsubscribe")
					{
						if (strHubVerifyToken != _strVerifyToken || strHubChallenge == null || strHubChallenge == "")
						{
							Console.WriteLine("HTTP/1.1 404 \"Unknown Request\"");		//unknown reqは404じゃないけど気にしない
							Console.WriteLine("Content-Type: text/plain");
							Console.WriteLine("");
							Console.WriteLine("failed(1).");
							return;
						}

						Console.WriteLine("HTTP/1.1 200 \"OK\"");
						Console.WriteLine("Content-Type: text/plain");
						Console.WriteLine("");
						Console.Write(strHubChallenge);		//WriteLine()はNG。チャンレンジコードを返す
						return;
					}
				}
				catch (Exception)
				{
				}

				Console.WriteLine("HTTP/1.1 404 \"Unknown Request\"");
				Console.WriteLine("Content-Type: text/plain");
				Console.WriteLine("");
				Console.WriteLine("failed(2).");
				return;
			}


			//POSTの場合は、情報の配信→受信内容を保存する
			if (strMethod == "POST")
			{
				//何らかのエラーがありえる状態でも、とりあえず先に200 okを返しておく
				Console.WriteLine("HTTP/1.1 200 \"OK\"");
				Console.WriteLine("Content-Type: text/html");
				Console.WriteLine("");

				try
				{
					using (Stream st = Console.OpenStandardInput())			//標準入力から配信内容取得
					using (BinaryReader br = new BinaryReader(st))			//StreamReader.ReadLine()などは使えない。BinaryReaderで処理
					{
						string strLength = Environment.GetEnvironmentVariable("CONTENT_LENGTH");
						int nLength = Int32.Parse(strLength);

						if (nLength < 1000000)		//約1MB以上ある場合は無視する(そんなに大きなサイズが来ることはないはず)
						{
							byte[] data = new byte[nLength];
							br.Read(data, 0, nLength);

							//効率は悪いけど一度文字列に変換しちゃう
							string strQuery = Encoding.UTF8.GetString(data, 0, nLength);

							//ファイル保存。ファイル名は日時ticks数値
							string strFile = _strFolerOut + DateTime.Now.Ticks + ".txt";
							using (StreamWriter sw = new StreamWriter(strFile, false, Encoding.UTF8))
							{
								sw.Write(strQuery);
							}

							try
							{
								//配信内容のダウンロードアプリを起動
								System.Diagnostics.Process.Start(_strFolerJmaDonwload + @"jmadownload.exe", "\"" + strFile + "\"");
							}
							catch (Exception)
							{
							}
						}
					}
				}
				catch (Exception)
				{
				}
				return;
			}
		}

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

第02回 subscriberを構築する

今回は気象庁防災情報XMLを受信するためのsubscriberを作成します。
サーバー側アプリはsubscriber、XML記事のダウンロード、XML記事内容を整形するツールの合計3つ以上作る予定です。



まずはソリューション/プロジェクトを作成します。
ソリューション名を「JmaForecast」、subscriber用のプロジェクト名を「jmasubscriber」、プロジェクトの種類は「コンソールアプリケーション」とします。

今回はVisual Studio 2012を利用しました。
「ファイル」メニューの「新規作成」から「プロジェクト」を選択し、「Visual C#」の「コンソールアプリケーション」としてプロジェクトを作成。
プロジェクト名は「JmaForecast」としました。

そしてソリューションウインドウの「JmaForecast」プロジェクトを右クリックして現れたメニューから「削除」を選択して作成したばかりのプロジェクトを削除。
これだけではプロジェクトは完全には削除されていないので、エクスプローラーでソリューションのあるフォルダを開き、そこにある「JmaForecast」フォルダを削除してしまいます。

これで名前が「JmaForecast」で、プロジェクトが何もない空のソリューションができました。

次にソリューションウインドウの「JmaForecast」ソリューションを右クリックして現れたメニューから「追加」にある「新しいプロジェクト」を選択し、
「Visual C#」の「コンソールアプリケーション」を追加します。
ここでプロジェクト名は「jmasubscriber」、.NETのバージョンは「.NET Framework 4」としました。
これでソリューション/プロジェクトの準備は完了です。



次に一気にsubscriberのコードを作ってしまいます。

これは一番古典的な方法ですが、
C#アプリをCGIとして扱いたい場合は、アプリをコンソールアプリケーションとして作成し、
・GETで渡された内容は環境変数の「QUERY_STRING」を読む
・POSTで渡された内容はConsole.OpenStandardInput()からStreamで読む
という形で作れます。
CGIはコンソールアプリケーションではなくasp.netなどを使うのが一般的ですが、デバッグや応用が簡単なので今回もこれでいきます。

今回は関係ないですが、C#アプリはwindowsのテンポラリフォルダへ勝手に一時ファイルを作ることがあります。
そのためサーバー側ではWEBサーバーのユーザーに対してc:\Windows\tmp\へ書き込み権限を与える必要があります。

■Program.cs
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;

namespace jmasubscriber
{
	class Program
	{
		const string _strVerifyToken = "test_token";
		const string _strFolerOut = "jma/";

		static void Main(string[] args)
		{
			//POSTかGETか、というアクセス種別を取得する
			string strMethod = Environment.GetEnvironmentVariable("REQUEST_METHOD");

			//GETの場合は、subscriberチェック→チャレンジコードを返す
			if (strMethod == "GET")
			{
				try
				{
					//GETで↓のような文字列が渡される
					// 「hub.topic=http://www.example.com/test.txt&hub.challenge=9567222552380101910&hub.verify_token=test_token&hub.mode=subscribe&hub.lease_seconds=432000」
					string strQuery = Environment.GetEnvironmentVariable("QUERY_STRING");

					NameValueCollection col = QueryToNameValueCollection(strQuery);

					string strHubMode = col["hub.mode"];
					string strHubChallenge = col["hub.challenge"];
					string strHubVerifyToken = col["hub.verify_token"];

					if (strHubMode == "subscribe" || strHubMode == "unsubscribe")
					{
						if (strHubVerifyToken != _strVerifyToken || strHubChallenge == null || strHubChallenge == "")
						{
							Console.WriteLine("HTTP/1.1 404 \"Unknown Request\"");		//unknown reqは404じゃないけど気にしない
							Console.WriteLine("Content-Type: text/plain");
							Console.WriteLine("");
							Console.WriteLine("failed(1).");
							return;
						}

						Console.WriteLine("HTTP/1.1 200 \"OK\"");
						Console.WriteLine("Content-Type: text/plain");
						Console.WriteLine("");
						Console.Write(strHubChallenge);		//WriteLine()はNG。チャンレンジコードを返す
						return;
					}
				}
				catch (Exception)
				{
				}

				Console.WriteLine("HTTP/1.1 404 \"Unknown Request\"");
				Console.WriteLine("Content-Type: text/plain");
				Console.WriteLine("");
				Console.WriteLine("failed(2).");
				return;
			}


			//POSTの場合は、情報の配信→受信内容を保存する
			if (strMethod == "POST")
			{
				//何らかのエラーがありえる状態でも、とりあえず先に200 okを返しておく
				Console.WriteLine("HTTP/1.1 200 \"OK\"");
				Console.WriteLine("Content-Type: text/html");
				Console.WriteLine("");

				try
				{
					using (Stream st = Console.OpenStandardInput())			//標準入力から配信内容取得
					using (BinaryReader br = new BinaryReader(st))			//StreamReader.ReadLine()などは使えない。BinaryReaderで処理
					{
						string strLength = Environment.GetEnvironmentVariable("CONTENT_LENGTH");
						int nLength = Int32.Parse(strLength);

						if (nLength < 1000000)		//約1MB以上ある場合は無視する(そんなに大きなサイズが来ることはないはず)
						{
							byte[] data = new byte[nLength];
							br.Read(data, 0, nLength);

							//効率は悪いけど一度文字列に変換しちゃう
							string strQuery = Encoding.UTF8.GetString(data, 0, nLength);

							//ファイル保存。ファイル名は日時ticks数値
							string strFile = _strFolerOut + DateTime.Now.Ticks + ".txt";
							using (StreamWriter sw = new StreamWriter(strFile, false, Encoding.UTF8))
							{
								sw.Write(strQuery);
							}
						}
					}
				}
				catch (Exception)
				{
				}
				return;
			}
		}


		/// <summary>
		/// 「a=123&b=456&c=789」という形のクエリ文字列を、名前/値に分離してNameValueCollectionに格納する
		/// </summary>
		static NameValueCollection QueryToNameValueCollection(string strQuery)
		{
			NameValueCollection ret = new NameValueCollection();

			string[] astrQuery = strQuery.Split('&');

			foreach (string strItem in astrQuery)
			{
				string[] astrPair = strItem.Split('=');

				if (astrPair.Length == 0)
					continue;

				string strName = "";
				string strValue = "";

				if (astrPair.Length >= 1)
					strName = astrPair[0];
				if (astrPair.Length >= 2)
					strValue = astrPair[1];

				if (strName == "")
					continue;

				ret.Add(strName, strValue);
			}

			return ret;
		}

	}
}




最後に作成したsubscriber CGIの動作確認を簡単に。

まずはビルドする前に、「プロジェクト」メニューにある「jmasubscriberのプロパティ」を選択し、構成「Release」にたいして、デバッグ」タブにある「Visual Studioホスティングプロセスを有効にする」のチェックをはずします。

そしてReleaseビルド。
出来上がった「jmasubscriber.exe」をサーバーへアップロードし、実行権限を付加します。
そしてアップロード先へWEBブラウザーでアクセスして404エラーが出れば成功です(「http://www.example.com/jmasubscriber.exe」などへアクセス)。

次にsubscriberのチェックをします。

汎用的なsubscriberのテストように公開されているサービス「https://pubsubhubbub.appspot.com/」を利用します。
Subscribe to a feed or debug your subscriber」へアクセスし、

・「Callback URL」にexeへのURL(「http://www.example.com/jmasubscriber.exe」など)
・「Topic URL」にダミーとなるURL(「http://www.example.com/test.txt」など。とりあえずなんでもok)
・「Verify type」を「Synchronous」
・「Mode」を「Subscribe」
・「Verify token」を「test_token」(ソースコード内で設定した値)

として、「Do It!」ボタンを押して何も起きなければ成功。ページが切り替わってエラーが表示されたら失敗です。
さらに「Verify token」を「aaaaaa」のように適当な文字列に設定して「Do It!」ボタンを押してエラーが表示されたら成功です。

同様にpublishの動作確認をしたい場合はTopic URLに適当に作成したatom feedのxmlを置いて「publish」。feedを更新して「publish」というようにします。




subscriberが出来上がったら、「Callback URL」のURLと「Verify token」の値や住所氏名などを添えて指定様式で気象庁に登録申請します。

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

第01回 気象庁防災情報XMLの概要

"天気予報"を表示する場合、ネックになる点が2つあります。

・天気予報をどこからどう取得するか
・天気予報をどのように表示するか

表示方法~晴れマークの絵をどうするかなどについてはとりあえず保留。
天気予報の取得方法は・・・
無料でAPIを利用できる天気予報の提供サービスは、openweathermap、ライブドア、msnなどなどがあります。
しかしどれも情報量が微妙だったりとイマイチぱっとしないので今回は気象庁の配信サービスを利用することにしました。


気象庁は「気象庁防災情報XML」の配信サービスを行っています。
このサービスでは地震発生情報、火山灰降灰予想、気象警報/注意報、海上予報など、天気予報以外の情報も提供されています。

※残念ながら気温/風速/降水量などの観測値に関しては配信されていません(瞬間最大風速などの臨時配信はあり)


「気象庁防災情報XML」は"気象庁が配信するサービス"です。

通常の情報提供形態は情報を見たい側が、提供している側に対して情報を取得(ダウンロード)しに行く形です。
それに対して気象庁防災情報XMLは、気象庁側から情報が"配信"されるpush形式のサービスです。

そのため気象庁防災情報XMLの提供を受けるためには、情報受信用のサーバー構築(subscriberの構築)が必須です。
また、気象庁側へ利用目的や利用者名/住所などを添えて、電子メールで申請する必要があります。

気象庁防災情報XMLフォーマット
http://xml.kishou.go.jp/index.html


利用開始までの流れ

1. サーバー用意
2. subscriber(CGI)の構築
3. subscriberの動作テスト
4. 気象庁へ申請
5. (申請から約1週間で配信開始。2週間程度かかることもあるとのこと)


データ受信の流れ

6. (気象庁 → 自サーバー)(数日に1回) 自サーバーが"生きている"かの確認アクセス(Subscribeチェック)
7. (気象庁 ← 自サーバー)(返信必須 ) "生きている"返答(チャレンジコード返信)

8. (気象庁 → 自サーバー)(毎日100回以上) 気象庁防災情報配信(URL情報のみの配信)
9. (気象庁 ← 自サーバー)(必要に応じて ) 気象庁防災情報受信(↑に記載のURLをダウンロード)

データ受信で特に重要なのが6~7番です。これに失敗すると配信登録が解除され、データ配信が止まってしまいます。
ちなみにすべての配信データをそのまま保存すると(9番でダウンロードファイルをすべて保管すると)、1日で20MBほど。概算で年間7GB以上になります。


今回はサーバーはWindowsベース、subscriberはC#で構築を行います。

参考までに、上記9番でダウンロードした1日分のXMLファイルが以下です。
XMLファイル数1071、合計23MB。ZIPで固めても2MB以上ありました。
2015/4/11の全配信XMLファイルをダウンロード

2016年04月10日

第18回 ルビ位置を設定する

ルビ位置がすべて左端だと読みにくいので、今回は少しはまともな位置に表示されるように調整します。

本来であれば文字列の横幅を使用フォントを利用して計算、そこから表示位置を逆算するべきなのですが、PDF内で使われている文字の幅取得など考えるだけで疲れそうなので割愛し、適当に設定することにしました。

■PDFTextWriter.cs
		/// <summary>
		/// "小説家になろう"の縦書きPDF変換処理
		/// </summary>
		public bool ConvertPDFFile(string strFile, PDFTextReader srcPDF)
		{
			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;



					//フォント埋め込み
					{
						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, strFontObjName, @"msgothic.otf");//, "MS-Gothic", "MS Gothic");
						}

						//フォント一覧のみのリソース
						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;
					{
						for (int i = 0; i < srcPDF.Pages.Count; i++)
						{
							listPage.Add(nObjIndex + 1 + i * 2);	//iページ目のインデックスを渡す
						}

						WritePages(bw, ref nObjIndex, listPage);
					}


					//サイズは適当に決定
					int nWidth = 455;
					int nHeight = 615;

					//ページ出力
					for (int i = 0; i < srcPDF.Pages.Count; i++)
					{
						PDFPage page = srcPDF.Pages[i];

						WritePageContentsIndex(bw, ref nObjIndex, nPagesReferenceIndex, nResourceIndex, nObjIndex + 1, nWidth, nHeight);

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

							//テキストの準備
							{
								float y = nHeight - 5;
								foreach (object item in page.Items)
								{
									if (item.GetType() != typeof(PDFText))
										continue;

									PDFText text = (PDFText)item;

									//ページ番号は左下に表示
									if ((int)(text.fY) == 56)		//"小説家になろう"PDFのページ番号y座標は56.7?
									{
										listTexts.Add(new PDFText("F0", text.fPoint, 5, 5, text.strText));
										continue;
									}

									//表紙(i == 0)と最終ページ以外はオリジナルの行間隔で表示
									if (i > 0 && i < srcPDF.Pages.Count - 1)
										y = (int)(text.fX * nHeight / 842.0);
									else
										y -= 17;		//表紙(i == 0)と最終ページは固定行間隔で表示


									float x = 10;

									//"小説家になろう"PDFのルビは文字サイズ7.0f?
									if ((int)text.fPoint == 7)
									{
										x = (float)(nWidth - text.fY * nWidth / 595.0) - 50;
										x *= 1.2f;			//フォントの幅を計算してどの位置に表示するか産出するのが面倒だから、MSゴシックと小説家になろうPDFの横幅差を1.2と決め打ち
										y += 3;
									}

									listTexts.Add(new PDFText("F0", text.fPoint, x, y, text.strText));
								}
							}


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

								//Kindleはコンテンツ内容によって勝手にズーム表示しちゃうから、
								//すべてのページに枠線を描くことで勝手にズームしないようにする
								//座標固定で枠線を描く
								PrepareLineContents(ms, 2, 2, 2, nHeight - 2);
								PrepareLineContents(ms, 2, 2, nWidth - 2, 2);
								PrepareLineContents(ms, nWidth - 2, 2, nWidth - 2, nHeight - 2);
								PrepareLineContents(ms, 2, nHeight - 2, nWidth - 2, nHeight - 2);

								ms.Flush();

								byte[] data = ms.ToArray();

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

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

			return true;
		}

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

2016年04月08日

日時文字列をDateTimeに変換する(C#)

日時の表記方法には様々なものがあります。
そのうち、以下のような表記をDateTimeに変換する処理です。

■使い方
DateTime dt;

StringToDateTime("   2016-04-08T12:34:56+09:00  ", out dt);		//日時オフセットが9時間。前後に空白
StringToDateTime("2016-04-08T01:34:56-02:00  ", out dt);		//日時オフセットがマイナス2時間。後ろに空白
StringToDateTime("2016-04-08T03:34:56Z", out dt);				//UTC表記の日時
StringToDateTime("2016:04:08 12:34", out dt);					//秒なし。日時区切りが「:」
StringToDateTime("2016/4/8 12:34", out dt);						//日時が1桁
StringToDateTime("2016/4/08(金)12:34:56", out dt);			//日本語表記。カッコで曜日がある
StringToDateTime("2016年04月08日", out dt);						//日付のみ
StringToDateTime("2016年04月08日(金)", out dt);
StringToDateTime("2016年04月08日(金)12時34分56秒", out dt);	//日付と時刻。全部日本語
StringToDateTime("2016年04月08日12:34:56", out dt);				//日付と時刻の間に空白なし
StringToDateTime("2016年04月08日 12:34:56", out dt);			//日付と時刻の間に空白あり
StringToDateTime("2016年04月08日 12:34", out dt);
■ソースコード
		/// <summary>
		/// 「yyyy/mm/dd」「yyyymmdd」「yyyy/mm/dd hh:mm」「yyyy/mm/dd hh:mm:ss」をDateTimeにする
		/// 
		///「2016-04-07T19:01:01+09:00」の形式対応(時差がプラス9時間でなければそれに合わせて変換後返す)
		///「2016-04-07T10:00:44Z」(UTC日時)の形式対応(プラス9時間して返す)
		/// 
		/// yyyymmddの区切りは「年月日」「/」「:」「-」に対応
		/// hhmmssの区切りは「時分秒」「:」に対応
		/// カッコで囲まれた曜日表記に対応 ex. 「(日)」「(水)」「(Wed)」「(sat.)」
		/// </summary>
		public static bool StringToDateTime(string strDate, out DateTime dtDate)
		{
			//「yyyy年mm月dd日」				→「yyyy/mm/dd」
			//「yyyy年mm月dd日hh時mm分」		→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日hh時mm分」		→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日 hh時mm分」		→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日hh時mm分ss秒」	→「yyyy/mm/dd hh:mm:ss」
			//「yyyy年mm月dd日 hh時mm分ss秒」	→「yyyy/mm/dd hh:mm:ss」
			//「yyyy年mm月dd日hh:mm」			→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日 hh:mm」			→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日hh:mm:ss」		→「yyyy/mm/dd hh:mm:ss」
			//「yyyy年mm月dd日 hh:mm:ss」		→「yyyy/mm/dd hh:mm:ss」
			{
				//カッコで囲まれた曜日の除去
				{
					//カッコを全角に統一
					strDate = strDate.Replace("(", "(");
					strDate = strDate.Replace(")", ")");

					//カッコを1つのスペースに変換
					Regex regex = new Regex(@"((.*))");
					MatchCollection matchCol = regex.Matches(strDate);
					if (matchCol.Count > 0)
						strDate = strDate.Replace(matchCol[0].Groups[1].Value, " ");
				}

				strDate = strDate.Replace(" ", " ");	//全角スペースの半角化
				strDate = strDate.Replace("年", "/");
				strDate = strDate.Replace("月", "/");
				strDate = strDate.Replace("日 ", " ");	//後ろにスペースのある「日」はそのまま除去
				strDate = strDate.Replace("時", ":");
				strDate = strDate.Replace("分", ":");
				strDate = strDate.Replace("秒", "");

				//2つ以上のスペースを1つに変換
				while (strDate.IndexOf("  ") >= 0)
				{
					strDate = strDate.Replace("  ", " ");
				}
				//前後のスペースを除去
				{
					strDate = strDate.TrimStart();
					strDate = strDate.TrimEnd();
				}

				//以下の4パターンを考慮して「日」を除去する
				//「yyyy/mm/dd日」→「yyyy/mm/dd」
				//「yyyy/mm/dd日hh:mm」→「yyyy/mm/dd hh:mm」
				//「yyyy/mm/dd日hh:mm:ss」→「yyyy/mm/dd hh:mm:ss」
				//「yyyy/mm/dd日 hh:mm:ss」→「yyyy/mm/dd hh:mm:ss」
				if (strDate.IndexOf("日") > 0)
				{
					strDate = strDate.Replace("日 ", "");			//スペース除去
					Regex regex = new Regex(@"(\d+)日(\d+)");
					MatchCollection matchCol = regex.Matches(strDate);
					if (matchCol.Count > 0)
						strDate = strDate.Replace("日", " ");
					else
						strDate = strDate.Replace("日", "");
				}
			}



			//「yyyy/mm/dd」「yyyy/mm/d」「yyyy/m/dd」「yyyy/m/d」
			if (strDate.Length == 10 || strDate.Length == 9 || strDate.Length == 8)
			{
				Regex regex = new Regex(@"(\d{4})[-/:](\d+)[-/:](\d+)");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 4)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value));
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}

			//「yyyymmdd」
			if (strDate.Length == 8)
			{
				Regex regex = new Regex(@"(\d{4})(\d{2})(\d{2})");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 4)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value));
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}

			//「yyyy/mm/dd hh:mm」「yyyy/mm/d hh:mm」「yyyy/m/dd hh:mm」「yyyy/m/d hh:mm」「yyyy/m/d h:mm」
			if (strDate.Length == 16 || strDate.Length == 15 || strDate.Length == 14 || strDate.Length == 13)
			{
				Regex regex = new Regex(@"(\d{4})[-/:](\d+)[-/:](\d+) (\d+):(\d{2})");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 6)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value)
									, Int32.Parse(matchCol[i].Groups[4].Value), Int32.Parse(matchCol[i].Groups[5].Value), 0);
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}

			//「yyyy/mm/dd hh:mm:ss」「yyyy/m/dd hh:mm:ss」「yyyy/mm/d hh:mm:ss」「yyyy/m/d hh:mm:ss」「yyyy/m/d h:mm:ss」
			if (strDate.Length == 19 || strDate.Length == 18 || strDate.Length == 17 || strDate.Length == 16)
			{
				Regex regex = new Regex(@"(\d{4})[-/:](\d+)[-/:](\d+) (\d+):(\d{2}):(\d{2})");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 7)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value)
									, Int32.Parse(matchCol[i].Groups[4].Value), Int32.Parse(matchCol[i].Groups[5].Value), Int32.Parse(matchCol[i].Groups[6].Value));
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}


			//「2016-04-07T19:01:01+09:00」の形式
			//
			//「2016-04-07T19:01:01」がオフセット+09:00(日本時間)という意味。戻り値は「2016-04-07 19:01:01」
			//「2016-04-07T19:01:01+00:00」の場合、オフセット+00:00(UTC)という意味。戻り値はJST変換して「2016-04-08 04:01:01」
			if (strDate.Length == 25)
			{
				//Parseですませちゃう
				try
				{
					dtDate = DateTime.Parse(strDate);
					return true;
				}
				catch (Exception)
				{
				}

				//Regex regex = new Regex(@"(\d{4})[-/:](\d+)[-/:](\d+)T(\d+):(\d{2}):(\d{2})([+-])(\d{2}):(\d{2})");
				//MatchCollection matchCol = regex.Matches(strDate);
				//for (int i = 0; i < matchCol.Count; i++)
				//{
				//	if (matchCol[i].Groups.Count == 10)
				//	{
				//		try
				//		{
				//			dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value)
				//					, Int32.Parse(matchCol[i].Groups[4].Value), Int32.Parse(matchCol[i].Groups[5].Value), Int32.Parse(matchCol[i].Groups[6].Value));


				//			//渡された文字列の時差取得
				//			bool bTimeZonePlus = (matchCol[i].Groups[7].Value == "+") ? true : false;
				//			TimeSpan spanTimeZone = new TimeSpan(Int32.Parse(matchCol[i].Groups[8].Value), Int32.Parse(matchCol[i].Groups[9].Value), 0);

				//			//UTCとシステムの時差取得
				//			TimeSpan spanUTC = TimeZoneInfo.Local.BaseUtcOffset;		//UTCオフセット取得(日本なら常に+09:00)

				//			TimeSpan span;

				//			//両者の差を求めて
				//			if (bTimeZonePlus)
				//				span = spanUTC - spanTimeZone;
				//			else
				//				span = spanUTC + spanTimeZone;

				//			//渡された文字列の時差に合わせて変換
				//			dtDate = dtDate + span;

				//			return true;
				//		}
				//		catch (Exception)
				//		{
				//		}
				//	}
				//}
			}

			//「2016-04-07T10:00:44Z」の形式
			//
			//↑はタイムオフセットゼロ=UTCという意味。日本時間にするため時差9時間プラスして返す
			if (strDate.Length == 20)
			{
				//Parseですませちゃう
				try
				{
					dtDate = DateTime.Parse(strDate);
					return true;
				}
				catch (Exception)
				{
				}

				//Regex regex = new Regex(@"(\d{4})[-/:](\d+)[-/:](\d+)T(\d+):(\d{2}):(\d{2})Z");
				//MatchCollection matchCol = regex.Matches(strDate);
				//for (int i = 0; i < matchCol.Count; i++)
				//{
				//	if (matchCol[i].Groups.Count == 7)
				//	{
				//		try
				//		{
				//			dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value)
				//					, Int32.Parse(matchCol[i].Groups[4].Value), Int32.Parse(matchCol[i].Groups[5].Value), Int32.Parse(matchCol[i].Groups[6].Value));


				//			//UTCとシステムの時差取得
				//			TimeSpan spanUTC = TimeZoneInfo.Local.BaseUtcOffset;		//UTCオフセット取得(日本なら常に+09:00)

				//			//渡された文字列の時差に合わせて変換
				//			dtDate = dtDate + spanUTC;

				//			return true;
				//		}
				//		catch (Exception)
				//		{
				//		}
				//	}
				//}
			}

			//Parseしてみる
			try
			{
				dtDate = DateTime.Parse(strDate);
				return true;
			}
			catch (Exception)
			{
			}


			dtDate = DateTime.MinValue;
			return false;
		}

2016年04月04日

第17回 androidアプリでPDFを表示する

先月中旬にMicrosoftがXamarinを買収し、
先月末にXamarin Studioなどが無料化されました。
これによりCommunity Editionを含むVisual Studio 2015にXamarinの関連プラグインが同梱され、
Visual Studio上のC#でandroidやiOSデバイス用アプリの開発が無料で可能になっています。

今回はVisual Studio 2015を利用してandroidで縦書きPDFを横書き表示するアプリを作成してみます。

処理自体は前回までの作業でほぼできているため、
実装が必要なのはandroid固有のUI操作とテキスト表示処理のみです。



まずはandroidプロジェクトの準備。

(Xamarinがインストールされている)Visual Studio 2015を起動し、
「ファイル」メニューにある「新規作成」から「プロジェクト」を選択。
「テンプレート」は「Visual C#」にある「andoroid」の「Blank App (Android)」を選択。
プロジェクト名は「PDFAndroid」としました。

プロジェクトが自動生成されたら、
「プロジェクト」メニューから「PDFAndroidのプロパティ」を選択し、プロジェクト設定画面を開きます。
「Application」タブにある「Compile using Android version」を手持ちのandroidデバイスのOSバージョンに合わせて設定します。私は「Android 4.3 (Jelly Bean)」にしました。
(この設定を間違えて新しいOSバージョンのままだと配置などができません)

androidスマートフォンをUSBデバッグ有効にしてPCへ接続すると、
ツールバーのデバッグ開始ボタンの横にスマートフォンの名前とOSバージョンが表示されます。
ここで試しに実行するとビルドされ、アプリがスマートフォンへ転送(配置)され、何も機能のないアプリが起動します。



androidプロジェクトの準備ができたら一気に実装します。
と言っても作業は前回までのソースコードをandroidプロジェクト内にコピペするのがほとんどです。
この辺は本当にC#様さまです。UIに関連する部分以外でプラットフォーム間の差異を感じることは(たまにしか)ありません。

・PDFPage.cs
・PDFRawReader.cs
・PDFTextReader.cs
の3ファイルを前回のプロジェクトからandroidプロジェクトのフォルダへコピー。
そして「プロジェクト」メニューの「既存の項目の追加」からそのファイルをプロジェクトへ追加します。

最後にMainActivity.csに表示/操作処理を用意すれば終わりです。

これで縦書きPDFを横書き表示できました。

ただし・・・ものすごく遅いです。最初にPDFを全部読むために数十秒かかります。
読み終わった後のページ送りなどは軽いのですが実用には向かない速度でした。
実用性を考えるなら根本的に処理を見なおしたほうがよさそうです。

■MainActivity.cs
using System;
using Android.App;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;
using Android.Graphics;
using System.IO;
using Pdf2Pdf;

namespace PDFAndroid
{
	class MyView : View
	{
		public MyView(Context context)
			: base(context)
		{
			SetBackgroundColor(Color.Gray);
		}

		int _nPage = 0;
		PDFTextReader _pr = null;


		public override void Draw(Canvas canvas)
		{
			base.Draw(canvas);

			//「小説家になろう」からダウンロードした縦書きPDFを指定
			//パスはandroid内のPDF保存場所を指定
			string strPDFFile = "/storage/emulated/0/N9442CW.pdf";

			if (File.Exists(strPDFFile))
			{
				if (_pr == null)
				{
					_pr = new PDFTextReader();
					_pr.Read(strPDFFile);
				}

				DrawPage(_pr, _nPage, canvas);
			}
			else
			{
				using (Paint paint = new Paint())
				{
					paint.Color = Color.Black;
					canvas.DrawText("PDFが見つかりません", 50, 50, paint);
				}
			}
		}

		public void PageNext()
		{
			if (_pr == null || _pr.Pages.Count == 0)
				return;

			_nPage++;
			if (_nPage >= _pr.Pages.Count)
				_nPage = _pr.Pages.Count - 1;

			Invalidate();
		}

		public void PageBack()
		{
			if (_pr == null || _pr.Pages.Count == 0)
				return;

			_nPage--;
			if (_nPage < 0)
				_nPage = 0;

			Invalidate();
		}


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

			PDFPage page = pr.Pages[nPage];

			int nWidth = Width;
			int nHeight = Height;

			using (Paint paint = new Paint())
			{
				paint.Color = Color.Black;

				float y = 10;
				foreach (object obj in page.Items)
				{
					if (obj.GetType() != typeof(PDFText))
						continue;

					PDFText text = (obj as PDFText);

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

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

					float x = 10;

					//表紙(i == 0)と最終ページ以外はオリジナルの行間隔で表示
					if (nPage > 0 && nPage < _pr.Pages.Count - 1)
						y = nHeight - (int)(text.fX * nHeight / 842.0);
					else
						y += 30;        //表紙(i == 0)と最終ページは固定行間隔で表示

					paint.TextSize = text.fPoint * 1.5f;

					canvas.DrawText(text.strText, x, y, paint);
				}
			}

			return true;
		}
	}




	[Activity(Label = "PDFAndroid", MainLauncher = true, Icon = "@drawable/icon")]
	public class MainActivity : Activity
	{
		MyView _view = null;

		protected override void OnCreate(Bundle bundle)
		{
			base.OnCreate(bundle);

			RequestWindowFeature(WindowFeatures.NoTitle);
			SetContentView(Resource.Layout.Main);

			LinearLayout layout = new LinearLayout(this);
			SetContentView(layout);

			_view = new MyView(this);
			layout.AddView(_view);
		}



		//前回タッチした座標を記録
		float _fLastX = 0;
		float _fLastY = 0;
		bool _bMove = false;

		/// <summary>
		/// タッチイベント処理
		/// </summary>
		public override bool OnTouchEvent(MotionEvent e)
		{
			if (e.Action == MotionEventActions.Down)
			{
				_fLastX = e.RawX;
				_fLastY = e.RawY;
				_bMove = false;
			}

			if (e.Action == MotionEventActions.Move)
			{
				_bMove = true;      //スライドした
			}

			if (e.Action == MotionEventActions.Up)
			{
				if (_bMove && Math.Abs(_fLastX - e.RawX) > 50)      //ちょっとしかスライドしなかったときは無反応
				{
					if (_fLastX < e.RawX)
					{
						//左から右にスライドされた
						_view.PageBack();
					}
					else
					{
						//右から左にスライドされた
						_view.PageNext();
					}
				}

				_bMove = false;
			}

			return base.OnTouchEvent(e);
		}

	}
}

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

第16回 フォント名をフォントファイルから取得する

今回はopen type fontのフォントファイルからフォント名/フォントファミリー名を取得します。

open typeのファイル仕様に沿ってファイルを読むのみです。
フォント名はプラットフォームや表示言語に応じて複数格納されています。
今回はWindowsのユニコード向け、英語圏用の名前を取得/利用しています。

■PDFTextWrite.cs
		/// <summary>
		/// フォントの埋め込み
		/// 
		/// フォント名/フォントファミリー名を指定しない場合はフォントファイルから自動取得を試みる
		/// </summary>
		void WriteFont_EmbeddedUnicode(PDFRawWriter bw, ref int nObjIndex, string strFontObjName, string strFontFile, string strFont = "", string strFontFamily = "")
		{
			ushort nRangeMin = 0xFFFF;
			ushort nRangeMax = 0;

			//opentypeフォントファイルからcmapを読み込む
			IDictionary<ushort, byte[]> cmap = LoadCMap(strFontFile, out nRangeMin, out nRangeMax, ref strFontFamily, ref strFont);
			_cmapFonts.Add(strFontObjName, cmap);


			_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++;





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

		/// <summary>
		/// open type fontファイルからcmapを読み取る
		/// 
		/// open type fontの仕様通りにファイルを読むだけ
		/// マジックナンバーなどでファイルチェックをするべきだがしていない
		///
		/// strFontFamilyName/strFontPostScriptName は==""だった場合のみ、フォントファイルから読み出す
		/// 
		/// 仕様
		/// https://www.microsoft.com/typography/otspec/otff.htm
		/// </summary>
		IDictionary<ushort, byte[]> LoadCMap(string strFontFile, out ushort nRangeMin, out ushort nRangeMax, ref string strFontFamilyName, ref string strFontPostScriptName)//, out int nBBXMin, out int nBBXMax, out int nBBYMin, out int nBBYMax, out int nAscender, out int nDescender)
		{
			IDictionary<ushort, byte[]> cmap = new Dictionary<ushort, byte[]>();

			nRangeMin = 0xFFFF;
			nRangeMax = 0;

			int nBBXMin = 0;
			int nBBXMax = 0;
			int nBBYMin = 0;
			int nBBYMax = 0;

			int nAscender = 0;
			int nDescender = 0;

			using (FileStream fs = new FileStream(strFontFile, FileMode.Open, FileAccess.Read))
			using (BinaryReader br = new BinaryReader(fs))
			{
				// https://www.microsoft.com/typography/otspec/otff.htm
				byte[] sfntVer = br.ReadBytes(4);
				uint nTableCount = ByteToUInt_BE(br.ReadBytes(2));
				uint nSearchRange = ByteToUInt_BE(br.ReadBytes(2));
				uint nEntrySelector = ByteToUInt_BE(br.ReadBytes(2));
				uint nRangeShift = ByteToUInt_BE(br.ReadBytes(2));

				uint nCMapOffset = 0;
				uint nCMapLength = 0;

				uint nHeadOffset = 0;
				uint nHeadLength = 0;

				uint nHheaOffset = 0;
				uint nHheaLength = 0;

				uint nOS2Offset = 0;
				uint nOS2Length = 0;

				uint nNameOffset = 0;
				uint nNameLength = 0;

				for (uint i = 0; i < nTableCount; i++)
				{
					byte[] tag = br.ReadBytes(4);
					uint checkSum = ByteToUInt_BE(br.ReadBytes(4));
					uint offset = ByteToUInt_BE(br.ReadBytes(4));		//	Offset from beginning of TrueType font file.
					uint length = ByteToUInt_BE(br.ReadBytes(4));

					string strTag = Encoding.ASCII.GetString(tag);
					if (strTag == "cmap")
					{
						nCMapOffset = offset;
						nCMapLength = length;
					}
					if (strTag == "head")
					{
						nHeadOffset = offset;
						nHeadLength = length;
					}
					if (strTag == "hhea")
					{
						nHheaOffset = offset;
						nHheaLength = length;
					}
					if (strTag == "OS/2")
					{
						nOS2Offset = offset;
						nOS2Length = length;
					}
					if (strTag == "name")
					{
						nNameOffset = offset;
						nNameLength = length;
					}
				}


				if (strFontFamilyName == "" || strFontPostScriptName == "")
				{
					if (nNameOffset > 0 && nNameLength > 0)
					{
						fs.Seek(nNameOffset, SeekOrigin.Begin);

						// https://www.microsoft.com/typography/otspec/name.htm
						uint format = ByteToUInt_BE(br.ReadBytes(2));
						uint count = ByteToUInt_BE(br.ReadBytes(2));			//Number of name records.
						uint stringOffset = ByteToUInt_BE(br.ReadBytes(2));		//Offset to start of string storage (from start of table).

						//プラットフォーム3、エンコーディング1(Windowsのunicode)で、さらにランゲージID1033(英語圏用の名前)のデータのみ収集
						List<uint> list31NameID = new List<uint>();
						List<uint> list31Length = new List<uint>();
						List<uint> list31Offset = new List<uint>();

						//Name Records
						for (uint i = 0; i < count; i++)
						{
							uint platformID = ByteToUInt_BE(br.ReadBytes(2));
							uint encodingID = ByteToUInt_BE(br.ReadBytes(2));
							uint languageID = ByteToUInt_BE(br.ReadBytes(2));
							uint nameID = ByteToUInt_BE(br.ReadBytes(2));
							uint length = ByteToUInt_BE(br.ReadBytes(2));		//String length (in bytes).
							uint offset = ByteToUInt_BE(br.ReadBytes(2));		//String offset from start of storage area (in bytes).

							//プラットフォーム3、エンコーディング1(Windowsのunicode)で、さらにランゲージID1033(英語圏用の名前)のデータのみ収集
							if (platformID != 3 || encodingID != 1 || languageID != 1033)
								continue;

							list31NameID.Add(nameID);
							list31Length.Add(length);
							list31Offset.Add(offset);
						}

						//format==1の場合のみ言語タグ情報がある
						if (format == 1)
						{
							uint langTagCount = ByteToUInt_BE(br.ReadBytes(2));

							//LangTagRecord
							for (uint i = 0; i < langTagCount; i++)
							{
								uint length = ByteToUInt_BE(br.ReadBytes(2));
								uint offset = ByteToUInt_BE(br.ReadBytes(2));		//	Language-tag string offset from start of storage area (in bytes).
							}
						}

						//Storage area開始
						long nStorageStart = fs.Position;

						for (int i = 0; i < list31NameID.Count; i++)
						{
							if (list31NameID[i] == 1)		//font family name
							{
								if (strFontFamilyName == "")
								{
									fs.Seek(nStorageStart + list31Offset[i], SeekOrigin.Begin);
									strFontFamilyName = Encoding.BigEndianUnicode.GetString(br.ReadBytes((int)list31Length[i]));
								}
							}

							if (list31NameID[i] == 6)		//Postscript name for the font
							{
								if (strFontPostScriptName == "")
								{
									fs.Seek(nStorageStart + list31Offset[i], SeekOrigin.Begin);
									strFontPostScriptName = Encoding.BigEndianUnicode.GetString(br.ReadBytes((int)list31Length[i]));
								}
							}
						}
					}
				}

				if (nHheaOffset > 0 && nHheaLength > 0)
				{
					fs.Seek(nHheaOffset, SeekOrigin.Begin);

					// https://www.microsoft.com/typography/otspec/hhea.htm
					byte[] version = br.ReadBytes(4);
					nAscender = ByteToInt_BE(br.ReadBytes(2));
					nDescender = ByteToInt_BE(br.ReadBytes(2));
					int LineGap = ByteToInt_BE(br.ReadBytes(2));
					uint advanceWidthMax = ByteToUInt_BE(br.ReadBytes(2));
					int minLeftSideBearing = ByteToInt_BE(br.ReadBytes(2));
					int minRightSideBearing = ByteToInt_BE(br.ReadBytes(2));
					int xMaxExtent = ByteToInt_BE(br.ReadBytes(2));
					int caretSlopeRise = ByteToInt_BE(br.ReadBytes(2));
					int caretSlopeRun = ByteToInt_BE(br.ReadBytes(2));
					int caretOffset = ByteToInt_BE(br.ReadBytes(2));
					int reserved1 = ByteToInt_BE(br.ReadBytes(2));
					int reserved2 = ByteToInt_BE(br.ReadBytes(2));
					int reserved3 = ByteToInt_BE(br.ReadBytes(2));
					int reserved4 = ByteToInt_BE(br.ReadBytes(2));
					int metricDataFormat = ByteToInt_BE(br.ReadBytes(2));
					uint numberOfHMetrics = ByteToUInt_BE(br.ReadBytes(2));
				}
		/// <summary>
		/// "小説家になろう"の縦書きPDF変換処理
		/// </summary>
		public bool ConvertPDFFile(string strFile, PDFTextReader srcPDF)
		{
			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;



					//フォント埋め込み
					{
						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, strFontObjName, @"msgothic.otf");//, "MS-Gothic", "MS Gothic");
						}

						//フォント一覧のみのリソース
						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++;
					}
				}
		/// <summary>
		/// PDF作成例
		/// </summary>
		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;



					//フォント埋め込み
					{
						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, strFontObjName, @"msgothic.otf", "MS-Gothic", "MS Gothic");
						}
						{
							string strFontObjName = "F1";
							listFonts.Add(new KeyValuePair<string, int>(strFontObjName, nObjIndex));
							WriteFont_Ascii(bw, ref nObjIndex, "Times-Italic", strFontObjName);						//欧文フォント指定
						}
						{
							string strFontObjName = "F2";
							listFonts.Add(new KeyValuePair<string, int>(strFontObjName, nObjIndex));
							WriteFont_UnicodeJapanese(bw, ref nObjIndex, "KozMinPr6N-Regular", strFontObjName);		//日本語フォント指定(フォント埋め込みなし)
						}

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

第15回 スレッドで処理する

複数のPDFファイルを一度に変換すると、変換中はUI動作が固まります。
それを防ぐため、変換処理をスレッド上で実行するように変更します。

今回はTaskを利用しました。
処理の開始中はドラッグアンドドロップを禁止し、新たな処理が開始されるのを防いでいます。

■Form1.cs
using System;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Pdf2Pdf
{
	public partial class Form1 : Form
	{
		public Form1()
		{
			InitializeComponent();

			Label label1 = new Label();
			label1.Parent = this;
			label1.Dock = DockStyle.Fill;
			label1.TextAlign = ContentAlignment.MiddleCenter;
			label1.Text = "ここに\"小説家になろう\"縦書きPDFをドラッグアンドドロップしてください";

			label1.AllowDrop = true;

			label1.DragEnter += (sender, e) =>
			{
				if (e.Data.GetDataPresent(DataFormats.FileDrop))
				{
					e.Effect = DragDropEffects.Copy;
				}
			};

			label1.DragDrop += (sender, e) =>
			{
				if (e.Data.GetDataPresent(DataFormats.FileDrop))
				{
					string[] items = (string[])e.Data.GetData(DataFormats.FileDrop);


					//タスク(スレッド)で実行
					Task.Run(() =>
					{
						string strLabel = label1.Text;		//ラベル文字を処理中は退避

						Invoke((MethodInvoker)delegate
						{
							label1.AllowDrop = false;		//処理中は新たなドラッグアンドドロップを禁止する
							label1.Text = "変換処理を開始します。\n処理合計ファイル数:" + items.Length;
						});

						for (int i = 0; i < items.Length; i++)
						{
							string file = items[i];

							if (File.Exists(file) == false)
								continue;

							//「小説家になろう」縦書きPDFを読み込む
							PDFTextReader pr = new PDFTextReader();
							bool ret = pr.Read(file);

							string strTitle = "";

							//読み込んだ縦書きPDFに含まれる小説のタイトルを取得
							//(タイトルは最初のページの最初のstrText)
							foreach (PDFPage page in pr.Pages)
							{
								foreach (object obj in page.Items)
								{
									if (obj.GetType() != typeof(PDFText))
										continue;

									strTitle = (obj as PDFText).strText;
									if (strTitle != "")
										break;
								}
								if (strTitle != "")
									break;
							}

							//小説タイトルをファイル名にする
							// →ファイル名に使えない文字を除去
							{
								//ありがちな文字は決め打ちで全角に
								strTitle = strTitle.Replace('*', '*');
								strTitle = strTitle.Replace('\\', '¥');
								strTitle = strTitle.Replace(':', ':');
								strTitle = strTitle.Replace('<', '<');
								strTitle = strTitle.Replace('>', '>');
								strTitle = strTitle.Replace('?', '?');
								strTitle = strTitle.Replace('|', '|');

								char[] pcbInvalid = Path.GetInvalidFileNameChars();

								//使えない文字除去
								foreach (char c in pcbInvalid)
								{
									strTitle = strTitle.Replace(c.ToString(), "");
								}

								//タイトルがなくなったら、元々のPDFのファイル名に"_cnv"を付加
								if (strTitle == "")
									strTitle = Path.GetFileNameWithoutExtension(file + "_cnv");
							}

							string strNewFile = "";

							//書き出したいファイル名がすでに存在していたら、上書きしないように存在しないファイル名を生成
							while (true)
							{
								strNewFile = Path.GetDirectoryName(file) + "\\" + strTitle + ".pdf";
								if (File.Exists(strNewFile) == false)
									break;

								strTitle += "_" + DateTime.Now.Ticks;
							}

							//横書きPDFを書き出す
							PDFTextWriter pw = new PDFTextWriter();
							pw.ConvertPDFFile(strNewFile, pr);

							Invoke((MethodInvoker)delegate
							{
								label1.Text = "変換処理中です。\n残りファイル数:" + (items.Length - i - 1);
							});
						}

						Invoke((MethodInvoker)delegate
						{
							label1.AllowDrop = true;		//ドラッグアンドドロップ受付可能にする
							label1.Text = strLabel;			//ラベル文字を元に戻す
						});
					});

				}
			};

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

1  2  3  4  5  6  7  8  9  10  11