読者です 読者をやめる 読者になる 読者になる

うな(。・ε・。)

Android, iOS, AppEngine まわりのめもめも

JPEGのヘッダーを探訪する

JPEGのバイナリフォーマットは案外シンプルで、けっこう簡単に分解することができます。

JPEGはKey-Valueデータの羅列です。 0xFFで始まる2バイトはマーカといい、一まとまりの値が格納されていることの印となっています。 例えば0xFF 0xDBの2バイトはDQTマーカといい、次のマーカまで続くデータ列をDQT(量子化テーブル)として解釈することができます。

JPEGにはこのような構造で、圧縮に使った情報(テーブル)と、画像の圧縮済みデータが格納されています。

圧縮に使った情報としては、DQT(量子化テーブル)DHT(ハフマンテーブル)が格納されており、両方とも伸長するためには不可欠です。 圧縮済みの実データはSOS(スキャンヘッダ)に続くデータ列に格納されています。

典型的なJPEGは次のような構造をしています。

マーカ マーカ名 意味
0xFF 0xD8 SOI JPEGファイルデータの開始
0xFF 0xDB DQT 量子化テーブル
0xFF 0xC0 SOF0 圧縮の種類や画像サイズなどの情報
0xFF 0xC4 DHT ハフマンテーブル
0xFF 0xDA SOS 画像データの開始
0xFF 0xD9 EOI JPEGファイルデータの終了

実例

# SOI - 画像データ開始

FF D8
# DQT - Define Quantization Table(s) http://hp.vector.co.jp/authors/VA032610/JPEGFormat/marker/DQT.htm
# 132 bytes

FF DB: 00 84 00 0D 09 0A 0B 0A 08 0D 0B 0A 0B 0E 0E 0D 0F 13 20 15 13 12 12 13 27 1C 1E 17 20 2E 29 31 30 2E 29 2D 2C 33 3A 4A 3E 33 36 46 37 2C 2D 40 57 41 46 4C 4E 52 53 52 32 3E 5A 61 5A 50 60 4A 51 52 4F 01 0E 0E 0E 13 11 13 26 15 15 26 4F 35 2D 35 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F 4F
# SOF0 - Start Of Frame (baseline) http://hp.vector.co.jp/authors/VA032610/JPEGFormat/marker/SOF.htm
# 17bytes

FF C0: 00 11 08 00 28 00 1E 03 01 22 00 02 11 01 03 11 01
# DHT - ハフマンテーブル定義 http://hp.vector.co.jp/authors/VA032610/JPEGFormat/marker/DHT.htm
# 418bytes

FF C4: 01 A2 00 00 01 05 01 01 01 01 01 01 00 00 00 00 00 00 00 00 01 02 03 04 05 06 07 08 09 0A 0B 10 00 02 01 03 03 02 04 03 05 05 04 04 00 00 01 7D 01 02 03 00 04 11 05 12 21 31 41 06 13 51 61 07 22 71 14 32 81 91 A1 08 23 42 B1 C1 15 52 D1 F0 24 33 62 72 82 09 0A 16 17 18 19 1A 25 26 27 28 29 2A 34 35 36 37 38 39 3A 43 44 45 46 47 48 49 4A 53 54 55 56 57 58 59 5A 63 64 65 66 67 68 69 6A 73 74 75 76 77 78 79 7A 83 84 85 86 87 88 89 8A 92 93 94 95 96 97 98 99 9A A2 A3 A4 A5 A6 A7 A8 A9 AA B2 B3 B4 B5 B6 B7 B8 B9 BA C2 C3 C4 C5 C6 C7 C8 C9 CA D2 D3 D4 D5 D6 D7 D8 D9 DA E1 E2 E3 E4 E5 E6 E7 E8 E9 EA F1 F2 F3 F4 F5 F6 F7 F8 F9 FA 01 00 03 01 01 01 01 01 01 01 01 01 00 00 00 00 00 00 01 02 03 04 05 06 07 08 09 0A 0B 11 00 02 01 02 04 04 03 04 07 05 04 04 00 01 02 77 00 01 02 03 11 04 05 21 31 06 12 41 51 07 61 71 13 22 32 81 08 14 42 91 A1 B1 C1 09 23 33 52 F0 15 62 72 D1 0A 16 24 34 E1 25 F1 17 18 19 1A 26 27 28 29 2A 35 36 37 38 39 3A 43 44 45 46 47 48 49 4A 53 54 55 56 57 58 59 5A 63 64 65 66 67 68 69 6A 73 74 75 76 77 78 79 7A 82 83 84 85 86 87 88 89 8A 92 93 94 95 96 97 98 99 9A A2 A3 A4 A5 A6 A7 A8 A9 AA B2 B3 B4 B5 B6 B7 B8 B9 BA C2 C3 C4 C5 C6 C7 C8 C9 CA D2 D3 D4 D5 D6 D7 D8 D9 DA E2 E3 E4 E5 E6 E7 E8 E9 EA F2 F3 F4 F5 F6 F7 F8 F9 FA
# SOS - スキャンヘッダー http://hp.vector.co.jp/authors/VA032610/JPEGFormat/marker/SOS.htm

