おでんはじめました。

required ちくわぶ and 巾着,optional はんぺん.

Xamarin.Formsで縦組み

Xamarin Advent Calendar 2016(その2)の21日目です。

はじめに

10月にJXUGC #17 お前の Xamarin アプリを見せてみろ!に登壇させていただいたのでそのまとめです。

goo.gl

なぜXamarin.Formsで縦組みか

最近のHTML+CSS縦組み機能が充実しており大抵のブラウザで表示可能です。ただしブラウザによってその実装はまちまちで、端末に依存することなく同じイメージで表示したいというのが始めたきっかけです。自分で実装することで例えば左にルビを表示する(そんな人はほとんどいないでしょうけど)など自由に機能追加できることが(当たり前ですが)魅力の一つです。

SkiaSharp

縦組みの描画は2Dの描画フレームワークSkiaSharpを使用しています。Xamarinが推奨してるわりに日本語情報は少ないのですが、最近になって記事も増えてきたので今後に期待しています。今回のサンプルではRendererを使ってAndroidiOSをそれぞれ実装していますが、最新バージョンではXamarin.Formsに対応しています。ということで早く書き直さないと(汗)。

日本語フォント

日本語フォントを表示するためにIPAIPAex明朝フォントを使用しています。サイトからダウンロードしたipaexm.ttfをリソースフォルダ(AndroidはAssetsフォルダ、iOSはResourcesフォルダ)へ追加しています。

縦組みエンジン(Hanako)

タグの読み込みと組版(Compose)はHanakoという縦組みエンジンで実装しています。この中で行末の折り返しやページごとに組版をおこなうといった処理をおこなっています。

縦組み描画コントロール(TategumiView)

組版されたページをTategumiViewコントロールで表示しています。SkiaSharpで用意されたCanvasにDrawTextで1文字ずつXYを指定して描画しています。

//文字の描画
static void drawHonbunChar(SKCanvas canvas, SKPaint paint, WaterTrans.TypeLoader.TypefaceInfo tfi, HKWaxBase ch)
{
  //文字をGlyphIdに変換
  var glyphs = stringToVerticalGlyphs(ch.Char, paint, tfi);
  //描画の文字サイズを設定
  paint.TextSize = ch.FontSize;
  //本文を描画
  drawText(canvas, glyphs, ch.DevX, ch.DevY, paint);
}
static unsafe void drawText(SKCanvas canvas, ushort[] glyphs, float x, float y, SKPaint paint)
{
  paint.TextEncoding = SKTextEncoding.GlyphId;
  fixed (ushort* p = glyphs)
  {
    canvas.DrawText((IntPtr)p, glyphs.Length * 2, x, y, paint);
  }
}

縦組み用の字形

上記の処理でstringやcharではなくGlyphIdで描画していますが、拗促音(ゃゅょっ)や括弧は縦組み用の字形に切り替える必要があり、これは@espresso3389さんが作った(というかこのために作ってもらった)WaterTrans.TypeLoaderを使用していて、ここでstringをGlyphIdに変換しています。

//縦組み用の文字に変換している
static ushort[] stringToVerticalGlyphs(string text, SKPaint paint
  , WaterTrans.TypeLoader.TypefaceInfo typefaceInfo)
{
  ushort[] glyphs;
  paint.Typeface.CharsToGlyphs(text, out glyphs);
  var conv = typefaceInfo.GetVerticalGlyphConverter();
  for (int i = 0; i < glyphs.Length; i++)
  {
    if (conv.CanConvert(glyphs[i]))
      glyphs[i] = conv.Convert(glyphs[i]);
  }
  return glyphs;
}

XAML

作成したTategumiViewをXAMLで配置してます。WidthやHeightをBindingしていてViewの向きが変わると再処理(Compose)→描画(View)処理がおこなわれます。

<local:TategumiView VerticalOptions="FillAndExpand"
 HorizontalOptions="FillAndExpand"
 Width="{Binding TateviewWidth.Value}"
 Height="{Binding TateviewHeight.Value}"
 CurrentPage="{Binding CurrentPage.Value}"
 PageIndex="{Binding Path=PageIndex.Value,Mode=TwoWay}">

データ形式

HTML形式で<p>タグと<ruby>タグを読み込むようにしています。タグのパースはHtmlAgilityPackを使用しています。

<p><ruby>私<rt>わたくし</rt></ruby>はその人を...</p>

サンプルデータ

青空文庫から直接スクレイピングしようとしたのですが、文字コードやタグの使い方が統一されていなかったり(HtmlAgilityPackのPCLではXPATHが使えなかったり)できれいに取得できなかったので、元のHTMLからルビと本文だけのHTMLに加工しました。作成したHTMLリソースフォルダ(AndroidのAssetフォルダ、iOSのResoucesそれぞれ)に置いて読み込む形になっています。

