cockscomblog?

cockscomb on hatena blog

Swift AWS Lambda Runtimeを試す

2020年5月末に、 SwiftをAWS Lambdaで動作させるプロジェクトが発表された。swift-server/swift-aws-lambda-runtimeがそれである。ということで、AWS CDKでAPI GatewayとSwiftのLambda Handlerを作ってみた。

Lambda Runtime

AWS Lambdaは、AWSのFaaS。提供されているランタイムを使えば、ソースコードをアップロードするだけで、関数が実行できる。提供されているランタイムはNode.jsやPythonRubyJava、Go、.NET Coreである(徐々に拡充されていった)。そして2018年のre:Inventで、Lambda LayerとLambda Runtime APIが発表され、swift-aws-lambda-runtimeでは、このRuntime APIを利用している。

Lambda Runtime APIを乱暴に説明すると、こうだ。bootstrapという実行ファイルを用意しておくと、自動的にこれを起動してくれる。bootstrapは内部でイベントループを回す。イベントループの内部では、HTTPでイベントを取得し、それを処理して、結果をHTTPで送る、というのを繰り返す。

swift-aws-lambda-runtimeは、イベントループを回して、イベントを取得して結果を返す、というところをやってくれる。

Lambda

f:id:cockscomb:20200615001416p:plain
AWS Lambda

swift-aws-lambda-runtimeの主要な開発者であるFabian Fettさんのチュートリアルを参考に進める。

Getting started with Swift on AWS Lambda

まずはSwift Package Managerでpackageを作る。

$ swift package init --type executable

API Gatewayを使いたいので、Package.swiftAWSLambdaEventsの依存も追加。

import PackageDescription

let package = Package(
    name: "Handler",
    platforms: [
        .macOS(.v10_13),
    ],
    products: [
        .executable(name: "Handler", targets: ["Handler"]),
    ],
    dependencies: [
        .package(
            url: "https://github.com/swift-server/swift-aws-lambda-runtime.git",
            .upToNextMajor(from: "0.1.0")),
    ],
    targets: [
        .target(
            name: "Handler",
            dependencies: [
                .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
                .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"),
            ]
        ),
    ]
)

API Gatewayのリクエストを受けて適当なJSONを返すのは、以下のようになる。

import AWSLambdaEvents
import AWSLambdaRuntime
import Foundation

struct Response: Codable {
    let message: String
}

Lambda.run {
    (
    context,
    request: APIGateway.Request,
    callback: @escaping (Result<APIGateway.Response, Error>) -> Void
    ) in
    context.logger.debug("\(request)")
    let encoder = JSONEncoder()
    do {
        let response = Response(message: "OK")
        let json = try encoder.encode(response)
        callback(
            .success(
                APIGateway.Response(
                    statusCode: .ok,
                    headers: ["Content-Type": "application/json"],
                    body: String(bytes: json, encoding: .utf8)
                )
            )
        )
    } catch {
        callback(.failure(error))
    }
}

Lambda Runtimeの説明で書いたようなイベントループとかそういうのは、すっかり抽象化されている。

あとはこれをbootstrapという実行ファイルにして、Lambdaにアップロードすればいい。

パッケージング

実行ファイルはAmazon Linux 2で作る。Dockerの公式なSwiftイメージに、Amazon Linux 2のものが用意されているので、今回はswift:5.2-amazonlinux2を使う。

チュートリアルを真似て、簡単なシェルスクリプトを用意する。

#!/bin/bash

set -eu

executable=$1

swift build --product $executable -c release

target=.build/lambda/$executable
rm -rf "$target"

mkdir -p "$target"
cp ".build/release/$executable" "$target/"
cp -Pv \
  /usr/lib/swift/linux/libBlocksRuntime.so \
  /usr/lib/swift/linux/libFoundation.so \
  /usr/lib/swift/linux/libFoundationNetworking.so \
  /usr/lib/swift/linux/libFoundationXML.so \
  /usr/lib/swift/linux/libdispatch.so \
  /usr/lib/swift/linux/libicudataswift.so \
  /usr/lib/swift/linux/libicudataswift.so.65 \
  /usr/lib/swift/linux/libicudataswift.so.65.1 \
  /usr/lib/swift/linux/libicui18nswift.so \
  /usr/lib/swift/linux/libicui18nswift.so.65 \
  /usr/lib/swift/linux/libicui18nswift.so.65.1 \
  /usr/lib/swift/linux/libicuucswift.so \
  /usr/lib/swift/linux/libicuucswift.so.65 \
  /usr/lib/swift/linux/libicuucswift.so.65.1 \
  /usr/lib/swift/linux/libswiftCore.so \
  /usr/lib/swift/linux/libswiftDispatch.so \
  /usr/lib/swift/linux/libswiftGlibc.so \
  "$target"
