20200124のSwiftに関する記事は8件です。

アプリ企画-殺処分を10年で0にする方法『犬組』

1匹の犬を5つの家族で、協力して飼うアプリです。
9B4D04D4-D933-4C0D-808A-8C9ED5100E00.jpeg

概要
犬を複数の家で持ち回りで飼えるようにするアプリです。
犬組は、複数の家族が責任を持って、一匹の犬を飼います。
複数の家が、力を出し合うことによって、犬の安心と安定が高まります。

例えば、
長く留守にする際、ペットショップに預けられるのではなく、犬は、犬組の中でいつもの生活をし続けられます。
飼い主が歳をとって、散歩が足りない時、動物病院に行けないとき、代わりに行ってもらえます。
わからない事があったとき、犬組の仲間から、真剣に教えてもらえます。
そして最も大きいのは、金銭的負担が減ることです。複数人で負担を分散しますから、その分余裕が出ます。余裕の一部をペット保険に回します。

犬にとって、一人で飼われるより、犬組を作って飼われた方が良いんです。
そんな、犬に良い、人にも街にも良いアプリが犬組です。

初めての犬組

犬組では、スマホの犬組アプリから、飼いたい犬を選択して、飼育中の犬組に参加します。
8D5C497D-FCAB-47B6-B635-1BD00C573B45.jpeg

初めて犬を飼う「シンカイ」さんを例に、一緒に犬組を使って見てみましょう。
ここでは、犬の名前は『小太郎』くん、犬組名は、「小太郎組」として、話を進めます。
犬組アプリから、参加したい犬組を探します。
今回は、小型犬の小太郎くんを選びました。
飼育している犬組「小太郎組」に、自撮りとチャット面接を行います。
お試し飼育が認められると、小太郎組から、候補日が送られてきます。
予定日がきたら、小太郎組の集合場所(公園やペットカフェ、組員の近くの交差点)に向かいます。
小太郎(犬の名前)とお世話セット(おもちゃなど)を受け取ります。
今回はお試し散歩なので、近くの公園に行きます。
公園で遊びます。
聞きたいことや、分からない事があったら、アプリ上から、小太郎組の仲間に相談します。
様子が見たい家族が居たら、アプリで撮影します。
散歩に行く際にはGPSをオンにしておくと、散歩の履歴が見れて安心です。
散歩が終わったら、集合場所で、小太郎くんを引き渡します。
ここからは、シンカイさんが、小太郎組に加入するまでです。
お試し散歩が無事済んだら、小太郎組にも準加入扱いで、グループチャットにも参加できます。
面接を受けていきます。
面接と言っても、散歩途中に合流して多少話をする程度です。
何度か散歩面接を繰り返します。
それと同時に、スマホで飼い主試験を受けます。
飼い主試験は、主に犬の飼い方や散歩の仕方、餌のあげ方を教えるものです。難しくはありません。
不合格でも、期間を空けて再度受験出来ます。
試験に合格すると、受け入れ判断をお願いできます。
1週間くらいの判断期間を置きます
小太郎組からの受け入れ許可出たので、小太郎組に加入しました。

犬組は、初めて犬を飼う方に特におすすめです。なぜなら、飼い慣れている飼い主と一緒に飼えるからです。ペットショップで買い主になるのではなく、犬組で飼い主になってみませんか?

なぜ殺処分がゼロになるのか

ゼロの理由は3つ
1. 子犬を選べなくなる。
2. 能力不足の飼い主が居なくなる。
3. 費用と労力が減るので、飼育可能家庭が増える。

529AF3FD-5538-4878-82AB-6F0A9A7E51C9.jpeg

原因の説明と解決の仕組み

1.なぜ子犬が選べないことが殺処分を減らすのか?

答え:子犬の需要が減るので、流通内の殺処分が減ります。
日本において、殺処分の最も根本的原因は、『子犬の需要が高く、利益率が高いので、ペットショップを維持するためには、子犬のみを売り続けなければならない』ことです。お店は、生き残るために犬を作り続ける必要があるのです。別に邪悪なわけではありません。その過程で、生後6ヶ月過ぎた犬やひどい扱いで無気力や攻撃的になってしまった犬が処分されるのです。大体流通過程で、1年につき1万4000頭行方不明になっています。1
犬組では、子犬はあまり選ばれません。なぜなら、初心者は、既に近くで飼われている犬を選択することになるからです。つまり初心者の子犬の需要が減ります。中級者や熟練の飼い主は、子犬の期間は非常に短く大変なので、子犬である事をあまり重視しません。結果として、子犬が売れなくなるので、パピーミル(子犬工場)の過剰生産を減らせます。
子犬問題は、子犬ばかり買う初心者の問題なのです。初心者が、子犬を選び難くなれば、問題は解決されます。
注:ペットショップは潰れません。わたしは潰すつもりもありません。サービス面(犬用のトリミングや犬用具)の売り上げが安定するのでより安定的(自分で犬を飼うリスクがない)に儲かります。ただし、名前はペットショップではなく、ペットセンターになると思います。

2.なぜ能力不足の飼い主が居なくなるのか?

答え:複数人で飼うので、ダメな人は支援を受けられます。
犬組の標語の通りです。みんなで飼うから、安心・安全です。
複数人いるので、困ったとき誰かがそばにいます。
個別の事象について説明します。
能力不足の飼い主は、大きく分けると2種類居ます。
2.1. 犬をもののように扱い、要らなくなったら捨てる飼い主
2.2.頭数管理が出来ずに、増やししづつけて、多頭飼育をする飼い主です。

まず捨てる飼い主について。
 飼うのが困難になったとき、今までは、代わりの飼い主を探すか、見つからなければ保健所に引き渡していましたよね。この辺なんかはまともな方で、酷い話だと冷蔵庫で凍死させて生ゴミに捨てるとかありますからね。
特殊な事例だと、病気や怪我で飼えなくなるとか、逮捕されてどうしようもないとか。
でも犬組なら、抜けちゃえばいいだけですから安全です。保証金は、返金されませんが、それだけです。犬もその家には、いけなくなるけれども、別の家があるから困りません。犬は捨てられなくなります。
 犬組に入っていなくても、犬組では、一人の負担量が少ないですから、犬組を利用している人に、ちょっとづつ負担して貰って、時間を稼げば、別の飼い主は見つかります。

多頭飼育の飼い主について
 多頭飼育と言うと10頭以上をイメージするかもしれませんが、飼い慣れているプロでさえ1人で飼うなら2頭飼うのが限界ですからね。3頭以上いるとまともな飼育はできなくなります。お年寄りが室内飼いしていて、1匹だけかわいそうだからと、2匹目飼ったら余裕で崩壊します。他にも、いつの間にか妊娠していて、いきなり6頭子犬が増えるとか、知り合いから、どうしても預かって欲しいと言われて、結局引き取りに来ないで多頭飼育に陥る人もいます。ちゃんと管理しているつもりでも、突然多頭飼育になってしまうことはあります。

 ですが犬組なら、2匹一緒に飼いたいなら、2匹を犬組に登録するだけ。同時に預かるようにすれば、犬がいるのは月に一週間だけ、それ以外は、他の人が飼ってくれます。今までと比べると圧倒的に手間が少ないのです。
 そして、犬組では、犬が複数人で管理され、去勢手術が確実に行われます。そのため、出産による多頭飼育崩壊のリスクも非常に少ないのです。
 犬がたくさんで動けなくなっていても、同時に複数頭いなければなんとかなるので、犬組を作って、周りから支援をもらえば、多頭飼育の状態を脱する事が出来ます。
 

3.どの程度、費用と労力が減るのか?そもそも犬を終身飼うといくら掛かるか?
その結果どの程度、飼育頭数が増えるのか?

答え:約3分の1になります。15年で100万円くらいです。犬組なしの場合300万前後になります。
単純に3倍になると言うことはありません。雑な計算ですが年間で1万頭ほどです。