表示!!

目次から書名をタップするとこんな感じで表示されます。左右のタップでページ移動します。

f:id:masatoru:20161220184545p:plain

GitHub

というわけで(不十分なところが多々ありますが)今回の縦組みサンプルをGitHubに上げました(現在Android版のみ動作します。iOSでなぜか動かないので調査中です)。奇特な方々の優しいダメだしを頂ければ大変幸いです。

最後に

今年もXamarinおよびXamariの関係者の方々と楽しく時間を過ごせました。ありがとうございました。来年もより一層よろしくお願いします。よいお年を。

.Net Framework2.0から4.6.1に上げたけどSystem.BadImageFormatExceptionで動かない

昔のプログラムを.Net Framework2.0から4.6.1に上げたけどSystem.BadImageFormatExceptionとかで動作しない。

下記をチェックしたけどどれも該当しない。

例外のトラブルシューティング : System.BadImageFormatException

わかったことはメインのexeのapp.configに

<supportedRuntime version="v2.0.50727"/>>

とあったのでこれを下記に書き換えたら動いた。

<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>

すっかりはまってしまった。

DbSetのAddをMockでTestする

書きなれないブログでタイトルからつまづいてますが。。。
EF6をServiceとMockを使っていい感じでテスト&モッキューできないかなと。
元ネタはなかじさんのブログ。

blog.nakajix.jp

ここの記事にあるEFのTestをMoqを使ってやる記事があります。

msdn.microsoft.com

この中の「Testing query scenarios」が、サンプルデータをServiceを通してGetAllBlogsで取得するとorder byされて返ってくるというTESTです。

[TestMethod] 
public void GetAllBlogs_orders_by_name() 
{ 
    var data = new List<Blog> 
    { 
        new Blog { Name = "BBB" }, 
        new Blog { Name = "ZZZ" }, 
        new Blog { Name = "AAA" }, 
    }.AsQueryable(); 

    var mockSet = new Mock<DbSet<Blog>>(); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(0 => data.GetEnumerator()); 

    var mockContext = new Mock<BloggingContext>(); 
    mockContext.Setup(c => c.Blogs).Returns(mockSet.Object); 

    var service = new BlogService(mockContext.Object); 
    var blogs = service.GetAllBlogs(); 

    Assert.AreEqual(3, blogs.Count); 
    Assert.AreEqual("AAA", blogs[0].Name); 
    Assert.AreEqual("BBB", blogs[1].Name); 
    Assert.AreEqual("ZZZ", blogs[2].Name); 
} 

次にServiceでAddBlogすると追加されるTestをおこなうために下記を追加。
ここでなぜか(当然?)4件にならない、ということで本題に入ります。

service.AddBlog("CCC", "http://blogs.msdn.com/adonet");
var blogs = service.GetAllBlogs(); 
Assert.AreEqual(3, blogs.Count); //4件にならない

ServiceでAddBlogしたときに追加されるようにしたいわけですが、見つけたのが下記のAnswerの箇所。
要するに、DbSetでAddされる時のMockがないので追加されないとのこと。

stackoverflow.com

...Callbackがよくわからないのでちょっと勉強してから。

github.com

というわけで試行錯誤した結果一番最後の行ができたやつ。

//コピペのままではコンパイルされず
//mockSet.Setup(m => m.Add(It.IsAny<Blog>())).Callback(blog => data.Add(blog));

//IQueryrableにAddはない
//mockSet.Setup(m => m.Add(It.IsAny<Blog>())).Callback<Blog>(blog => data.Add(blog));   

//AddがないのでConcatで配列を作り直す→なぜかダメ
//mockSet.Setup(m => m.Add(It.IsAny<Blog>())).Callback((Blog blog) => data = data.Concat<Blog>(new[] { blog }))

//ListとIQueryableを上で使い分ける
mockSet.Setup(d => d.Add(It.IsAny<Blog>())).Callback<Blog>((s) => source.Add(s));

動いたソースを一応全部のっけておきます。