cd "$target"
ln -s "$executable" "bootstrap"
zip --symlinks lambda.zip *

swift buildして、Swiftのランタイムや標準ライブラリ(shared objectファイル)を集めてきて、実行ファイルをbootstrapという名前でsymlinkして、ZIPにまとめている。

$ docker run --rm -it swift:5.2-amazonlinux2 ls /usr/lib/swift/linux
libBlocksRuntime.so       libicui18nswift.so.65.1
libFoundation.so          libicuucswift.so
libFoundationNetworking.so    libicuucswift.so.65
libFoundationXML.so       libicuucswift.so.65.1
libXCTest.so              libswiftCore.so
lib_InternalSwiftSyntaxParser.so  libswiftDispatch.so
libdispatch.so            libswiftGlibc.so
libicudataswift.so        libswiftRemoteMirror.so
libicudataswift.so.65         libswiftSwiftOnoneSupport.so
libicudataswift.so.65.1       libswift_Differentiation.so
libicui18nswift.so        x86_64
libicui18nswift.so.65

あとはこれをDockerで動かす。

FROM swift:5.2-amazonlinux2

RUN yum -y update && yum -y install \
  zip

COPY build.sh /build.sh
WORKDIR /src
ENTRYPOINT ["/build.sh"]

ソースコード/srcにマウントするつもりなので、Dockerfileはこれだけ。

$ docker build --tag swift-lambda-builder .
$ docker run --rm --volume /Users/cockscomb/swift-lambda/handler:/src swift-lambda-builder Hanlder

あとはこういう感じで実行すると、マウントされたディレクトリ下に.build/lambda/Handler/lambda.zipができる。

AWS CDKを使う

パッケージングからデプロイの作業は退屈なので、Infrastructure as Codeって感じで、AWS CDKを使ってまとめてしまう。AWS CDKというのは、AWS CloudFormationをいい感じにしてくれるやつ。

CloudFormationは、YAMLとかで書かれたテンプレートをもとに、AWS上のリソース(ここではLambdaとか)を作成したり、更新したりしてくれる。CloudFormationで作ったリソースは、手で書き換えてはいけない。CloudFormationする時の単位をスタックと言う。

AWS CDKを使うと、CloudFormationのテンプレートをTypeScriptなどのプログラミング言語で書けるようになる。今回は使わないが、スタック間のリソースの依存関係もうまく表現できる。

CDKをセットアップしたら、以下のようなスタックを定義する。

import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
import * as apigateway from "@aws-cdk/aws-apigateway";

export class ApiGatewaySwiftStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const code: lambda.AssetCode = // TODO: ここをなんとかする

    const handler = new lambda.Function(this, "Handler", {
      code,
      handler: "Handler",
      runtime: lambda.Runtime.PROVIDED,
    });

    new apigateway.LambdaRestApi(this, "Api", {
      handler,
    });
  }
}

Lambda Functionを作って、ランタイムをProvidedにして、API Gatewayにくっつけている。AWS CDKのAPI Referenceに、各パッケージの主要な使い方が載っているので、とっつきやすい。

import "source-map-support/register";
import * as cdk from "@aws-cdk/core";
import { ApiGatewaySwiftStack } from "../lib/api-gateway-swift-stack";

const app = new cdk.App();
new ApiGatewaySwiftStack(app, "ApiGatewaySwiftStack", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});

ここで作ったclass ApiGatewaySwiftStackを、appの側でこういう感じでnewしてやればOK。

// TODO: ここをなんとかする、と書いたところでさっきのlambda.zipを作ってやりたいのだけど、どうしたものかと思っていたら、オフィシャルの@aws-cdk/aws-lambda-nodejsパッケージで、Builder classを作ってDockerでなんかしているのを見つけたので、真似する。

import * as lambda from "@aws-cdk/aws-lambda";
import { spawnSync, SpawnSyncReturns } from "child_process";
import * as path from "path";

interface Options {
  dir: string;
  executable: string;
}

export class Builder {
  private static imageName: string = "swift-lambda-builder";

  constructor(private readonly options: Options) {}

  private docker(args: string[]): SpawnSyncReturns<string> {
    const returns = spawnSync("docker", args);
    if (returns.error) {
      throw returns.error;
    }
    if (returns.status !== 0) {
      throw new Error(
        `[Status ${
          returns.status
        }] stdout: ${returns.stdout?.toString().trim()}\n\n\nstderr: ${returns.stderr?.toString().trim()}`
      );
    }
    return returns;
  }

  public build(): lambda.AssetCode {
    this.docker(["build", "--tag", Builder.imageName, path.join(__dirname, "../builder")]);
    this.docker(["run", "--rm", "--volume", `${this.options.dir}:/src`, Builder.imageName, this.options.executable]);
    return lambda.Code.fromAsset(
      path.join(this.options.dir, "./.build/lambda/", this.options.executable, "lambda.zip")
    );
  }
}

