海外ロケーションと日付フォーマット dd/MM/yyyy

先日、ヨーロッパからの案件でXamarin Androidの開発をしていたときのこと。SQLServerへの日付型のデータで更新エラーが発生するという連絡をクライアントから受けたのですが、こちらでは何度やっても正常に動く。開発環境はすべてEnglish(UK)のOSでそろえているので言語環境も違いはないはず。違うのはロケーション、つまりタイムゾーンだけ。

詳細に調査したところ、System.Data.SqlClientを使ってSQLServerから日付データを取得するとき、AndroidOSが自動判別したタイムゾーンに応じて日付フォーマットが変化することが分かりました。どおりで日本だとエラーにならないわけです。

SQLServerで日付型のデータを保存するとき、既定では、

English(UK) : dd/MM/yyyy  -- 保存NG
English(US) : MM/dd/yyyy  -- 保存OK
Japanese    : yyyy/MM/dd  -- 保存OK

となっています。なので、以下のようなDateTime型カラムを更新するSQLはエラーになります。

--エラーになる
INSERT INTO T_TEST (InputDate) VALUES ('30/06/2022') 

「out-of-range value」というエラーを吐き出して、30月6日なんて日付はだめだよと怒られます。グローバルなアプリを開発する際は文化の違いを意識して作らなければいけませんね。対策は、DMYオプションを宣言して、

--エラーにならない
SET DATEFORMAT DMY
INSERT INTO T_TEST (InputDate) VALUES ('30/06/2022')

としてSQLServer側に欧州フォーマットでの処理をお願いするか、プログラム側で常にyyyy/MM/ddにフォーマット変換してからSQLServerに渡すことを習慣づけるかのどちらかですね。

面倒でも後者の方がバグは発生しにくいかもしれません。

ZXingでバーコードアプリ – 公開

Google Play Consoleからアプリを公開しました。製品版にリリースして、審査の状態からリリースの状態まで4時間ぐらいかかったでしょうか。

バーコードアプリは腐るほど世の中に出ているので、検索の上位に食い込むのはかなり難しそうです。

ということで、ここにリンクを貼っておきますので、興味があればダウンロードしてみてください。Android版のみになりますが、勉強のつもりで作ったので無料で広告もなしで、Android11に対応しています。よろしくお願いいたします。

Google Play で手に入れよう

バグなど、気づいたことがあれば、Google Playでコメントするか、ここに書いてもらえるとうれしいです。

【公開して思ったこと】

どのバーコードアプリも、似たり寄ったりなんですよね。その中で、マルチバーコードスキャンに対応しているアプリはかなり少ないことに気が付きました。

マルチバーコードスキャンとは、2つのバーコードを同時に読み込める機能です。そして、ZXingも機能として持っています。実は、実装前の調査でマルチバーコードスキャンのプログラムはテストしたことがあって、動作がどうしてももっさりしたので、仕様から外したんですよね。

でも、広告のチラシとか見ると、2つのQRコードを並べて掲載していることがとても多いのです。FacebookとLINEのコードを並べたりとか。そして、LINEのQRコードが欲しいのにFacebookを読み込んじゃったりするんです。そんなときは片方を指で隠してスキャンするんですが、指の影でうまく読み込めなかったりするんで、そんなときはマルチバーコードスキャンで2つとも読み込んじゃった方がいいのかもしれません。

実装はそんなに難しくないので、近日、着手してみようかと思います。

あと、Google Playの検索上位に入るにはどうしたらいいのか、少し、興味がわいてきました。どうするんでしょうか? これも調べてみます。

XamarinでAndroidアプリ公開(内部テスト)

VisualStudio2019のXamarin.Formsでのバーコードアプリ開発ですが、テスト版をGoogle Playにリリースしました。 つまづいたポイントをメモ。

(1)ReleaseモードにしてProGuardでコンパイルしたらエラー

「 ”java.exe” はコード 1 を伴って終了しました。」

というエラーです。直接の解決はできなかったのですが、下図の通りshrinkerを「ProGuard」から「r8」に変更したらエラーが出なくなりました。これに伴い、Dexコンパイラも「d8」に変更しています。なんだったんだろうか?

(2)文字コードのエラー

履歴データをCSVで吐き出す機能を付けたのですが、文字コードにShift-JISを使っていて、ReleaseモードでCSV出力するとエラーになりました。

デバッグモードのときは「共有ランタイムの使用」が有効になって問題は出なかったのですが、以下のように、「サポートされているその他のエンコーディング」で「CJK」を指定すればReleaseモードでもOKでした。

(3)Rsourceに指定した画像ファイルが画面に表示されない

全く原因が分からなかったのですが、Xamarin.Formsのバージョンを上げたら治りました。よく分からんです。

「4.8.0.1451 → 4.8.0.1534」

