Kotlinでjava.io.FileをInputStream/OutputStreamやReader/Writerに変換する

Kotlinのコードレビューをしていて、java.io.File から InputStream に変換するとき、Javaっぽく Files#newInputStream を使っているコードがあった。

拡張関数で直接 File から変換できると指摘したら、知られていなかったのでメモ。

拡張関数の定義されたクラス

kotlin.io.FileReadWrite.kt に定義されている。

github.com

FileからInputStream/OutputStream

File#InputStream()FileInputStreamFile#outputStream()FileOutputStream に変換できる。

バッファリングはされていないが、Kotlinの InputStreamOutputStreamkotlin.io.IOStreams.kt に拡張関数が定義されており、 buffered(bufferSize: Int = DEFAULT_BUFFER_SIZE)BufferedInputStreamBufferedOutputStream で、バッファリングされた形式に変換できる。

// InputStream
file.inputStream().buffered().use { ... }

// OutputStream
file.outputStream().buffered().use { ... }

FileからReader/Writer

File#bufferedReader(charset: Charset = Charsets.UTF_8, bufferSize: Int = DEFAULT_BUFFER_SIZE)BufferedReaderFile#bufferedWriter(charset: Charset = Charsets.UTF_8, bufferSize: Int = DEFAULT_BUFFER_SIZE)BufferedWriter で、直接バッファリングされた形式に変換できる。

// Reader
file.bufferedReader().use { ... }

// Writer
file.bufferedWriter().use { ... }

振り返り

JavaだとNIOを使うのがいいだろうが、Kotlinだと java.io.File への拡張関数が便利なので、 java.io.File ベースで実装していくのがよさそう。

JavaとKotlinを行き来している身としては、両方の書き方を覚えないといけないのが面倒ではある。

Intellij IDEAでSonarLintの自動解析を抑制する

Javaでの開発時、Intellij IDEAにSonarLintプラグインを入れてSonarQubeによる解析を行っているが、Intellijのキャッシュクリアをして再起動した後などは、解析完了までCPUを全部持っていかれ、プチフリーズ状態になってしまう。

ファイル単位で手動実行できるし、以前書いた方法でコードレビュー時には解析結果が連携されるので、自動解析は止めてもいいんじゃないかと思い、方法を調べたのでメモ。

停止方法

Stack Overflowにあった。

stackoverflow.com

