スキップしてメイン コンテンツに移動

Create Indexed PNG Using C# .Net

Currently I am working on optimizing image size. In this post, I will demonstrate the C# code for creating indexed png image.
The code might not be perfect yet, but hope this code will give you some hints ;)

A few things I am struggling with while I am writing the example code:

  • Palette: you should take ColorPalette from Bitmap, modify the ColorPalette object and set buck it to the Bitmap object.
  • BitmapData.Stride is often bigger than BitmapData.Width. You should be careful to check both values difference when you manipulating the image array index.
  • The example code uses classes in System.Drawing namespace. But looks there is another option to manipulate image in C# .Net - System.Windows.Media namespace.

C# Source Code for Creating Indexed PNG

  • The following implementation of CreateIndexedPng method is able to take only non-indexed image like Format24bppRgb or Format32bppArgb. (I might add other image formats in future.)
  • If the source image has only 2 colors, we can use Format1bppIndexed, but the following code apply Format1bppIndexed in that case because of the code simplicity.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.IO;
using System.Runtime.InteropServices;

namespace Utility
{
    public static class ImageUtils
    {
        const int ALPHA_255 = 255 << 24;

        public static Bitmap CreateIndexedPng(this Bitmap orig)
        {
            var rgbValues = orig.ToByteArray();
            var block = orig.DetermineBlockSize();
            var colorSet = CreateColorSet(rgbValues, block, orig.Width, orig.Height);

            var result = new Bitmap(orig.Width, orig.Height, determinePixelFormat(colorSet.Count));

            // setup palette and color map
            var pal = result.Palette;
            var colorToIndex = new Dictionary<int, int>();
            int index = 0;
            foreach (int color in colorSet)
            {
                pal.Entries[index] = Color.FromArgb(color);
                colorToIndex[color] = index;
                index++;
            }
            result.Palette = pal;

            // fill bits
            var data = result.LockBits(new Rectangle(0, 0, result.Width, result.Height),
                ImageLockMode.ReadWrite, result.PixelFormat);

            byte[] colorData = new byte[data.Stride * data.Height];
            int len = rgbValues.Length;
            if (block == 3)
            {
                if (result.PixelFormat == PixelFormat.Format8bppIndexed)
                {
                    fillBitmapData8bppIndexedNoAlpha(rgbValues, colorToIndex, colorData, len, orig.Width, orig.Height, data.Stride);
                }
                else if(result.PixelFormat == PixelFormat.Format4bppIndexed)
                {
                    fillBitmapData4bppIndexedNoAlpha(rgbValues, colorToIndex, data, colorData, len, orig.Width);
                }
                else if (result.PixelFormat == PixelFormat.Format1bppIndexed)
                {
                    throw new NotImplementedException(PixelFormat.Format1bppIndexed + " is not supported");
                }
            }
            else
            {
                if (result.PixelFormat == PixelFormat.Format8bppIndexed)
                {
                    fillBitmapData8bppIndexedAlpha(rgbValues, colorToIndex, colorData, len, orig.Width, orig.Height, data.Stride);
                }
                else if (result.PixelFormat == PixelFormat.Format4bppIndexed)
                {
                    fillBitmapData4bppIndexedAlpha(rgbValues, colorToIndex, colorData, len, orig.Width, orig.Height, data.Stride);
                }
                else if (result.PixelFormat == PixelFormat.Format1bppIndexed)
                {
                    throw new NotImplementedException(PixelFormat.Format1bppIndexed + " is not supported");
                }
            }

            Marshal.Copy(colorData, 0, data.Scan0, colorData.Length);

            result.UnlockBits(data);

            return result;
        }

        private static void fillBitmapData8bppIndexedAlpha(byte[] rgbValues, Dictionary<int, int> colorToIndex, 
                                                              byte[] colorData, int len, int width, int height, int outStride)
        {
            int stride = len / height;
            int colLimit = width * 4;

            for (int row = 0; row < height; row++)
            {
                for (int col = 0; col < colLimit; col += 4)
                {
                    int offset = row * stride + col;
                    int b = rgbValues[offset];
                    int g = rgbValues[offset + 1];
                    int r = rgbValues[offset + 2];
                    int a = rgbValues[offset + 3];
                    int index = row * outStride + col / 4;
                    colorData[index] = (byte)colorToIndex[to32BitIntARGB(a, r, g, b)];
                }
            }
        }