メイン画面のメニューボタンはにイメージボタンを使っていたので、起動したら真っ黒な画面しか表示されなかったときは少し焦りました。デバッグモードの時は問題なかったのですが・・・。

(4)2020年8月からAndroid9.0のアプリは新規公開できなくなった

Releaseモードで手持ちのスマホでは問題がなくなったところでGoogle Playへの公開作業です。

そして、、、Google Play Consoleでのアップロードの途中で「Android10以降で作ってね」とエラーになりました・・・。

知りませんでした。ターゲットフレームワークはAndroid9.0で作っていました。後戻りして最初からコンパイルしなおしです。

(5)Android10に上げたらコンパイルエラー

振り出しに戻ったら、一歩目でいきなりエラーです。まあ、しょうがないですね。ここはがまんのしどころです。

java関連のエラーがうじゃうじゃと10件以上。

開発環境のOSはWindows10。nugetパッケージの参照パスが漢字のユーザー名を含むパスになっていて、こいつが悪さをしていたようです。ユーザーをAdministratorに切り替え、参照パスに日本語文字を含まないようにしてからコンパイルするとうまくいきました。

日本語を含む参照パスが悪さをすることって時々あるけど、Android9.0のときはエラーにならなかったのは今でも不明。おかげで気づくまで半日ほどかかってしまった。

こんな感じで回り道しながらも、ようやく内部テスト版を公開できました。

今日はここまで。

ZXingでバーコードアプリ開発(4)

やっと、Xamarin.Formsのアプリとして詰め込みたい機能がそろってきたのでテストフェーズに入ってきたんですが、ここでZXingのバグに気づきました。

バーコードをスキャンする際、ZXing.Net.Mobileを使って ZXingScannerView のインスタンスをコードで生成して画面表示しています。

インスタンス生成のコードはこんな感じです。

//使っているZXing.Net.Mobile.Formsのバージョンは v3.0.0-Beta5

//スキャン可能なBarcodeフォーマットの一覧
List<ZXing.BarcodeFormat> barcodeFormats = new List<BarcodeFormat>
{
    BarcodeFormat.QR_CODE,      //QR
    BarcodeFormat.CODABAR,      //NW-7 (JANCODEよりはBARが短くて済む)
    BarcodeFormat.CODE_128,     //CODE128, GS1-128
    BarcodeFormat.CODE_39,      //CODE39
    BarcodeFormat.CODE_93,      //CODE93
    BarcodeFormat.DATA_MATRIX,  //DataMatrix
    BarcodeFormat.EAN_8,        //JANCODE (8桁)
    BarcodeFormat.EAN_13        //JANCODE (13桁)                    
};

//インスタンス生成 
zxing = new ZXingScannerView()
{
    HorizontalOptions = LayoutOptions.FillAndExpand,
    VerticalOptions = LayoutOptions.FillAndExpand,
    AutomationId = "zxing",
    IsScanning = true,
    IsAnalyzing = true,
    Options = new ZXing.Mobile.MobileBarcodeScanningOptions
    {
        UseFrontCameraIfAvailable = false,  //フロントカメラの設定
        TryHarder = true,                   //厳しめに解析(速度が遅くなる)
        PossibleFormats = _barcodeFormats,  //バーコードフォーマット指定
        AssumeGS1 = true,                   //GS1の読み込みに必要
        UseNativeScanning = false,          //iOSのスキャナエンジンを使う場合に必要
        AutoRotate = true,                  //90度回転して解析するかどうか
        DisableAutofocus = true      //オートフォーカス
    }
};

ここで気づいたのですが、例えば後から CODE_39 をPorssibleFormats から外す処理を実行しても、残念なことに CODE_39 が読み込めてしまうんですね。PossibleFormatsを絞り込むとバーコードの読み取り速度が上がるので、重要な設定です。

でも、PossibleFormats をCODE_39を外した状態でインスタンスを再作成する方法だと、CODE_39は読めなくなり期待通りの動きをします。

どうやら、都度、インスタンスを作り直さないと設定変更が反映されないということみたいですね。 PossibleFormatsだけでなく、他のプロパティも疑った方がいいかもしれません。

なにかしらの読み込み設定の変更処理を行うときは、インスタンスを作成しなおすようにコード修正することにしました。

ZXingでバーコードアプリ開発(3)

GS1-128, GS1 DataMatrixの生成時にFNC1を埋め込む件

GS1を扱うときにFNC1の処理は避けて通れないのですが、ZXingがFNC1をどう扱っているのかが不明でした。日本語のページでもそのことに言及しているサイトが見つからなかったので、ZXingでエンコードしたバーコードをデコードしてRawデータを確認することにしました。

FNC1をバーコードに埋め込むルールは以下の通りです。