FF DA: 00 0C 03 01 00 02 11 03 11 00 3F 00 C0 9E C3 50 9F 54 9E 4B 16 91 22 75 52 C7 61 23 9E 0F 38 C0 3C 56 54 96 97 C2 47 F2 ED 5D A2 2C 76 E1 03 02 3B 7B D7 A4 F8 7A 28
FF 00 E1 13 9E E0 5D 3C AC 15 A5 66 60 06 70 0F 04 7B 55 0B 2B 75 16 F1 03 D9 40 FC 71 51 49 CA 4A CD EC 54 DA 4C E1 ED E3 9D 67 4C DA 15 C1 07 3E 41 1C FD 6A E7 8A EC 60 B2 86 C2 68 90 AC B3 40 AD 2A B1 38 2C DC E4 7A 74 AE E1 6D 40 20 80 6B 96 F8 96 71 A8 43 10 E8 8A AB F9 28 FF 00 E1 13 9E E0 5D 3C AC 15 A5 66 60 06 70 0F 04 7B 55 0B 2B 75 16 F1 03 D9 40 FC 71 51 49 CA 4A CD EC 54 DA 4C E1 ED E3 9D 67 4C DA 15 C1 07 3E 41 1C FD 6A E7 8A EC 60 B2 86 C2 68 90 AC B3 40 AD 2A B1 38 2C DC E4 7A 74 AE E1 6D 40 20 80 6B 96 F8 96 71 A8 43 10 E8 8A AB F9 28
FF 00 1A A6 E4 9D AF A0 2B 34 6B E9 57 46 2F 08 B5 B6 06 6E 56 48 C9 1E ED B6 B6 E1 8D 30 3F C2 B0 A7 86 2B 2B 4B 1B 75 66 28 24 EA 7A 9C 1C 9F E5 5A 76 F7 23 6A 65 D7 27 D5 48 A8 C3 6B 16 C5 5F E2 B1 6D 64 65 61 90 D8 3D 7E 5E 95 42 FA DA CF 56 BA B9 96 74 DC 04 E4 2E 47 A0 02 AC 7D A9 4A 02 0C 44 6E C7 0F 54 20 93 75 AB C8 38 DD 71 2F E8 D8 FE 95 18 BD 21 74 6B 84 5C D3 B1 47 5D B9 8E DE E2 C5 66 91 23 51 BC 82 DD 3F CF 34 FB 2B E8 A6 8C 32 CF 6E 58 8E 40 6C F3 58 FE 3F FF 00 59 67 F4 6A CF F0 FF 00 41 55 87 76 A4 88 AD F1 9D 73 7D A4 22 0F 32 29 36 B7 52 BD 45 41 65 6E F3 E9 11 B0 99 E3 6F 3A 52 76 74 39 62 6A D2 7D C5 A4 D2 7F E4 0D 1F FD 74 6F E6 6A 31 6E D0 46 D8 6F 8C
# 終端

