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

前の10件 1  2  3

2016年04月 記事一覧

第11回 フォントファイルからCMAP変換表を取り出す

今回はopen type fontのフォントファイルから変換表を取り出し、利用する処理を実装します。

これまではttfdump.exeでcmap_msgothic.txtを手動作成していました。
この処理を自動化します。
取り出す変換表はplatformID == 3 && encodingID == 1のもののみに対応します。

open type fontの仕様を見ると、
https://www.microsoft.com/typography/otspec/otff.htm

ファイル先頭に4バイトのsfnt version、続いて2バイトのnumTables(テーブル数)、2バイトのsearchRange、2バイトのentrySelector、2バイトのrangeShiftが格納されており、

続いてnumTablesの分だけ「テーブル」が並びます。
1つのテーブルはtag、checkSum、offset、lengthがそれぞれ4バイト、合計16バイト分あり、

tagが"cmap"のテーブルの(ファイル先頭からの)offset位置に変換表が用意されています。


変換表の仕様を見ると、
https://www.microsoft.com/typography/otspec/cmap.htm

先頭から、2バイトのversion、2バイトのnumTables(テーブル数)があり、
続いてnumTablesの分だけ「テーブル」が並びます。
1つのテーブルは2バイトのplatformIDとencodingID、4バイトのoffsetの合計8バイト分あり、

(ファイル先頭ではなく"cmap"の先頭からの)offset位置に、platformIDとencodingIDの示す変換表が格納されています。
変換表の先頭には2バイトのformatがあり、このformatの値により変換表の格納形式が異なります。

今回利用する変換表はplatformIDが3、encodingIDが1なので、format=4(Format 4: Segment mapping to delta values)の形式で格納されています。

format 4の変換表の中には様々な情報が格納されていますが、その中で以下の3つの配列が必要になります。
・startCount[]
・endCount[]
・idDelta[]

例えばユニコードで0x5511を変換する場合には、
 startCount[n] <= 0x5511 <= endCount[n]
を満たす n の値を探し、以下のように変換します。
変換後の値=0x5511 + dDelta[n]

今回はユニコードで0x0000~0xFFFFまですべての文字についての変換表が欲しいので、
すべての値において上記の計算を行って変換表を作成しています。