(1)可変長のセクションの後に付ける

  AI(10)のロットや、AI(30)の入り数などにおいて、セクションの終わりの印として使います。これは、GS1-128 と GS1 DataMatrix の両方に共通するルールです。

(2)GS1-128のスタートコードの後に付ける

  GS1-128とCODE128を区別するため、スタートコードの後にFNC1を埋め込むことになっています。

(3)GS1 DataMatrixの先頭

  GS1 DataMatrixでは必ずFNC1を先頭に付けます。

尚、読み取り(デコード)時はFNC1は「GS (グループセパレータ)」に置き換えて出力することになっています。 GSはASCIIコード29の制御文字なので、印刷しても出力されません。そのため、市販のバーコードスキャナではGSをスペースに置き換えるなどの機能がついていたりします。

で、ZXingでバーコードを生成するときに、FNC1やSTARTコードをどのように指定すればよいのかを調べたところ、以下の通りとなりました。

表.ZXing.BarcodeWriterのGS1エンコードの動作

フォーマットコード解釈C#でZxingに
渡す文字
バーコード内のコード
GS1-128105START C不要 (注1)105
GS1-128102FNC1(char)0x00F193 (注2)
GS1 DataMatrix232先頭のFNC1(char)29232 (注3)
GS1 DataMatrix232可変長項目の
終端につけるFNC1
(char)2930(注4)

【注1】 ZXingではBarcodeFormatの値を「CODE_128」に指定すると自動的にスタートコードとして「105」が埋め込まれます。 ちなみに、文字列の途中でアルファベットを検知すると「CODE B」への切り替え文字も自動的にバーコードに埋め込んでくれます。

【注2】 GS1-128での「93」は制御文字「GS (ASCII 29)」を表します

【注3】 GS1 DataMatrixでは先頭は必ずFNC1と決められています。ZXingはBarcodeFormatの値を「DATA_MATRIX」にすると、GS (ASCII 29)を「232(FNC1)」に変換してバーコードに埋め込んでくれます。

【注4】 ZXingはBarcodeFormatを「DATA_MATRIX」にすると、先頭以外のGS (ASCII 29)は 「30 (GS)」をバーコードに埋め込みます。

例えば、C#で書くとこんな感じになります、、

// (01)14512345678903(10)ABCD123(17)201201 をエンコードする例
//writer.Optionsによる画像サイズや余白マージンの指定などはここでは省きます

BitMatrix bitMatrix;
ZXing.BarcodeWriterPixelData writer = new BarcodeWriterPixelData();
string _code;

//GS1-128の場合
writer.Format = BarcodeFormat.CODE_128;
_code = $"{(char)0x00F1}"
      + "0114512345678903"
      + "10ABCD123" + $"{(char)0x00F1}"
      + "17201201";
bitMatrix = writer.Encode(_code);  

//-----------------------------------
//bitMatrixからBitmapを作成する処理 (省略)
//-----------------------------------


//GS1 DataMatrixの場合
writer.Format = BarcodeFormat.DATA_MATRIX;
_code = $"{ (char)29}" 
      + "0114512345678903"
      + "10ABCD123" + $"{ (char)29}" 
      + "17201201"
bitMatrix = writer.Encode(_code);

//-----------------------------------
//bitMatrixからBitmapを作成する処理 (省略)
//-----------------------------------

調査してみて、、、

DataMatrixの可変長項目のFNC1に「GS(ASCII 29) を指定すると、コードワード232を指定したことになる」っていうところが分かりにくかったですね。コードワードにも「30」というGSを表すコードがあるのがややこしい・・・。調査に何日かかかってしまいました。

ZXingでバーコードアプリ開発(2)

前回の投稿の続きです。

2値画像化してからのバーコード認識にトライしていましたが、ようやくできました! とりあえずは。

Xamarin.AndroidのコードなのでAndroid限定ですが、Android.Hardware.Camera2 のキャプチャ画面からByte配列の画像データを取得するところは、

https://github.com/vtserej/Camera2Forms

を参考にしました。

このサンプルの中にある CameraViewServiceRenderer.OnPhoto() にて、画像のバイト配列が取得できるようになっています。

private void OnPhoto(object sender, byte[] imgSource)
{
    //imgSourceにカメラビュー画像が入っているので、
    //ここで煮るなり焼くなり


    Device.BeginInvokeOnMainThread(() =>
    {
        _currentElement?.PictureTaken();
    });
}

AndroidのCamera2からシャッター音を鳴らさずに無音で画像を取得するには、このサンプルがちょうどいいかと思います。

画像はARGB画像です。ARGBの4種類がそれぞれ1バイト(256諧調)なので、1ピクセル4バイトになります。

2値化画像にする手順は、

  • グレースケール化(RGBの3色の数値を平均化する)
  • 閾値を決めて、1ピクセルごとに「0 or 255」にする