1つの家族で飼うなら、
散歩は、毎日。月1で病院。ブラッシングは週2、お風呂も月1~2。
金銭的負担も合計で250万~450万くらいかかります。犬種と病気の頻度によって大きく差があります。
犬自身に15万
様々なものを揃えるのに、10万
通年費用(食事や医療費保険)で15万
15年一緒に居れたとして、最小でも合計250万
別途、介護が必要になる程度によりますが、介護費用は、一回手術するだけで30万くらい。年間100万以上かかります。

それが何と犬組なら、散歩は、月に3日を2回だけ。
飼い初めは、無料 (すである犬組に加入する場合)
細々とした準備に5万円くらい (アプリでも購入可です わんこセット、ゲージ、ベッドとトイレ5万円也)
年間費用は、約6万円(アプリ費用2万4千円+保険料4千円+その他)
保険は一頭につき年間約3万円の費用をいただきます。犬種によって価格は違います。医療は、揉めるので保険に入っていただきます。
同じく15年一緒に居れたとして、合計100万円
介護の手間も5分の1かつ、亡くなった際、揉めないためにできる限り介護施設(ペットホテル)を用いるので、介護ほぼなし。
保険が毎年払われているので、保険は介護も対象です。

飼育増加頭数は、現在の保護件数から予測しています。
現在どれくらい保護犬がいるかと言うと、年間で3万5千頭くらい。
飼育受け入れ家族が年間2万5千家族。
犬組で受け入れ家族が増える家族数を、一人で受け入れていた家庭の2倍と仮定すると約5万家族。
犬組で、負担量が5分の1になるので、追加で1万頭つまり、保護犬の全てを保護できる計算になります。

諸経費のおおまかな比較

名目             一人飼い      犬組一人あたり
購入費用 150,000円 0円
餌その他 80,000円 20,000円
アプリ費用 0円 24,000円(月2,000円)
保険料 30,000円 6,000円
医療費 20,000円 4,000円
予防接種+ダニ除け 10,000円 2,000円
雑費(リードなど) 10,000円 2,000円
合計 15万+年15万円 年5万8千円

注:個体によって費用が大きく違う介護費は含んでいません

犬組によって、日本のペット飼育文化は大きく変わります。所有する犬から、所属する犬への変化です。
費用に関しても上記に示したように、3分の1になります。
安易な購入をする飼い主は、確実に減ります。ショーケースに入れての販売は利益が出なくなります。
多頭飼育の飼い主は、偶に会える状態のまま犬を手放せます。
保健所は、引き受け家族に求める費用や手間が、3分の1以下になります。今より多くの人が受け入れてくれます。

犬組は、殺処分をゼロにします。保健所での殺処分がゼロと言う意味ではありません。
流通過程を含む殺処分をゼロにします。ただし、10年かかります。

犬組の機能と収入構造

犬組の機能は、犬組メンバー向け機能と犬と誰でも使える機能に分かれています。

964254C7-1CB8-41AD-A1C7-82155159DF5A.jpeg

犬のレンタルについて
 犬組で飼っているいるを、レンタルペットとして貸し出せるサービスです。
貸し出し費用を所属している犬組で受け取る事が出来ます。犬組にはまだ入らないないけれど、
犬を飼うことを体験してみたい方、もしくは、一時的に犬と遊びたい方向けのサービスです。
 なぜ必要なのか。それは、飼育経験者を増やすためです。
被災時や過剰飼育崩壊で、地域で飼育せざる得ない場合があります。
その際、犬を飼ったことのある方、世話をしたことのある方が多いと対応しやすのです。
ペット同伴可能な避難所でも、飼ったことのある方が多いだけで助かります。
ですから、経験者を増やすためにレンタルペットに対応しています。

プライムサービスについて
 プライムサービスとは、ペットセンターで様々な恩恵を受けれるサブスクションサービスです。
サービス内容は、全国どこでも使えるものと、契約したセンターのみで使えるものがあります。

930D8384-6E1C-4038-8078-A8D53A19017D.jpeg

プライムサービスがセンターごとの契約になっているのは、2つ理由があります。

1.犬種毎に求められるサービスが大きく違う。そのために、統一が難しい。
トレーニングをするとしても、小型犬と大型犬では全く違います。トリミングにしても、ペットホテルにしても、ドッグランにしても全く違うものが必要です。
超大型犬に至ったっては、2人でも相手は無理です。
全ての犬種を受けるセンターは大変ですし、犬にとっても危険です。
そのために、サービスを受ける犬種とセンターの能力を確認して、センターと契約していただく必要があります。

2.保健所とペットショップに、犬組作りに協力してもらいたいからです。
 保健所について
企業が公共の施設を買収するとかないですよね。なぜなら、責任を取れませんし、儲からないかなら民間では到底運営できないです。
しかし、犬組を、最も必要としているのは保健所です。ですから、犬組のプライム提供者になって利用してもらうのです。
犬組が傘下に加えるのではなく、保健所が犬組を利用するのです。ヤフオクの官公庁オークションと同じ構造です。
保健所のプライムサービはかなりお得です。なぜなら、税金で人件費と場所代が出ているからです。
内容は、おそらくですが犬の無償譲渡、職員によるトリミングと健康診断、2〜3日の無料お預かり、
施設内のドックラン無料、職員による飼い方指導などになると思います。
職員による指導が特に強力です。なぜならプロだから。
 ペットショップについて
いちばんの売れ筋である子犬が販売できなくなると、小さいペットショップは潰れます。
ですが潰れると預かってもらえるインフラが無くなってしまうので、飼えなくなる人が出ます。つまり潰れもらうのは困るわけです。
潰れないためには、何が必要なのか。それは安定した顧客数です。そのために、お客さんをプライムサービスで定期的に来店させようと言うことです。
プライムサービスで、トリミングやお散歩代行、ペットホテルの利用敷居を下げます。なかなか使った事がないと使わないですからね。
契約の際には、犬組で複数人が確認します。そのため安定的に来店してもらえますし、お店も質が保たれます。
 保健所と需要を食い合うこともありません。なぜなら、価格と距離が違うからです。
旧来の一人で飼っていた飼い主は、基本的にお金持ちなので、低価格の売りの保健所プライムを選択しません。
より高級で、フルサービスを選択するので、保健所プライムの影響はありません。
距離も違います。ペットショップは、近所にありますよね。保健所は郊外にあります。
新規の飼い主は、最初の2~3年程度は、保健所プライムかもしれませんが、飼い慣れてくれば近所のペットセンタープライムに切り替える犬組が多くなります。

どのような収入があるか。

一つの犬組あたり平均10万円ほどの収入を見込んでいます。
アプリ使用料、一匹あたり月1万円。
共同購入の販売益、リードやブラシ、ドッグフードなど、共同で買うべきもの月5千円。
犬レンタルの手数料、飼い主がレンタルようを設定できる。1泊2日で8千円程度を想定。保険料と手数料含めて、一回2千円程度。
プライムサービス、月一人1千円から。ペットセンター毎に、違う料金になる。
物販の販売益 共同ではない形で買うもの おやつやおもちゃなど
保険 一匹あたり年間3万円

何を主体に収益を上げるのか。

 利益を上げる主体は、3つ。
 

1.アプリ使用料利益が足りなければ、遠慮なく値上げします。
生物を扱う以上、持続性を一番に重視します。嫌われるお金の取り方ですが、間接的に取るより、頻繁に値上げすることで、継続的経営を目指します。

2.保険料犬を健康にする事で、総支払い量を減らし、個別の支払いを分厚くします。
共同で管理し、複数の眼で見る事で、病気や怪我の重症化を防ぎます。
GPSと画像診断を用いて、健康増進を進めます。つまり先行医療にする事で、保険収益を増やします。

3.プライムサービス
プライム店舗から、売り上げに応じた割合で支払っていただきます。店舗を立ち上げた最初の3年は、無料にします。
そして新規に犬組を構築した場合、アプリ使用量から一定金額を支払います。
そのためすぐには、大きな収入にはつながりませんが、規模が大きくなるにつれ、安定的に利益になります。

おそらく、アプリ使用料で利益の7割を出すことになると思います。保険料とプライムサービスの収入は、1割前後だと思います。

 収益が上がる可能性の低いもの

