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