見出し画像

不具合が私に教えてくれたエンジニアとして大事なたった一つのコト

📅 この記事は現在開催中のSTORES Advent Calendar 2022における18日目の記事となります 🎄

はじめに


私はiOS10の時代にiOS開発を始め、今の最新OSバージョンがiOS16という事を考えると約6年間もiOS開発を行なっています。しかし、未だにiOS開発のiの字もわからないような状況で心と奥歯が擦り切れるような思いで日夜開発を行なっております。しかし先日見た「ヴァイオレット・エヴァーガーデン」(※)という作品の中で主人公ヴァイオレットが「『iしてる』を、知りたいのです」という言葉と共に邁進する姿を見て「ヴァイオレットも私と同じ気持ちだったのか….」と気づき勇気づけられ、何とか残りわずかな奥歯を残しています。

(※)小説、アニメーション作品、およびそれらの作品の主人公の名称。両腕がサイボーグであり、タイピングがめちゃくちゃに速い。

そんなこんなで最近、仕事では決済機能を提供するSDKやアプリから利用しているプリンター機能のリファクタリングや不具合改修を担当する事が多いのですが、ハードウェア依存の機能なためか想像以上にタフなタスクが多く残り僅かな奥歯もすぐに無くなりそうな勢いです。

今回は会社主催のアドベントカレンダーとのことで、せっかくなので仕事でどういう感じでプリンターの不具合に対応したかと、不具合から学んだ事を記事にしたいと思います🙌

発生していた不具合

多くの方が決済した後にレシートを受け取った経験があるかと思いますが、決済グループが開発しているSDKやアプリでも同様にレシートを発行することができます。そのレシートで下記の画像のようにレイアウト崩れする不具合が以前から報告されていました。より具体的には特定のプリンタから印刷すると左側の余白より右側の余白がずいぶん大きくレイアウトされてしまう、という不具合です。

左側の余白より右側の余白が大きくなっているレシート

ただし、このレイアウト崩れは単にコード上で設定されている印字幅の値が実際に使われる用紙幅と一致していなかった事が自分が調査する前に判明していました。その修正をして印刷したレシートが下記です。余白は揃いましたが、今度はレシートの一部が斜線のような表示になっています。

支払い金額が表示されるはずの箇所が斜線になっている様子

この斜線が入っている箇所はビットマップ画像としてプリンターに送信している箇所です。そのため、問題を特定の印字幅の設定にて画像にノイズが入る事と解釈して調査する事にしました。

問題箇所の特定

まず大雑把にプリンターにレシートが印刷されるまでに下図のようなフローを辿ります。左側黄色い領域がiOSが独自に行う実装であり、決済の内容と印刷先のプリンターに依存してレシートに送信するビットマップ画像を生成します。対して右側はiOSとAndroidが共有して利用している実装であり、ビットマップ画像を基にプリンターに送信するバイナリを生成し送信します。

レシートがプリンターに印刷されるまでの処理フロー

対象の不具合はAndroidアプリでは発生していなかったため、問題が発生している箇所はSwiftで実装している箇所だと当たりをつけて問題箇所の特定を試みました。

ビットマップ画像を端末に保存してみる

「プリンターに送信するビットマップ画像の生成」工程では、画像の2値化やリサイズなどの複数の加工を行います。まずは、この加工処理のどこかが印字幅に依存しており、結果としてプリンターに送信する画像が壊れてしまっていると仮説を立てて検証することにしました。

仮説を検証するために生成した画像を端末に保存して目視で確認します。iOSで端末のフォトライブラリーに写真を保存するには、下記のメソッドの第一引数にUIImageを渡します。

UIImageWriteToSavedPhotosAlbum(UIImage(), nil, nil, nil)

試しに、「コイニーサポート(※)」という文字列を不具合がある印字幅の設定でビットマップ画像にして、それを各画像処理の工程の後にフォトライブラリーに保存しました。その結果、各工程において下記のような画像を得られました。ノイズが入っていない綺麗な画像です………この画像に斜線が入っている事を期待していたのですが、どうやら仮説は間違っていたようです。

(※)STORES 決済 はCoineyという名前でした。