FF D9

tips

DQTはqに依存

JPEGのqによって、DQTは画像によらず一意に決まります。

DHTはほとんど同一

ハフマン符号化の性質上、本来はエンコード時に画像データを解析して最適のハフマンテーブルを作るべきです。しかし、処理量が大きいため Standard Huffman Table としてGeneral Purposeに使えるハフマンテーブルがJPEGの標準として定義されています。

最適化したハフマンテーブルを使うオプションを明示的に指定しなければ、この標準ハフマンテーブルが使用されます。

ですから、大部分のJPEGはqにも画像にもよらずDHTは同一です。

参考文献

iCloudのユーザIDを使って、端末を移行したユーザをセキュアに紐付ける

アプリのデータを新しいiPhoneに移行したい!というのはよくあるニーズだと思います。 単純にはiCloud Documentsを使ってアプリを作っていれば良いのですが、デバッグのしやすさなどの兼ね合いから自社サーバを使っているところが大半だと思います。

データの移行のためには安全にIDを紐付ける必要があります。 広く用いられているのはemail, passwordを使ったアカウントや, 各種SNSでのSSOを使った紐付けかと思います。

しかしながら、一つに事前にユーザにアカウントを作ってもらってから移行してもらうことの運用の難しさです。 パスワードというのは簡単に忘れるものであり、そのパスワードを送るためのメールアドレスでさえ変わったり、忘れたりすることがしょっちゅうです。

二つに日本においてはほぼ確実に間違いなくアカウントを所持しているSNSは存在しません。LINEくらいでしょうか。そのLINEでさえも、IDの紐付けはわりと無頓着であり、携帯を替えるたびにLINEアカウントが変わるのはしょっちゅうです。 紐付けたSNSがなんだったのかを忘れてしまうこともしょっちゅうですね。

他には、電話番号認証は良い手段ですが、実は電話番号は廃止後使いまわせてしまうので、セキュアに紐付けることはできません。

他にも、QRコードなどを使って認証情報を移行するのも一つの手段ですが、実は確実ではありません。なぜなら、携帯を変えるときは古いスマホをその場で下取りに出してしまうことが多いからです。

iPhone -> iPhone の移行なら Apple ID が使える?

上述の問題が全て解決されるわけではありません。しかしながら、アカウントを移行できる可能性を一つでも上げるため、サポート用のロジックを用意することを考えます。

iPhoneを使っている人は新しい携帯もiPhoneであることがほとんどでしょうから、Apple IDを使えば紐付けは可能ではないでしょうか。

基本的には最もベーシックであるEmail+Passwordを使い、Apple IDを使った紐付けでID, Password忘れ防止など利便性を向上することができれば良いですね。

結論から言えば、iCloudの提供するCloudKitを使えば紐付けが可能です。

CloudKitを使ったアカウント紐付け

鍵となるのはCloudKitのCKContainer - fetchUserRecordIDWithCompletionHandler:です。このメソッドiCloudユーザに一意のユーザレコードを取得します。 ここで取得したレコードの - recordName は33文字の文字列IDとなっています。このIDは特定アプリのなかのiCloudユーザに一意です。ですから、他のアプリで同一ユーザが- recordNameを取得したとしても、それは違う値になります。

取得できるのは次のような文字列であり、次のようなコードで取得することができます。

_cd2f38d1db30d2fe80df12c89f463a9e

[[CKContainer defaultContainer] fetchUserRecordIDWithCompletionHandler:^(CKRecordID *recordID, NSError *error) {
    if (error != nil) {
        NSLog(@"%@", error);
        return;
    }
    NSLog(@"%@", recordID.recordName);
}];