1.共同購入と物販の販売益
アマゾンやメルカリに、配送費用や価格交渉力で、勝てるとは思いません。ですが共同飼育の際は、何を買うかもめるので、意見をまとめる機能を持った公式の通販ショップを用意する必要があります。
まとめる機能について。購入候補を、皆に提案します。3人以上が同意したら、共同ポイントから購入されます。
機能があるからと言って、割高にするわけにもいかないので収益は上がりません。

2.犬レンタルの手数料
災害時に預かっていただける方を増やすための活動なので、利益を上げることを考えていません。
ただし利用料は、共同ポイントとして飼い主還元されるので、ある程度物販に繋がります。

10年後の売り上げ予想。
一つの犬組で年10万、2018年度の犬の飼育頭数が900万頭。1割が犬組を利用するとして、90万犬組。
900億くらいの規模になりますね。みどり安全の売上高と同じくらいです。

誰かを犠牲にしない殺処分ゼロ

 今の殺処分ゼロは、引き取り屋とNPOボランティアの犠牲の上に成り立っています。
殺処分が見かけの上でゼロになったのは、動物愛護法で殺処分目的の犬の保護を拒否するようにしたからです。ですから実際には、事実上の殺処分は行われています。それ以外にも様々な犠牲を出しています。
5D370D2D-3ACA-4008-99D2-40960CB31B91.jpeg

引き取り屋とは
一匹1万円から3万円で引き取って、一応飼い主を探して引き渡す団体です。
ですが、引き渡される犬は、状態があまり良くないので、処分されたり、母体として悲惨な飼い方をされています。
それと、引取の際の代金が、主な収入なので、犬が出ていく先がほとんどないです。
そのために、監視の目がないので多頭飼育崩壊しがちです。

 今までのNPOをとの違いお金を定期的に取ります。ボランティアも利用しません。。
そのため、給料も保証もない現在のNPOとは違い、しっかりと福利厚生を支払う事が出来ます。
もちろん、まともに給料と保証をするNPOがないわけではありませんが、ごく少数です。

 犬組運営会社は、直飼育をおこないません。
あくまで飼う主体は、個々の犬組の方達です。
犬組運営会社が行うのは、犬の飼育を継続出来るようにすることです。
システムとしては、メルカリに近いですね。
人と人の物の交換支援サービスがメルカリなら、
人と人と犬の協力支援サービスが犬組です。
そのために、獣医師やプロトレーナーたちによる相談サービスを行います。
場合によっては、弁護士による対応も行います。
ちゃんと飼えるかどうかの試験も行います。

犬組を社会に導入して、犬の飼い方、社会との関係性、商売のあり方を、犠牲のない方向に変えましょう。

以上で、犬組の説明を終わります。細かな質問や説明を省いてしまった点は、この後の質問とその答えでお答えします。
長い文章に、お付き合いいただきありがとうございました。一応書いておきますが、現在犬組と言うアプリはリリースされていません。
この文章は企画文書を公開しているだけです。勘違いをさせたようなら申し訳ございません。

質問とその答え

質問:新たに犬を購入して、犬組を作れますか。

答え:作れます。ただし、素人だけで飼い始めると揉めるのでプライムで指導をしてもらうことをお勧めします。
保健所やペットセンター、生産者(ブリーダー)で、新規の犬組募集がありますから、利用してください。

質問:うちで飼っている雑種犬を犬組にできますか。

答え:出来ます。どのような犬種でも対応しております。
純血種を混ぜたミックス犬の場合、病気の際のリスクが若干高いことを説明するのを忘れないでください。
それと、シリアスブリーダー(純血種維持繁殖家)からは、プライムサービスを受けらない可能が高いです。

質問:自分が犬を購入して、犬組を作ったら、その代金を皆で払ってもらえますか。

答え:分割して払ってもうことは可能です。
アプリメニュー内の共同購入から、アプリ外支払いを選択し代金を請求してください。
ただし、事前に相談してから支払いを求めてください。相手には、アプリ外支払いを支払わない自由があります。
それと、犬組に新たに参加する人物に購入代金を求める場合は、犬組説明にその旨を書いて下さい。
犬組アプリの基準として、新規加入者に購入代金を請求できるのは、
犬が満2歳になるまで、かつ一人当たりの代金は、3万円を上限として下さい。
以上に反した場合、支払われた代金の払い戻しや使用を停止する場合がございます。

質問:飼っていた犬が死亡した際、犬組はどうなりますか。

答え:アプリは継続利用可能です。支払いは不要です。
犬組チャットやその他の機能も利用可能です。
ポイントに関しても、払い戻しできます。

質問:犬組のメンバーが出て行ってしまいました。どうしたらいいでしょうか。

答え:人数不足でも解散になるわけではありません。安心して下さい。人数が足りない場合、新たに人を募集して下さい。
募集方法はたくさんあります。
アプリから募集をかける
以前レンタルしたことのある人物に、声を掛ける。
保健所のイベントで犬を連れて参加する。
ペットセンターで募集を出す。

質問:犬のレンタルは、いくら位にしたらいいですか。

答え:標準価格に設定すると、犬種と年齢によって自動設定されます。

基準は、1時間1000円 一泊二日 8000円 一週間2万円

質問:レンタルは最大どれくらい出来ますか。

答え:犬組の設定によりますが、最大期間は1週間です。

レンタル期間が長すぎると、誘拐に利用される場合がございますので上限は短くなっています。

質問:近所の家が、多頭飼育で崩壊しています。どうしたらいいですか。

答え:すぐに最寄りの保健所に連絡して下さい。迷っていると手遅れになる場合があります。

多頭飼育崩壊の場合、飼い主が蒸発してトラブルが発覚する事があります。
その場合、内部はかなり悲惨な状況になるので、直ちに保健所の立ち入りを求めて下さい。
間違っても突入はやめて下さい。素人では無理です。怪我をする可能性が高いです。
保護された犬は、保健所から、犬組に登録されますので安心して、通報して下さい。

質問:共同購入にクレジットカードは使えますか。

答え:使えます。直接は、利用できないので犬組PAY支払いを選択して下さい。

犬組PAYは、スマホ決済アプリです。犬組アプリでの割り勘払いに対応しています。
お手数とは思いますが、安全のためアプリが分割されています。
アプリストアからインストールして下さい。

質問:犬が病気で、家から動かせなくなりました。どうしたらいいですか。

答え:一人で抱え込まず、動物病院や介護施設を利用して下さい。

施設に預ける事で、他のメンバーも気軽に会いにいくこともできます。
犬自体も移動の負荷がなくなり安全です。

質問:このアプリってメルカリを犬シェア用に作り替えただけですよね。

答え:メルカリに大きな影響を受けているの確かです。

ですが、メルカリでは、継続的シェアサービスありません。
そして犬組は、グループができた後のほうが長いサービスです。
サブスクションを主体にしたりサービスなので、業務内容はだいぶ違います。


  1. 犬を殺すのは誰か ペット流通の闇 太田匡彦 (著) より 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golang, Node.js, Kotlin, Swift で ECDSA

はじめに

楕円曲線DSA (ECDSA) は楕円曲線暗号を利用した電子署名方式で、通信のセキュリティ確保のために広く使われています。様々なプログラミング言語の標準的なライブラリでサポートされているため、言語をまたいで利用することができます。

ただし、実際に言語をまたいで署名生成 & 検証をしようとしてみると API の違いやフォーマット方式によりハマること多々あったため、書き方をまとめておくことにしました。

この記事に書かれていること

Golang, Node.js, Kotlin, Swift での(できる限り)標準ライブラリを利用した キーペア生成、署名生成・検証方法を記載します。

以降の記述は基本的に ECDSA を前提に書かれています。

全体的に、エラーハンドリングは省略しているので注意してください。

この記事に登場するキーワード

