lz4圧縮を使った画像ファイル(.liz)を作る。

コンピュータ

lz4圧縮は圧縮展開速度が速く、圧縮率はそれなりですが、キャッシュとして使いやすい圧縮方式だと思います。今回lz4圧縮で画像ファイルを作成してみたいと思います。

.liz ファイル形式仕様(リズ形式)

.liz は、LZ4圧縮された画像データを格納する独自形式のファイルフォーマットです。ヘッダーは32バイトの固定長で構成され、画像の基本情報と圧縮サイズを含みます。

ファイル構造

+------------+--------+--------------------------------------------+
| オフセット | サイズ | 内容                                       |
+------------+--------+--------------------------------------------+
| 0          | 3バイト | マジックバイト 'l' 'i' 'z'                 |
| 3          | 1バイト | バージョン(例: 1)                         |
| 4          | 1バイト | ビット深度(例: 8/24/32)                   |
| 5          | 1バイト | チャンネル数(例: 1=G, 3=RGB, 4=RGBA)     |
| 6          | 2バイト | 予約領域(将来拡張用、0埋め)               |
| 8          | 4バイト | 幅(Width)int32(リトルエンディアン)     |
| 12         | 4バイト | 高さ(Height)int32                        |
| 16         | 4バイト | 展開後サイズ(UncompressedSize)バイト単位 |
| 20         | 4バイト | 圧縮後サイズ(CompressedSize)バイト単位   |
| 24         | 8バイト | 拡張用予約領域(0埋め)                     |
+------------+--------+--------------------------------------------+

圧縮データ

ヘッダーの直後(32バイト目以降)に、LZ4で圧縮されたピクセルデータが格納されます。展開時には UncompressedSize をもとにバッファを確保し、LZ4デコードで復元します。

WPFとの統合(WriteableBitmap)

展開されたピクセルデータ(byte[])は、WPFの WriteableBitmap に直接書き込んで表示できます。
これは BitmapImage のような圧縮画像ではなく、生のピクセルマップ(BGRA/RGBA形式)を想定しています。

使用例(C#)


// 展開後の byte[] を WriteableBitmap に書き込む例
var bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null);
bitmap.WritePixels(
    new Int32Rect(0, 0, width, height),
    pixelData,       // LZ4で展開したバッファ
    width * 4,       // stride(4バイト/pixel)
    0                // バッファの先頭から
);

特徴

  • ヘッダーは32バイト固定長で、高速なパースが可能
  • LZ4圧縮による高速な展開性能
  • 画像のキャッシュや一時保存用途に適する軽量形式
  • 今後の拡張に対応可能な予約領域を確保

備考

  • ビット深度:8=グレースケール、24=RGB、32=RGBA を想定
  • チャンネル数はピクセルフォーマットと一致させる
  • 圧縮形式は LZ4 固定。マジックバイト 'l''i''z' で識別可能
  • 構造が単純なため、C/C++ や他言語でも容易に実装可能

実装プログラム

使用ライブラリ

  • K4os.Compression.LZ4
  • WPF標準 (System.Windows.Media.Imaging)

プロジェクトの作成

cd (mkdir LizFormat01 -Force)
dotnet new console -f net8.0
dotnet add package K4os.Compression.LZ4

ソースコード

ファイル名:LizFormat01.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="K4os.Compression.LZ4" Version="1.3.8" />
  </ItemGroup>

</Project>

ファイル名:LizFormat.cs

using K4os.Compression.LZ4;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace CommonLib;

public static class LizFormat
{
    private const int HeaderSize = 32;

    public static void EncodeToLiz(WriteableBitmap bitmap, Stream output)
    {
        int width = bitmap.PixelWidth;
        int height = bitmap.PixelHeight;
        int channels = 4;
        int bitDepth = 32;
        int stride = width * channels;
        int rawSize = stride * height;
        byte[] rawPixels = new byte[rawSize];

        bitmap.CopyPixels(rawPixels, stride, 0);

        byte[] compressed = new byte[LZ4Codec.MaximumOutputSize(rawSize)];
        int compressedLength = LZ4Codec.Encode(rawPixels, 0, rawSize, compressed, 0, compressed.Length);
        Array.Resize(ref compressed, compressedLength);

        using BinaryWriter writer = new(output, System.Text.Encoding.UTF8, leaveOpen: true);

        writer.Write(new byte[] { (byte)'l', (byte)'i', (byte)'z' });  // magic
        writer.Write((byte)1);           // version
        writer.Write((byte)bitDepth);    // bit depth
        writer.Write((byte)channels);    // channels
        writer.Write((ushort)0);         // reserved
        writer.Write(width);
        writer.Write(height);
        writer.Write(rawSize);           // uncompressed size
        writer.Write(compressedLength);  // compressed size
        writer.Write(new byte[8]);       // reserved
        writer.Write(compressed);        // data
    }