このIDは36^32通りの名前空間がありますから推論は極めて困難ですが、もう一段階ロックを付ければよりセキュアです。

CloudKitにはUserのみにアクセスできるPrivate Containerがあります。 このPrivate Containerに十分長いキーsecretを保存しておけば2つの十分長いトークンで認証を行うことができ、セキュアです。

フローとしては、次のようになります。

  1. (端末A) アプリはrecordID.recordNameが取得できたら、十分に長いキーsecretを生成しPrivate Containerに格納します。
  2. (端末A) アプリは自社サーバにrecordID.recordName, secretを送信します。
  3. (端末B) 携帯を移行してアプリを起動したとき、recordID.recordNameを取得します。また、secretをPrivate Containerから取得します。
  4. (端末B) これらの値を自社サーバに送信し、(2)で送ったものと同一かどうかを確かめます。
  5. (4)が成功すれば、端末Aと端末Bは紐付けされ、同一ユーザとみなすことができます。

コードの注意点としては、secretの書き込みはいわゆるFindOrInsert処理になります。

gist.github.com

ASImageNodeのすゝめ

ASImageNodeは、Facebookオープンソース非同期UIライブラリAsyncDisplayKitに含まれるUIImageView相当のコンポーネントです。

このコンポーネントを使用することで、かんたんに画像表示のパフォーマンスを改善できることをご紹介します。

UIImageView の問題点

UIImageViewは画像を表示するのに必要なすべての処理をメインスレッドで行ってしまいます。画像を表示するための処理は大きなバイナリデータを扱うため、非常に重たい処理です。メインスレッドでこれを行うと簡単にUIをブロックしてしまいます。

表示する画像が重かったり、または UICollectionView などで画像を表示していると、顕著に感じられると思います。

対して、ASImageNode は画像を表示するための下準備すべて(デコード、レンダリング)をバックグラウンドスレッドで行います。メインスレッドは ASImageNode よりレンダリング済みのデータを受け取り、View にベタッと貼り付けるだけ。メインスレッドの処理量が大幅に削減されています。

実際の効果(続報あり?)

いまのプロジェクトでは、4列のUICollectionViewに200x200ほどの画像を順次 - setImage: するだけで20-30fps程度まで下がってしまっていました。 体感的にも、カクカクしています。

今回、ASImageNode を使って描画を行うことで、60fps付近をキープするようになりました。 体感的には、スクロールでカクカクを全く感じなくなりました。

コード

UICollectionView でのコードを掲載します。

// KIExampleCollectionViewCell

@interface KIExampleCollectionViewCell ()

@property ASImageNode *imageNode;

@end

- (void)prepareForReuse {
    [super prepareForReuse];
    
    // (1) で追加した View を消す。`UIImageView-setImage:nil` の代わり。
    [_imageNode.view removeFromSuperview];
}

- (void)setImage:(UIImage *)image {
    if (image == nil) {
        return;
    }
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if (!_imageNode) {
            _imageNode = [[ASImageNode alloc] init];
            _imageNode.frame = self.contentView.frame;
        }
        _imageNode.image = image;
        
        dispatch_async(dispatch_get_main_queue(), ^{
            // (1) _imageNode.view で描画結果の View を取得できます。
            [self.contentView addSubview:_imageNode.view];
        });
    });
    
}

Background Fetch が起きたあと、アプリを起動しても didFinishLaunching を通らない

Background Fetch を使うと、アプリのライクサイクルイベントが少し変わってくるのでメモ書きをします。

通常のアプリ起動時のライフサイクルイベント

  1. - application:didFinishLaunchingWithOptions:
  2. - applicationDidBecomeActive:

こののち、「アプリを閉じて再度開く」「写真へのアクセス許可など OS ダイアログが閉じられる」などが起こると、-applicationDidBecomeActive: のみが呼ばれます。