キーワード 概要
EC 楕円曲線、もしくは楕円曲線暗号のこと。
P-256 利用する楕円曲線の種類。キーペア生成、署名生成・検証時にパラメータとして指定します。NIST で規定されているものとしては他には P-384 などがあります。
キーペア 秘密鍵とそれに対応する公開鍵のペアのこと。 ECDSA では秘密鍵は 1 つの整数, 公開鍵は楕円曲線上の点を表す 2 つの整数で構成されます。
SHA256 256 ビット(32 バイト)のハッシュ値を生成するハッシュ関数。
r, s ECDSA の署名値。 r, s のどちらも整数値です。
ASN.1 データ構造を定義するための標準インターフェイス記述言語(wikipedia のグーグル翻訳まま)
DER エンコード ASN.1 の標準符号化規則の一つ。秘密鍵・公開鍵や署名データのシリアライズに利用する。
PEM 形式 DER エンコードの結果のバイナリデータを base64 エンコードして -----BEGIN [TYPE]-----, -----END [TYPE]----- で囲ったもの。

Golang, Node.js, Kotlin, Swift での ECDSA

それぞれの言語でキーペア生成、署名生成、署名検証をおこないます。

公開鍵は PEM 形式, 署名データは ASN.1 エンコードした結果のバイナリデータを Base64 形式で出力します。

公開鍵を PEM 形式としたのはただ単によく見かけるから、という理由からです。(検証をおこなうという意味では DER エンコード されたバイナリデータを Base64 もしくは Hex 形式で出力するだけで十分だと、後から気づきました...)

Golang

Golang は標準ライブラリが充実しているため、さほど苦労することなく扱うことができます。

参考: golang ecdsa パッケージ

以下のコードは version: 1.13.3 で動作を確認しています。

package main

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "crypto/sha256"
    "crypto/x509"
    "encoding/asn1"
    "encoding/base64"
    "encoding/pem"
    "fmt"
    "math/big"
    "os"
)

const (
    msg = "Hello, ECDSA!"

    targetPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExW7riGWvlxmRofxQNuRhsF9anb+8
F/1NRGzZziCC/utzFMXSg9YwzaRb0Yw+K2n0+1IkWH7lQT9j4DZhF6Npfg==
-----END PUBLIC KEY-----`
    targetSignature = "MEUCICzZzFaPemBrWBLNlbbEG+CyXEdAbum9YnOe7lK0rNonAiEA8p1QN/1VcuWRvrPSDnELXedMfiP1FPtk/dmP3Sf/7gA="
)

type rawSignature struct {
    R, S *big.Int
}

func main() {
    sign()
    verify()
}

func sign() {
    fmt.Println("================================ start signing ================================\n")

    // P-256 をパラメータに指定してキーペアを生成
    privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    publicKey := privateKey.PublicKey

    // 秘密鍵の整数値を出力
    fmt.Printf("private key is %d\n", privateKey.D)
    fmt.Println()

    // 秘密鍵を SEC 1, ASN.1 DER エンコード
    sec1FormPrivateKey, _ := x509.MarshalECPrivateKey(privateKey)
    // PEM 形式で出力
    _ = pem.Encode(os.Stdout, &pem.Block{
        Type:    "EC PRIVATE KEY",
        Headers: nil,
        Bytes:   sec1FormPrivateKey,
    })
    fmt.Println()

    // 公開鍵の整数値のペアを出力
    fmt.Printf("public key is (x: %d, y: %d)\n", publicKey.X, publicKey.Y)
    fmt.Println()

    // 公開鍵を PKIX, ASN.1 DER エンコード
    pkiFormPublicKey, _ := x509.MarshalPKIXPublicKey(&publicKey)
    // PEM 形式で出力
    _ = pem.Encode(os.Stdout, &pem.Block{
        Type:    "PUBLIC KEY",
        Bytes:   pkiFormPublicKey,
    })
    fmt.Println()

    // メッセージのハッシュ値を取得
    hash := sha256.Sum256([]byte(msg))

    // 署名生成
    r, s, _ := ecdsa.Sign(rand.Reader, privateKey, hash[:])
    fmt.Printf("signature: (r: %d, s: %d)\n", r, s)

    // 署名を ASN.1 エンコード
    asn1Signature, _ := asn1.Marshal(rawSignature{r, s})
    // Base64 形式で出力
    fmt.Printf("asn1 base64 encoded signature: %s\n\n", base64.StdEncoding.EncodeToString(asn1Signature))
}

func verify() {
    fmt.Println("================================ start verification ================================\n")

    // PEM ブロックを取得
    block, _ := pem.Decode([]byte(targetPublicKeyPEM))
    if block == nil || block.Type != "PUBLIC KEY" {
        panic("invalid public key pem data")
    }

    publicKey, _ := x509.ParsePKIXPublicKey(block.Bytes)

    asn1Signature, _ := base64.StdEncoding.DecodeString(targetSignature)

    var sig rawSignature
    asn1.Unmarshal(asn1Signature, &sig)

    // メッセージのハッシュ値を取得
    hash := sha256.Sum256([]byte(msg))

    // 署名検証
    valid := ecdsa.Verify(publicKey.(*ecdsa.PublicKey), hash[:], sig.R, sig.S)
    fmt.Printf("signature was verified: %t\n", valid)
}

出力結果:

================================ start signing ================================

private key is 86406366532313532520773863615456167011096149492537621067924417740068666801996

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIL8IRTYAiQtNKvAMDxMtucbcrF40K9lPEJr1eFG3JP9MoAoGCCqGSM49
AwEHoUQDQgAECLCYIbdaHGU4phHj28OXTy04YcKD2wsL0fqbSCP4pMQIghdIGvCd
jwZ9nntlLfpdY/d6Wnp/GcwEosAYSCQFjg==
-----END EC PRIVATE KEY-----

public key is (x: 3930517846499297788187286115327721111010190045004457380847771725537278993604, y: 3848353591206560331525005698968473002174715783271413427180613072969827353998)

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECLCYIbdaHGU4phHj28OXTy04YcKD
2wsL0fqbSCP4pMQIghdIGvCdjwZ9nntlLfpdY/d6Wnp/GcwEosAYSCQFjg==
-----END PUBLIC KEY-----

signature: (r: 47681670912106433244589806297495994583210073185120436994285114290076291204903, s: 93735733074916934648422947032629918680486834787857816571963967793396929295074)
asn1 base64 encoded signature: MEUCIGlq3o447llhyWn8G/p9GN3e1NMDC7zZm21OUIj+RIcnAiEAzzyLeJtUyecBmFvxA/bV0uXEuZ5B1fN4xyEcilv8cuI=

================================ start verification ================================

signature was verified: true

Node.js

Node.js も標準ライブラリを利用できますが、秘密鍵・公開鍵や署名はエンコードされた情報はとれるものの、整数値を直接取得することはできないようです。
(DER, PEM デコードするライブラリは数多く存在していたので、必要があれば簡単に取得はできそうです)

参考: Node.js crypto モジュール

以下のコードは version: 12.14.1 で動作を確認しています。

const crypto = require("crypto");

const msg = "Hello, ECDSA!";

const targetPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExW7riGWvlxmRofxQNuRhsF9anb+8
F/1NRGzZziCC/utzFMXSg9YwzaRb0Yw+K2n0+1IkWH7lQT9j4DZhF6Npfg==
-----END PUBLIC KEY-----`
const targetSignature = "MEUCICzZzFaPemBrWBLNlbbEG+CyXEdAbum9YnOe7lK0rNonAiEA8p1QN/1VcuWRvrPSDnELXedMfiP1FPtk/dmP3Sf/7gA="

main();

function main() {
  sign();
  verify();
}

function sign() {
  console.info("================================ start signing ================================\n")
  // P-256 をパラメータに指定してキーペアの生成
  const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", {
    namedCurve: "P-256",
  });

  // 秘密鍵を SEC 1, ASN.1 DER エンコード & PEM 形式で出力
  console.info(privateKey.export({
    type: "sec1",
    format: "pem",
  }));

  // 公開鍵を PKIX, ASN.1 DER エンコード & PEM 形式で出力
  console.info(publicKey.export({
    type: "spki",
    format: "pem",
  }));

  // 署名生成
  const signer = crypto.createSign("SHA256"); // ハッシュ関数を指定
  signer.update(msg);
  signer.end();
  const signature = signer.sign(privateKey, "base64");

  // 署名は ASN.1 エンコード され、 Base64 形式で出力されている
  console.info(`asn1 base64 encoded signature: ${signature}\n`);
}

