👏

.NETにてSystem.Drawing未使用で二値のマルチページTIFFのQRコードを読み取る

に公開

非Windows環境を考えた場合、System.Drawingが利用できないのですが、その場合にTIFFを扱うのは少々考慮が必要です。

TIFFのデコードにはLibTiffを利用します。LibTiffを選択した理由は下記の記事を参照してください。

QRコード(と言いつつバーコード全般)の読み取りにはZXing.Netを利用します。

ZXing.Netには多様な画像処理ライブラリに対するバインドが存在しますが、残念ながらLibTiff用のバインドが存在しないため、それを用意するのが本記事の目的となります。

前提

下記の環境、ライブラリの利用が前提となります。

  • .NET Framework 4.8.1 or .NET 8.0+
  • BitMiracle.LibTiff.NET 2.4.660+
  • ZXing.Net 0.16.10+

概要

ZXing.Netで非対応の画像処理ライブラリ用のバインドを作成するにはつぎの2つのクラスを用意する必要があります。

  • LuminanceSourceの実装クラス
  • BarcodeReader<T>の実装クラス

これらを実装した上で、つぎのように使います。

using BitMiracle.LibTiff.Classic;
using ZXing.Net.Bindings.LibTiff;

using var stream = File.Open("MultiPage.tif", FileMode.Open);
using var tiff = Tiff.ClientOpen("in-memory", "r", stream, new TiffStream());

var reader = new BarcodeReader();
int page = 1;
do
{
    Console.WriteLine($"Page {page++}");
    var barcodes = reader.DecodeMultiple(tiff);
    foreach (var barcode in barcodes)
    {
        Console.WriteLine($"Format: {barcode.BarcodeFormat}, Text: {barcode.Text}");
    }

} while (tiff.ReadDirectory());

では先のクラスの実装を見ていきましょう。

実装

LuminanceSourceの実装クラス

このクラスが実際ほぼすべてと言って過言ではないです。

using BitMiracle.LibTiff.Classic;

namespace ZXing.Net.Bindings.LibTiff;

/// <summary>
/// TIFF画像から輝度情報を抽出するためのクラスです。
/// </summary>
public sealed class TiffLuminanceSource : BaseLuminanceSource
{
    /// <summary>
    /// 指定されたTIFF画像から新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="tiff">輝度情報を抽出するTIFF画像。</param>
    /// <exception cref="ArgumentException">TIFF画像が1ビットでない場合にスローされます。</exception>
    public TiffLuminanceSource(Tiff tiff)
        : base(GetWidth(tiff), GetHeight(tiff))
    {
        var width = Width;
        var height = Height;

        // TIFF画像のビット深度を確認
        var bitsPerSampleField = tiff.GetField(TiffTag.BITSPERSAMPLE);
        if (bitsPerSampleField == null || bitsPerSampleField[0].ToInt() != 1)
            throw new ArgumentException(@"The provided TIFF image is not 1-bit.", nameof(tiff));

        var scanlineSize = tiff.ScanlineSize();
        var scanline = new byte[scanlineSize];

        // 各スキャンラインを読み取り、輝度情報を抽出
        for (var row = 0; row < height; row++)
        {
            tiff.ReadScanline(scanline, row);
            unsafe
            {
                fixed (byte* scanlinePtr = scanline)
                {
                    for (var col = 0; col < width; col++)
                    {
                        var byteIndex = col / 8;
                        var bitIndex = 7 - (col % 8); // TIFFは上位ビットから順に格納
                        var isWhite = (scanlinePtr[byteIndex] & (1 << bitIndex)) != 0;
                        luminances[row * width + col] = isWhite ? (byte)255 : (byte)0;
                    }
                }
            }
        }
    }

    /// <summary>
    /// 指定された輝度情報、幅、高さから新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="luminances">輝度情報の配列。</param>
    /// <param name="width">画像の幅。</param>
    /// <param name="height">画像の高さ。</param>
    private TiffLuminanceSource(byte[] luminances, int width, int height)
        : base(luminances, width, height)
    {
    }