こういうDocker CLIを呼び出すものを作って、さっきのところに埋める。

import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
import * as apigateway from "@aws-cdk/aws-apigateway";
import * as path from "path";
import { Builder } from "./builder";

export class ApiGatewaySwiftStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const code = new Builder({
      dir: path.join(__dirname, "../handler"),
      executable: "Handler",
    }).build();

    const handler = new lambda.Function(this, "Handler", {
      code,
      handler: "Handler",
      runtime: lambda.Runtime.PROVIDED,
    });

    new apigateway.LambdaRestApi(this, "Api", {
      handler,
    });
  }
}

あとはこれを使ってデプロイする。事前にAWS CLIの設定をしておくとよい。

$ npm run cdk deploy

AWSアカウントでまだCDKを使ったことがなければ、先に$ npm run cdk bootstrapが必要かもしれない。

うまくいくと、API GatewayのURLが出力されるだろう。AWS ConsoleのCloudFormationを見ると、作成されたスタックが表示される。

f:id:cockscomb:20200615001508p:plain
AWS CloudFormation

適当にcurlしてみると、実際に動いている様子がわかる。

$ curl --include https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
HTTP/2 200 
content-type: application/json
content-length: 16
date: Sun, 14 Jun 2020 13:25:02 GMT
x-amzn-requestid: xxx
x-amz-apigw-id: xxx
x-amzn-trace-id: xxx
x-cache: Miss from cloudfront
via: 1.1 xxx.cloudfront.net (CloudFront)
x-amz-cf-pop: xxx
x-amz-cf-id: xxx

{"message":"OK"}

まとめ

上記の素朴なLambda Functionでは、コールドスタートとみられる場合で300から400 msの時間がかかった。一方でスタンバイからの実行では、2 ms程度だった。コールドスタートは、Lambda内部の初期化で150 ms、Lambda Functionの初期化が180 msというところ。それほど悪くはないと思う。メモリの消費は、メモリ容量を128 MBに設定したうちの51 MB。

コスト的には、Lambdaだけなら100万リクエストで1ドルかからないくらい、API Gatewayと合わせても5ドルくらいか。

ということで、SwiftでLambda Functionを作って、API Gateway経由で呼び出せるようにした。デプロイにはAWS CDKを使っている。完全なサンプルコードは以下。

実際にAWS LambdaをSwiftで開発するのかどうかというと、現時点では微妙なところである。しかしServer-side Swiftのエコシステムが、少しずつでも整っていくのは興味深い。

紛らわしい文字列をもっと紛らわしくする「InkBleed」をMac App Storeで販売開始

f:id:cockscomb:20200506153050p:plain
紛らわしい文字列をもっと紛らわしくするアプリの登場です。

インクがにじんだような読みにくいテキストを作れるMac向けの新しいアプリケーション

2020年5月7日、インクがにじんだような読みにくいテキストを作れる、Mac®︎向けの新しいアプリケーション、InkBleedがMac App Store®︎で販売開始となりました。InkBleedは、シンプルなインターフェイスにテキストを入力すると、インクがにじんだような画像をリアルタイムに生成します。画像を保存したり、コピーしたり、ドラッグしたりすることで、読みにくいテキストの画像を簡単に共有することができます。

InkBleedアプリケーションは、テキストを識別しにくくするためにmacOS®︎の最先端の技術を活用しています。Metal、Core TextとCore Graphics、Core Image、そしてユーザーインターフェイスにはSwiftUIを採用しています。

「インクがにじんでいると、テキストは判別しにくくなります。わかりやすいことが重要とされる現代で、わかりにくいことの豊さをお届けできることをとても嬉しく思います。インクのにじみのデジタル表現をぜひお楽しみください」と、ソフトウェアエンジニアリング担当、id:cockscombは述べています。

InkBleedのアイコンは、職場の同僚であり友人でもあるid:murata_sにデザインしてもらいました。美しいアイコンをDockに並べる喜びをお楽しみください。

【販売について】

InkBleedは本日からMac App Storeで有料で販売されます。InkBleedは、macOS Catalina 10.15以降を搭載し、Metalに対応しているMacコンピュータ1で利用することができます。

Download_on_the_Mac_App_Store_Badge_US-UK_RGB_blk_092917

id:cockscombについて】

id:cockscombは、趣味の一環としてこのソフトウェアを開発しています。「紛らわしい文字列をもっと紛らわしくする」というエントリで説明したように、紛らわしいテキストをもっと紛らわしくすると、ちょっとかっこいい、という表現をアプリにしたものです。どうかご了承ください。アプリの売り上げはid:murata_sと山分けし、Apple製品を購入するために使います。

在宅勤務にかこつけて自宅のデスク周りをアップグレード