function verify() {
  console.info("================================ start verification ================================\n")
  const publicKey = crypto.createPublicKey(targetPublicKeyPEM)

  // 署名検証
  const verifier = crypto.createVerify("SHA256"); // ハッシュ関数を指定
  verifier.update(msg);
  verifier.end();
  const valid = verifier.verify(publicKey, targetSignature, "base64");
  console.info(`signature was verified: ${valid}`);
}

出力結果:

================================ start signing ================================

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEILtZOGwW/gh1geY6yu4bfEuzSrwa4BJnuE37gwAsZb/IoAoGCCqGSM49
AwEHoUQDQgAEPu/QDDiV4ry2T4Ki9r9VIXgvLH09x/4J32HVdOXUlnVQegD52191
DQJ3Q2H41MTnD+uZdlGnQAUkgYSRt1A7jw==
-----END EC PRIVATE KEY-----

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPu/QDDiV4ry2T4Ki9r9VIXgvLH09
x/4J32HVdOXUlnVQegD52191DQJ3Q2H41MTnD+uZdlGnQAUkgYSRt1A7jw==
-----END PUBLIC KEY-----

asn1 base64 encoded signature: MEQCIBdoySVlQAjUSVb61H+7FzPI3+b4m4Agy62MO6/vVFkEAiAWPRjje4g/6/LpY/dUg+4dteQRK/qMI/kn3s0zIJbrTQ==

================================ start verification ================================

signature was verified: true

Kotlin (Android)

Android 開発においては Keystore システムでサポートされる API を利用することができます。
鍵データの管理を委譲できるのは大きなメリットである反面、秘密鍵の情報にアクセスする API が用意されていないようです。
例えば、独自にバックアップを取るなど、特殊なことをする場合は工夫が必要そうです。

署名については Node.js 同様に ASN.1 エンコード後のバイナリデータが返されます。

参考: Android Keystore システム

以下のコードは android SDK version: 29, kotlin version: 1.3.61 で動作を確認しています。

(PEM 形式を扱う箇所はかなり強引な書き方をしています。適切なライブラリを使ったほうが良いです)

package com.example.ecdsa

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.Signature
import java.security.interfaces.ECPublicKey
import java.security.spec.X509EncodedKeySpec

class MainActivity : AppCompatActivity() {

    companion object {
        const val msg = "Hello, ECDSA!"

        const val targetPublicKeyPEM = """-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExW7riGWvlxmRofxQNuRhsF9anb+8\
F/1NRGzZziCC/utzFMXSg9YwzaRb0Yw+K2n0+1IkWH7lQT9j4DZhF6Npfg==
-----END PUBLIC KEY-----"""
        const val targetSignature = "MEUCICzZzFaPemBrWBLNlbbEG+CyXEdAbum9YnOe7lK0rNonAiEA8p1QN/1VcuWRvrPSDnELXedMfiP1FPtk/dmP3Sf/7gA="
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        sign()
        verify()
    }

    private fun sign() {
        println("================================ start signing ================================")

        val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
            "ECPrivateKey",
            KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
        ).run {
            setDigests(KeyProperties.DIGEST_SHA256) // ハッシュ関数を指定
            build()
        }

        // キーペア生成
        val keyPair = KeyPairGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_EC,
            "AndroidKeyStore"
        ).let {
            it.initialize(parameterSpec)
            it.generateKeyPair()
        }

        val publicKey = keyPair.public as ECPublicKey

        // 秘密鍵は KeyStore 内で管理される前提であるためか、内部のデータにアクセスする API が見当たらなかった

        // 公開鍵の整数値のペアを出力
        println("public key is (x: ${publicKey.w.affineX}, y: ${publicKey.w.affineY})")

        // 公開鍵を PKIX, ASN.1 DER エンコード & PEM 形式で出力 (かなり強引...)
        println("-----BEGIN PUBLIC KEY-----")
        Base64.encodeToString(publicKey.encoded, Base64.DEFAULT).trim().chunked(64).forEach {
            println(it.replace("\n", "\\n"))
        }
        println("-----END PUBLIC KEY-----")

        // 署名生成
        val signature = Signature.getInstance("SHA256withECDSA").run {
            initSign(keyPair.private)
            update(msg.toByteArray())
            sign()
        }

        // 署名は ASN.1 エンコード されているため、Base64 形式で出力
        println(String.format(
            "asn1 base64 encoded signature: %s",
            Base64.encodeToString(signature, Base64.DEFAULT).trim().replace("\n", "\\n")
        ))

    }

    private fun verify() {
        println("================================ start verification ================================\n")
        val signature = Base64.decode(targetSignature, Base64.DEFAULT)

        val spec = X509EncodedKeySpec(
            Base64.decode(
                targetPublicKeyPEM.trim()
                    .replace("-----BEGIN PUBLIC KEY-----\n", "")
                    .replace("-----END PUBLIC KEY-----", ""),
                Base64.DEFAULT
            )
        )

        val pubKey = KeyFactory.getInstance("EC").generatePublic(spec)
        val valid: Boolean = Signature.getInstance("SHA256withECDSA").run {
            initVerify(pubKey)
            update(msg.toByteArray())
            verify(signature)
        }
        println("signature was verified: $valid")
    }
}

出力結果:

I/System.out: ================================ start signing ================================
I/System.out: public key is (x: 82167081552335602286200448410416710140443532428724715799809599812531686098238, y: 51175083263095894836653222459656189252260595373682081626878418494739276551753)
I/System.out: -----BEGIN PUBLIC KEY-----
I/System.out: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtajriseSquTJ0f2EQZQli7czMp6v
I/System.out: pHAHTW2Tq25e\nRT5xJBIYA6AgPrEdKuPtVcgamRFSKE82w1YEdxMBQCrWSQ==
I/System.out: -----END PUBLIC KEY-----
I/System.out: asn1 base64 encoded signature: MEQCIBeZSNHoN3VD7laNSDl0CGGgjrqGp50RCG6azqXmjrR/AiBKUHXJyXNLmIUCPwv33zvRfwfr\n83mfi5cJOV5Zf2QVgQ==
I/System.out: ================================ start verification ================================
I/System.out: signature was verified: true

Swift

Swift ではネイティブ API の扱いが煩雑だったため、外部ライブラリを利用しました...。

利用ライブラリ: BlueECC

ネイティブ API の扱いについてはこちらの記事が参考になります。

以下のコードは iOS: 13.1, Swift: 5.1.3 で動作を確認しています。

let msg = "Hello, ECDSA!"
let targetPublicKeyPEM = """
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV4ZTwqTk5Sd5no5ibjjTXSTZCHQV
vpe4qdp2rodC\nMdgCmdl/ZyuCpg/6PH6arDviA2HYVR13/ssin6/Etp93RQ==
-----END PUBLIC KEY-----
"""
let targetSignature = "MEUCIAU0/hEz2+RRIwzXkau64jfmUSbFoFMltXEGtl3LHlZHAiEAqak5H/QdRlheYpSpfTGTInQs\nWOUq0mDavgif8+X5uAM="

func ecdsa() {
    sign()
    verify()
}

func sign() {
    print("================================ start signing ================================\n")

    // P-256 をパラメータに指定して秘密鍵を生成
    let privateKey = try! ECPrivateKey.make(for: .prime256v1)

    // 秘密鍵を SEC 1, ASN.1 DER エンコード & PEM 形式で出力
    print(privateKey.pemString)
    print()

    // 公開鍵を PKIX, ASN.1 DER エンコード & PEM 形式で出力
    let publicKey = try! privateKey.extractPublicKey()
    print(publicKey.pemString)
    print()

    // 署名生成
    let signature = try! msg.sign(with: privateKey)

    // 署名を ASN.1 エンコードしたものを Base64 形式で出力
    print("asn1 base64 encoded signature: \(signature.asn1.base64EncodedString())\n")
}