ということになります。

これも変換関数を作って何とかなりました。・・・が、肝心のバーコード認識率はほとんど変わりませんでした。

それぞれ、生画像、グレースケール、2値化画像を比較したのですが、2値化の段階で1次元バーコードの斜め線になったところが「ギザギザ」になっていました。

また、照明の当たり具合はバーコードの部分ごとに異なることが当然あります。すると、ハレーションのような現象が出て、バーコードが部分的につぶれてしまいます。

下の画像は桁数がまあまあ多いGS1-128のバーコードで、真ん中あたりが照明で明るく照らされていたものです。

2値化したことにより、バーコードそのものが部分的にかすれてしまっています。

つまり、単純な2値化の閾値設定では状況の変化に対応できず、意味がないことが分かりました。今まで画像処理に縁がなかったのですが、なかなか手ごわいです。

画像処理のサイトを検索すると、いろいろな画像処理のテクニックがあるようですが、今のところ有効そうなのは、

  • カメラビューまたはグレースケール化のタイミングで解像度を上げる
  • 閾値方式だけではなく、周囲のピクセルとの比較によって白 or 黒を決定し、なめらかな線を目指す
  • 1ピクセルあたりのバイト数を減らす (高速化のため)

といったところでしょうか。(自分にできるのか不安)

GooglePlayを漁っていると、GS1-128でアルファベットや桁数を多く含み、しかも撮影条件が悪くても高速に読み取るソフトが中には存在します。キーエンスのハンディターミナルと同等レベル? ってぐらい優秀です。

一体、どうやって実現しているんだろうか?

[追記 2020-09-11]

GS1-128の読み取りが悪いですが、逆に、GS1-128以外はそれなりの読み取り速度なので、画像処理は時間がかかりそうなのであきらめました。今後の努力課題として、取り急ぎGS1 DataMatrixの生成処理をすることにしました。

ZXingでバーコードアプリ開発

最近、スマホアプリ開発の勉強として VS2019のxamarinとC#とZXingの組み合わせでAndroid版のバーコードアプリを作っています。

バーコードの読み取り、書き出しの両方に対応させようとしていますが、おかげで、かなり勉強になりました。

GS1-128のFNC1のエンコードを通して文字コードの勉強になりましたし、QRコードはデンソーウェーブの登録商標であるとか、デンソーウェーブがQRコードサービスで位置情報を無断で収集していたとか、興味深い情報も初めて知りました。

最初は雑誌のQRコードを読み込んでから自作アプリでエンコードすると同じ模様のQRコードが作れなくて悩みました。

バーコードは枯れた技術ですが奥が深い・・・。

ちなみに、バーコードSDKのZxingには問題がいくつかあったのでメモ。

(1)ZXingScannerViewが頻繁にフリーズ

バーコードをスキャンしてからスキャン結果の画面に移動し、再びZXingScannerViewを埋め込んだ画面に戻るとZXingScannerViewがわりと簡単にフリーズします。こちらは、ZXing.Net.Mobileが2020年3月に2年ぶりにバグ修正をして更新されています。バージョンは「3.0.0-beta5」とベータ版なのですが、こちらにアップデートするとフリーズしなくなりました。

NuGetで検索キーワードに「ZXing.Net.Mobile」を指定する際に「プレリリース」にチェックを入れると開発環境にインストールできます。

(2)EAN13やEAN8、GS1-128の下段の数字が印字できない

バーコードを作成するときのオプションで「PureBarcode」プロパティがあります。EAN13などは通常、下段にバーコードモジュールの下部に数字が印字されますが、この「PureBarcode」をfalseに設定すると数字も一緒に出力されます。

しかし、

ZXing.BarcodeWriterPixelData writer = new BarcodeWriterPixelData();

writer.Format = BarcodeFormat.EAN_13;

writer.Options.PureBarcode = false;

としても、バーコード画像の下段にエンコードする前の数字が印字されません。どうやら、Androidでは有効にはならないようです。

対策として、自分で数字のBitmapを作ってCanvasオブジェクトで重ね合わせれば何とかなります。力技ですね。

(3)GS1-128などの高密度の1次元コードの読み取りが極端に弱い

ZXingScannerViewを使っているのですが、わりとQRコードなどは高密度でもすんなりとデコードしてくれます。しかし、GS1-128などの高密度の1次元コードは全然、読み込んでくれません。でも、他社さんのスマホバーコードアプリで高速読み取りを謳い文句にしているソフトは速攻でGS1-128も読み込みます。

違いはなんだろ・・・?

そこで、「ZXing 認識率」で検索すると結構、対策がヒットします。2値画像処理など、まだまだやれることはありそうです。

ZXingScannerViewに頼らず、カメラ画像を加工してからZXingに食わすのがいいのかもしれません。