メモリが不足するなどしてプロセスがキルされない限り、- application:didFinishLaunchingWithOptions: は二度と呼ばれないことに注意してください。

Background Fetch が起こり、その後アプリを起動した場合

f:id:liedderoptik:20151208201652p:plain

  1. - application:didFinishLaunchingWithOptions: (Background Fetch が起こったとき)
  2. - application:performFetchWithCompletionHandler:
  3. - applicationDidBecomeActive: (アイコンをタッチして、アプリを起動したとき)

Background Fetch が起きたときは、ユーザがアプリを開くなどしなくても - application:didFinishLaunchingWithOptions: が呼ばれることに注意してください。なお、この際 launchOptions には nil が渡されます。

取り扱いに注意すべきこと

didFinishLaunchingperformFetch での KeyChain の取り扱いに注意

KeyChain は applicationState によって値が取り出せずエラーになることがあります。(設定次第です)

performFetch では気をつけていても、didFinishLaunching でユーザIDをトラッキング用に取得するなどして意図せず値が取り出せない状況が起こり得ます。 例外は起きませんが、適切にエラーハンドリングをする必要があります。

たとえば、「値が取り出せない場合は新しく割り当てる」といった処理をしている場合は、Background Fetch が起きるたびに値が変わってしまいます。 対策としては、KeyChain の accessibility として kSecAttrAccessibleAfterFirstUnlock を使うか、値を取得するときに起こりうるエラーを適切にハンドリングします。

Gilt: iOS7でbackground fetchを利用するとログアウトしてしまうバグへの対応 - ワザノバ | wazanova

ユーザがアプリを明示的に起動しなくても didFinishLaunching が呼ばれることに注意

Background Fetch をつかうと、ユーザが明示的にアイコンをタップしてアプリを起動しなくても、- application:didFinishLaunchingWithOptions: が呼ばれてしまいます。

このとき、メソッド内で「アプリ起動イベント」などを計測していると、誤ったデータが取得されてしまいます。

- applicationDidBecomeActive: を「アプリ起動イベント」として数えるほうが正確です。この場合、「アプリを閉じてすぐ再度アプリを開く」「写真へのアクセス許可など OS ダイアログが閉じられる」といった状況でも「アプリ起動イベント」として計測されてしまうことに注意します。

アカウント移行用に、iCloud アカウントに紐付いた一意の ID を取得する

複数iOS 端末でのアカウント共有をしてもらうため、iCloud アカウントの ID を利用することを考えます。

様々な方法がありますが、CloudKit を使うのが一番簡単で考えることも少なくラクだと思います。

事前準備

f:id:liedderoptik:20151130185543p:plain

  1. iCloud Capability を ON にします。あわせて、CloudKit にチェックを入れます。

Capability の変更がありますが、iCloud の場合は既存のアプリからアップデートしても正常に Capability が移行されます。

コード

次のようにして1.iCloudアカウントに一意で2.アプリ毎に異なるIDを取得することができます。(トークン置換攻撃が予防されてます)

[[CKContainer defaultContainer] fetchUserRecordIDWithCompletionHandler:^(CKRecordID *recordID, NSError *error) {
    if (error != nil) {
        NSLog(@"%@", error);
        return;
    }
    NSLog(@"%@", recordID.recordName); // recordID.recordName が ID
}];

このコードを実行すると、_cd2f38d1db30d2fe80df12c89f463a9e のような NSString が取得できます。この ID の実体は UUID であり、推測可能性や名前空間の大きさは UUID に準じます。

一回の fetch には 2 秒ほどかかり、自動的にキャッシュされることもありません。したがって、KeyChain などを用いてキャッシュする運用が必要になります。

iCloud アカウントにログインしていない場合は、error のみが返ります。 AppStore でアプリをインストールしていることを考えると、このケースは数は多くないように思います。