UIImageWriteToSavedPhotosAlbumメソッドを使い出力した画像

また、iOSのフォトライブラリーではiボタンをタップすることにより、下記のように写真のサイズを確認できるのですが、こちらも意図通りの値となっていました。

フォトライブラリーから写真の情報を表示している様子

以上から、iOS側では正しそうな見た目の画像を生成しておりAndroidと共通の処理に移行しているという事がわかりました。不具合がないAndroidとの共通処理に不具合は潜んでないと踏んでいたのですが…..😥


プリンターに送信しているバイナリを可視化してみる

Androidとの共通で利用しているビットマップ画像からプリンターへ送信するバイナリへパースする処理ですが、他に仮説がなかったので、念の為パース処理が正しそうか検証することにしました。

プリンターの仕様書を見ると画像を印刷する際にどのような形式のバイナリを要求するか書いてあったので、それを基に既存のパース処理と逆のパースアルゴリズムを書いてパース結果をコンソールに出力してみました。コンソールに出力される逆パースした結果が元の画像と同じような見た目だった場合、パースアルゴリズムは間違ってないはず、という前提の下の検証です。なお、画像をそのままコンソールに出力することはできないので、画像バイナリの0と1を□と■のような文字列に対応させた上で、適切な位置に改行を入れて文字列として出力しました。

その結果、不具合が発生していない印字幅では下記のような文字列が出力されました。ひとまず、逆パースのアルゴリズムは正しそうです。

不具合が無い印字幅でバイナリを可視化している様子

続いて、不具合が発生している印字幅で逆パースして出力しました。レシートに印刷されているのと同様にノイズが入っています。

不具合がある印字幅でバイナリを可視化している様子


うーん、なんでだろう??フォトライブラリの画像では斜線が入ってなかったので、逆パースをして元の画像バイナリに戻した結果が斜線になるのは意外でした。

そういえば、パース処理が正しいかの検証なので、パース前のバイナリも出力して変化を見る必要がありそうです。

まず、こちらが不具合が発生していない印字幅の設定での出力です。正しいビットパターンのように見えます。

不具合が無い印字幅でパース前のバイナリを可視化している様子

続いて、不具合が発生している印字幅での出力です。本来コイニーサポートと表示されてほしいところが、斜線によって見えなくなっています。妙です。iOSのフォトライブラリでは正しく表示できていたのに!という事はパースする前から画像のバイナリは壊れていた….???

不具合がある印字幅でパース前のバイナリを可視化している様子


期待値が低い仮説でしたが、2つの事実に気づくことができました!

  • そもそも壊れているバイナリをプリンター向けにパースしようとしている

  • iOSのフォトライブラリーで表示を確認できても、パース処理が期待しているバイナリとは限らない



このときにはほとんど奥歯はありませんでしたが希望が見えてきたため、最悪奥歯がなくなってもヴァイオレット・エヴァーガーデンの両腕のように奥歯をサイボーグにすれば良いか、と前向きに考えられるようになりました。

不具合をミニマムに再現する

以上の試行からiOSが特定の印字幅で生成するバイナリが壊れている確信と、バイナリをコンソールに出力する術を手に入れたのでXcodeのPlaygroundで不具合が再現されるミニマムなプロジェクトを作り、試行錯誤しやすい環境を作ることにしました。

結果、以下のコードで不具合を再現できました。CGContextのwidhtに設定している384が不具合が生じない印字幅で、432が生じる印字幅です。

import UIKit

for width in [432, 384] {
    let image = UIImage(named: "\(width).JPG")!
    
    let context = CGContext(
        data: nil,
        width: width,
        height: 31,
        bitsPerComponent: 8,
        bytesPerRow: 0,
        space: CGColorSpaceCreateDeviceGray(),
        bitmapInfo: CGImageAlphaInfo.none.rawValue
    )
    
    context?.draw(
        image.cgImage!,
        in: .init(origin: .zero, size: .init(width: width, height: 31))
    )
    
    let bitmapImagePointer = context!.data!
    
    for i in 0...(image.cgImage!.width * image.cgImage!.height - 1) {
        let offsetPointer = bitmapImagePointer + i
        let raw = offsetPointer.load(as: UInt8.self)
        if i % width == 0 {
            print("")
        }
        
        print("\(200 < raw ? 1 : 0)", terminator: "")
    }
    print("")
    print("")
    print("")
}