        private static void fillBitmapData4bppIndexedAlpha(byte[] rgbValues, Dictionary colorToIndex, byte[] colorData, int len, int width, int height, int outStride)
        {
            int stride = len / height;
            int colLimit = width * 4;

            for (int row = 0; row < height; row++)
            {
                for (int col = 0; col < colLimit; col += 8)
                {
                    int offset = row * stride + col;
                    int b = rgbValues[offset];
                    int g = rgbValues[offset + 1];
                    int r = rgbValues[offset + 2];
                    int a = rgbValues[offset + 3];
                    int color1 = to32BitIntARGB(a, r, g, b);
                    if (col / 4 + 1 >= width)
                    {
                        int index1 = row * outStride + col / 8;
                        colorData[index1] = (byte)(colorToIndex[color1] << 4);
                        continue;
                    }

                    b = rgbValues[offset + 4];
                    g = rgbValues[offset + 5];
                    r = rgbValues[offset + 6];
                    a = rgbValues[offset + 7];
                    int color2 = to32BitIntARGB(a, r, g, b);
                    int index2 = row * outStride + col / 8;
                    colorData[index2] = (byte)(colorToIndex[color1] << 4 | colorToIndex[color2]);
               }
            }
        }

        private static void fillBitmapData8bppIndexedNoAlpha(byte[] rgbValues, Dictionary<int, int> colorToIndex, 
                                                                byte[] colorData, int len, int width, int height, int outStride)
        {
            int stride = len / height;
            int colLimit = width * 3;

            for (int row = 0; row < height; row++)
            {
                for (int col = 0; col < colLimit; col += 3)
                {
                    int offset = row * stride + col;
                    int b = rgbValues[offset];
                    int g = rgbValues[offset + 1];
                    int r = rgbValues[offset + 2];
                    int index = row * outStride + col / 3;
                    colorData[index] = (byte)colorToIndex[to32BitIntARGB(r, g, b)];
                }
            }
        }

        private static void fillBitmapData4bppIndexedNoAlpha(byte[] rgbValues, Dictionary<int, int> colorToIndex, 
                                                                          BitmapData data, byte[] colorData, int len, int width)
        {
            int outStride = data.Stride;
            int height = data.Height;
            int stride = len / height;
            int colLimit = width * 3;

            for (int row = 0; row < height; row++)
            {
                for (int col = 0; col < colLimit; col += 6)
                {
                    int offset = row * stride + col;
                    int b = rgbValues[offset];
                    int g = rgbValues[offset + 1];
                    int r = rgbValues[offset + 2];
                    int color1 = to32BitIntARGB(r, g, b);
                    if (col / 3 + 1 >= width)
                    {
                        int index1 = row * outStride + col / 6;
                        colorData[index1] = (byte)(colorToIndex[color1] << 4);
                        continue;
                    }

                    b = rgbValues[offset + 3];
                    g = rgbValues[offset + 4];
                    r = rgbValues[offset + 5];
                    int color2 = to32BitIntARGB(r, g, b);
                    int index2 = row * outStride + col / 6;
                    colorData[index2] = (byte)(colorToIndex[color1] << 4 | colorToIndex[color2]);

                }
            }
        }

        private static HashSet<int> CreateColorSet(byte[] rgbValues, int block, int width, int height)
        {
            var colorSet = new HashSet<int>();
            int len = rgbValues.Length;
            int stride = len / height;
            if (block == 3)
            {
                for (int row = 0; row < height; row++)
                {
                    for (int col = 0; col < width * 3; col += 3)
                    {
                        int offset = row * stride + col;
                        int b = rgbValues[offset];
                        int g = rgbValues[offset + 1];
                        int r = rgbValues[offset + 2];
                        colorSet.Add(to32BitIntARGB(r,g,b));
                    }
                }

            }
            else
            {
                for (int row = 0; row < height; row++)
                {
                    for (int col = 0; col < width * 4; col += 4)
                    {
                        int offset = row * stride + col;
                        int b = rgbValues[offset];
                        int g = rgbValues[offset + 1];
                        int r = rgbValues[offset + 2];
                        int a = rgbValues[offset + 3];
                        colorSet.Add(to32BitIntARGB(a, r, g, b));
                    }
                }
            }
            return colorSet;
        }