func verify() {
    print("================================ start verification ================================\n")

    let publicKey = try! ECPublicKey(key: targetPublicKeyPEM)
    let signature = try! ECSignature.init(
        asn1: Data(base64Encoded: targetSignature,
                   options: Data.Base64DecodingOptions.ignoreUnknownCharacters)!)
    let valid = signature.verify(plaintext: msg, using: publicKey)
    print("signature was verified: \(valid)");
}

出力結果:

================================ start signing ================================

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEILu54xIBwH3Vd45Fgx9yCCgOTynjxvIMh+PnL86qOx7roAoGCCqGSM49
AwEHoUQDQgAENa6T19s23zEVLBvUYyVbZjRGPqhUkYJcv7SA8J05F8Vql7Aw9GR+
G/uxgYFqe6j1MYQ2tPF9MN32cc+xG2OCUw==
-----END EC PRIVATE KEY-----

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENa6T19s23zEVLBvUYyVbZjRGPqhU
kYJcv7SA8J05F8Vql7Aw9GR+G/uxgYFqe6j1MYQ2tPF9MN32cc+xG2OCUw==
-----END PUBLIC KEY-----

asn1 base64 encoded signature: MEYCIQD+fGwKEVX8aTzdbRgpEy9/nWHAsAw0JQXAKH4IJo4uEgIhAJKfFkN1Akl18rrnyfwwsqMa2dWwWXLbX1yRaHLZwdRG

================================ start verification ================================

signature was verified: true

まとめ

標準化をされている技術ではあるものの、各言語ごとに書き方の癖があってハマりがちな処理を並べました。デバッグのしづらさはデジタル署名のセキュリティの高さの裏返しではあるものの、ハマってしまった方にこの記事が少しでも役にたてば嬉しいです。

記載・認識ミス、もっと良い書き方などありましたら、ご指摘お願いします。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift4 の String.count を Go でやる

やりたいこと

家族?‍?‍?‍? を3文字として数えたい。

Swift4 の String.count はすごい

内部でGrapheme Clusterなるアルゴリズムを使っていて、それによってUnicode文字列の文字数を正確に計算できているらしい(参考)。

print("家族?‍?‍?‍?".count) // -> 3

Go の utf8.RuneCountInString では対応できない

?‍?‍?‍?が7文字として数えられてしまう。

import "unicode/utf8"

func main() {
    print(utf8.RuneCountInString("家族?‍?‍?‍?")) // -> 9
}

Go でも Swift4 の String.count がやりたい

github.com/rivo/unisegを使うとGrapheme Clusterを利用して文字列を処理できる。
Playgroundでやるとこんな感じになる。

import "github.com/rivo/uniseg"

func main() {
    print(uniseg.GraphemeClusterCount("家族?‍?‍?‍?")) // -> 3
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftで簡単なカウントアップして遷移するアプリの実装 

  • 実装内容 
  • コード

実装内容

1 ボタンを押すとラベルにボタンを押した回数が表示される。
2 特定の数字にいくと、次の画面に遷移する。
3 次の画面のラベルに特定の数字が反映される。
4 下の画面に戻るとラベルと数字が初期化されて、0に戻す

コード

import UIKit

class ViewController: UIViewController {    
    // count変数
    var count:Int = 0

    @IBOutlet weak var countLabel: UILabel!


    override func viewDidLoad() {
        super.viewDidLoad()

        countLabel.text = "0回ボタンが押されました"
        // Do any additional setup after loading the view.
    }


    @IBAction func tap(_ sender: Any) {
        // タップされたらカウントを1ずつ足す
        count = count + 1
        // labelに表示
        countLabel.text = "\(count)" + "回ボタンが押されました"
        // 今回は10回ボタンが押されたとのが確認されたら遷移します。
        if count == 10 {

            self.performSegue(withIdentifier: "next", sender: nil)

            // 初期化する
            countLabel.text = "また最初からです"
            count = 0
        }
    }


    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        ///ここで次の画面のラベルに渡す値を書く
        if segue.identifier == "next" as String {
            let nextVC = segue.destination as! NextViewController
            nextVC.count2 = count

        }
    }

}
import UIKit

class NextViewController: UIViewController {
 
   // このcount2の中には,前の画面のcountが入っている
    var count2:Int = 0

    @IBOutlet weak var label: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        label.text = String(count2) + "です"
    }


    @IBAction func back(_ sender: Any) {
        // 前の画面に戻る
        dismiss(animated: true, completion: nil)
    }


}

簡単なアプリのコードになりましたが、Swiftの勉強をし始めた人などご活用ください。
また今回は特定の数を指定して、その数に到達したら遷移という形ですが
特定の数をランダムにしたりできます。

ではまた!!!!!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftでちょっとしたスクリプトを書く

SwiftというとiOS開発(あるいはiPad/Macアプリ)でしか使いませんが、コマンドラインからの実行もできます。
(当たり前っちゃ当たり前ですが)
Swiftをシェルスクリプトみたいに使えないかな? とふと思って、調べてみたら普通に色々できたので、書いてみます。

実行

「script.swift」というSwiftファイルを動かしたいとしたら、下記で実行できます。

$ swift script.swift

コマンドライン引数も指定できます。

$ swift script.swift arg1 arg2 ...

できること

  • 標準入出力
  • ファイル操作
  • WebのAPIを叩く

スクリプトでやらせたいことはだいたいできる気がします。
慣れ親しんだSwiftでツール作成もできるとなれば嬉しいですね。

AtCoderやる人だったら、Playgroundだと標準入力受け取れないので、
コマンドラインで実行すると捗ると思います。

コマンドライン引数の受け取り方

let arg1 =  CommandLine.arguments[1]

標準入力

let input = readLine() //これでコマンドライン上では入力待ち状態になります

標準出力

print()です。

APIを叩く

自分では試してませんが、下記でできるとのこと。

Swiftをシェルスクリプトのように使う一番簡単な方法

各種ディレクトリパスの取得

FileManagerを使います。
なおFoundationのクラスなので、import必須です。

let filemanager = FileManager.default

print(filemanager.homeDirectoryForCurrentUser)
print(filemanager.temporaryDirectory)
print(filemanager.currentDirectoryPath)

ファイル読み込み

//ファイル名をコマンドライン引数の1番目でもらっている前提
let fileName = "./" + CommandLine.arguments[1]
if let text = try? String(contentsOfFile: fileName, encoding: String.Encoding.utf8) {
    print(text)
}

ファイル書き込み

do {
    try text?.write(toFile: "./hoge.txt", atomically: true, encoding: String.Encoding.utf8)
} catch let message {
    print("error!")
    print(message)
}

スクリプトが動いているディレクトリ直下にhoge.txtというファイルが作られます。

サンプルツール

やり方を紹介するだけだとつまらないので、スクリプトを書いてみました。

$ swift script.swift input.txt output.txt

こんな感じで実行してやると、インプットファイルの「。」の位置で改行してくれます。
つまり、

input.txt
 二人の若い紳士しんしがすっかりイギリスの兵隊のかたちをしてぴかぴかする鉄砲てっぽうをかついで白熊しろくまのような犬を二疋ひきつれてだいぶ山奥やまおくの木の葉のかさかさしたとこをこんなことを云いいながらあるいておりました。「ぜんたいここらの山は怪けしからんね鳥も獣けものも一疋も居やがらんなんでも構わないから早くタンタアーンとやって見たいもんだなあ。」「鹿しかの黄いろな横っ腹なんぞに二三発お見舞みまいもうしたらずいぶん痛快だろうねえくるくるまわってそれからどたっと倒たおれるだろうねえ。」

これが、

output.txt
 二人の若い紳士しんしがすっかりイギリスの兵隊のかたちをしてぴかぴかする鉄砲てっぽうをかついで白熊しろくまのような犬を二疋ひきつれてだいぶ山奥やまおくの木の葉のかさかさしたとこをこんなことを云いいながらあるいておりました