■PDFTextWriter.cs
		public void Close()
		{
			_cmapFonts.Clear();
			_listnXref.Clear();
			_mapFontType.Clear();
		}
		/// <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;



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


			////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]);
		}










		/// <summary>
		/// open type fontファイルからcmapを読み取る
		/// 
		/// open type fontの仕様通りにファイルを読むだけ
		/// マジックナンバーなどでファイルチェックをするべきだがしていない
		/// 
		/// 仕様
		/// https://www.microsoft.com/typography/otspec/otff.htm
		/// </summary>
		IDictionary<ushort, byte[]> LoadCMap(string strFontFile, out ushort nRangeMin, out ushort nRangeMax)//, 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;

				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 (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));
				}


				if (nOS2Offset > 0 && nOS2Length > 0)
				{
					fs.Seek(nOS2Offset, SeekOrigin.Begin);

					// https://www.microsoft.com/typography/otspec/os2.htm
					uint version = ByteToUInt_BE(br.ReadBytes(2));
					int xAvgCharWidth = ByteToInt_BE(br.ReadBytes(2));
					uint usWeightClass = ByteToUInt_BE(br.ReadBytes(2));
					uint usWidthClass = ByteToUInt_BE(br.ReadBytes(2));
					uint fsType = ByteToUInt_BE(br.ReadBytes(2));
					int ySubscriptXSize = ByteToInt_BE(br.ReadBytes(2));
					int ySubscriptYSize = ByteToInt_BE(br.ReadBytes(2));
					int ySubscriptXOffset = ByteToInt_BE(br.ReadBytes(2));
					int ySubscriptYOffset = ByteToInt_BE(br.ReadBytes(2));
					int ySuperscriptXSize = ByteToInt_BE(br.ReadBytes(2));
					int ySuperscriptYSize = ByteToInt_BE(br.ReadBytes(2));
					int ySuperscriptXOffset = ByteToInt_BE(br.ReadBytes(2));
					int ySuperscriptYOffset = ByteToInt_BE(br.ReadBytes(2));
					int yStrikeoutSize = ByteToInt_BE(br.ReadBytes(2));
					int yStrikeoutPosition = ByteToInt_BE(br.ReadBytes(2));
					int sFamilyClass = ByteToInt_BE(br.ReadBytes(2));
					byte[] panose = br.ReadBytes(10);
					uint ulUnicodeRange1 = ByteToUInt_BE(br.ReadBytes(4));
					uint ulUnicodeRange2 = ByteToUInt_BE(br.ReadBytes(4));
					uint ulUnicodeRange3 = ByteToUInt_BE(br.ReadBytes(4));
					uint ulUnicodeRange4 = ByteToUInt_BE(br.ReadBytes(4));
					byte[] achVendID = br.ReadBytes(4);
					uint fsSelection = ByteToUInt_BE(br.ReadBytes(2));
					uint usFirstCharIndex = ByteToUInt_BE(br.ReadBytes(2));
					uint usLastCharIndex = ByteToUInt_BE(br.ReadBytes(2));
					int sTypoAscender = ByteToInt_BE(br.ReadBytes(2));
					int sTypoDescender = ByteToInt_BE(br.ReadBytes(2));
					int sTypoLineGap = ByteToInt_BE(br.ReadBytes(2));
					uint usWinAscent = ByteToUInt_BE(br.ReadBytes(2));
					uint usWinDescent = ByteToUInt_BE(br.ReadBytes(2));
					uint ulCodePageRange1 = ByteToUInt_BE(br.ReadBytes(4));
					uint ulCodePageRange2 = ByteToUInt_BE(br.ReadBytes(4));
					int sxHeight = ByteToInt_BE(br.ReadBytes(2));
					int sCapHeight = ByteToInt_BE(br.ReadBytes(2));
					uint usDefaultChar = ByteToUInt_BE(br.ReadBytes(2));
					uint usBreakChar = ByteToUInt_BE(br.ReadBytes(2));
					uint usMaxContext = ByteToUInt_BE(br.ReadBytes(2));
					uint usLowerOpticalPointSize = ByteToUInt_BE(br.ReadBytes(2));
					uint usUpperOpticalPointSize = ByteToUInt_BE(br.ReadBytes(2));
				}


				if (nHeadOffset > 0 && nHeadLength > 0)
				{
					fs.Seek(nHeadOffset, SeekOrigin.Begin);

					// https://www.microsoft.com/typography/otspec/head.htm
					byte[] version = br.ReadBytes(4);
					byte[] fontRevision = br.ReadBytes(4);
					uint checkSumAdjustment = ByteToUInt_BE(br.ReadBytes(4));
					uint magicNumber = ByteToUInt_BE(br.ReadBytes(4));
					uint flags = ByteToUInt_BE(br.ReadBytes(2));
					uint unitsPerEm = ByteToUInt_BE(br.ReadBytes(2));
					byte[] created = br.ReadBytes(8);
					byte[] modified = br.ReadBytes(8);

					nBBXMin = ByteToInt_BE(br.ReadBytes(2));
					nBBYMin = ByteToInt_BE(br.ReadBytes(2));
					nBBXMax = ByteToInt_BE(br.ReadBytes(2));
					nBBYMax = ByteToInt_BE(br.ReadBytes(2));

					uint macStyle = ByteToUInt_BE(br.ReadBytes(2));
					uint lowestRecPPEM = ByteToUInt_BE(br.ReadBytes(2));
					int fontDirectionHint = ByteToInt_BE(br.ReadBytes(2));
					int indexToLocFormat = ByteToInt_BE(br.ReadBytes(2));
					int glyphDataFormat = ByteToInt_BE(br.ReadBytes(2));
				}

				if (nCMapOffset > 0 && nCMapLength > 0)
				{
					fs.Seek(nCMapOffset, SeekOrigin.Begin);

					// https://www.microsoft.com/typography/otspec/cmap.htm
					uint version = ByteToUInt_BE(br.ReadBytes(2));
					uint numTables = ByteToUInt_BE(br.ReadBytes(2));

					uint nCMap31Offset = 0;

					for (uint i = 0; i < numTables; i++)
					{
						uint platformID = ByteToUInt_BE(br.ReadBytes(2));
						uint encodingID = ByteToUInt_BE(br.ReadBytes(2));
						uint offset = ByteToUInt_BE(br.ReadBytes(4));		//	Byte offset from beginning of table to the subtable for this encoding.

						if (platformID == 3 && encodingID == 1)		//ユニコードはWindows(3)の(1)
						{
							nCMap31Offset = offset;
							break;
						}
					}

					if (nCMap31Offset > 0)
					{
						fs.Seek(nCMapOffset, SeekOrigin.Begin);			//cmap先頭に移動してから
						fs.Seek(nCMap31Offset, SeekOrigin.Current);		//テーブルに移動

						//Format 4: Segment mapping to delta values		//16bitユニコードは4
						uint format = ByteToUInt_BE(br.ReadBytes(2));

						if (format == 4)		//16bitユニコードは4
						{
							uint length = ByteToUInt_BE(br.ReadBytes(2));
							uint language = ByteToUInt_BE(br.ReadBytes(2));

							uint segCountX2 = ByteToUInt_BE(br.ReadBytes(2));
							uint searchRange = ByteToUInt_BE(br.ReadBytes(2));
							uint entrySelector = ByteToUInt_BE(br.ReadBytes(2));
							uint rangeShift = ByteToUInt_BE(br.ReadBytes(2));

							uint[] endCount = new uint[segCountX2 / 2];
							for (uint i = 0; i < endCount.Length; i++)
							{
								endCount[i] = ByteToUInt_BE(br.ReadBytes(2));
							}

							uint reservedPad = ByteToUInt_BE(br.ReadBytes(2));

							uint[] startCount = new uint[segCountX2 / 2];
							for (uint i = 0; i < startCount.Length; i++)
							{
								startCount[i] = ByteToUInt_BE(br.ReadBytes(2));
							}

							int[] idDelta = new int[segCountX2 / 2];
							for (int i = 0; i < idDelta.Length; i++)
							{
								byte[] data = new byte[2];
								data[0] = br.ReadByte();
								data[1] = br.ReadByte();
								idDelta[i] = ByteToInt_BE(data);
							}

							uint[] idRangeOffset = new uint[segCountX2 / 2];
							for (uint i = 0; i < idRangeOffset.Length; i++)
							{
								idRangeOffset[i] = ByteToUInt_BE(br.ReadBytes(2));
							}


							for (uint i = 0; i < startCount.Length; i++)
							{
								for (uint charCode = startCount[i]; charCode <= endCount[i]; charCode++)
								{
									if (charCode == 0xFFFF)
										continue;

									int index = (int)((short)charCode + (short)idDelta[i]);
									if (index < 0)
										index = (index & 0xFFFF);			//uint16で無理やり処理

									//Console.WriteLine("	Char {0:X4} -> Index {1}", charCode, index);

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

									cmap.Add((ushort)charCode, bytes);

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



						}

					}
				}
			}
			return cmap;
		}




		int ByteToInt_BE(byte[] data)
		{
			if (data == null || data.Length == 0)
				return 0;

			if (data.Length == 2)
			{
				ushort ret = 0;
				foreach (byte cb in data)
				{
					ret <<= 8;
					ret += cb;
				}

				return (short)ret;
			}

			if (data.Length == 4)
			{
				uint ret = 0;
				foreach (byte cb in data)
				{
					ret <<= 8;
					ret += cb;
				}
				return (int)ret;
			}

			Debug.Assert(false);
			return (int)ByteToUInt_BE(data);
		}


		uint ByteToUInt_BE(byte[] data)
		{
			if (data == null || data.Length == 0)
				return 0;

			uint ret = 0;
			foreach (byte cb in data)
			{
				ret <<= 8;
				ret += cb;
			}
			return ret;
		}

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