        public static PixelFormat determinePixelFormat(int colorCount)
        {
            if (colorCount <= 2)
            {
                // you can use Format1bppIndexed but for simplicity return Format4bppIndexed
                return PixelFormat.Format4bppIndexed;
            }
            else if (colorCount <= 16)
            {
                return PixelFormat.Format4bppIndexed;
            }
            else if (colorCount <= 255)
            {
                return PixelFormat.Format8bppIndexed;
            }
            else
            {
                throw new ArgumentException("number of colors in image must equal or less than " + 256);
            }
        }

        public static int DetermineBlockSize(this Bitmap bmp)
        {
            var pixelFormat = bmp.PixelFormat;
            if (pixelFormat.HasFlag(PixelFormat.Format24bppRgb))
            {
                return 3;
            }
            else if (pixelFormat.HasFlag(PixelFormat.Format32bppArgb))
            {
                return 4;
            }
            else
            {
                throw new NotImplementedException("Unsupported type of image:" + pixelFormat);
            }
        }

        private static int to32BitIntARGB(int a, int r, int g, int b)
        {
            return (a << 24) | (r << 16) | (g << 8) | (b);
        }

        private static int to32BitIntARGB(int r, int g, int b)
        {
            return ALPHA_255 | (r << 16) | (g << 8) | (b);
        }

        private static byte[] ToByteArray(this Bitmap src)
        {
            var rect = new Rectangle(0, 0, src.Width, src.Height);
            var bmpData = src.LockBits(rect, ImageLockMode.ReadOnly, src.PixelFormat);

            var ptr = bmpData.Scan0;

            int bytes = Math.Abs(bmpData.Stride) * src.Height;
            var rgbValues = new byte[bytes];

            Marshal.Copy(ptr, rgbValues, 0, bytes);

            src.UnlockBits(bmpData);
            return rgbValues;
        }
    }
}

Example Result of Creating Indexed PNG

See the below result which demonstrated generating indexed png image from non indexed png image by the above example code.

RGBA 32bpp to 292x292 8bpp indexed

The left image is the original 292x292 RGBA 32bpp png image and the right image is the 292x292 8bpp indexed png image generated by the example code.
The size is reduced 12,717 bytes to 9,586 bytes.

Single color 32RGBA to Single color 1bpp indexed

The left image is the original 512x512 RGBA 32bpp single color png image and the right image is the 512x512 1bpp indexed png image generated by the example code.
The size is reduced 1,864 bytes to 315 bytes.

Consideration