Windows版の場合、以下の手順で自動解析を停止できる。

  1. FileからSettingsを開く
  2. Toolsを展開し、SonarLintをクリック
  3. 右フレームにSonarLintの設定が開くので、Settingsタブの `Automatically trigger Analysis' のチェックを外し、OKボタンでウィンドウを閉じる

振り返り

SonarLintの自動解析が行われる条件がよくわからず、いきなり実行されてはCPU100%近くになることがあったので、かなり快適になった。

Intellij IDEAのインデックスとか解析系の処理、なんであんなにCPU使うんだろう...

Javaで画像の特定の色を透過させる

前回、アップロードされた画像の加工をJavaでやったところ、追加で「透過していない画像であれば、白背景を透過できないか」との要望が。

ちょっと試してみたところ、何とかなったのでメモ。

環境

OpenJDK 11

実装

前回同様、 java.awt.image のクラスを利用。

透過済みかの判定

BufferedImage#isAlphaPremultiplied で、アルファが事前に乗算されている*1かは判定できるが、そうでない場合の透過は判定できないので、画像のピクセルを総当たりしてアルファが設定されているか判定する。

boolean hasAlpha(BufferedImage image) {
    // アルファが乗算済みか判定
    if (image.isAlphaPremultiplied()) {
        return true;
    }

    // 乗算済みでない場合、各ピクセルをチェックし、アルファが設定されているか判定
    var imageWidth = image.getWidth();
    var imageHeight = image.getHeight();

    for (int x = 0; x < imageWidth; x++) {
        for (int y = 0; y < imageHeight; y++) {
            var alpha = new Color(image.getRGB(x, y), true).getAlpha();

            // アルファ未設定の場合255になる
            if (alpha != 255) {
                return true;
            }
        }
    }

    return false;
}

特定の色を透過

RGBImageFilter を使って、特定の色の場合に透過させることができる。

class TransparentFilter extends RGBImageFilter {

    private static final int RGB_BITS = 0xFFFFFF;

    private final int targetRgb;

    TransparentFilter(int targetRgb) {
        // RGBだけ抽出
        this.targetRgb = targetRgb & RGB_BITS;
        canFilterIndexColorModel = true;
    }

    TransparentFilter(Color color) {
        this(color.getRGB());
    }

    @Override
    public int filterRGB(int x, int y, int rgb) {
        // ビット演算でRGBを抽出し、対象と一致した場合透過させる
        // アルファが0になればいいので、0を返す
        return (rgb & RGB_BITS) == targetRgb ? 0 : rgb;
    }

}

のようなサブクラスを作成。

以下のような処理で、色を指定して透過できた。

void transparent(File imageFile, Color color) throws IOException {
    var originalImage = ImageIO.read(imageFile);

    // 前述のメソッドで透過済みか判定し、透過済みなら何もせず返す
    if (hasAlpha(originalImage)) {
        return;
    }

    var imageFilter = new TransparentFilter(color);
    var imageProducer = new FilteredImageSource(
            originalImage.getSource(), imageFilter
    );
    var transparentImage = Toolkit
                .getDefaultToolkit()
                .createImage(imageProducer);

    // 元画像と同じ大きさの透過画像を作成
    var newImage = new BufferedImage(
            originalImage.getWidth(),
            originalImage.getHeight(),
            BufferedImage.TYPE_INT_ARGB
    );
    var graphics = newImage.createGraphics();
    graphics.drawImage(transparentImage, 0, 0, null);

    // ファイルを上書き
    ImageIO.write(newImage, "png", imageFile);
    return;
}

実行サンプル

表計算ソフトで適当な画像を用意。

f:id:hepokon365:20211115000117p:plain
適当にもほどがある画像

Color.WHITEColor.BLACK を指定してそれぞれ実行した結果が以下。

f:id:hepokon365:20211115000307p:plainf:id:hepokon365:20211115000356p:plain
左がWHITE、右がBLACK指定

振り返り

まさか2021年にもなって、Javaで画像処理をガッツリやるとは思わなかった。

もろもろの画像編集ツールとかを使うと簡単にできることだが、情報が少ない&古いので、なかなか面倒だった。

ただ、 java.awt.image は枯れ切ってるので、10年以上前の情報もそのまま使えたりするのはちょっと面白かった。

参考

TYPE_INT_ARGBTYPE_INT_ARGB_PRE の違いについて

stackoverflow.com

*1:RGBと別にAlphaを持って画像を描画する際に乗算する方法と、あらかじめRGBにAlphaを乗算しておく方法があり、後者を指す模様。それぞれ、TYPE_INT_ARGB と TYPE_INT_ARGB_PRE に相当する。

Javaで画像の縦横比を維持してリサイズする

アップロードされた画像を特定の高さに揃える、みたいな作業をすることになった。

サムネイル画像など、表示用であればCloudinaryimgixのような、リサイズ機能付きCDNサービスを使えばいいが、サーバー側で別途画像処理する必要があったので使えず。

Javaで実装しているので、ImageMagickをインストールして ProcessBuilder で動かそうとも思ったが、 ImageIO 使えばPure Javaで実装できるかと思ったが、縦横比を自前で計算する必要がありそうだった。

自動でリサイズする方法はないかと調べてみると、ライブラリがあったのでメモ。

縦横比を維持してリサイズするライブラリ

imgscalrというライブラリがあった。Mavenリポジトリにも登録済み

ライブラリといっても、含まれるのはstaticメソッドを持つ2クラスだけ、うち1クラスは非同期処理用なので実質1クラス。

実装内容もJava標準の画像処理系クラスのラッパーで、縦横比を維持してリサイズ後の幅と高さを自動で計算し、 java.awt.image.BufferedImage 経由でリサイズしている。

最新バージョンは4.2だが、リリースが2012年1月なので枯れている。ライセンスもApache License 2.0で使いやすいので、こちらを利用することとした。

使い方

ImageIO.read などで変換したい画像を BufferedImage として読み込み、 Scalr.resize メソッドを実行する。

引数違いで同名メソッドが複数オーバーロードされているが、 Scalr.Mode を指定することで、リサイズ方法を変更できる。

モード 概要
FIT_EXACT 指定された幅・高さにリサイズ
FIT_TO_WIDTH 指定された幅にリサイズ
FIT_TO_HEIGHT 指定された高さにリサイズ

今回のように高さを固定したい場合、 FIT_TO_HEIGHT してやれば、縦横比を保ったままリサイズされる。

File originalImageFile = Path.of(...).toFile();
int newHeight = 120;
BufferedImage originalImage = ImageIO.read(originalImageFile);
BufferedImage resizedImage = Scalr.resize(
        originalImage,
        Scalr.Method.QUALITY,
        Scalr.Mode.FIT_TO_HEIGHT,
        newHeight
);
File tempFile = Files.createTempFile("resized-", ".png").toFile();
ImageIO.write(resizedImage, "png", tempFile);

と書いてやると、画像の高さを120pxに変換できる。

Modeを FIT_EXACT にすると縦横120px、FIT_TO_WIDTH にすると幅120pxになる。

この例では数値を1つしか渡していないが、 widthheight の2つを渡すメソッドもある。幅と高さをそれぞれ指定し、Modeで FIT_EXACT を使用すると、指定した幅と高さに変換できる模様。

振り返り

地味に面倒な縦横比計算を任せられたので助かった。

余談

当初はImageMagickを使おうかなと思ったが、JavaからImageMagickを使うライブラリもある模様。

backport.net

JNIを使ってAPIを実行するJMagickと、ProcessBuilder を使うim4javaの2つ。

両方ともライセンスがLGPLなので、Javaで依存性管理ツール経由で利用するならアプリケーション側をLGPLにする必要はなさそう。

LGPLとJava - GNUプロジェクト - フリーソフトウェアファウンデーション

使うならim4javaのほうかと思う。最終リリースが2012年12月なので、最新のバージョンで使用できるかわからなかったが、こちらの記事を見ると、ImageMagick 7系でも動く模様。

Javaでバイトの単位(KB,MB,GB)をわかりやすくする

1 * 1024 * 1024 で1メガバイト、みたいなやつをよく見る。

コードレビューしていると、1000 だったり 1024 だったり、気を利かせて 1_024 と書かれていたり、たまに 124 とか 1240 とタイポしていたりと、結構揺らぎがち。また、KB/MB/GBかKiB/MiB/GiBか、みたいな議論になったりすることもあって面倒。

TimeUnit のように、Javaで単位をわかりやすくできるライブラリはないかと思い、調べたのでメモ。

SpringのDataSize

Springに、各バイト単位を変換する DataSize 、および単位自体を表す列挙型の DataUnit があった。

DataSize#of...bytes(long) メソッドで DataSize インスタンスを生成し、 to...Bytes() で指定した単位の long に変換する。また、 DataSize#parse(CharSequence) で文字列の解析も可能。

DataUnit の値名は KILOBYTES, MEGABYTES, GIGABYTES だが、それぞれ2の10乗、20乗、30乗なので、 long 値は 1,024, 1,048,576, 1,073,741,824 となる。

// 単位系がわかっている場合、それに応じたofメソッドを使用
assert DataSize.ofMegabytes(1).toBytes() == 1 * 1024 * 1024;

// of(long, DataUnit)で、任意の単位に変換可能
assert DataSize.of(2, DataUnit.GIGABYTES).toMegabytes() == 2 * 1024;

// parse(CharSequence[, DataUnit])で文字列を解析
assert DataSize.parse("20MB").toBytes() == 20 * 1024 * 1024;

Apache Commons

Commons IOの FileUtils に、各単位の long および BigIntegerstatic で宣言されている。

ONE_KB, ONE_MB, ONE_GB などあるが、 DataSize と同様、それぞれ 1,024, 1,048,576, 1,073,741,824 となる。

FileUtils.byteCountToDisplaySize(long) というメソッドで、バイト数を文字列に変換できるが、小数点以下が無視されるので扱いづらい。

その他

byteunits というライブラリもあった。こちらは KBKiB などを区別する模様。

github.com

振り返り

今日のJava開発であれば、Springはだいたい使っていると思うので、 DataSize を使っておけばシンプルになりそう。

また、SpringもCommonsも1024の倍数をKB、MB、GBとして使っているので、下手にKiBなど使わず、KBとかにしておくのが無難かと思った。

余談

以下のスタックオーバーフローの記事で、数値から文字列に変換するスニペットが記載されている。

stackoverflow.com

どうも、スタックオーバーフローで最もコピーされたスニペットらしいが、間違っていた模様。

programming.guide

JavaでMessageDigestにないハッシュ関数を使う

ユーザーの入力値から適当な文字列を生成する要件があった。

Javaで標準的に使えるハッシュ関数だとMD5SHA1,2,3あたりになるが、計算速度は早いほうがいいということで、高速なハッシュ関数が使えないか調べてみたのでメモ。

環境

OpenJDK 11 11.0.12.7

Javaで標準的に使えるハッシュ関数

MessageDigestJavadocにリンクのある、Javaセキュリティ標準アルゴリズム名#MessageDigestアルゴリズムに記載がある。

Java11の時点で、MD2, MD5, SHA-1, SHA-2, SHA-3が使える。

JVM実装に依存するため、実際に指定できるアルゴリズムを確認するには、 java.security.Security.getAlgorithms("MessageDigest") を使用すればいい。

System.out.println(new TreeSet(Security.getAlgorithms("MessageDigest")));

OpenJDK 11だと、[MD2, MD5, SHA, SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, SHA-512/256, SHA3-224, SHA3-256, SHA3-384, SHA3-512] となった。「Javaセキュリティ標準アルゴリズム名」のページ記載とは多少差異があるが、別名で設定されているのだろう。

標準外のハッシュ関数

これら以外に利用できるハッシュ関数がないか調べたところ、GuavaApache Commons Codecにいくつか実装を見つけた。

Guava

com.google.common.hash.Hashingにて、ハッシュ関数を返すstaticメソッドが用意されている。

MD5やSHA系の MessageDigest で使用可能なハッシュ関数以外に、以下のハッシュ関数がある。

  • CRC32 (32bit)
  • CRC32C (32bit)
  • FarmHash Fingerprint64 (64bit)
  • MurmurHash3 (32bit, 128bit)
    • Guava v31.0にて、BMP(基本多言語面)に含まれない文字が文字列に含まれている場合、生成されるハッシュ値が不正ということで、 Hashing#murmur3_x_fixed が追加された。
  • SipHash 2-4 (64bit)

速度優先のハッシュ関数を返す、 Hashing.html#goodFastHash メソッドなんてのもあった。

Commons Codec

org.apache.commons.codec.digestパッケージに、ハッシュや暗号化関連のクラスが用意されている。

ハッシュだと以下。

  • CRC32
  • CRC32C
  • MurmurHash2
  • MurmurHash3
  • xxHash32

Guavaと同様、 MessageDigest で利用可能なアルゴリズムを返す DigestUtils も用意されている。

その他

FNV実装もいくつか見つけたが、MurmurHashやSipHashがあれば不要と判断し、調査せず。

振り返り

思ったよりもたくさん見つかった。

実際のハッシュ生成には、内部的にしか使わず、衝突しても問題なしという要件だったため、GuavaのMurmurHash3 32bitを利用。

参考資料

プログラミング言語内でのハッシュ関数としては、SipHashが広く使われている。その経緯は以下に詳しい。

kazu-yamamoto.hatenablog.jp

PythonでFNVからSipHashに切り替える際のPEP 456に、それぞれの比較結果が載っている。

SipHashも、当初のSipHash24から、ラウンド数を減らしても安全ということで、SipHash13への置き換えが進んでいるとのこと。

methane.hatenablog.jp

Spockのテストコンパイルが遅いので、GradleでGroovyの増分コンパイルを有効化する

Spockでユニットテストを書いている、Gradleを使ったJavaプロジェクトがある。

Intellijユニットテストを実行するとき、ソースコードに変更があると毎回ユニットテストのすべてのクラスがコンパイルされてしまい、実行までに2分前後のコンパイル待ちが発生していた。

Javaのようにインクリメンタルコンパイル(増分コンパイル)ができないかと思って調べたら、設定できたのでメモ。

環境

Gradle 7.1.1、OpenJDK 11にて確認。

Gradle 5.6以降、JDK 7以上で利用可能。

問題

Gradleプロジェクトにて、Spockを使ったGroovyテストクラスに1つでも変更があると、常にすべてのクラスがコンパイルされる。

また、テストクラスに変更がなくても、Javaのメインクラスに変更があった場合も、同様にすべてのクラスがコンパイルされる。

Gradleによるコンパイル時、Javaであればデフォルトでインクリメンタルコンパイル(増分コンパイル・差分コンパイル)が有効になっているため、一度コンパイルした後は、変更のあったクラスおよび変更の影響を受けるクラスだけがコンパイルされる。

Groovyの場合、デフォルトではインクリメンタルコンパイルが有効になっていないため、テストクラスに変更があった場合や、メインクラスが変更された場合、すべてのテストクラスが再コンパイルされていると思われる。

Groovyテストクラス数が多く、フルコンパイルにはおおよそ2分程度かかるため、短縮したい。

対応

インクリメンタルコンパイルをGroovyコンパイルにも適応できるが、デフォルトでは無効になっているため、有効化する。

手順としては以下の2つ。

  1. コンパイル回避の有効化
  2. インクリメンタルビルドの有効化

また、これらの変更により、テストクラスのコンパイルUP-TO-DATE と判定された場合、デフォルトではユニットテストの実行自体が行われなくなるため、Gradleの test タスクを常に実行するよう build.gradle にも変更を行う。

設定手順

Groovyコンパイル回避の有効化

インキュベーション機能のため、明示的に有効化する必要がある。

こちらの手順通り、Javaプロジェクトの settings.gradle に、以下の記述を追加。

enableFeaturePreview('GROOVY_COMPILATION_AVOIDANCE')

Groovyインクリメンタルビルドの有効化

Groovy Pluginの説明を基に、 build.gradle に設定を追加。

tasks.withType(GroovyCompile).configureEach {
  options.incremental = true
}

GroovyのコンパイルオプションであるgroovyOptionsではなく、Javaコンパイルオプションである[options]が設定の対象。

手順通り GroovyCompile タスクすべてに設定しているが、ユニットテストだけが対象であれば、 compileTestGroovy タスクに対してのみ設定すればいいと思われる。

常にユニットテストを実行するよう設定

先の2つの設定で、Groovyのインクリメンタルコンパイルは有効となるが、 compileTestJava および compileTestGroovy タスクが両方とも UP-TO-DATE になると、 test タスクも UP-TO-DATE 扱いで実行されなくなる。

これを避けるために、常に test タスクを実行するよう、 build.gradle に以下の設定を追加。

test {
  outputs.upToDateWhen { false }
}

TaskOutputs#upToDateWhenfalse を返すことで、常に test タスクを実行できる。

結果

これらの設定を行ったことで、Javaのメインクラスを変更した場合でも、メソッドの内部のみの変更であれば、Groovyテストクラスは UP-TO-DATE となり、再コンパイルが行われなくなった。

また、テストクラスの一部を変更した場合、差分に対してのみコンパイルが行われるようになったため、1クラスの変更であれば数秒で再コンパイルが完了するようになった。

振り返り

インクリメンタルコンパイルについては、インクリメンタルJavaコンパイルおよびインクリメンタルJavaコンパイルの既知の問題を参照。

有効化したことで劇的な効果があった。Gradle 7.2時点ではデフォルトで有効化されていないので、Spockなどを使っていればメリットがありそう。