ぜんたいここらの山は怪けしからんね
鳥も獣けものも一疋も居やがらん
なんでも構わないから早くタンタアーンとやって見たいもんだなあ
」「鹿しかの黄いろな横っ腹なんぞに二三発お見舞みまいもうしたらずいぶん痛快だろうねえ
くるくるまわってそれからどたっと倒たおれるだろうねえ

こうなります。
ツールの仕様上、インプットの末尾に「。」が来た場合、出力されません。

script.swift
import Foundation

let filemanager = FileManager.default
let currentPath = filemanager.currentDirectoryPath
var text = ""

func check() {
    guard CommandLine.arguments.count == 3 else {
        print("""
        This tool make a new file separated the words on different lines each Japanese period(。).
        This tool need two arguments.
        (example:) > swift script.swift input.txt output.txt
         argument 0: script file name
         argument 1:  input file name
         argument 2: output file name
        """)
        exit(0) //アプリ開発だと使っちゃダメ
    }
}

func input() {
    let fileName = currentPath + "/" + CommandLine.arguments[1]
    let inputFile = try? String(contentsOfFile: fileName, encoding: String.Encoding.utf8)
    guard let t = inputFile else {
        print("input file is not found!!!")
        return
    }
    text = t
}

func output() {
    text = text.split(whereSeparator: { $0 == "。" }).joined(separator: "。\n")
    //フルパスでなくても、1) ./hoge.txt 2) hoge.txt でもカレントディレクトリ直下にファイルが作成される
    let ouputFileName = currentPath + "/" + CommandLine.arguments[2]
    do {
        try text.write(toFile: ouputFileName, atomically: true, encoding: String.Encoding.utf8)
    } catch let e {
        print("write file failed!!!")
        print(e)
    }
}

check()
input()
output()

このサンプルは著作権フリーなので、お好きに改変してお使いください。

試行錯誤のメモ

  • 最初句点の置換をreplaceSubrange(_:with:)でやろうとしたら、最初にヒットした文字しかできなくて断念
  • 更にString → [Character] → map使って"。"と一致したら"。\n"としたらCharacterに二文字は入れられません、でエラー(そりゃそうだ)

書いてから気づいたこと

上記のサンプルツール書くとき、新しいSwiftファイルとして作成して、
開発ディレクトリを直で編集していたんですが、これでやると入力補完が十分効かなくなって辛かったです。
ちょっと大袈裟にはなりますが、Command Line ToolとしてProjectつくってやるとキー補完も効くし、git管理もできるし、ビルドもできるんで便利ですね。
コマンドライン引数は流石に無理やろ……? と思ったら、それもイケるみたいです。すごいぞXcode。

XcodeでCommand Line Tool実行時にコマンドライン引数を渡す

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift]アクセスレベル

今回はアクセスレベルについて書いていきたいと思います?
よく変数や関数の前にPrivateといったのを見たことはありませんか?それの事です?

対象はSwift5です。
公式ドキュメントはこちら

アクセスレベルとは

簡単に言うと宣言した変数やメソッドなどを何処でも使えるようにするか、又は制限した範囲でのみ使用可能にするか設定することです。

全部で5つあり、それぞれ特徴があります。

アクセスレベル 説明
Private 宣言したコードブロック内でのみアクセス可能(これの中→{})
FilePrivate 宣言されたファイル内でのみアクセス可能
Internal アプリのモジュール内でアクセス可能(デフォルトはこれで、設定しなかったら自動でInternalになる)
Public 他のモジュールへのアクセスが可能になるが、継承やオーバーライドは不可
Open 継承もオーバーライドも可能になる、つまり何でもできる

なぜアクセスレベルを使うのか

アクセスレベルを設定しないとデフォルトでInternalになるので、モジュール内でアクセス可能になってしまいます。
モジュール内で使うことを想定しているのなら問題ないのですが、特定の処理に使うことを想定した変数やメソッドを関係無いところで使われるのは問題です?
それを防ぐためにアクセスレベルを設定し、意図しない利用を防ぎます!?‍♂️

このアクセスレベルはいつでも変更が可能なので、初期段階で利用範囲が決まっていないのなら、基本的にPrivateで設定して置くのが無難です。
利用範囲が大きくなったタイミングで段階的にアクセスレベルを広げていきましょう!?‍♂️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】touchesMovedが実行される最小移動量について【Tips】

連続的なジェスチャーの検知に使われる touchesMoved が実行されるタイミングに関して調査してみたので簡単にまとめる。

検証環境

  • Swift5.0
  • iPhone6s(iOS13.3)

検証方法

以下の様なコードでコンソールに測定した位置を出力することで検証した。

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
    let latestTouch = touches.first
    let newPoint: CGPoint = (newTouch?.location(in: view))!
// previousPointはクラスのプロパティとして保持
    let translation = CGPoint(x: previousPoint.x - newPoint.x, y: previousPoint.y - newPoint.y)
    previousPoint = newPoint
    print(translation)

実行される最小移動量

上記の検証コードを用いて上方向にまっすぐゆっくりとスクロールしていくと以下のようなログが出力される。

(0.0, 0.5)
(0.0, 0.5)
(0.0, 0.5)
(0.0, 0.5)
(0.0, 0.5)
(0.0, 0.5)

つまり、 touchesMoved が実行されるには最小で0.5ptの移動が必要、ということになる。
間違い等あればコメントをおねがいします!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mediapipeをiOSアプリに組み込む

mediapipeとは

Googleの開発しているML推定のパイプラインを簡単に設定できるライブラリ。Windowsを初めiOSやAndroidなどマルチプラットフォームに対応している。

https://github.com/google/mediapipe

mediapipeにはプリセットで手や関節の位置推定や顔の位置推定のモデルがバンドルされていてexampleで確認することができます。
https://github.com/google/mediapipe/tree/master/mediapipe/models

sample.gif

iOSアプリにmediapipeを組み込む

iOSアプリにmediapipeで作ったパイプラインを組み込むには、一度フレームワークとしてビルドするかiOSアプリをbazelで管理する必要があります。
ここでは既存のアプリに組み込むことを想定してフレームワークとしてビルドしてみましょう。

また今回の作業は以下のブランチで行っていたものです。
説明を省いている箇所はコミットなどを覗いてみてください。(もしくはqiitaにコメントいただければ補足します)
https://github.com/noppefoxwolf/mediapipe/tree/develop/TrackerFramework

mediapipeをクローンする

git clone git@github.com:google/mediapipe.git

bazelをインストールする

mediapipeはbazel 1.1.0が必要なため、bazelをインストールします。
bazelはgoogle製のビルドツールで、mediapipeはbazelで構築されているため必要になります。
installスクリプトを下記からダウンロードして、実行します。

https://github.com/bazelbuild/bazel/releases/download/1.1.0/bazel-1.1.0-installer-darwin-x86_64.sh

$ ./bazel-1.1.0-installer-darwin-x86_64.sh

トラッカーのコードを書く

Swiftから使いやすいように簡単にトラッカーを実装します。

mediapipe/develop/HandTracker.h
#import <Foundation/Foundation.h>
#import <CoreVideo/CoreVideo.h>

@class Landmark;
@class HandTracker;

@protocol TrackerDelegate <NSObject>
@optional
- (void)handTracker: (HandTracker*)handTracker didOutputLandmarks: (NSArray<Landmark *> *)landmarks;
- (void)handTracker: (HandTracker*)handTracker didOutputPixelBuffer: (CVPixelBufferRef)pixelBuffer;
@end

@interface HandTracker : NSObject
- (instancetype)init;
- (void)startGraph;
- (void)processVideoFrame:(CVPixelBufferRef)imageBuffer;
@property (weak, nonatomic) id <TrackerDelegate> delegate;
@end

@interface Landmark: NSObject
@property(nonatomic, readonly) float x;
@property(nonatomic, readonly) float y;
@property(nonatomic, readonly) float z;
@end