そもそも432がどういう意味を持つ値なのかや、不具合の発生する境界値を知りたかったので下記のようにforの条件を変更しました。この変更でwidthを384〜432に変化させていくと、どのように出力が変わるかが確認できます。

// before
for width in [432, 384] {
    let image = UIImage(named: "\(width).JPG")!

// after
for width in 384...433 {
    print("=> \(width)\n")
    let image = UIImage(named: "\(432).JPG")!

その結果が下記です。432のような特別な値以外では、どのwidthにも余分なビットが入ることによってバイナリにずれが生じている事がわかりました。その結果、画像が歪みレシートに印刷されていたような斜線になるようです。

いろんな画像幅で出力した様子

以上の調査から、CGContextが何らかのルールに従い画像バイナリにビットを挿入している事がわかりました。

不具合の原因

iOSのフォトライブラリでは正しく表示できていたという事は、ビットマップ画像の仕様なのでは?と思い調べたところ下記の記述を見つけました。

ただし、いずれの形式も、コンピュータで効率的に扱えるように1スキャンラインのサイズが4バイトの倍数となるよう、必要に応じて詰め物(パディング)が末尾に付加されることが多い。

https://ja.wikipedia.org/wiki/%E3%83%93%E3%83%83%E3%83%88%E3%83%9E%E3%83%83%E3%83%97%E7%94%BB%E5%83%8F

例えば、ビットマップ画像のフォーマットであるBMPでは下記のような仕様があるようです。

水平方向のバイト数が4倍数ではないときは、0x00で埋めて4の倍数にする。

https://ja.wikipedia.org/wiki/Windows_bitmap

同様にCGContextが生成するビットマップ画像のバイナリには0x00が挿入されてるのに関わらず、プリンターに送信するためにバイナリをパースする際は補正される前のbit数を前提にパースしてしまっていました。そのため、本来のビットマップのバイナリサイズとパースアルゴリズムが期待するバイナリサイズが異なり、パース結果に不具合が生じていました。

解決

CGContextにはbytesPerRowというパラメータがあるので、そこに横幅のbit数を設定すると、そのbit数でバイナリを生成できます。APIの詳細は以下の通りです。

The number of bytes of memory to use per row of the bitmap. If the data parameter is NULL, passing a value of 0 causes the value to be calculated automatically.

https://developer.apple.com/documentation/coregraphics/cgcontext/1455939-init

このパラメータに適切な値を設定することで、どの印字幅でも下記のようにバイナリの生成とパースが成功するようになりました!こうして見るとバイナリが補正されているのがよくわかります。

正しく出力されている様子

学んだ事

以上までで不具合を解決する事ができました。そして、それだけではなくこの不具合は自分にたった1つエンジニアとして大事な事も教えてくれました。
それは


ということです(当たり前の事ですが😂)。なまじ慣れてくると雰囲気でソースコードを読めてしまいますが、1つずつの処理の仕様を確実に把握していくのはやはり大事だと改めて気づかせてもらいました。特に既存のコードでうまく動いてるっぽく見える箇所には注意が必要です。今回のように特定のケースしかカバーできていない可能性もあるからです。

しかし、実際にプリンターを使ってロール紙に印刷しながらデバッグする手法を選択しなかったので、その点はロール紙を無駄に消費せず地球環境に優しく良かったかなと思います。

さいごに

最後に、今回のデバッグで生産され廃棄される予定だった画像たちを繋ぎ合わせて1つのgif動画を作りました。

私はSDGsを支持します

SDKの開発をしているためか、最近はUIの実装をあまりしていなかったのですが、レシートをレンダリングしていると書いたものが視覚的に現れるフロントエンド実装の楽しさを少し思い出す事ができて良かったです。

明日以降もSTORES Advent Calendar 2022は更新される予定です。ぜひ見てください。

https://product.st.inc/entry/adventcalendar2022


この記事が気に入ったらサポートをしてみませんか?