The code itself works fine but maybe you noticed that the size reducing percentage is not sufficient compared with other existing image optimization tools :( or comparing with Java version result - Create Indexed PNG Image Using Standard Java Image API in this blog
The cause looks compression level. Unfortunately C# Png Encoder which Microsoft provides doesn't seem support compress level option.
So I have investigated more usable and efficient .Net png library. And I found pngcs, which is the C# version of pngj.
In Nitropan さんの投稿…

Wow! I've taken your code and hope it works :-)!.. Thank you very much, although I'm not sure how you've done it. Quite stupid from MS that the palette is not remapped.. Bye, Andy
dahut さんの投稿…
Thank for your work
dahut さんの投稿…
Line 65 copy paste error, you call fillBitmapData8bppIndexedAlpha() instead of fillBitmapData4bppIndexedAlpha(). For externals readers just replace the method and use the same parameter as fillBitmapData4bppIndexedNoAlpha() (line 50)
Lonely Developer さんの投稿…
Hi dahut.
Thank you for pointing out the bug in my code. I have fixed it.

このブログの人気の投稿

Eclipseでコードカバレッジのハイライトを削除する方法

Eclipseには便利なコードカバレッジ表示機能が搭載されていますが、コード内に緑、赤、黄の色付けがされて煩く感じるときもあると思います。 1度カバレッジの色付けが出てしまった後に消す方法の紹介です(方法は簡単)。 下記のキャプチャの青いマーカーで示した「Remove All Sessions」のボタンを押せばすべて消えます。

「特定の文字から始まらない文字列」にマッチする正規表現

「特定の文字から始まらない文字列」 にマッチする正規表現の例です。  以下の例では、Aから始まらない文字列にマッチする正規表現を示しています。 ^(?!A).*$ 私も正規表現の組み方で四苦八苦することがあります。以下の書籍は実践的に様々な正規表現のパターンを例示してくれているので、重宝しています。

ダイソーで買った200円のドライバーセットでHDDを分解

HDDの処分 最近は個人情報の問題もあって、HDDを処分する前にちゃんとデータの消去を気にすることも多くなってきました。消去方法としては大きく分けて下記の3つがあります。 データ消去ソフトでフォーマット HDD内部のプラッタを物理破壊 データ消去を行ってくれる専門の業者や家電量販店(Sofmapやビックカメラで実施していると思います。費用発生。)に持ち込み。 データ消去ソフトでのフォーマットは簡単ですが、欠点として「フォーマットに時間がかかる」「セクタ破損などで中途半端に壊れたディスクのフォーマットができない」などがあります。 またHDD内部のプラッタの物理破壊については、HDDを分解するために、通常のプラスやマイナスドライバーではなく、星形ネジに対応したトルクスドライバーが必要とのこともあって、少し面倒です。 筆者は今回、今後もHDDの廃棄をするだろうなあと思い、思い切って自分で分解して廃棄することにチャレンジしてみました。(家電量販店に持って行くよりも安くできないかというどケチ丸出しですw) HDDの星形ネジ こんなやつです。ちなみに写真はSeagateのST2000DL003というHDDで撮影しました。 トルクスドライバー というわけで、分解のために Amazonでトルクスドライバー を探しました。 調べると T8のもだと使えそう とのことで、いろいろと物色。 セットのものとか T8一本で立派なやつとか 色々あったのですが、HDD壊すだけで800円かぁ(←どケチ)、と思って購入を躊躇。 ネット上で調べると100円ショップのダイソーでも、トルクスドライバーを販売しているとの情報をキャッチ!近所のダイソーに行って、探したところ星形のヘッド交換に対応した精密ドライバーセットがありました。 プラスが10種類、マイナスが8種類、六角が6種類、星形が6種類(今回ほしかったもの)のセットで、何とお値段税抜き200円!、税抜き200円!と安かったので、ダメもとで購入しました。 結論から言うと 買って大正解 でした。 ダイソーの精密ドライバーセット こんな商品です! 星形対応のヘッドを装着するとこんな感じ。ドライバーのグリップもゴムで滑らない様になっていて使いやす

SQLで特定の文字を組み合わせたランダムな文字列を生成

簡易的な方法として「指定した文字列からランダムに1文字選ぶ」を必要な文字の長さ分concat関数でつなげれば実現できます。 1文字ずつ文字を選ぶので、あまり性能もよくない上、セキュリティ的な観点からのランダム性も担保されていないので、あくまで開発中に必要になった時に使う程度が無難だと思います。 下記に英数字大文字小文字を含んだランダムな3文字の文字列を生成するクエリを示します。 # RAND関数で指定した文字列からランダムに1文字選択。 # 下記の例の62の部分はa~z、A~Z、1~9の文字数の合計値を入れた結果 SELECT CONCAT( SUBSTRING('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', FLOOR(RAND() * 62 + 1), 1), SUBSTRING('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', FLOOR(RAND() * 62 + 1), 1), SUBSTRING('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', FLOOR(RAND() * 62 + 1), 1) ) AS random_string;

PHPの配列(array)のメモリ使用量の考察

はじめに 最近PHP上に大量のデータをメモリ上に展開していたのですが、配列(array)の形式(連想配列 or 単純な配列)や配列の要素のデータ構造(数字、配列、文字列など)で大きくメモリ使用量に差が出てくることに気づき、簡単なプログラムを組んで調べてみました。 あくまで筆者の環境での結果なので、細かい数値は参考程度に見てください。 測定環境と方法 OS: Windows 10 PHP 7.4.5 (php-7.4.5-nts-Win32-vc15-x64) 配列に要素を追加するプログラムを書いて、PHPのmemory_get_usage(true)関数を使って実メモリ使用量を計測しました。 計測結果 No. 方式 1MB当たり作成できる 要素数 プログラム 補足 1 キーも値も整数の配列 (整数IDを想定) 28571 // 2,000,000 / 70MB $row = []; for($i = 0; $i < 2000000; $i++) { $row[] = $i; } No.2~6でテストしたプログラム中の要素数は200,000。これだけ一桁多い! 2 キーが文字列、値が整数の連想配列 8333 // 200,000 / 24MB $row = []; for($i = 0; $i < 200000; $i++) { $row[$i.'_key_string'] = $i; } キーの文字列が長い方がメモリ使用量多くなる。 3 キーが整数、値が連想配列の配列 DBから取得してきたデータを想定 2325 // 200,000 / 86MB $row = []; for($i = 0; $i < 200000; $i++) { row[] = ['id' => $i]; } 4 キーが整数、値が連想配列の配列(配列に複数の値を保持) DBから取得してきたデータを想定 2127 // 200,000 /

ADODB.streamオブジェクトを使って文字列とByte配列を相互変換(Excel VBA)

ADODB.streamオブジェクトを使って文字列をByte配列に変換するコードのサンプルです。 ExcelVBAでADODB.streamを使う際には、 1. ExcelのMicrosoft Visual Basic エディタのメニューバーから「ツール->参照設定」とたどる。 2. 表示されたダイアログからMicrosoft ActiveX Data Objectsにチェックを入れる。 という手順が必要です。 文字列からByte配列へ Private Function ADOS_EncodeStringToByte(ByVal cset As String, ByRef strUni As String) As Byte() On Error GoTo e Dim objStm As ADODB.stream: Set objStm = New ADODB.stream objStm.Mode = adModeReadWrite objStm.Open objStm.Type = adTypeText objStm.Charset = cset objStm.WriteText strUni objStm.Position = 0 objStm.Type = adTypeBinary Select Case UCase(cset) Case "UNICODE", "UTF-16" objStm.Position = 2 Case "UTF-8" objStm.Position = 3 End Select ADOS_EncodeStringToByte = objStm.Read() objStm.Close Set objStm = Nothing Exit Function e: Debug.Print "Error occurred while encoding characters" & Err.Description If objStm Is No

MySQL: SELECTの結果をUNIONして ORDER BYする際の最適化方法

SELECTの結果をUNIONして ORDER BY する際には下記の点に注意する必要があります。 無駄なメモリ消費 ソートにINDEXが利かない (≒CPU負荷増大) 対応策 可能であればPush-down Condition (各サブクエリ内でORDER BY, LIMIT, OFFSETを適用してからUNION, ORDER BYを実行する)を利用することで、 パフォーマンスを改善できる場合があります。 下記に例を示します。 もともとのクエリ SELECT tmp.* FROM ( SELECT tableA.column1, tableA.column2 FROM tableA WHERE (条件) UNION ALL SELECT tableB.column1, tableB.column2 FROM tableB WHERE (条件) ) AS tmp ORDER BY tmp.column1, tmp.column2 LIMIT 100, 20 Push-down Conditionを用いて書き直したクエリ SELECT tmp.* FROM ( SELECT tableA.column1, tableA.column2 FROM tableA WHERE (条件) ORDER BY tableA.column1, tableA.column2 LIMIT 30 # <- 10 (offset) + 20 (limit) UNION ALL SELECT tableB.column1, tableB.column2 FROM tableB WHERE (条件) ORDER BY tableB.column1, tableB.column2 LIMIT 30 # <- 10 (offset) + 20 (limit) ) AS tmp ORDER BY tmp.column1, tmp.column2 LIMIT 10, 20 ただしこのPush-down Conditionの手法も下記の場合は、効果が半減しますので注意が必要です。 OFFSETの値が大きい場合は、結局全結果セットUNIONと変わらない サブクエリ内のソートで、INDEXが効かない場合

Visual Studio 2010 SP1のアンインストール

Visual Studio 2013に乗り換えるためにVisual Studio 2010をアンインストールしようとしたところで問題発生。。。 先にVisual Studio 2010本体をアンインストールした後、Visual Studio 2010 SP1をアンインストールできなくて困っていました。 Google先生で調べたところ、以下の情報が見つかり、書かれていた通り実施したところ無事Visual Studio 2010 SP1のアンインストールに成功しました。 How to uninstall/remove Visual Studio SP1 アンインストール手順は以下の通りです。 http://www.microsoft.com/en-gb/download/details.aspx?id=23691 からMicrosoft Visual Studio 2010 Service Pack 1 (Installer)をダウンロード VS10sp1-KB983509.exeというファイル名でダウンロードされる(はず)。 コマンドプロンプトから以下のコマンドを実行 (以下の例は、c:\tempにVS10sp1-KB983509.exeがある場合) c:\temp\VS10sp1-KB983509.exe /uninstall /force ダイアログが立ち上がるので、アンインストールを選択して次へ進めばOK!

MySQLのSQL_CALC_FOUND_ROWS使用上の注意

MySQLのSQL_CALC_FOUND_ROWSを使用する際の無駄なメモリ消費に注意 ページング機能の実装などのために、ヒットした全件数を取得する必要のある場合があるかと思います。 その場合、下記のようにSQL_CALC_FOUND_ROWSを使うと、検索結果に加えて、そのクエリの全ヒット件数が取得できます。 SELECT SQL_CALC_FOUND_ROWS table.column1, table.column2 ...(途中省略)... FROM table WHERE (条件) しかし、SQL_CALC_FOUND_ROWSを使うと、「絞り込んだ結果にヒットする全べての行の結果のセットを作成する」という大きな欠点があります。 これは、LIMIT, OFFSETを指定していても実行されてしまうので、 SELECTで指定するカラム数やデータ量が多い SELECTの結果返ってくる行数が多い 場合、無駄に領域(≒メモリ)を消費してしまうので注意が必要です。 例えば、100万行検索対象としてヒットするクエリの場合、仮にLIMIT 20と指定して最初の20行を取得するようにクエリを書いても、その100万行分の結果セットが作成されてしまうということになります。 対応策 根本的な対応策としては、SQL_CALC_FOUND_ROWSを使わない(結果行数の取得はCOUNTを用いた別クエリで取得する、結果行数をあらかじめサマリーテーブルに保持しておく)ことですが、 SQL_CALC_FOUND_ROWSをどうしても使う必要がある場合は、 SELECT SQL_CALC_FOUND_ROWS primary_id のように最低限のカラムを指定して結果行セットを取得 (LIMIT OFFSET指定前提) 取得したprimary_idを使って必要なデータを取得 して、SQL_CALC_FOUND_ROWSで使用する領域をできるだけ減らすことで、対応するしかないと思います。 世の中ではLIMIT, OFFSETは使わない方がよいとよく書かれていますが、 SQL_CALC_FOUND_ROWSは、書いてしまえばどんなときも検索にヒットする全結果行セットを作成するので、同じくらい使用する際には注意が必要です。

PHPでファイルを指定した行数ごとに分割

ファイルを指定した行数ごとに分割するためには、Linuxのsplitコマンドを使えば簡単に実現できます。 PHPではexec関数にsplitコマンドを渡して実行すればよいですが、下記の弱点があります。 Linuxのコマンドに依存 (PHPの場合はほとんどLinux環境で動作させることが普通なのでそこまで問題にならないかも知れません)。 exec関数は慎重に引数を渡さないと、OSコマンドインジェクション脆弱性を引き起こす可能性がある。 そこで、今回はPHPでファイルを指定した行数ごとに分割するプログラムを書いてみました。 <?php class FileSplitter { private $lines; private $fileCount; public function split($filePath, $linesPerFile, $outputDir) { $this->fileCount = 0; $this->lines = null; $file = new \SplFileObject($filePath); $lineCount = 0; try{ while (!$file->eof()) { if($lineCount % $linesPerFile === 0) { $this->writeToFile($this->generateOutputFilePath($outputDir, $file)); } $this->lines[] = $file->fgets(); $lineCount++; } $this->writeToFile($this->generateOutpu