全体的な性質を鑑みると、ユーザ ID としては自社のものを用意し、こちらの iCloud ID はアカウント紐付け用に利用するのがよいと思います。

参考

iOS Onboarding without Signup Screens — welcome aboard — Medium

「リアクティブ」ほんとに要る?

リアクティブは値の変化に対して、対応した処理を動かせることができる。これは直感的にはとても合理的であり、キレイな対応となっている。

実際、リアクティブに書くと処理が重複しにくかったり、値の変化に対して起こる動作がわかりやすく整理される、といった効果がある。

しかしながら、実際的にはリアクティブよりも地道な一本道コードのほうが最終的には保守しやすい、という話。 というものを書こうとしたけれども面倒になったので見出しだけ。

  1. 値に対してリアクティブに動作をさせないほうがいい
    • ほんとうに、その動作は値に対してリアクティブか?
    • 動作はアクションに紐付いている
      • User の on_create に Mail を送るのはほんとうに Model でやるべきことか?
      • よくよく考えると、「ユーザの作成」というアクションが「Mail の送信」という動作を引き起こすべき。
      • User の on_create に対応してなにが起こるか?が一貫的に書かれているよりも、アクション(リクエスト)から全ての動作が一本道に追えるほうが遥かに大事
      • これはクライアントアプリでも同様(UIのほうがこういったことは起こりやすいと思う)
    • 真に値にリアクティブなのは Computed Value (Cache など) くらいでは?
  2. リアクティブなコードは暗黙知の温床
    • コードから動作が「追えなく」なる
    • 汚いコードより、非同期に動きまくるコードのほうがつらい(そもそも追えない)
      • 暗黙知をより多く要求する
      • というか本人しか読めない
    • 前提知識なく読みやすいのはアクションから入り、一本道に処理が書かれるコード
  3. なんだかんだで、泥臭く一本道に実装したほうが良い
    • 処理が重複したり、あちこちで状態もったりするのは設計/書き方で解決すべき話です。

ところで、なぜこのような結論(感想?)になるのかというと、既存のプログラミング言語が上から下に書かれ、処理もまたそうであるからです。

つらい。

Medium方式のプログレッシブ画像読み込みの予備調査

Medium は 30x30pixel の画像を表示しておいてから、原寸画像を表示しているらしい。

そこで、30x30pixel というのがどの程度のクオリティ、サイズなのかを調査してみた。

iPhone 5S で撮った写真を題材にします。

原寸

2448 × 3264, 1.8MB

30x30 画像

30 × 40, 7KB (8016 bytes)

単純にリサイズするだけでは 7KB もあり大きさにしてはちょっと重い。

ここまで小さいと画像のクオリティも不必要なので色情報を落とすことにする。