    public static WriteableBitmap DecodeFromLiz(Stream input)
    {
        using BinaryReader reader = new(input, System.Text.Encoding.UTF8, leaveOpen: true);

        byte[] magic = reader.ReadBytes(3);
        if (magic[0] != 'l' || magic[1] != 'i' || magic[2] != 'z')
            throw new InvalidDataException("Invalid .liz format");

        byte version = reader.ReadByte();
        byte bitDepth = reader.ReadByte();
        byte channels = reader.ReadByte();
        reader.ReadUInt16(); // reserved
        int width = reader.ReadInt32();
        int height = reader.ReadInt32();
        int uncompressedSize = reader.ReadInt32();
        int compressedSize = reader.ReadInt32();
        reader.ReadBytes(8); // reserved

        byte[] compressed = reader.ReadBytes(compressedSize);
        byte[] rawPixels = new byte[uncompressedSize];
        LZ4Codec.Decode(compressed, 0, compressedSize, rawPixels, 0, uncompressedSize);

        var bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null);
        bitmap.WritePixels(new Int32Rect(0, 0, width, height), rawPixels, width * 4, 0);
        return bitmap;
    }
}

ファイル名:Program.cs

using System.Diagnostics;
using System.IO;
using System.Windows.Media.Imaging;

using CommonLib;

class Program
{
    static void ConvertPngToLiz(string inputPngFile, string outputLizFile)
    {
        byte[] pngBytes = File.ReadAllBytes(inputPngFile);

        var sw = Stopwatch.StartNew();
        // --- PNG -> WriteableBitmap ---
        BitmapImage png = new();
        png.BeginInit();
        png.CacheOption = BitmapCacheOption.OnLoad;
        png.StreamSource = new MemoryStream(pngBytes);
        png.EndInit();
        sw.Stop();
        Console.WriteLine($"PNG Decode:{sw.ElapsedMilliseconds}ms");

        WriteableBitmap wbmp = new(png);

        sw.Restart();
        using var lizStream = new MemoryStream();
        // --- WriteableBitmap -> .liz (Streamで書き込み) ---
        LizFormat.EncodeToLiz(wbmp, lizStream);
        sw.Stop();
        Console.WriteLine($"Liz Encode:{sw.ElapsedMilliseconds}ms");

        File.WriteAllBytes(outputLizFile, lizStream.ToArray());
    }
    static void ConvertLizToPng(string inputLizFile, string outputPngFile)
    {
        byte[] lizBytes = File.ReadAllBytes(inputLizFile);

        var sw = Stopwatch.StartNew();
        // --- .liz -> WriteableBitmap (Streamで読み込み) ---
        WriteableBitmap restoredBitmap;
        var lizInput = new MemoryStream(lizBytes);
        restoredBitmap = LizFormat.DecodeFromLiz(lizInput);
        sw.Stop();
        Console.WriteLine($"Liz Decode:{sw.ElapsedMilliseconds}ms");

        sw.Restart();
        // --- WriteableBitmap -> PNG 保存 ---
        using var pngOut = new MemoryStream();
        PngBitmapEncoder encoder = new();
        encoder.Frames.Add(BitmapFrame.Create(restoredBitmap));
        encoder.Save(pngOut);
        sw.Stop();
        Console.WriteLine($"PNG Encode:{sw.ElapsedMilliseconds}ms");

        File.WriteAllBytes(outputPngFile, pngOut.ToArray());

    }
    static void Main()
    {
        string inputPngPath = @"H:\csharp\console\LizFormat01\sample2.png";
        string outputLizPath = @"H:\csharp\console\LizFormat01\sample2.liz";
        string restoredPngPath = @"H:\csharp\console\LizFormat01\restored2.png";

        ConvertPngToLiz(inputPngPath, outputLizPath);
        ConvertLizToPng(outputLizPath, restoredPngPath);

        Console.WriteLine("変換完了: PNG → LIZ → PNG");
        var fi = new FileInfo(inputPngPath);
        Console.WriteLine($"変換前PNG:{fi.Length / 1024}KB");
        var fi2 = new FileInfo(outputLizPath);
        Console.WriteLine($"変換後liz:{fi2.Length / 1024}KB");
    }
}
/*
結果
Celeron 3965U
素材:写真
PNG Decode:213ms
Liz Encode:29ms
Liz Decode:10ms
PNG Encode:32ms
変換完了: PNG → LIZ → PNG
変換前PNG:281KB
変換後liz:410KB

素材:アプリのスクショ
PNG Decode:274ms
Liz Encode:11ms
Liz Decode:11ms
PNG Encode:19ms
変換完了: PNG → LIZ → PNG
変換前PNG:7KB
変換後liz:10KB

*/

PNGよりファイルサイズが大きいですが、その分エンコード・デコードの時間が短い傾向になりました。

コメント