    /// <summary>
    /// 新しい輝度情報、幅、高さから新しいLuminanceSourceを作成します。
    /// </summary>
    /// <param name="newLuminances">新しい輝度情報の配列。</param>
    /// <param name="width">新しい画像の幅。</param>
    /// <param name="height">新しい画像の高さ。</param>
    /// <returns>新しいLuminanceSourceのインスタンス。</returns>
    protected override LuminanceSource CreateLuminanceSource(byte[] newLuminances, int width, int height)
    {
        return new TiffLuminanceSource(newLuminances, width, height);
    }

    /// <summary>
    /// TIFF画像の幅を取得します。
    /// </summary>
    /// <param name="tiff">幅を取得するTIFF画像。</param>
    /// <returns>TIFF画像の幅。</returns>
    /// <exception cref="ArgumentException">TIFF画像にIMAGEWIDTHフィールドが含まれていない場合にスローされます。</exception>
    public static int GetWidth(Tiff tiff)
    {
        // TIFF画像からIMAGEWIDTHフィールドを取得
        var widthField = tiff.GetField(TiffTag.IMAGEWIDTH);
        // IMAGEWIDTHフィールドが存在しない場合は例外をスロー
        return widthField == null
            ? throw new ArgumentException("TIFF image does not contain IMAGEWIDTH field.", nameof(tiff))
            : widthField[0].ToInt();
    }

    /// <summary>
    /// TIFF画像の高さを取得します。
    /// </summary>
    /// <param name="tiff">高さを取得するTIFF画像。</param>
    /// <returns>TIFF画像の高さ。</returns>
    /// <exception cref="ArgumentException">TIFF画像にIMAGELENGTHフィールドが含まれていない場合にスローされます。</exception>
    public static int GetHeight(Tiff tiff)
    {
        // TIFF画像からIMAGELENGTHフィールドを取得
        var heightField = tiff.GetField(TiffTag.IMAGELENGTH);
        // IMAGELENGTHフィールドが存在しない場合は例外をスロー
        return heightField == null
            ? throw new ArgumentException("TIFF image does not contain IMAGELENGTH field.", nameof(tiff))
            : heightField[0].ToInt();
    }

}

BarcodeReader<T>の実装クラス

using BitMiracle.LibTiff.Classic;

namespace ZXing.Net.Bindings.LibTiff;

/// <summary>
/// TIFF画像をデコードするためのバーコードリーダークラスです。
/// </summary>
public class BarcodeReader() : BarcodeReader<Tiff>(null, DefaultCreateLuminanceSource, null)
{
    /// <summary>
    /// デフォルトの輝度ソースを作成するためのデリゲートです。
    /// </summary>
    private static readonly Func<Tiff, LuminanceSource> DefaultCreateLuminanceSource =
        (image) => new TiffLuminanceSource(image);
}

その他、便利拡張メソッド

前述のコードだけでも利用できますが、つぎのような拡張メソッドを作っておくと利用しやすいです。

using BitMiracle.LibTiff.Classic;

namespace ZXing.Net.Bindings.LibTiff;

/// <summary>
/// バーコードリーダーの拡張メソッドを提供するクラスです。
/// </summary>
public static class BarcodeReaderExtensions
{
    /// <summary>
    /// TIFF画像からバーコードをデコードします。
    /// </summary>
    /// <param name="reader">バーコードリーダーのインスタンス。</param>
    /// <param name="image">デコードするTIFF画像。</param>
    /// <returns>デコードされたバーコードの結果。</returns>
    public static Result Decode(this IBarcodeReaderGeneric reader, Tiff image)
    {
        // TIFF画像から輝度情報を取得するためのソースを作成
        var luminanceSource = new TiffLuminanceSource(image);
        // 輝度情報を使用してバーコードをデコード
        return reader.Decode(luminanceSource);
    }

    /// <summary>
    /// TIFF画像から複数のバーコードをデコードします。
    /// </summary>
    /// <param name="reader">バーコードリーダーのインスタンス。</param>
    /// <param name="image">デコードするTIFF画像。</param>
    /// <returns>デコードされた複数のバーコードの結果。</returns>
    public static Result[] DecodeMultiple(this IBarcodeReaderGeneric reader, Tiff image)
    {
        // TIFF画像から輝度情報を取得するためのソースを作成
        var luminanceSource = new TiffLuminanceSource(image);
        // 輝度情報を使用して複数のバーコードをデコード
        return reader.DecodeMultiple(luminanceSource);
    }
}

以上!

Discussion