第12回 PDF内で複数のフォントを扱う

これまではPDF内のテキストは1つのフォントのみ利用可能でした。
今回は複数のフォントを利用できるようにします。

ついでに書き出すページも2ページに増やしておきました。
ここまで処理ができればツールの作成も目前です。

■PDFTextWriter.cs
		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, "MS-Gothic", "MS Gothic", @"msgothic.otf");
						}
						{
							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);		//日本語フォント指定(フォント埋め込みなし)
						}

						//埋め込みフォント一覧のみのリソース
						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ページ目のインデックスを渡す
						listPage.Add(nObjIndex + 3);				//2ページ目のインデックスを渡す
						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"));
							listTexts.Add(new PDFText("F1", 40, 50, 590, @"abAAA23 timesItalic"));
							listTexts.Add(new PDFText("F2", 40, 50, 640, @"abAAA2てすと3 "));


							//コンテンツの書き出し
							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);
							}
						}
					}

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

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

							listTexts.Add(new PDFText("F0", 40, 50, 540, @"ぺーじ2.abcあいうえお3"));
							listTexts.Add(new PDFText("F1", 40, 50, 590, @"page2 abAAA23 timesItalic"));
							listTexts.Add(new PDFText("F2", 40, 50, 640, @"abAAA2てすと3 "));


							//コンテンツの書き出し
							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>
		/// 欧文フォントの指定
		/// 
		/// デフォルトで用意されているフォントを利用する場合はこれで指定、
		/// 使えるフォント名(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);
		}



		/// <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);
		}
		/// <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 = null;

					if (type == FONT_TYPE.EMBEDDED)
					{
						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);
						else 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);
			}
		}

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