30x30 画像(gif

色を落とすのに一番楽な gif にしてみた。ここまですると 2KB.

30 x 40, 2KB (2146 bytes)

dataURL 形式

gif 画像を dataURL にし、html に直接埋め込めるようにしてみる。

2866 bytes

コード

画像スワップのもっとも単純なコードは次のような感じ

document.addEventListener("DOMContentLoaded", function() {
    Array.prototype.forEach.call(document.querySelectorAll("img"), function(el) {
        var originalSrc = el.getAttribute("data-original");
        if (!originalSrc) return;
        var shdwImg = new Image();
        shdwImg.src = originalSrc;
        shdwImg.onload = function () {
            el.src = originalSrc;
        };
    });
});

↑リロードしてみてください(Shift + F5 でも!)

追記

Color reduction + jpegoptim + quality down で 30x40 px で 716 bytes まで落ちました。 1,200pixel ありますから、そんなものでしょうか。 ちなみに Color reduction は適当に 256 色に減色しました。

1pxあたり4-5bit程度になりました。 JPEGですから単純には言えませんが、色空間をもっと絞れば軽くなるでしょう… クライアントがアプリならJPEGのヘッダーまるまる落とすことも可能です。

Base64 956bytes

ちなみに Base64 の結果を deflate すると 677 bytes になります

eJylkEvPsjoUhX+QgyJykcEZtFCl3JQCCswEBAGVl4u0+us/zJec5LzT02Sne6dP1l6rQGuABKEftKlFK4igD5dpuaEBRIYMCDsTscpEVUd0WDl61TnGrfKNSvCMm3Da3XBC+PZu6aQkNX7Yn3y26lawA1y7GHPPaNVjI62sQMZB5G4DI9eOzfxX2+8cnUmLDvMxYqqtk+7wGdX/VzNgEFJ9MQ6RZMCAQHjDCHKMwNaH+2++yl3y/T4RRlVufBmHfxmCICO67vvGv4hPDIQwRJTcXD9FmPt1FO9Rl5kI8eSdpCYVZrAA1fKPGEL2e4e72ML4u8Pky7sfLT6I8R/EhzpkPiWUNG7jUaB10IBfv4QuGeCR6YNY/Gwum7no4+x5r28tPee1Ox5mvzno8Ppo28nIuQ263m383YeR2oCDbB5zzbtoUjTwcgw+2U4MijuVN+ldl5khkO1YoJ900rxTz/rS2sWyJORWLiUhs+VF1vH60ftxgrxHoQFl7UwUYEaA8+mUZZfVWkwS8XzhZyqvkycqqrBN1eIlTta56hIMNS8atGTPXyMmiO/r7qiwCihr6lSScXV8NtllZl29kunaEcoxvtfpTQAtMFXz1AycSmYG9v0nGZia+qLYNaOQ3JVWE9mGgOiSr7NO4tXbeR+uivuiK49GCrun6epqX8ePrWyrDX72VzML5rw/b1RQvEKRrC61wOLWLSq08gKwdAlxLZSZ4xuowjCEuRK1TMnVXCxbbkrCSW2vjRZGVA6SaNqGMbnc5ywaQ8rt+WePyulphqqiZMVr9B7WOfhEjjtPKEaHsnnSbl1Eso5zSbvFBYhPTQMf1UPyNnbB1Tk/Dr03aUGwHl1nSNo6P1t1SbXmnT8/wsUzywNtdkIExhho7T9/APAjNFk=

WebPにすると492bytesにもなりました。 Base64 -> deflate で 515 bytes の文字列に出来ます(ASCII)

UklGRs4BAABXRUJQVlA4IMIBAADQCACdASoeACgAPp1GnUslo6KhpWswsBOJYgCdMwNtvSYiyvtM
w56Ala3goMH6PLiFhzia0QdinyIhepGlg8ibpRNrfC1Nl3pkkmttCnnAAP7qXRTmseb0z2b0x2nq
LIVjG0hhDqxf9Wfk8TDunPCdwd5mVkDjOH0j/QRIdp2y1UYdvdTVHe0LU0WZSv3aURxWNVRdqjpp
wnDo9T+jga2qtPXFffebScNv6xxA1b5SDFB9GuH0UcCielWXizpEAge3+OI9It23/hZcOmq0JW8O
x8iHu0dpMh8681nw0q90Ommw2SJiLg/8bNdlvGg3f+4DieLt/ONMj3Tg7CF8iqh3NT4EwiJ4hsR4
kjiEWcfE/VUFzHKr5QhajTRtuof6WY0mpW+GV7Y6P0akaI/pCdap+uL9hOePenx+kUwqumODIhat
9WrbfvsmPDSNHrKuWoLFPlrLNCDfcc3HCWBdhnCXXkQX4GcxUVk1W1byotQvJprhDxWpfAI79afz
dJNJ3TJ6JoCc0eB2lsq0Gzwjwq4EfmBFXIbZbvX2Yoo8Uh/geZx/OfN02oOGZDQzmwezSElxw6V7
NEE9ggqzFYFjrQdAAAA=