[Test]
public void GetAllBlogs_with_mock()
{
  var source = new List<Blog>
  {
    new Blog { Name = "BBB" },
    new Blog { Name = "ZZZ" },
    new Blog { Name = "AAA" },
  };
  var data = source.AsQueryable();
  
  var mockSet = new Mock<DbSet<Blog>>();
  mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

  //DbSetのAddをモックする
  //ListとIQueryableを上で使い分ける(これは仕方ない?ここをもうちょいきれいにしたい)
  mockSet.Setup(d => d.Add(It.IsAny<Blog>())).Callback<Blog>((s) => source.Add(s));
  
  var mockContext = new Mock<BloggingContext>();
  mockContext.Setup(c => c.Blogs).Returns(mockSet.Object);
  
  var service = new BlogService(mockContext.Object);
  var blogs = service.GetAllBlogs();
  
  Assert.AreEqual(3, blogs.Count,"追加前");
  Assert.AreEqual("AAA", blogs[0].Name);
  Assert.AreEqual("BBB", blogs[1].Name);
  Assert.AreEqual("ZZZ", blogs[2].Name);
  
  //もう一件追加する
  service.AddBlog("CCC", "http://blogs.msdn.com/adonet");
  blogs = service.GetAllBlogs();

  Assert.AreEqual(4, blogs.Count, "追加後");
  Assert.AreEqual("AAA", blogs[0].Name);
  Assert.AreEqual("BBB", blogs[1].Name);
  Assert.AreEqual("CCC", blogs[2].Name);
  Assert.AreEqual("ZZZ", blogs[3].Name);
}

で、下記に回答がありましたというオチです。

stackoverflow.com

追伸、 もっきゅーもっきゅーって居酒屋のメニューにしか聞こえないのは自分だけ?

Bitmapとbyte[]の変換

いざ書くといつも忘れてしまうので。

画像ファイル(Bitmap)からbyte[]に変換

Bitmap bmp = new Bitmap(画像のPATH);  //using System.Drawing
MemoryStream ms = new MemoryStream();
bmp.Save(ms,ImageFormat.Png);   //using System.Drawing.Imaging;

byte[]から画像ファイル(Bitmap)に変換

MemoryStream ms = new MemoryStream(byte[]のデータ);
Bitmap bmp = new Bitmap(ms);
ms.Close();
bmp.Save(画像のPATH);

byte[]って何者かいまだによく知らない。

「しょうゆ味のだし汁で煮ただいこんが最高」を英語で言うと

Daikon radish simmered in soy broth is the best!!

The broth in Kanto(関東のだし汁) is made with bonito flakes(かつお節) and "kombu" seaweed.

In Hokkaido, dried baby fish(煮干し) are added to enrich the flavor.

日経より引用。

cakebox/cakephpでphpunitが認識されない

cakeboxでcakephp(2.7)とphpunit(4.4.4)がインストールされているけどcakephpからphpunitが認識されない件。

ブラウザからtest.phpを表示すると、

Warning include(PHPUnit/autoload.php) failed open stream...

とか表示されてテストを実行できない。

composer.jsonのvendor-dirディレクトリを修正して解決。

(修正前)

"config": { "vendor-dir": "app/Vendor/Composer" },

(修正後)

"config": { "vendor-dir": "Vendor/Composer" },

app/Vendor/Composer/phpunit/...となればOKみたい。

cakephpがどこでphpunitのディレクトリを読み込んでいるのかはよくわからない。

WordPressの投稿をJSON形式で受け取る

XMLRPCを使って最近の投稿をJSON形式で受け取るまで。

XMLRPCのライブラリーをWordPressにコピーする

下記からIXR_Library.phpをダウンロードして自分のWordPressのサイトへコピーする。

http://scripts.incutio.com/xmlrpc/ The Incutio XML-RPC Library for PHP

(ページの上にある「Download the Library」をクリック)

投稿をJSONで受け取るPHPを書く

下記をgetposts.phpとしてWordPressのサイトへ保存する。

変更箇所はサイト名、ユーザー名、パスワード。

文字コードUTF-8などWordPressに合わせる。

<?php
// inctioのライブラリ呼び出し
include_once('IXR_Library.php');

$wp_username = 'admin';
$wp_password = 'admin';
$siteurl     = 'http://sample.jp/';

$filter = array( 
  'number' => 10,   //最近の10件取得
  'offset' => 0,
  );
$client = new IXR_Client( $siteurl . 'xmlrpc.php' );
$status = $client->query(
  "wp.getPosts", //POSTを取得する
  1, // blog ID: 通常は1、マルチサイト時変更
  $wp_username, // ユーザー名
  $wp_password, // パスワード
  $filter
  );
$posts = $client->getResponse(); //arrayで戻ってくる
echo json_encode( $posts );  //JSONで返す

確認する

http://sample.jp/getposts.phpへアクセスしてJSONが戻ってくればOK。

うまくいかない時

ブラウザで表示が真っ白の時はphpの構文が違ってる可能性があるのでphp -l getposts.phpで構文チェックするとか。

IXR_Library.phpのエラー内容がわかりづらい。ユーザー名とパスワードが違ってないか、new IXR_Clientで渡すURLが違ってないか確認など。