第13回 小説家になろう縦書きPDFを変換する

今回はようやく目的の機能実装です。

「小説家になろう」からダウンロードした縦書きPDFファイルを読み込む処理は以前に作成しました。
そこに含まれるテキスト情報を横書きPDFとして変換しながら書き出します。
PDFの書き台処理もこれまでに作成したものがそのまま利用できるため、体裁を整える作業のみです。

DSC_1343.JPG
「小説家になろう」からダウンロードした縦書きPDFに対してAcrobatでフォントを埋め込んで、Kindleに転送/表示した状態がこれ。

文字が小さく表示が薄いなど読みにくい状態。

フォントの埋め込み処理もかなり時間がかかります。


DSC_1342.JPG
今回作成した処理で縦書きPDFから横書きPDFへ変換した状態がこれ。

かなり改善して読みやすくなりました。変換速度も気になるほど遅くありません。

難はルビ位置。ルビが正しい位置に表示されません。

■Form1.cs
		public Form1()
		{
			InitializeComponent();

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

			PDFTextWriter pw = new PDFTextWriter();
			pw.ConvertPDFFile("test.pdf", pr);

			PictureBox pictureBox1 = new PictureBox();
			pictureBox1.Parent = this;
			pictureBox1.Dock = DockStyle.Fill;
■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, "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;
					{
						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)と最終ページは固定行間隔で表示

									listTexts.Add(new PDFText("F0", text.fPoint, 10, 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;
		}


		/// <summary>
		/// ストリームに線データを書き込む
		/// </summary>
		void PrepareLineContents(Stream strm, int x1, int y1, int x2, int y2)
		{
			byte[] data = Encoding.ASCII.GetBytes(string.Format("{0} {1} m {2} {3} l S\r", x1, y1, x2, y2));
			strm.Write(data, 0, data.Length);
		}

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

2016年04月04日

第14回 ドラッグアンドドロップで処理する

これまではソースコード内で直接ファイル名を指定した形で処理していました。
今回はウインドウにドラッグアンドドロップしたPDFを処理するように変更します。

「小説家になろう」の縦書きPDFは最初のページの最初のテキストがその小説のタイトルになっています。
そこから小説のタイトルを抜き出し、ファイル名として使えない文字がある場合はそれを置換し、
小説のタイトルを変換後のファイル名としてPDFを書き出すようにします。

また、今までウインドウに表示していた小説表示機能は削除します。

これにより複数のファイルを一度に変換可能になりました。

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


			//PDFTextWriter pw = new PDFTextWriter();
			//pw.ConvertPDFFile("test.pdf", pr);


			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);

					foreach (string file in items)
					{
						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);
					}
				}
			};


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

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

第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;			//ラベル文字を元に戻す
						});
					});

				}
			};

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

第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);		//日本語フォント指定(フォント埋め込みなし)
						}

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

第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);
		}

	}
}

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

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月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月12日

第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ファイルをダウンロード

前の10件 1  2  3





usefullcode@gmail.com

About 2016年04月

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

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

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

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