京都も緊急事態宣言の対象になった4月中旬、職場で原則在宅勤務のお達しが出た。正直に言えば、3歳と0歳の子供がいる中での在宅勤務には乗り気になれなかった。それでも、社会が急速に変化する中で、自分もまた変化しなくてはいけないと思った。

f:id:cockscomb:20200504155910p:plain:w300

あまり広くない部屋に住んでいるので、以前からリビングの端に幅80センチの狭いデスクを置いている。

はじめ、12.9インチのiPad ProをSidecarにして、せめてものマルチディスプレイにした。SidecarはUSBケーブルを使うことで、Wi-Fi経由より安定して使える。またMagic Keyboardのおかげで、画面の角度が自由になる。

在宅勤務がある程度の期間続くだろうと見込んで、ディスプレイも注文した。本当は職場で使っている4Kディスプレイを持ち帰ることもできるのだけど、(どうせ前から欲しかったし、とか)いろいろと理由をつけて買ってしまうことにした。

一時的な在宅勤務のためとはいえ、長く使えるものを買いたい。4K解像度IPSパネル、USB Type-Cで繋ぐことができ、24インチから27インチ、一桁万円台、という条件でいろいろ調べ、候補を3つに絞った。

一般的にはDell U2720Qが一番バランスに優れた選択肢だろう。LG 27UL850-Wも悪くはない。しかしApple製品との連携を重視して、筆者はLG UltraFine 4K Displayを選んだ。画質もすばらしい。皆さんは真似しないでください1

合わせて ディスプレイアームも購入して、デスクに固定している。何年か前にデスクを買ったとき、ディスプレイアームのクランクが取り付けられるものを選んでいた。ディスプレイアームを使うことで、デスクをいくらか広く使える。小さいデスクでは重要なことである。そして何より、子供のイタズラでディスプレイがデスクから落下するのを防げる。

さらに有線LANも使えるようにBelkin USB-C to Gigabit Ethernet Adapter も導入。ディスプレイのUSBハブを活用している。

ディスプレイを購入したことで、生産性が劇的に向上した。ウインドウを並べて作業できる利便性は計りしれない。またUSB Type-Cのケーブル1本で、電源とディスプレイ、そして有線LANの全てにアクセスできる。便利そのもの。

f:id:cockscomb:20200502153117j:plain

ともあれ、家族の理解と協力のおかげでなんとか仕事ができている。このご時世にありがたい。私たちは、この不可逆的に変容してしまった社会で、この先も生きていかなければならない。先はあまり見えないけど、できることをやろうと思う。


  1. USB Power Deliveryが何Wとか、入力端子の充実度とか、グレアかノングレアか、DCI-P3あるいはAdobeRGBのカバー率、HDRへの対応、視野角とコントラスト比に最大輝度、スピーカーの有無、応答速度や可変フレームレートへの対応、電源が内蔵かどうかなど、色々な判断材料があると思います。

SwiftUIではSingletonの代わりにEnvironmentを使うことができる

ここで言うSingletonというのは、ある種のグローバル変数を指している。そもそもクライアントアプリケーションの開発においては、実質的なグローバル変数が出現しやすい。環境にたった一つしか存在しない、存在すべきでない、というものが見出せる。例えばタイムゾーンがそれである。アプリケーション内のタイムゾーンはシステムに合わせるのが自然であり、アプリケーション中で複数存在することはほとんど起こり得ない。

iOSmacOSでは、タイムゾーンを表すのはFoundation frameworkのTimeZoneである。システムのタイムゾーンTimeZone.currentで取得できる。これはSingletonパターンのインターフェースに近い(本当にインスタンスが一つであるかどうかをここでは問題にしない)。

このように環境中で存在する個数が限定されるような値を表すのに、Singletonパターンが用いられることがある。しかしSingletonパターンを用いると、単体テストを書くのに不便だったり、あるいは多用することでコードベースのメンテナンス性が低下するなどとして、嫌われることも多い。このような問題は、DIのようなアプローチを用いることで軽減できるだろう。

SwiftUIのEnvironment

SwiftUIでは、タイムゾーンの取得に@Environmentを使う。以下のようにしてTimeZoneインスタンスを得られる。

struct TimeZoneView: View {
    @Environment(\.timeZone) var timeZone

    var body: some View {
        Text("\(timeZone)")
    }
}

@Environment property wrapperを使い、EnvironmentValuesのpropertyをKeyPathとして(\.timeZoneのように)与えると、環境の値を読み取ることができる。

Environmentの上書き

環境の値は上書きすることもできる。以下のように、何らかのviewに対してenvironment(_:_:) modifierを呼び出すと、そのviewの子孫の環境を上書きできる。

SomeView()
    .environment(\.timeZone, TimeZone(abbreviation: "GMT")!)

