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

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

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

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

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

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


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

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

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

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

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

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



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

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



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


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

			int nObjIndex = 1;

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

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

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


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

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

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

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

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

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


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

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

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


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

								ms.Flush();

								byte[] data = ms.ToArray();

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

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

			return true;
		}

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



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


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



			ushort nRangeMin = 0xFFFF;
			ushort nRangeMax = 0;



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

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

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

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

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

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

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

							cmap.Add(nChar, bytes);

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

						m = m.NextMatch();
					}
				}
			}



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

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




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

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

				byte[] compress = CompressDeflate(data);

				nEncodedLength = compress.Length;

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


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








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




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

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

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


					string strText = text.strText;

					byte[] raw2 = null;

					{
						byte[] tmp = null;

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

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

							raw2 = ms_esc.ToArray();
						}
					}


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

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

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



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

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

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

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

				return ms.ToArray();
			}
		}


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


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