JPG画像の解像度(DPI)変更アプリの開発(4)ー製造(Ⅱ)
前回の記事「JPG画像の解像度(DPI)変更アプリの開発(3)ー製造(Ⅰ)」でJPG画像ファイルフォーマット関連の技術調査が終わったので、これからは製造段階について解説していきます。
製造とは、選ばれたプログラミング言語で実際にプログラミング(ソースコードを書く)することです。前回の技術調査を通しやり方が分かったので、これからは調査したやり方通りにソースコードを書くだけです。
製造段階では、設計段階で既に決まったプログラミング言語ーC#で実際にプログラミングしていきます。
なお、統合開発環境(IDE)はマイクロソフト社製の
Microsoft Visual Studio Community 2019(個人開発者無料)を使います。
プログラムは「JPG画像の解像度(DPI)変更アプリの開発(2)ー設計」を基に画面とクラスを作成していきます。
なお、本記事はあくまでもC#言語で実際にアプリを開発する方法について解説していきますので、C#言語自体の解説は行いません。
本記事はC#開発の初心者向けではないので、基礎知識(winform方式開発方法、コントロールの使用方法、プロパーティ設定、イベント指定方法等)を身に着けてから読むことをお勧めします。
プロジェクトの作成
Visual Studioを開き、下記のように新しいプロジェクトを作成します。
今回は「.Net Framework」を使ったWinForms形式のアプリを作成するので、上図の②の形式を選択します。
上図からも分かるように、Visual StudioというIDE(統合開発環境)では様々な種類のアプリケーションを作成することができます。
基本的に、Visual Studioではwindows上で動くすべての形式のアプリケーションが作成できます。
「次へ」を押すと下記の画面に遷移します。
プロジェクト名 : DPIChanger
場所 : 好きな場所を指定してください。
フレームワーク :.NET Framework4.5
を指定し、「作成」ボタンを押します。
フレームワークバージョンは現在最新が 4.8ですね(これも最後のバージョンになります)。基本的に最新版を使って問題ありませんが、古いOS上では実行されない可能性があります。
.NET Framework4.5だと、2012年にリリースされたもので、今現在(2021年)より9年も前になっているので、大体のwindowsマシンを網羅したかなあと思います。
このように、バージョン選びは慎重に考える必要があります。余りにも古すぎるとその後に追加された新機能が使えなくなるし、最新しすぎると今回は又実行できるwindowsのバージョンの数が限定されることになります。
下記のようなアプリ開発環境が起動されます。
プロジェクト名、フレームワークのバージョンを指定しただけで、Visual Studioがすでにアプリ開発の基本となる実装作業を行ってくれています。
「開始」を押して実行してみます。
まだC#のコードは何も書いてないのに、すでに一番基礎的なアプリはできていることが分かります(下図)。これからは設計通り画面と機能をどんどん実装していきます。
windowsアプリ画面(GUI)の基本要素:タイトル、コントロールボタン(最大化・最小化・クローズ)と画面のリサイズなど。
上記の基本要素はVisual Studioが自動的に作成してくれます。「プログラマーはアプリの実現したい機能の実現に集中できる」、という開発理念こそVisual Studioのすごいところです。
windows上で動作するアプリケーション(いわゆるwindowsアプリ)の開発にはVisual Studioを強くお勧めします。
画面作成
画面は下記のように詳細設計されているので、その通りに作成することになります(「JPG画像の解像度(DPI)変更アプリの開発(2)ー設計」参照)。
詳細設計書(画面)
下図のように、画面の詳細設計書に既に各コントロールの種類などを明記しているので、設計通り画面を作成します。
上記②はC#のSystem.Windows.Forms.NumericUpDownコントロールを使います。他のラベルやテキスト、ボタン等はよく見るものなので、C#でGUIアプリ開発の基礎知識があればすぐ分かると思います。
作成した画面は下記のようになります。
画面上のコントロールのIDなどは自分で適当に指定してか構いません。
例えば「実行」ボタンのIDは「executeBtn」とするとか。「ディレクトリ参照」ボタンのIDは「selectDirectoryBtn」とか。
コントロールのIDは自分好みで指定してください。指定したIDで以降のソースコードを読み替えてください。
画面ができたら次からはプログラミングしていきます。
プログラミング(C#言語)
winFormベースのアプリ開発はご覧の通り、まずは画面を作成し、画面上の各コントロールのイベントにC#コードを書く形になります。
まずはプログラム設計書を見ながら必要なクラスをすべて作成しておきます。
プログラム設計段階で設計したクラス図
①「FrmMain」クラス
これはメイン画面のクラスになります。C#のGUIプログラミングを勉強すれば分かりますが、画面付きのクラス(画面を操るクラス)はVisual Studioが自動的に作成してくれます。
メイン画面はプロジェクト作成時に自動的に作成された「Form1」クラスになります。このクラスの名前を「FrmMain」に変更すれば出来上がりです。
VS(Visual Studioのこと)が自動作成してくれたクラス「Form1」を「FrmMain」に改名
②「JpegFormat」クラス
JPEG画像の2つのフォーマットを識別し、保存するクラスです。
(※前回の記事「JPG画像の解像度(DPI)変更アプリの開発(3)ー製造(Ⅰ)」の②の部分で説明)
「FrmMain」クラス内に下記のようにJpegFormatクラスを作成します(独自のJpegFormatクラスファイルを作成しても構いません)。
private class JpegFormat{
public FileStream fs;
public int fmt; // 1:JFIF ; 2:EXIF
public long blockLen; //APP0 or APP1の長さ(識別子以降からの長さ)
public long identifierOffset; //識別子の位置(識別子含む) JFIF.ここ / EXIF..ここ
public string errMsg;
}//end class
・public FileStream fs:画像ファイルを読み込むためのFileStream型のインスタンスを保持
・public string errMsg:JPEGファイルを解析する場合、何等かのエラーが発生した場合、呼び出し元にエラーメッセージを渡すために必要
③「ResolutionInfo」クラス
DPI値の保存位置の情報を管理するクラスです。
/// <summary>
/// X, Y方向の解像度値の保存位置情報
/// </summary>
private class ResolutionInfo
{
public short type;
public int cnt;
public int pos;
}//end class
このクラスはEXIF形式の「フィールド」の情報を保存するものです。
※「JPG画像の解像度(DPI)変更アプリの開発(3)ー製造(Ⅰ)」の「⑤ IFDの構造、フィールドの構造」
上記2つのクラスを作成したプロジェクトのソースコード(FrmMain.cs)は下記のようになります。
これでクラスの新規作成は完了しました。
これからは設計書通りに各機能を実装していきます。
④「ドラッグ&ドロップ」機能
詳細設計書にドラッグ&ドロップ機能をサポートする記述になってるため、「画像フォルダ」に対するドラック&ドロップ機能を実装します。
実際のIT会社で働くプログラマー達は、基本詳細設計書を参照しながら設計通りに実装していきます。設計書に書いてる機能をすべて実装しないともちろん納品できないものです。
※「画像フォルダ」のコントロールIDを「directoryBox」と指定した場合
ステップ1:FrmMainクラス内に下記のソースを書きます
#region Drag & Drop機能関連
/// <summary>
/// ドラッグ処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void directroyBox_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
e.Effect = DragDropEffects.Copy;
else
e.Effect = DragDropEffects.None;
}
/// <summary>
/// ドロップ処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void directroyBox_DragDrop(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop))
return;
string[] strs = (string[])e.Data.GetData(DataFormats.FileDrop);
//マルチ指定に対応無し。常に最初1個目を取得する。
string file = strs[0];
if (Directory.Exists(file))
directroyBox.Text = file;
else
{
directroyBox.Text = Path.GetDirectoryName(file);
}//end if
}
//画面上のどこでドラッグ&ドロップしても、画像ディレクトリが指定されるように
//リストとメイン画面にもドラッグ&ドロップ機能実装
private void listBox_DragEnter(object sender, DragEventArgs e)
{
directroyBox_DragEnter(sender, e);
}
private void listBox_DragDrop(object sender, DragEventArgs e)
{
directroyBox_DragDrop(sender, e);
}
private void FrmMain_DragEnter(object sender, DragEventArgs e)
{
directroyBox_DragEnter(sender, e);
}
private void FrmMain_DragDrop(object sender, DragEventArgs e)
{
directroyBox_DragDrop(sender, e);
}
#endregion
ドラッグ&ドロップ機能
ステップ2:FrmMain画面のドラッグ&ドロップイベントを設定します
デザイナーで画面をクリック > プロパーティ > イベント > 「ドラック アンド ドロップ」カテゴリの 「DragDrop」と「DragEntet」イベントに図のようにメソッドを指定します。
上記は画面上でのドラッグ&ドロップ機能で、又「画像フォルダ」にもドラッグ&ドロップ機能を指定します(下図参照)。
上記のように実装すると、画面は下記のようにドラッグ&ドロップ機能が効くようになります。
⑤初期処理実装
下記のように画面の初期化処理を実装します。
private void Form1_Load(object sender, EventArgs e)
{
dpiValue.Value = 96;
directroyBox.Text = Environment.CurrentDirectory;
clearMsg();
}
画面が起動すると、DPI値の初期値を96に、画像ディレクトリをアプリ実行したディレクトリに設定しています。
clearMsg()はメッセージ表示欄の初期化処理になります。
private void clearMsg()
{
listBox.Items.Clear();
listBox.Items.Add("DPI値と画像フォルダを指定し(ドラッグ&ドロップ可能)、「実行」ボタンを押してください。");
listBox.Items.Add("");
}
⑥「参照」ボタン処理実装
ローカルPCのディレクトリへのアクセスは.Net Frameworkが提供してくれた「System.Windows.Forms.FolderBrowserDialog」クラスを使います。
先ずは、下図のように該当クラスをプロジェクトにインポートします。
次は、下記のように「参照」ボタンのクリック処理を実装します。
private void directroyBtn_Click(object sender, EventArgs e)
{
folderBrowserDialog1.SelectedPath = directroyBox.Text;
if (folderBrowserDialog1.ShowDialog() == DialogResult.OK)
{
directroyBox.Text = folderBrowserDialog1.SelectedPath;
}
}
「参照」ボタンでディレクトリが選択できれば完成です。
⑦「実行」ボタン処理実装
実行ボタンの仕様書は「JPG画像の解像度(DPI)変更アプリの開発(2)ー設計」を参照してください。
実行を押すと、指定したディレクトリ配下のすべてのJPG画像ファイルのDPI値を修正するので、画像ファイル数によって実行時間が長くなったりします。
実行途中に又実行ボタンが押されることを避けるために、実行ボタンが押されるとすぐに該当ボタンを非活性に変更しています。
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false; //非活性化
clearMsg();
メイン処理
button1.Enabled = true; //活性化
}
実行処理の枠ができたら、これからはメイン処理の部分を実装していきます。
先ずは指定したディレクトリ配下のすべての拡張子が「*.jpg」のファイルを取得します。
//指定ディレクトリ内のファイルを取得
string[] files = Directory.GetFiles(directroyBox.Text, "*.jpg", SearchOption.AllDirectories);
取得したすべてのjpgファイルを1個ずつ繰り返しながらDPI値を変更していきます。
foreach (string file in files){
//該当画像のDPI値を変更
string msg = changeDpi(file, (Int16)dpiValue.Value, out isOkFlg);
fileCnt++;
if (isOkFlg)
okFileCnt++;
showMsg(0, fileCnt + " " + msg);
Application.DoEvents();
}//end foreach
上記のメソッド「changeDpi()」が実際にDPI値を変更する処理になります。
これで、「実行」ボタンのコードが完成です(呼び出すchangeDpi()の実装は後で行う)。「実行」ボタンのソースは下記のようになります。
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
clearMsg();
try
{
//指定ディレクトリ内のファイルを取得
string[] files = Directory.GetFiles(directroyBox.Text, "*.jpg", SearchOption.AllDirectories);
int fileCnt = 0;
int okFileCnt = 0;
bool isOkFlg;
foreach (string file in files){
string msg = changeDpi(file, (Int16)dpiValue.Value, out isOkFlg);
fileCnt++;
if (isOkFlg)
okFileCnt++;
showMsg(0, fileCnt + " " + msg);
Application.DoEvents();
}//end foreach
int ngFileCnt = fileCnt - okFileCnt;
string fileCntMsg;
fileCntMsg = string.Format("画像数:[{0,7}] DPI変更数:[{1,7}] フォーマット異常数:[{2,7}]", fileCnt, okFileCnt, ngFileCnt);
showMsg(0, "");
showMsg(0, "Processing have done! " + fileCntMsg);
} catch (Exception ex)
{
showMsg(0, ex.ToString());
}//end try
button1.Enabled = true;
}
⑦メソッド「changeDpi()」の実装
JPG画像のDPI値を変更するメソッドです。
本メソッドのソースコードは下記の通りになります。
/// <summary>
/// DPI値変更
/// </summary>
/// <param name="jpgfile">変更する画像ファイル名</param>
/// <param name="dpi">変更するDPI値</param>
/// <param name="isOkFlg">true:正常; false:異常発生</param>
/// <returns></returns>
private string changeDpi(string jpgfile, Int16 dpi, out bool isOkFlg)
{
string retMsg = "";
string procSta = "";
isOkFlg = false;
try
{
//ファイルストリームを開いて、一連の操作を行う
using (FileStream fs = new FileStream(jpgfile, FileMode.Open, FileAccess.ReadWrite))
{
byte[] tmp = new byte[4];
fs.Read(tmp, 0, 2);
fs.Seek(-2, SeekOrigin.End);
fs.Read(tmp, 2, 2);
if (!(tmp[0] == 0xFF && tmp[1] == 0xD8 &&
tmp[2] == 0xFF && tmp[3] == 0xD9)) //JPGファイルではない
{
retMsg = jpgfile + " [Not a JPG file]";
return retMsg;
}//end if
//フォーマット特定
JpegFormat jpegFmt = new JpegFormat();
jpegFmt.fs = fs;
if (getFormat(ref jpegFmt) != 0)
throw new Exception(jpegFmt.errMsg);
if (jpegFmt.fmt == 1) // JPEG/JFIF
{
procSta = changeJFIFDpi(jpegFmt, dpi);
}
else if (jpegFmt.fmt == 2) // JPEG/EXIF
{
procSta = changeEXIFDpi(jpegFmt, dpi);
}
else
{
throw new Exception();
}
}//end using
}
catch(FileNotFoundException ex)
{
retMsg = jpgfile + " [ファイル読み込み失敗。]";
return retMsg;
}
catch(Exception ex)
{
string errMsg = ex.Message;
if (String.IsNullOrEmpty(errMsg))
retMsg = jpgfile + " [Not a JPG Format]";
else
retMsg = jpgfile + " " + errMsg;
return retMsg;
}//end try
//create message
retMsg = jpgfile + " " + procSta;
if (procSta.Equals("OK"))
isOkFlg = true;
return retMsg;
}
・FileStream fs = new FileStream(jpgfile, FileMode.Open, FileAccess.ReadWrite)
画像ファイルをバイナリモードで開く処理です。
・ fs.Read(tmp, 0, 2);
先頭2バイトを読み込む処理です。
・fs.Seek(-2, SeekOrigin.End);
・fs.Read(tmp, 2, 2);
ファイル末尾の2バイトを読み込む処理です。
・if (!(tmp[0] == 0xFF && tmp[1] == 0xD8 &&
tmp[2] == 0xFF && tmp[3] == 0xD9)) //JPGファイルではない
{
retMsg = jpgfile + " [Not a JPG file]";
return retMsg;
}//end if
先頭2バイトが「FF,D8」で末尾2バイトが「FF, D9」かどうかでJPG画像ファイルなのかどうかを判定しまいます。
(※「JPG画像の解像度(DPI)変更アプリの開発(3)ー製造(Ⅰ)」の①の部分で説明)
・if (getFormat(ref jpegFmt) != 0)
throw new Exception(jpegFmt.errMsg);
if (jpegFmt.fmt == 1) // JPEG/JFIF
{
procSta = changeJFIFDpi(jpegFmt, dpi);
}
else if (jpegFmt.fmt == 2) // JPEG/EXIF
{
procSta = changeEXIFDpi(jpegFmt, dpi);
}
else
{
throw new Exception();
}
JPG画像がJFIF形式かEXIF形式かを判定し、それぞれの形式にあったDPI値変更メソッド(changeJFIFDpi() / changeEXIFDpi())を呼び出しています。
⑧メソッド「getFormat()」の実装
JPG画像がJFIF形式か、それともEXIF形式かを判定するメソッドになります。
※判定方法は「JPG画像の解像度(DPI)変更アプリの開発(3)ー製造(Ⅰ)」の②、③の部分で説明
ソースコードは下記のようになります。
/// <summary>
/// JPEGファイルのフォーマットを取得する。
///
/// </summary>
/// <param name="jpegFmt">フォーマット</param>
/// <returns>0:正常; 0以外:異常</returns>
private int getFormat(ref JpegFormat jpegFmt)
{
byte[] buff2 = new byte[2];
byte[] buff5 = new byte[5];
byte[] buff10 = new byte[10];
jpegFmt.fs.Seek(2, SeekOrigin.Begin);
jpegFmt.fs.Read(buff2, 0, 2);
if(buff2[0] == 0xFF && buff2[1] == 0xE0) //APP0
{
jpegFmt.fmt = 1; //JFIF
//APP0長さ取得
jpegFmt.fs.Read(buff2, 0, 2);
Array.Reverse(buff2);
jpegFmt.blockLen = BitConverter.ToInt16(buff2, 0);
//次のマーク位置
long nextMarkerPos = jpegFmt.fs.Position + jpegFmt.blockLen - 2;
//JFIFフォーマットチェック
jpegFmt.fs.Read(buff5, 0, 5);
if (!(buff5[0] == 'J' && buff5[1] == 'F' && buff5[2] == 'I' && buff5[3] == 'F' && buff5[4] == 0x00))
{
jpegFmt.errMsg = "JPEG/JFIFフォーマット異常";
return 1;
}//end if
jpegFmt.identifierOffset = jpegFmt.fs.Position;
jpegFmt.blockLen -= 2 + 5; //長さ自身の2バイトと識別子5バイトを引く
//次にEXIFマーカがあるかどうか探してみる。
//APP0マークの次の最初1個目のマークまで確認する。
//次のマークがAPP1の場合、EXIFフォーマットとみなす。
//次のマークがAPP1でない場合、もう探さない(本画像はJFIFとみなす)
jpegFmt.fs.Seek(nextMarkerPos, SeekOrigin.Begin);
jpegFmt.fs.Read(buff2, 0, 2);
if(buff2[0] == 0xFF && buff2[1] == 0xE1)
{
jpegFmt.fmt = 2; //EXIFに変更
//APP1の長さ取得
jpegFmt.fs.Read(buff2, 0, 2);
Array.Reverse(buff2);
jpegFmt.blockLen = BitConverter.ToInt16(buff2, 0);
//EXIFフォーマットチェック
jpegFmt.fs.Read(buff10, 0, 6);
if (!(buff10[0] == 'E' && buff10[1] == 'x' && buff10[2] == 'i' && buff10[3] == 'f' && buff10[4] == 0x00 && buff10[5] == 0x00))
{
jpegFmt.errMsg = "JPEG/EXIFフォーマット異常(JFIF->EXIF)";
return 1;
}//end if
jpegFmt.identifierOffset = jpegFmt.fs.Position;
jpegFmt.blockLen -= 2 + 6; //長さ自身の2バイトと識別子6バイトを引く
}//end if
}
else if(buff2[0] == 0xFF && buff2[1] == 0xE1) //APP1
{
jpegFmt.fmt = 2; //EXIF
//APP1長さ取得
jpegFmt.fs.Read(buff2, 0, 2);
Array.Reverse(buff2);
jpegFmt.blockLen = BitConverter.ToInt16(buff2, 0);
//EXIFフォーマットチェック
jpegFmt.fs.Read(buff10, 0, 6);
if(!(buff10[0] == 'E' && buff10[1] == 'x' && buff10[2] == 'i' && buff10[3] == 'f' && buff10[4] == 0x00 && buff10[5] == 0x00))
{
jpegFmt.errMsg = "JPEG/EXIFフォーマット異常";
return 1;
}//end if
jpegFmt.identifierOffset = jpegFmt.fs.Position;
jpegFmt.blockLen -= 2 + 6; //長さ自身の2バイトと識別子6バイトを引く
}
else
{
jpegFmt.errMsg = "内部のフォーマット異常";
return 1;
}//end if
return 0;
}
・if(buff2[0] == 0xFF && buff2[1] == 0xE0) //APP0
・else if(buff2[0] == 0xFF && buff2[1] == 0xE1) //APP1
SOIの後ろのバイトが「FF, E0」はAPP0 (JFIF)で、「FF, E1」はAPP1(EXIF)形式になります。
JFIF形式とEXIF形式の判定ができたら、それぞれの形式定義書にあった解析方法でDPI値の位置を探していきます。
特別に注意したいのは、APP0構造があるからといって必ずJFIF形式になるとは限らない点です。たまにAPP0(JFIFマーカー)とAPP1(EXIFマーカー)両方存在する画像ファイルもあります。この場合は、上記ソースのように、APP1を優先にし、EXIF形式と判定しています。
⑨メソッド「changeJFIFDpi()」の実装
JFIF形式画像のDPI値を変更するメソッドになります。
ソースコードは下記のようになります。
/// <summary>
/// JPEG/JFIFフォーマット画像のdpi値を変更する。
///
/// </summary>
/// <param name="jpegFmt">フォーマット管理</param>
/// <param name="dpiValue">dpi値</param>
/// <returns>実行状態</returns>
private string changeJFIFDpi(JpegFormat jpegFmt, Int16 dpiValue)
{
string ret = "OK";
byte[] dpiBytes = new byte[2];
dpiBytes = BitConverter.GetBytes(dpiValue);
Array.Reverse(dpiBytes); // JFIFはMotorola固定なので、変換(C#はIntel)
long offset = jpegFmt.identifierOffset + 2; //version 2byte
jpegFmt.fs.Seek(offset, SeekOrigin.Begin);
jpegFmt.fs.WriteByte(0x01); // 1:pixel/inch固定
jpegFmt.fs.Write(dpiBytes, 0, 2);
jpegFmt.fs.Write(dpiBytes, 0, 2);
return ret;
}
ソースからみても分かるように、JFIF形式はかなり簡単にDPI値の変更が可能になります。
※「JPG画像の解像度(DPI)変更アプリの開発(3)ー製造(Ⅰ)」の②「JFIF」フォーマットの判定 にJFIF形式の解析図があります。
⑩メソッド「changeEXIFDpi()」の実装
EXIF形式のDPI値を変更するメソッドになります。
EXIF形式はJFIF形式より複雑なため、ソースコードも長くなります。
※「JPG画像の解像度(DPI)変更アプリの開発(3)ー製造(Ⅰ)」の③、⑤、⑥でEXIF形式を説明しています。
技術調査資料を基に実装したソースコードは下記のようになります。
/// <summary>
/// JPEG/EXIFフォーマット画像のdpi値を変更する。
///
/// </summary>
/// <param name="jpegFmt">ファイルストリーム</param>
/// <param name="dpiValue">dpi値</param>
/// <returns>実行状態</returns>
private string changeEXIFDpi(JpegFormat jpegFmt, Int16 dpiValue)
{
string ret = "OK";
byte[] buff2 = new byte[2];
byte[] buff4 = new byte[4];
int bigEndian = 1; //1:Intel /2:Motorola (e.g. 0x1234 -> I:0x34,0x12 ; M:0x12,0x34)
//TIFF有効性判定
jpegFmt.fs.Seek(jpegFmt.identifierOffset, SeekOrigin.Begin);
jpegFmt.fs.Read(buff4, 0, 4);
if(buff4[0] == 'I' && buff4[1] == 'I' && buff4[2] == 0x2A && buff4[3] == 0x00) //Intel式
{
bigEndian = 1;
}
else if (buff4[0] == 'M' && buff4[1] == 'M' && buff4[2] == 0x00 && buff4[3] == 0x2A) //Motorola式
{
bigEndian = 2;
}
else
{
ret = "EXIF/TIFFフォーマット異常";
return ret;
}//end if
//次のIFDブロック位置取得
jpegFmt.fs.Seek(4, SeekOrigin.Current);
//IFD個数取得
jpegFmt.fs.Read(buff2, 0, 2);
//Motorola式時はビッチ配列を逆転換(C#内部はIntel式)
if (bigEndian == 2)
Array.Reverse(buff2);
int itmCntOfIFD0 = BitConverter.ToInt16(buff2, 0);
//X方向、Y方向の解像度情報を格納する
//ResolutionInfo[] resolutionInfo = new ResolutionInfo[2];
List<ResolutionInfo> resolutionInfoLst = new List<ResolutionInfo>(2);
//X/Y解像度は常にIFD0ブロック内にあるので、IFD0ブロックのみ調べる。
//IFD0ブロック内の各要素は12バイト固定
// マーク 名称 タイプ cnt 備考
// 0x011A XResolution unsigned rational 1 def: 1/72 inch
// 0x011B YResolution unsigned rational 1
// 0x0128 ResolutionUnit unsigned short 1 XResloution/YResloutionの単位. 1:無し;2:inch;3:cm
// DPIの設定なので、単位には常にinchを設定する。
for(int iLoop = 0; iLoop < itmCntOfIFD0; iLoop++)
{
//IFD0内のタグ名取得
jpegFmt.fs.Read(buff2, 0, 2);
if (bigEndian == 1)
Array.Reverse(buff2);
if((buff2[0] == 0x01 && buff2[1] == 0x1A) || //XResolution
(buff2[0] == 0x01 && buff2[1] == 0x1B)) //YResolution
{
ResolutionInfo ri = new ResolutionInfo();
//type取得
jpegFmt.fs.Read(buff2, 0, 2);
if (bigEndian == 2)
Array.Reverse(buff2);
ri.type = BitConverter.ToInt16(buff2, 0);
//コンポーネント数取得
jpegFmt.fs.Read(buff4, 0, 4);
if (bigEndian == 2)
Array.Reverse(buff4);
ri.cnt = BitConverter.ToInt32(buff4, 0);
//解像度フォーマットチェック
if(ri.type != 5 || ri.cnt != 1) //フォーマット異常
{
ret = "JPEG/EXIF DPI値のフォーマット異常[type:" + ri.type + " cnt:" + ri.cnt + "]";
return ret;
}//end if
//解像度記録位置取得
jpegFmt.fs.Read(buff4, 0, 4);
if (bigEndian == 2)
Array.Reverse(buff4);
ri.pos = BitConverter.ToInt32(buff4, 0);
resolutionInfoLst.Add(ri);
}
else if(buff2[0] == 0x01 && buff2[1] == 0x28) //ResolutionUnit
{
//type取得
jpegFmt.fs.Read(buff2, 0, 2);
if (bigEndian == 2)
Array.Reverse(buff2);
short type = BitConverter.ToInt16(buff2, 0);
/*
if(type == 5 || type == 8 || type == 12) //これらは8バイトのタイプ
{
ret = "";
return ret;
}//end if
*/
//コンポーネント数取得
jpegFmt.fs.Read(buff4, 0, 4);
if (bigEndian == 2)
Array.Reverse(buff4);
int cnt = BitConverter.ToInt32(buff4, 0); //必ず1個 チェック要?
//ResolutionUnitの値は常にinch(2)にする
int resolutionUnit = 2;
byte[] resolutionUnitBuff = BitConverter.GetBytes(resolutionUnit);
if (bigEndian == 2)
{
//Array.Reverse(resolutionUnitBuff);
//type=3は2バイトなので、
resolutionUnitBuff = new byte[4]{ 0, 2, 0, 0 };
}//end if
//書き込む
jpegFmt.fs.Write(resolutionUnitBuff, 0, 4);
}
else
{
//次のマークの先頭位置へ
jpegFmt.fs.Seek(10, SeekOrigin.Current);
}//end if
}//end for
//解像度の情報がない場合
if(resolutionInfoLst.Count == 0)
{
ret = "JPEG/EXIF 該当画像にDPI情報がありません。";
return ret;
}//end if
//解像度変更処理
//ForTest 解像度を取得してみる
foreach(ResolutionInfo ri in resolutionInfoLst)
{
//解像度データの位置へ
jpegFmt.fs.Seek(jpegFmt.identifierOffset + ri.pos, SeekOrigin.Begin);
//DPIの小数点対応
byte[] dpiBunshiBytes = BitConverter.GetBytes(dpiBunshi);
if (bigEndian == 2) //Motorola式
Array.Reverse(dpiBunshiBytes);
jpegFmt.fs.Write(dpiBunshiBytes, 0, 4);
//dpi分母書き込み
byte[] dpiBunboBytes = BitConverter.GetBytes(dpiBunbo);
if (bigEndian == 2)
Array.Reverse(dpiBunboBytes);
jpegFmt.fs.Write(dpiBunboBytes, 0, 4);
}//end foreach
return ret;
}
・if(buff4[0] == 'I' && buff4[1] == 'I' && buff4[2] == 0x2A && buff4[3] == 0x00) //Intel式
・else if (buff4[0] == 'M' && buff4[1] == 'M' && buff4[2] == 0x00 && buff4[3] == 0x2A) //Motorola式
「II」か「MM」かを判定し、バイトオーダーを決めています。これは一番重要な処理になります。
※「JPG画像の解像度(DPI)変更アプリの開発(3)ー製造(Ⅰ)」の④ バイトオーダー で説明しています。
EXIF形式の場合は、必ず 「II」か「MM」の1つです。両方でもない場合はフォーマット異常という判定をしています。
・for(int iLoop = 0; iLoop < itmCntOfIFD0; iLoop++)
IFD0内のすべてのフィールドを繰り返しながら、タグが「01,1A」、「01, 1B」であるフィールドを探しています。
タグ「01, 1A」:横方向の解像度(DPI)
タグ「01, 1B」:縦方向の解像度(DPI)
※「JPG画像の解像度(DPI)変更アプリの開発(3)ー製造(Ⅰ)」の⑥ EXIF形式のDPI(解像度)値 で説明しています。
最後は、EXIF形式のDPI値は分数構造になっているので、分母、分子の値をそれぞれ設定し、画像ファイルに書き込んでいます。
まとめ
この記事ではDPI Changerアプリの実装方法について解説しました。
実装はC#言語の知識があればそんなに難しくありません。
今回のDPI ChangerアプリはJPG画像ファイルの中身を弄るので、「JPG画像ファイルフォーマット」について事前に理解・把握する必要があります。これを技術調査といいますね。
技術問題(JPG画像内のDPI値をどうやって変更するのか)さえ解決すれば、あとは設計書通りに実装していくだけです。
次回はDPI Changerアプリの実行画面と単体テスト方法について解説していきます。(本記事が長くなってしまって、実行画面は次回の記事にします)
おまけ
最後に、DPI Changerアプリの全ソース(FrmMain.cs)を掲載しておきます。
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
namespace dpiChanger
{
public partial class FrmMain : Form
{
private class JpegFormat{
public FileStream fs;
public int fmt; // 1:JFIF ; 2:EXIF
public long blockLen; //APP0 or APP1の長さ(識別子以降からの長さ)
public long identifierOffset; //識別子の位置(識別子含む) JFIF.ここ / EXIF..ここ
public string errMsg;
}//end class
/// <summary>
/// X, Y方向の解像度値の保存位置情報
/// </summary>
private class ResolutionInfo
{
public short type;
public int cnt;
public int pos;
}//end class
public FrmMain()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
dpiValue.Value = 96;
directroyBox.Text = Environment.CurrentDirectory;
clearMsg();
}
private void directroyBtn_Click(object sender, EventArgs e)
{
folderBrowserDialog1.SelectedPath = directroyBox.Text;
if (folderBrowserDialog1.ShowDialog() == DialogResult.OK)
{
directroyBox.Text = folderBrowserDialog1.SelectedPath;
}
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
clearMsg();
try
{
//指定ディレクトリ内のファイルを取得
string[] files = Directory.GetFiles(directroyBox.Text, "*.jpg", SearchOption.AllDirectories);
int fileCnt = 0;
int okFileCnt = 0;
bool isOkFlg;
foreach (string file in files){
string msg = changeDpi(file, (Int16)dpiValue.Value, out isOkFlg);
fileCnt++;
if (isOkFlg)
okFileCnt++;
showMsg(0, fileCnt + " " + msg);
Application.DoEvents();
}//end foreach
int ngFileCnt = fileCnt - okFileCnt;
string fileCntMsg;
fileCntMsg = string.Format("画像数:[{0,7}] DPI変更数:[{1,7}] フォーマット異常数:[{2,7}]", fileCnt, okFileCnt, ngFileCnt);
showMsg(0, "");
showMsg(0, "Processing have done! " + fileCntMsg);
} catch (Exception ex)
{
showMsg(0, ex.ToString());
}//end try
button1.Enabled = true;
}
private void showMsg(int level, string msg)
{
listBox.Items.Add(msg);
listBox.TopIndex = listBox.Items.Count - 1;
}
private void clearMsg()
{
listBox.Items.Clear();
listBox.Items.Add("DPI値と画像フォルダを指定し(ドラッグ&ドロップ可能)、「実行」ボタンを押してください。");
listBox.Items.Add("");
}
/// <summary>
/// DPI値変更
/// </summary>
/// <param name="jpgfile">変更する画像ファイル名</param>
/// <param name="dpi">変更するDPI値</param>
/// <param name="isOkFlg">true:正常; false:異常発生</param>
/// <returns></returns>
private string changeDpi(string jpgfile, Int16 dpi, out bool isOkFlg)
{
string retMsg = "";
string procSta = "";
isOkFlg = false;
try
{
//ファイルストリームを開いて、一連の操作を行う
using (FileStream fs = new FileStream(jpgfile, FileMode.Open, FileAccess.ReadWrite))
{
byte[] tmp = new byte[4];
fs.Read(tmp, 0, 2);
fs.Seek(-2, SeekOrigin.End);
fs.Read(tmp, 2, 2);
if (!(tmp[0] == 0xFF && tmp[1] == 0xD8 &&
tmp[2] == 0xFF && tmp[3] == 0xD9)) //JPGファイルではない
{
retMsg = jpgfile + " [Not a JPG file]";
return retMsg;
}//end if
//フォーマット特定
JpegFormat jpegFmt = new JpegFormat();
jpegFmt.fs = fs;
if (getFormat(ref jpegFmt) != 0)
throw new Exception(jpegFmt.errMsg);
if (jpegFmt.fmt == 1) // JPEG/JFIF
{
procSta = changeJFIFDpi(jpegFmt, dpi);
}
else if (jpegFmt.fmt == 2) // JPEG/EXIF
{
procSta = changeEXIFDpi(jpegFmt, dpi);
}
else
{
throw new Exception();
}
}//end using
}
catch(FileNotFoundException ex)
{
retMsg = jpgfile + " [ファイル読み込み失敗。]";
return retMsg;
}
catch(Exception ex)
{
string errMsg = ex.Message;
if (String.IsNullOrEmpty(errMsg))
retMsg = jpgfile + " [Not a JPG Format]";
else
retMsg = jpgfile + " " + errMsg;
return retMsg;
}//end try
//create message
retMsg = jpgfile + " " + procSta;
if (procSta.Equals("OK"))
isOkFlg = true;
return retMsg;
}
/// <summary>
/// JPEGファイルのフォーマットを取得する。
///
/// </summary>
/// <param name="jpegFmt">フォーマット</param>
/// <returns>0:正常; 0以外:異常</returns>
private int getFormat(ref JpegFormat jpegFmt)
{
byte[] buff2 = new byte[2];
byte[] buff5 = new byte[5];
byte[] buff10 = new byte[10];
jpegFmt.fs.Seek(2, SeekOrigin.Begin);
jpegFmt.fs.Read(buff2, 0, 2);
if(buff2[0] == 0xFF && buff2[1] == 0xE0) //APP0
{
jpegFmt.fmt = 1; //JFIF
//APP0長さ取得
jpegFmt.fs.Read(buff2, 0, 2);
Array.Reverse(buff2);
jpegFmt.blockLen = BitConverter.ToInt16(buff2, 0);
//次のマーク位置
long nextMarkerPos = jpegFmt.fs.Position + jpegFmt.blockLen - 2;
//JFIFフォーマットチェック
jpegFmt.fs.Read(buff5, 0, 5);
if (!(buff5[0] == 'J' && buff5[1] == 'F' && buff5[2] == 'I' && buff5[3] == 'F' && buff5[4] == 0x00))
{
jpegFmt.errMsg = "JPEG/JFIFフォーマット異常";
return 1;
}//end if
jpegFmt.identifierOffset = jpegFmt.fs.Position;
jpegFmt.blockLen -= 2 + 5; //長さ自身の2バイトと識別子5バイトを引く
//次にEXIFマーカがあるかどうか探してみる。
//APP0マークの次の最初1個目のマークまで確認する。
//次のマークがAPP1の場合、EXIFフォーマットとみなす。
//次のマークがAPP1でない場合、もう探さない(本画像はJFIFとみなす)
jpegFmt.fs.Seek(nextMarkerPos, SeekOrigin.Begin);
jpegFmt.fs.Read(buff2, 0, 2);
if(buff2[0] == 0xFF && buff2[1] == 0xE1)
{
jpegFmt.fmt = 2; //EXIFに変更
//APP1の長さ取得
jpegFmt.fs.Read(buff2, 0, 2);
Array.Reverse(buff2);
jpegFmt.blockLen = BitConverter.ToInt16(buff2, 0);
//EXIFフォーマットチェック
jpegFmt.fs.Read(buff10, 0, 6);
if (!(buff10[0] == 'E' && buff10[1] == 'x' && buff10[2] == 'i' && buff10[3] == 'f' && buff10[4] == 0x00 && buff10[5] == 0x00))
{
jpegFmt.errMsg = "JPEG/EXIFフォーマット異常(JFIF->EXIF)";
return 1;
}//end if
jpegFmt.identifierOffset = jpegFmt.fs.Position;
jpegFmt.blockLen -= 2 + 6; //長さ自身の2バイトと識別子6バイトを引く
}//end if
}
else if(buff2[0] == 0xFF && buff2[1] == 0xE1) //APP1
{
jpegFmt.fmt = 2; //EXIF
//APP1長さ取得
jpegFmt.fs.Read(buff2, 0, 2);
Array.Reverse(buff2);
jpegFmt.blockLen = BitConverter.ToInt16(buff2, 0);
//EXIFフォーマットチェック
jpegFmt.fs.Read(buff10, 0, 6);
if(!(buff10[0] == 'E' && buff10[1] == 'x' && buff10[2] == 'i' && buff10[3] == 'f' && buff10[4] == 0x00 && buff10[5] == 0x00))
{
jpegFmt.errMsg = "JPEG/EXIFフォーマット異常";
return 1;
}//end if
jpegFmt.identifierOffset = jpegFmt.fs.Position;
jpegFmt.blockLen -= 2 + 6; //長さ自身の2バイトと識別子6バイトを引く
}
else
{
jpegFmt.errMsg = "内部のフォーマット異常";
return 1;
}//end if
return 0;
}
/// <summary>
/// JPEG/JFIFフォーマット画像のdpi値を変更する。
///
/// </summary>
/// <param name="jpegFmt">フォーマット管理</param>
/// <param name="dpiValue">dpi値</param>
/// <returns>実行状態</returns>
private string changeJFIFDpi(JpegFormat jpegFmt, Int16 dpiValue)
{
string ret = "OK";
byte[] dpiBytes = new byte[2];
dpiBytes = BitConverter.GetBytes(dpiValue);
Array.Reverse(dpiBytes); // JFIFはMotorola固定なので、変換(C#はIntel)
long offset = jpegFmt.identifierOffset + 2; //version 2byte
jpegFmt.fs.Seek(offset, SeekOrigin.Begin);
jpegFmt.fs.WriteByte(0x01); // 1:pixel/inch固定
jpegFmt.fs.Write(dpiBytes, 0, 2);
jpegFmt.fs.Write(dpiBytes, 0, 2);
return ret;
}
/// <summary>
/// JPEG/EXIFフォーマット画像のdpi値を変更する。
///
/// </summary>
/// <param name="jpegFmt">ファイルストリーム</param>
/// <param name="dpiValue">dpi値</param>
/// <returns>実行状態</returns>
private string changeEXIFDpi(JpegFormat jpegFmt, Int16 dpiValue)
{
string ret = "OK";
byte[] buff2 = new byte[2];
byte[] buff4 = new byte[4];
int bigEndian = 1; //1:Intel /2:Motorola (e.g. 0x1234 -> I:0x34,0x12 ; M:0x12,0x34)
//TIFF有効性判定
jpegFmt.fs.Seek(jpegFmt.identifierOffset, SeekOrigin.Begin);
jpegFmt.fs.Read(buff4, 0, 4);
if(buff4[0] == 'I' && buff4[1] == 'I' && buff4[2] == 0x2A && buff4[3] == 0x00) //Intel式
{
bigEndian = 1;
}
else if (buff4[0] == 'M' && buff4[1] == 'M' && buff4[2] == 0x00 && buff4[3] == 0x2A) //Motorola式
{
bigEndian = 2;
}
else
{
ret = "EXIF/TIFFフォーマット異常";
return ret;
}//end if
//次のIFDブロック位置取得
jpegFmt.fs.Seek(4, SeekOrigin.Current);
//IFD個数取得
jpegFmt.fs.Read(buff2, 0, 2);
//Motorola式時はビッチ配列を逆転換(C#内部はIntel式)
if (bigEndian == 2)
Array.Reverse(buff2);
int itmCntOfIFD0 = BitConverter.ToInt16(buff2, 0);
//X方向、Y方向の解像度情報を格納する
//ResolutionInfo[] resolutionInfo = new ResolutionInfo[2];
List<ResolutionInfo> resolutionInfoLst = new List<ResolutionInfo>(2);
//X/Y解像度は常にIFD0ブロック内にあるので、IFD0ブロックのみ調べる。
//IFD0ブロック内の各要素は12バイト固定
// マーク 名称 タイプ cnt 備考
// 0x011A XResolution unsigned rational 1 def: 1/72 inch
// 0x011B YResolution unsigned rational 1
// 0x0128 ResolutionUnit unsigned short 1 XResloution/YResloutionの単位. 1:無し;2:inch;3:cm
// DPIの設定なので、単位には常にinchを設定する。
for(int iLoop = 0; iLoop < itmCntOfIFD0; iLoop++)
{
//IFD0内のタグ名取得
jpegFmt.fs.Read(buff2, 0, 2);
if (bigEndian == 1)
Array.Reverse(buff2);
if((buff2[0] == 0x01 && buff2[1] == 0x1A) || //XResolution
(buff2[0] == 0x01 && buff2[1] == 0x1B)) //YResolution
{
ResolutionInfo ri = new ResolutionInfo();
//type取得
jpegFmt.fs.Read(buff2, 0, 2);
if (bigEndian == 2)
Array.Reverse(buff2);
ri.type = BitConverter.ToInt16(buff2, 0);
//コンポーネント数取得
jpegFmt.fs.Read(buff4, 0, 4);
if (bigEndian == 2)
Array.Reverse(buff4);
ri.cnt = BitConverter.ToInt32(buff4, 0);
//解像度フォーマットチェック
if(ri.type != 5 || ri.cnt != 1) //フォーマット異常
{
ret = "JPEG/EXIF DPI値のフォーマット異常[type:" + ri.type + " cnt:" + ri.cnt + "]";
return ret;
}//end if
//解像度記録位置取得
jpegFmt.fs.Read(buff4, 0, 4);
if (bigEndian == 2)
Array.Reverse(buff4);
ri.pos = BitConverter.ToInt32(buff4, 0);
resolutionInfoLst.Add(ri);
}
else if(buff2[0] == 0x01 && buff2[1] == 0x28) //ResolutionUnit
{
//type取得
jpegFmt.fs.Read(buff2, 0, 2);
if (bigEndian == 2)
Array.Reverse(buff2);
short type = BitConverter.ToInt16(buff2, 0);
/*
if(type == 5 || type == 8 || type == 12) //これらは8バイトのタイプ
{
ret = "";
return ret;
}//end if
*/
//コンポーネント数取得
jpegFmt.fs.Read(buff4, 0, 4);
if (bigEndian == 2)
Array.Reverse(buff4);
int cnt = BitConverter.ToInt32(buff4, 0); //必ず1個 チェック要?
//ResolutionUnitの値は常にinch(2)にする
int resolutionUnit = 2;
byte[] resolutionUnitBuff = BitConverter.GetBytes(resolutionUnit);
if (bigEndian == 2)
{
//Array.Reverse(resolutionUnitBuff);
//type=3は2バイトなので、
resolutionUnitBuff = new byte[4]{ 0, 2, 0, 0 };
}//end if
//書き込む
jpegFmt.fs.Write(resolutionUnitBuff, 0, 4);
}
else
{
//次のマークの先頭位置へ
jpegFmt.fs.Seek(10, SeekOrigin.Current);
}//end if
}//end for
//解像度の情報がない場合
if(resolutionInfoLst.Count == 0)
{
ret = "JPEG/EXIF 該当画像にDPI情報がありません。";
return ret;
}//end if
//解像度変更処理
//ForTest 解像度を取得してみる
foreach(ResolutionInfo ri in resolutionInfoLst)
{
//解像度データの位置へ
jpegFmt.fs.Seek(jpegFmt.identifierOffset + ri.pos, SeekOrigin.Begin);
//DPIの小数点対応
int dpiBunbo = 10000;
int dpiBunshi = dpiBunbo * dpiValue;
//dpi分子書き込み
byte[] dpiBunshiBytes = BitConverter.GetBytes(dpiBunshi);
if (bigEndian == 2) //Motorola式
Array.Reverse(dpiBunshiBytes);
jpegFmt.fs.Write(dpiBunshiBytes, 0, 4);
//dpi分母書き込み
byte[] dpiBunboBytes = BitConverter.GetBytes(dpiBunbo);
if (bigEndian == 2)
Array.Reverse(dpiBunboBytes);
jpegFmt.fs.Write(dpiBunboBytes, 0, 4);
}//end foreach
return ret;
}
#region Drag & Drop機能関連
/// <summary>
/// ドラッグ処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void directroyBox_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
e.Effect = DragDropEffects.Copy;
else
e.Effect = DragDropEffects.None;
}
/// <summary>
/// ドロップ処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void directroyBox_DragDrop(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop))
return;
string[] strs = (string[])e.Data.GetData(DataFormats.FileDrop);
//マルチ指定に対応無し。常に最初1個目を取得する。
string file = strs[0];
if (Directory.Exists(file))
directroyBox.Text = file;
else
{
directroyBox.Text = Path.GetDirectoryName(file);
}//end if
}
private void listBox_DragEnter(object sender, DragEventArgs e)
{
directroyBox_DragEnter(sender, e);
}
private void listBox_DragDrop(object sender, DragEventArgs e)
{
directroyBox_DragDrop(sender, e);
}
private void FrmMain_DragEnter(object sender, DragEventArgs e)
{
directroyBox_DragEnter(sender, e);
}
private void FrmMain_DragDrop(object sender, DragEventArgs e)
{
directroyBox_DragDrop(sender, e);
}
#endregion
private void groupBox1_Enter(object sender, EventArgs e)
{
}
}//end class
}//end namespace
では、バイバイ! Have a nice day!
この記事が気に入ったらサポートをしてみませんか?