全てを上書きできるわけではなく、EnvironmentValuesのpropertyにsetが設定されているものだけを上書きできる。

タイムゾーンを上書きするのはあまり有意義とは言えないが、例えばEnvironmentValues.isEnabledを上書きするのは理解しやすいユースケースである。特定のview以下を全てdisabledにするのは、リーズナブルだ。ということで、SwiftUIのviewにはdisabled(_:) modifierがあり、実質的に.environment(\.isEnabled, false)のショートハンドであろう。

新しいEnvironmentを作る

EnvironmentValuesを拡張してpropertyを追加することで、独自の値をEnvironmentに持たせることができる。extensionしてもstored propertyは追加できないが、幸いなことにEnvironmentValuesEnvironmentValues.subscript(_:)によってストレージを提供してくれる。

例えばmacOSで、viewが所属するNSWindowを環境から取得できるようにしてみる。(Singletonの例にはならないが……。)

struct WindowKey: EnvironmentKey {
    static var defaultValue: NSWindow? {
        return NSApp.mainWindow
    }
}

extension EnvironmentValues {
    var window: NSWindow? {
        get {
            return self[WindowKey.self]
        }
        set {
            self[WindowKey.self] = newValue
        }
    }
}

このようにEnvironmentKeyに準拠した型を作って、EnvironmentValuesを拡張する。そして以下のように、NSHostingViewを初期化するときにNSWindowへの参照を与える。

let window = NSWindow(...)
let contentView = ContentView()
    .environment(\.window, window)
window.contentView = NSHostingView(rootView: contentView)

あとはいつでも@Environment(\.window) var windowとして、NSWindowにアクセスできる。

EnvironmentとDIコンテナ

こうしてみると、EnvironmentはViewをターゲットとしたDIコンテナのような役割を果たしている。これはObservableObjectを対象としたEnvironmentObjectでも同じだが、Environmentの場合はObservableObject以外もその対象になる。

またEnvironmentは、ViewModifierからも参照できる。EnvironmentalModifierがそれだ。EnvironmentalModifierは、それ自体がViewModifierでありながら、resolve(in:)メソッドを実装することで、EnvironmentValuesの値を元にして別のViewModifierを解決することができる。つまりEnvironmentはViewModifierに対しても依存を提供できる。

DIコンテナとしては万能ではないが、これまでSingletonパターンを使っていたようなものの置き換えとしては十分だろう。

EnvironmentとContext

Reactに親しんでいれば、EnvironmentはReactのContextとほとんど同じということに気づくだろう。つまり、Reactでは内部的にContextを使って実現されるようなライブラリと似たようなことが、Environmentでも実現できそうだ。

まとめ

SwiftUIのEnvironmentを説明して、独自に拡張する方法を紹介した。そしてDIコンテナとの類似性から、Singletonパターンの置き換えとしての可能性を示した。

SwiftUIは昨秋にリリースされたばかりで、不完全な印象を抱きがちではあるが、Environmentのような仕組みがしっかりと整備されており、高いポテンシャルを持っているように思う。

参考

SwiftUI 徹底入門

SwiftUI 徹底入門

紛らわしい文字列をもっと紛らわしくする

同僚にid:yashigani_wヤシガニ)とid:yigarashi(ワイイガラシ)がいる。いかにも似ているが、あるとき一緒に仕事をすることになって、紛らわしさが限界を超えた。

yashigani
yigarashi

そうとわかって見たら、なるほど「y」で始まって「i」で終わるのが似ているね、くらいに思うかもしれない。しかし不意にSlackやGitHubで見かけると、一瞬どちらかわからない。

この紛らわしさをより俯瞰してみるため、画像にしてみました。

f:id:cockscomb:20200101161439j:plain
Photoshopで作った

どうだ紛らわしいだろう。全体的な形がなんだか似ている。

ところでこの画像はPhotoshopで作った。テキストにガウシアンぼかしをかけて、その後から二値化した。なかなかかっこいいから、もっといろいろ試してみたい。

前置きが長くなったが、テキストにガウシアンぼかしをかけて、その後から二値化 するのを、macOSiOSの仕組みでやってみるのが、本題である。

Core Textでテキストの画像を作る

まずはテキストを画像化しないと始まらない。テキストの画像を作るのは、Core Text(とCore Graphics)で素朴に実装できる。

import CoreGraphics
import CoreText
import Foundation

struct Padding {
    let top: CGFloat
    let left: CGFloat
    let bottom: CGFloat
    let right: CGFloat

    static var zero: Padding {
        return Padding(top: 0, left: 0, bottom: 0, right: 0)
    }
}