mediapipe/develop/HandTracker.mm
#import "HandTracker.h"
#import "mediapipe/objc/MPPGraph.h"
#import "mediapipe/objc/MPPCameraInputSource.h"
#import "mediapipe/objc/MPPLayerRenderer.h"
#include "mediapipe/framework/formats/landmark.pb.h"

static NSString* const kGraphName = @"hand_tracking_mobile_gpu";
static const char* kInputStream = "input_video";
static const char* kOutputStream = "output_video";
static const char* kLandmarksOutputStream = "hand_landmarks";
static const char* kVideoQueueLabel = "com.google.mediapipe.example.videoQueue";

@interface HandTracker() <MPPGraphDelegate>
@property(nonatomic) MPPGraph* mediapipeGraph;
@end

@interface Landmark()
- (instancetype)initWithX:(float)x y:(float)y z:(float)z;
@end

@implementation HandTracker {}

#pragma mark - Cleanup methods

- (void)dealloc {
    self.mediapipeGraph.delegate = nil;
    [self.mediapipeGraph cancel];
    // Ignore errors since we're cleaning up.
    [self.mediapipeGraph closeAllInputStreamsWithError:nil];
    [self.mediapipeGraph waitUntilDoneWithError:nil];
}

#pragma mark - MediaPipe graph methods

+ (MPPGraph*)loadGraphFromResource:(NSString*)resource {
    // Load the graph config resource.
    NSError* configLoadError = nil;
    NSBundle* bundle = [NSBundle bundleForClass:[self class]];
    if (!resource || resource.length == 0) {
        return nil;
    }
    NSURL* graphURL = [bundle URLForResource:resource withExtension:@"binarypb"];
    NSData* data = [NSData dataWithContentsOfURL:graphURL options:0 error:&configLoadError];
    if (!data) {
        NSLog(@"Failed to load MediaPipe graph config: %@", configLoadError);
        return nil;
    }

    // Parse the graph config resource into mediapipe::CalculatorGraphConfig proto object.
    mediapipe::CalculatorGraphConfig config;
    config.ParseFromArray(data.bytes, data.length);

    // Create MediaPipe graph with mediapipe::CalculatorGraphConfig proto object.
    MPPGraph* newGraph = [[MPPGraph alloc] initWithGraphConfig:config];
    [newGraph addFrameOutputStream:kOutputStream outputPacketType:MPPPacketTypePixelBuffer];
    [newGraph addFrameOutputStream:kLandmarksOutputStream outputPacketType:MPPPacketTypeRaw];
    return newGraph;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.mediapipeGraph = [[self class] loadGraphFromResource:kGraphName];
        self.mediapipeGraph.delegate = self;
        // Set maxFramesInFlight to a small value to avoid memory contention for real-time processing.
        self.mediapipeGraph.maxFramesInFlight = 2;
    }
    return self;
}

- (void)startGraph {
    // Start running self.mediapipeGraph.
    NSError* error;
    if (![self.mediapipeGraph startWithError:&error]) {
        NSLog(@"Failed to start graph: %@", error);
    }
}

#pragma mark - MPPGraphDelegate methods

// Receives CVPixelBufferRef from the MediaPipe graph. Invoked on a MediaPipe worker thread.
- (void)mediapipeGraph:(MPPGraph*)graph
  didOutputPixelBuffer:(CVPixelBufferRef)pixelBuffer
            fromStream:(const std::string&)streamName {
      if (streamName == kOutputStream) {
          [_delegate handTracker: self didOutputPixelBuffer: pixelBuffer];
      }
}

// Receives a raw packet from the MediaPipe graph. Invoked on a MediaPipe worker thread.
- (void)mediapipeGraph:(MPPGraph*)graph
       didOutputPacket:(const ::mediapipe::Packet&)packet
            fromStream:(const std::string&)streamName {
    if (streamName == kLandmarksOutputStream) {
        if (packet.IsEmpty()) { return; }
        const auto& landmarks = packet.Get<::mediapipe::NormalizedLandmarkList>();

        NSMutableArray<Landmark *> *result = [NSMutableArray array];
        for (int i = 0; i < landmarks.landmark_size(); ++i) {
            Landmark *landmark = [[Landmark alloc] initWithX:landmarks.landmark(i).x()
                                                           y:landmarks.landmark(i).y()
                                                           z:landmarks.landmark(i).z()];
            [result addObject:landmark];
        }
        [_delegate handTracker: self didOutputLandmarks: result];
    }
}

- (void)processVideoFrame:(CVPixelBufferRef)imageBuffer {
    [self.mediapipeGraph sendPixelBuffer:imageBuffer
                              intoStream:kInputStream
                              packetType:MPPPacketTypePixelBuffer];
}

@end


@implementation Landmark

- (instancetype)initWithX:(float)x y:(float)y z:(float)z
{
    self = [super init];
    if (self) {
        _x = x;
        _y = y;
        _z = z;
    }
    return self;
}

@end

BUILDファイルを書く

bazelはBUILDファイルを書くことで、ビルドオプションを設定します。
また、mediapipeはいくつかのビルドオプションが既に設定されているので、mediapipeのディレクトリ以下に次のように配置します。

mediapipe/develop/BUILD
load("@build_bazel_rules_apple//apple:ios.bzl", "ios_framework")

ios_framework(
    name = "HandTracker",
    hdrs = [
        "HandTracker.h",
    ],
    infoplists = ["Info.plist"],
    bundle_id = "com.noppelab.HandTracker",
    families = ["iphone", "ipad"],
    minimum_os_version = "10.0",
    deps = [
        ":HandTrackingGpuAppLibrary",
        "@ios_opencv//:OpencvFramework",
    ],
)

# To use the 3D model instead of the default 2D model, add "--define 3D=true" to the
# bazel build command.
config_setting(
    name = "use_3d_model",
    define_values = {
        "3D": "true",
    },
)

genrule(
    name = "model",
    srcs = select({
        "//conditions:default": ["//mediapipe/models:hand_landmark.tflite"],
        ":use_3d_model": ["//mediapipe/models:hand_landmark_3d.tflite"],
    }),
    outs = ["hand_landmark.tflite"],
    cmd = "cp $< $@",
)

objc_library(
    name = "HandTrackingGpuAppLibrary",
    srcs = [
        "HandTracker.mm",
    ],
    hdrs = [
        "HandTracker.h",
    ],
    data = [
        ":model",
        "//mediapipe/graphs/hand_tracking:hand_tracking_mobile_gpu_binary_graph",
        "//mediapipe/models:palm_detection.tflite",
        "//mediapipe/models:palm_detection_labelmap.txt",
    ],
    sdk_frameworks = [
        "AVFoundation",
        "CoreGraphics",
        "CoreMedia",
        "UIKit"
    ],
    deps = [
        "//mediapipe/objc:mediapipe_framework_ios",
        "//mediapipe/objc:mediapipe_input_sources_ios",
        "//mediapipe/objc:mediapipe_layer_renderer",
    ] + select({
        "//mediapipe:ios_i386": [],
        "//mediapipe:ios_x86_64": [],
        "//conditions:default": [
            "//mediapipe/graphs/hand_tracking:mobile_calculators",
            "//mediapipe/framework/formats:landmark_cc_proto",
        ],
    }),
)

Info.plistを作る

フレームワークにバンドルするInfo.plistを適当に作ります。

https://github.com/noppefoxwolf/mediapipe/blob/develop/TrackerFramework/mediapipe/develop/Info.plist

を参考にしてください。

フレームワークをビルドする

bazelを使ってフレームワークをビルドします。

$ bazel build --config=ios_fat --define 3D=true mediapipe/develop:HandTracker

ビルドが成功するとbazel-bin/mediapipe/develop/にzipファイルが生成されているので、それを解凍すると.frameworkファイルが出てきます。
framework search pathやheader search pathを設定すれば、iOSアプリから呼び出すことができます。

面倒な場合

とりあえず3D HandTrackerはcarthageで入れられるようにしてみたので、面倒な場合はこちらを使ってみてください。

https://github.com/noppefoxwolf/HandTracker

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む