func render(text: NSAttributedString, padding: Padding = .zero) -> CGImage {
    let framesetter = CTFramesetterCreateWithAttributedString(text)
    let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(
        framesetter,
        CFRange(),
        nil,
        CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
        nil)

    let width = Int(ceil(frameSize.width + padding.left + padding.right))
    let height = Int(ceil(frameSize.height + padding.top + padding.bottom))

    let ctx = CGContext(
        data: nil,
        width: width,
        height: height,
        bitsPerComponent: 8,
        bytesPerRow: width * 4,
        space: CGColorSpaceCreateDeviceRGB(),
        bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
    guard let context = ctx else {
        fatalError()
    }

    context.saveGState()
    context.setFillColor(.white)
    context.fill(CGRect(origin: CGPoint.zero, size: CGSize(width: width, height: height)))
    context.restoreGState()

    let path = CGPath(
        rect: CGRect(
            origin: CGPoint(
                x: (CGFloat(width) - frameSize.width) / 2,
                y: (CGFloat(height) - frameSize.height) / 2),
            size: frameSize),
        transform: nil)
    let frame = CTFramesetterCreateFrame(framesetter, CFRange(), path, nil)
    CTFrameDraw(frame, context)

    guard let image = context.makeImage() else {
        fatalError()
    }
    return image
}

NSAttributedStringからCore TextのAPIを使ってCGImageを作る。このときpaddingを設定できるようにすると、あとで都合がいい。

Core Imageで画像を加工する

画像の加工ではいつものように、Core Imageを使うことを検討する。以前CoreImage.CIFilterBuiltinsで試したように、CIFilter.gaussianBlur()を使えば、ガウシアンぼかしは簡単だ。あとは二値化するだけだが、ここで手が止まる。意外なことに、組み込みのフィルターには二値化が存在しない。

Metal Shading Languageによるカスタムカーネル

二値化は簡単そうなので手作りする。Core ImageカーネルをMetal Shading Languageで書く。ThresholdBinary.metalを以下の内容で作った。またXcodeのBuild Settingsで、Other Metal Compiler Flags-fcikernelを、User-Defined Settingsに追加したMTLLINKER_FLAGSキーに-cikernelを、それぞれ設定する必要がある。

#include <metal_stdlib>
#include <CoreImage/CoreImage.h>
using namespace metal;

extern "C" {
    namespace coreimage {
        float4 threshold_binary(sample_t source, float threshold) {
            float4 image = premultiply(source);
            float y = 0.299 * image.r + 0.587 * image.g + 0.114 * image.b; // BT.601
            float binary = y < threshold ? 0.0 : 1.0;
            return float4(binary, binary, binary, 1.0);
        }
    }
}

RGB値から輝度成分を取り出すために、YUV色空間のYを計算する。これが閾値を超えていれば1、そうでなければ0とすることで、二値化できる。

このカーネルは以下のように読み込める。

import CoreImage

let thresholdBinaryKernel: CIColorKernel = {
    guard
        let url = Bundle.main.url(forResource: "default", withExtension: "metallib"),
        let data = try? Data(contentsOf: url)
    else {
        fatalError("Unable to get metallib")
    }
    guard
        let kernel = try? CIColorKernel(
            functionName: "threshold_binary", fromMetalLibraryData: data)
    else {
        fatalError("Unable to create CIKernel from threshold_binary")
    }
    return kernel
}()

Core Imageによる画像の加工

二値化のカーネルが書けたので、あとは実際に画像を加工するだけだ。

import CoreImage
import CoreImage.CIFilterBuiltins

let ciContext: CIContext = CIContext()
func process(image: CGImage, sigma: Float, threshold: Float) -> CGImage? {
    let ciImage = CIImage(cgImage: image)

    let gaussianBlur = CIFilter.gaussianBlur()
    gaussianBlur.inputImage = ciImage.clampedToExtent()
    gaussianBlur.radius = sigma
    guard let blurredImage = gaussianBlur.outputImage else {
        return nil
    }

    guard
        let processedImage = thresholdBinaryKernel.apply(
            extent: ciImage.extent, arguments: [blurredImage, threshold]
        )
    else {
        return nil
    }

    guard
        let cgImage = ciContext.createCGImage(
            processedImage, from: processedImage.extent)
    else {
        fatalError()
    }
    return cgImage
}

CIImage.clampedToExtent()を使うのがコツで、ガウシアンぼかしをかけるときに画像の端が変な色になってしまうのを防げる。

どうですか

ここまでで、テキストにガウシアンぼかしをかけて、その後から二値化 ができた。iOSでもmacOSでも同じように動くはずだ。

f:id:cockscomb:20200106103459p:plain

なんだかちょっとかっこよくないですか。

参考文献

Metalは初めて使うので、ちょっとだけ勉強しました。

apple/swift-formatを試す

オフィシャル感のあるSwiftのformatter/linterであるところの、swift-formatを試した。

SwiftのGoogleフォークで生まれたものがベースになっているようだ。開発が進んでいるmasterブランチと、Swift 5.1に対応するswift-5.1-branchブランチがあり、後者を試している。

インストール

HomebrewでインストールしたかったのでFormulaを用意した。

$ brew install https://gist.githubusercontent.com/cockscomb/183acd19d2f5e127045dc43c6c472535/raw/63c935672b4e8c9d6f2056785283a6d6b7d31b77/swift-format.rb

ビルドに2分くらいかかった。

(もしかするとMintというのを使うといいのかもしれないが、試していない。)

ヘルプを見てみる。

$ swift-format --help
OVERVIEW: Format or lint Swift source code.

When no files are specified, it expects the source from standard input.

USAGE: swift-format [options] [filename or path ...]

OPTIONS:
  --assume-filename       When using standard input, the filename of the source to include in diagnostics.
  --configuration         The path to a JSON file containing the configuration of the linter/formatter.
  --in-place, -i          Overwrite the current file when formatting ('format' mode only).
  --mode, -m              The mode to run swift-format in. Either 'format', 'lint', or 'dump-configuration'.
  --recursive, -r         Recursively run on '.swift' files in any provided directories.
  --version, -v           Prints the version and exists
  --help                  Display available options

POSITIONAL ARGUMENTS:
  filenames or paths      One or more input filenames

dump-configuration

swift-formatの設定は、JSONで表現される。デフォルトの設定は以下のようにdumpできる。

$ swift-format --mode dump-configuration > .swift-format
{
  "blankLineBetweenMembers" : {
    "ignoreSingleLineProperties" : true
  },
  "indentation" : {
    "spaces" : 2
  },
  "indentConditionalCompilationBlocks" : true,
  "lineBreakBeforeControlFlowKeywords" : false,
  "lineBreakBeforeEachArgument" : false,
  "lineLength" : 100,
  "maximumBlankLines" : 1,
  "respectsExistingLineBreaks" : true,
  "rules" : {
    "AllPublicDeclarationsHaveDocumentation" : true,
    "AlwaysUseLowerCamelCase" : true,
    "AmbiguousTrailingClosureOverload" : true,
    "BeginDocumentationCommentWithOneLineSummary" : true,
    "BlankLineBetweenMembers" : true,
    "CaseIndentLevelEqualsSwitch" : true,
    "DoNotUseSemicolons" : true,
    "DontRepeatTypeInStaticProperties" : true,
    "FullyIndirectEnum" : true,
    "GroupNumericLiterals" : true,
    "IdentifiersMustBeASCII" : true,
    "MultiLineTrailingCommas" : true,
    "NeverForceUnwrap" : true,
    "NeverUseForceTry" : true,
    "NeverUseImplicitlyUnwrappedOptionals" : true,
    "NoAccessLevelOnExtensionDeclaration" : true,
    "NoBlockComments" : true,
    "NoCasesWithOnlyFallthrough" : true,
    "NoEmptyTrailingClosureParentheses" : true,
    "NoLabelsInCasePatterns" : true,
    "NoLeadingUnderscores" : true,
    "NoParensAroundConditions" : true,
    "NoVoidReturnOnFunctionSignature" : true,
    "OneCasePerLine" : true,
    "OneVariableDeclarationPerLine" : true,
    "OnlyOneTrailingClosureArgument" : true,
    "OrderedImports" : true,
    "ReturnVoidInsteadOfEmptyTuple" : true,
    "UseEnumForNamespacing" : true,
    "UseLetInEveryBoundCaseVariable" : true,
    "UseShorthandTypeNames" : true,
    "UseSingleLinePropertyGetter" : true,
    "UseSynthesizedInitializer" : true,
    "UseTripleSlashForDocumentationComments" : true,
    "ValidateDocumentationComments" : true
  },
  "tabWidth" : 8,
  "version" : 1
}

Documentation/Configuration.mdというドキュメントがある。

デフォルトでインデントがスペース2つだった。開発の最初期からスペース2つだったようで、要するにGoogleのSwift Style Guideに倣っているためである。SwiftUIやFunction buildersのようなDSL的なユースケースが増えてきたときに、インデントが深くなりやすいから、スペース2つの方がいいというトレンドになるかもしれない。しかしひとまず、一般的なスペース4つに変えた。

lint

$ swift-format --mode lint --configuration .swift-format --recursive .

けっこういろいろ出てくる。

--configuration .swift-formatは、ファイル名が.swift-formatの場合には省略できる。

SwiftLintを真似て、XcodeBuild PhasesRun Script Phaseを追加。

if which swift-format >/dev/null; then
  swift-format --mode lint --recursive . || true
else
  echo "warning: swift-format not installed"
fi

(lintが通らないと終了コードが1になり、後続のphaseに進まないので、|| trueしておくといい。)

こうすると、出力がXcodeの求める形式と合っているので、エディタにwarningが表示される。

OnlyOneTrailingClosureArgumentという、引数にクロージャが2つ以上あるときtrailing closureを許さないルールに引っかかる。可読性が落ちるので妥当なルールとも思うが、SwiftUIのTutorialをみると、Buttonで使っているパターンである。actionクロージャをメソッドとして切り出すのが正攻法だろうが、無効にしてもいいだろう。

NeverUseImplicitlyUnwrappedOptionalsというのでも引っかかる。var str: String!のようなのは、あまり行儀がいいとはいえないものの、Xcodeのテンプレートでも使われるパターンである。部分的にswift-formatのルールを無効にできるといいのだが。

部分的なlintの無効化

masterにはDocumentation/IgnoringSource.mdというのがあった。以下のようなコメントを書くことで、コメントが書かれた次の行からASTで1ノード分、swift-formatが無視してくれるというものらしい。

// swift-format-ignore
// swift-format-ignore: [comma delimited list of rule names]

使いたいけど、swift-5.1-branchには入っておらず、Swift 5.1ではまだ使えなさそうだった。

format

swift-formatなので、もちろんformatできる。

$ swift-format --mode format --recursive --in-place .

Makefileも用意しておく。

.PHONY: format lint
format:
    swift-format --mode format --recursive --in-place .
lint:
    swift-format --mode lint --recursive .

おもしろいところでは、ネームスペース代わりのinitを潰したstructenumに書き換えてくれた。


フォーマットの感じは悪くなく、(プロジェクトが小さいためか)パフォーマンスも特に気にならない。手元でちょっと使うのには適しているだろう。

CIで動かそうとするともうちょっと準備が必要で、競合するSwiftLintの方がノウハウが蓄積されていて便利かもしれない。

そのうち安定版がリリースされて、Swiftのツールチェーンにバンドルされたり、Xcodeとのインテグレーションが整備されたりすると、さらに使い勝手がよくなりそう。

追記

2019/12/19 18:50

--configuration [json file]が、ファイル名が.swift-formatの場合に省略できる旨を追加し、全体的に省略するようにした。

参考

息子にカメラを与える

まだ2歳半で早すぎると思うけど、普段から僕のカメラを羨ましがっているので、少しくらい乱雑に扱われても大丈夫そうなカメラを買った。

いろいろ調べて、NikonCOOLPIX W150というのにした。耐衝撃性能を宣伝しているくらいなので、きっと頑丈なつくりになっている。ブルーが子供らしくてかわいいかと思ったが、息子は「白がいい」というので、ホワイトにした。子供は子供らしいものを欲しがらない。

Nikonの「(子供も含めた)家族みんなで楽しめる」というコンセプトは、2012年のCOOLPIX S30の頃に始まったようで、2013年のCOOLPIX S31、2014年のセンサーが変更されたCOOLPIX S32、2015年のCOOLPIX S33とシリーズが続き、2016年にはWi-FiBluetoothに対応したCOOLPIX W100が発売されている。そして毎年だったモデルチェンジのサイクルが変わったのか、2019年になって発売になったのが、COOLPIX W150ということのようだ。

画質は何世代か前のiPhoneくらいだが、耐衝撃・耐水・防塵で、子供にも扱いやすいインターフェースを備えている。光学3倍ズームとかフラッシュとかも付いているけど、今のところそんなに活用していない。特にフラッシュは、誤って子供が有効にしないか気がかりである。上面にストラップ取り付け部が2つ付いているのはポイントが高い。ネックストラップは付属しないので別に買った。子供はカメラを落としたり、その辺に置いてきたりしがちなので、ネックストラップがあるとよいが、長さの調節幅が大きいものを選ばないといけない。さらに別売のソフトケースも買った。

f:id:cockscomb:20191026095816j:plain
カメラを構える息子

ということで妻が入院している最中にカメラを買って、一週間と少し経った。息子はどこへ行くにもカメラを首から下げて、たまに思い出したら写真を撮る。大人の真似をして妹である赤ちゃんの写真を撮っているときは感慨深かった。とはいえちゃんとした写真が撮れるのはまだ稀だ。しかし息子自身はそんなことお構いなしで、自分用のカメラを喜び、写真を撮る行為そのものを楽しんでいる。まだおもちゃみたいなものだけど、そのうち真価に気付くことを期待している。

小さい子供にカメラを持たせると困ることもある。カメラに夢中になって注意散漫になるので、道を歩くときに危ない。ストラップを持ってカメラを振り回すことがある。撮っていいとか悪いとかの区別がない。家の中とか明らかに安全な場所以外では、親がちゃんと見ていられる時にだけカメラを持たせるようにしないといけない。

なんにせよこれで僕のGR IIIへの関心が下がって、寿命が伸びた。GR IIIもおすすめです。