どうも私は飽きっぽい性格らしくて,ずっとコーディングばかりは続けてられません。という事で最近は HTML をいじくってます。・・・コーディングとどう違うよ・・・。
そんなわけで今日は今までの雑記をすべて見直し,必要な部分はすべてマークアップし直してしまいました。ほぼ 14 時間ぶっ続け作業。なかなか過激な気分転換でした。
それからこのリソースは私が勝手に Xemem を冠してたんだけど,やはりふさわしくない気がするんでやめました。主催者フジモトさんの言うところの「ヒドラの頭」になるんだけど,Google で「Xemem」を検索してみると明らかに"浮いて"いました。これは興ざめ。遊びに加わるならば,もっと楽しくなるように努めないとね。てへへ。
というか別に JPEG デコーダ作製をやめたわけではありません。CSS,NC4.x に対応するのをやめました。そもそも CSS が無くたって読めるのが (x)html なわけで。
どうしても許せないのが float や clear 周 りのバグ。こういうスタイルを指定すると
<!-- HTML としては正しくありません -->
<style>
.l { float: left;
border: 1px solid #aca;
width: 5em; }
.r { float: left;
border: 1px solid #f00;
width: 5em; }
.c { clear: both; }
</style>
<div class="l">左 1 行目<br />左 2 行目<br />左 3 行目</div><div class="r">右</div>
<div class="c">下</div>
まず mozilla 1.3b は次のように表示します。
正解です。 次, NC4.78 の表示。
「下」が上部のボックスに食い込んでます。次,この「下」という文字があるボックス,すなわち div.c に枠をつけて見ます。枠指定の周辺はやはり NC4.x の鬼門。
.c { border: 1px solid #00f;
clear: both; }
mozilla は次のように表示します。
やはり正解っす。mozilla さん,押忍,押忍! 次は NC4.78 の表示。
なんか clear: both; 指定を完全に無視しきった感じです。それではと,上部と下部のボックスの間に特別に clear: both スタイルを持った div を挿入してみます。
<div class="l">左 1 行目<br />左 2 行目<br />左 3 行目</div><div class="r">右</div>
<div style="clear: both;"></div> <!-- ここを追加 -->
<div class="c">下</div>
そして NC4.78 の表示はこう。
むん,堂々とめり込んでます。これでもう嫌気がさしました。このあたりのバグについて回避する方法もあるらしいんだけど,汚い CSS を書いてまで NC4.x に対応する必要もないかな,と。幸いにも link 要素で CSS を指定する時,media 属性に 2 つ以上のメディアを指定しておく事で NC4.x では CSS を無効にする事が出来るので,NC4.x でこのページを見ても特に問題は起こらないはずです。
<link rel="stylesheet" title="k|m diary" href="d.css" type="text/css"
media="screen,tv"/> <!-- media に複数のメディアを指定 -->
また NC4.x は @import にも対応していないので,これで NC4.x の CSS を無効にする事も可能。
<style type="text/css">
@import 'd.css';
</style>
他のブラウザの CSS を無効にする技もあったりして奥が深いですね。「CSS」「バグ」というキーワードで Google を漁ると,有益なサイトが山ほど出てきました。
とりあえず代表的なサイトはここかなー。他の参考になるサイトへのリンクも充実しています。
cppll の議論を眺めている限り,どうも function-try-block を正常にサポートしているコンパイラは少ないらしいとの事。コンストラクタ内で例外を投げる設計にはすべきではないのかも知れません。気持ち悪いしね。参考 ->「[cppll:3217] コンストラクタについて」以下一連の議論;
ちょうどこのあたりの事も練習してみたかったので,よい機会です。コンストラクタが例外を投げないような設計として,私の FileDescriptor クラスをこんな感じにしてみました。
/**
* ファイル記述子
*/
class FileDescriptor {
bool init_;
FileHandle file_handle_;
const char *file_name_;
char mode[4];
/**
* 初期化が行われている事を保証する
*/
void require_initialized() {
if (init_) return;
file_handle_ = fopen(file_name_, mode_);
if (! file_handle_)
throw FileNotFoundException();
init_ = true;
}
public:
/**
* コンストラクタ (ただし初期化はまだ行わない)
*/
FileDescriptor(const char *fname, const FileOpenMethod &fom) {
init_ = false;
file_handle_ = 0;
file_name_ = fname;
strcpy(mode_, fom.getModeString());
}
/**
* デストラクタ
*/
~FileDescriptor(){
if (! init_) return;
if (file_handle_) {
fclose(file_handle_);
file_handle_ = 0;
}
}
/**
* ファイルサイズを得る
*/
unsigned long getLength() {
require_initialized();
// (本体略)
}
/**
* データを読む
*/
unsigned long read(void *buf, unsigned long size) {
require_initialized();
return fread(buf, 1, size, file_handle_);
}
/**
* データを書き込む
*/
unsigned long write( void *buf, unsigned long size ) {
require_initialized();
return fwrite(buf, 1, size, file_handle_);
}
};
要するにコンストラクタ内では本当に単純な初期化のみを行い,すべての public なメソッドについて,先頭で private なメソッド require_initialized() を呼び出すわけですね。これで初期化に関する例外は,インスタンスを生成して一番最初呼び出されたメソッドから発生するようになります。
メソッド数が増えたり,どうしても const メソッドが必要な場合はかなり面倒な事になるんだけど,今のところこれが最良なのかなーと。
ファイルストリームを開くにあたりその挙動を指示するため,fopen() は引数に "r" などの文字列を与え,CreateFile() はの 2 つの引数に定数を与えます。このあたりを整理してみると,つまりファイルを開く時の挙動は次の組み合わせで全てを説明できそうです。
fopen() の挙動を,上記の組み合わせで説明してみます。
fopen() | 読み/書き | 既存 | 未存 |
|---|---|---|---|
| "rb" | 読み | 先頭から | エラー |
| "wb" | 書き | クリア | 生成 |
| "ab" | 書き | 末尾から | 生成 |
| "r+b" | 両方 | 先頭から | エラー |
| "w+b" | 両方 | クリア | 生成 |
| "a+b" | 両方 | 末尾から | 生成 |
そういえばちょっと気づいただけの事なんだけど:「ファイルが既存の場合はエラーとする」という挙動は fopen() にはできません。しかし ::CreateFile() の場合,dwCreationDisposition に CREATE_NEW を指定する事でファイルが既存の場合にエラーとなります。
私はまだ ::CreateFile() の仕様を全て把握している自信がないので,もしかしたら上記の組み合わせで表現できない挙動を起こせるのかも知れません。しかし,上記の組み合わせで表現できない挙動なんか必要ないんじゃないかと。
ということで今日のまとめ。ファイルストリームクラスを開く時にはその挙動をクラスの利用者が決定しなければいけないんだけど,それの一般化を目指してみました。上のリストのような機能の選択を実現しておけば,必要な事は大抵実現できるかと。
これに加え,私の脳内 OS では次のようなセットも追加したりしたいと思ってみました。
・・・極端に処理が複雑になるので却下。
JPEG の話とは少し離れていますが気にしません。以前に fopen() の第 2 引数の違いによる挙動をうまくまとめているリソースを紹介しました。しかし WinAPI の ::CreateFile() はモードの指定方法が若干違う上に,挙動もやはり異なります。いつも fopen() を使っている人が期待するような挙動を得るには,どこにどのようなパラメータを与えればよいのかに悩みます。
fopen() |
dwDesiredAccess | dwCreationDistribution |
|---|---|---|
| "rb" | GENERIC_READ | OPEN_EXISTING |
| "wb" | GENERIC_WRITE | CREATE_ALWAYS |
| "ab"(※) | GENERIC_WRITE | OPEN_ALWAYS |
| "r+b" | GENERIC_READ | GENERIC_WRITE | OPEN_EXISTING |
| "w+b" | GENERIC_READ | GENERIC_WRITE | CREATE_ALWAYS |
| "a+b"(※) | GENERIC_READ | GENERIC_WRITE | OPEN_ALWAYS |
これでよいかと。"r","w" や "a" のような,いわゆる MS-DOS の負の遺産「テキストモード」はエミュレートできない模様です ( あっても使わないけど )。また "ab" と同じ挙動を得るにはもうひと工夫が必要でした。::SetFilePointer() を用いて,こんなコードを書きます。ちなみに,設定してもあまり面白くなさそうな引数は 0 や NULL にするのが一番だと思います。hehehe.
// "ab" の正確なエミュレーション
HANDLE fp = ::CreateFile(
file_name, // LPCSTR lpFileName
GENERIC_WRITE, // DWORD dwDesiredAccess
0, // DWORD dwShareMode
NULL, // LPSECURITY_ATTRIBUTES lpSecurityAttributes
OPEN_ALWAYS, // DWORD dwCreationDistribution
FILE_ATTRIBUTE_NORMAL, // DWORD dwFlagsAndAttributes
NULL // HANDLE hTemplateFile
);
if (fp == INVALID_HANDLE_VALUE) {
::MessageBox(0, "::CreateFile() 失敗", "", 0);
return 0;
}
::SetFilePointer( fp, 0, NULL, FILE_END );
// 以下,WriteFile() で書き込みなど ...
よし,少しずつ慣れて来ている感じがします。って,なんだかリハビリやってる気分だー。。。
む,私は今まで ::ReadFile() などを扱って来ずに標準 C の fread() を扱って来たため,ほんの少しだけてこずっています。しかしこれは WinAPI でのファイル操作を憶えるチャンス。
で,ちょっとネタになる話はありません。
元ネタは「バーチャルネット法律娘 真紀菜17歳」さんより。さらに元となる日経へのリンクは ( 仕様かな? ) 使えない模様。要するに,著作権の保護を司法面でバックアップするって事ですか。
ソフトウェアの料金はまさに著作権が根拠になっているらしく,料金を払わずに有料のソフトウェアを使うのは「著作権の侵害」に当たります。だからソフトウェアを売って生活している私にとっては,どうやら歓迎すべき事らしくて。
でも形而下に著された芸術作品について考えれば,あんまし歓迎したくないなー。このあたりはネタ元の「真紀菜17歳」さんをはじめ,多くの人が主張している事ですね。
とりあえずファイルを取り扱う汎用的なクラスでも作ってみます。JAVA では FileInputStream と FileOutputStream に分かれていますが,ここではそれを踏襲してみます。まず先立つモノとして,Input とも Output とも区別されない,C のファイル記述子の単純なラッパクラス "FileDescriptor" を作ってみます ( JAVA における "FileDescriptor" の位置付けは知らないけど )。
で,こんな設計にしてみたんだけど・・
#ifndef STRICT
# define STRICT
#endif
#include <windows.h>
typedef HANDLE FileHandle;
class FileDescriptor {
public:
FileDescriptor(const char *fname, const char *mode);
~FileDescriptor();
unsigned long getLength() const;
unsigned long getPosition() const;
void setPosition( unsigned long );
unsigned long read( void *buf, unsigned long size );
unsigned long write(void *buf, unsigend long size );
operator FileHandle ();
operator void * ();
};
特別に FileHandle 型を定義しておく事で,Windows の HANDLE 型を扱いたい場合も C 標準の FILE * 型を扱いたい場合も,細かい部分をちまちまと修正する必要がなくなります。コンストラクタは fopen() の形をそのまま持ってきていますが,Windows が扱うファイルはより複雑なパラメータを与える事が出来ます。よって再考の価値あり。FileHandle 型を引数にとる API のために演算子をオーバーロードしておきました。また if (file) ...; としてオブジェクトの正当性を確認できるように,void * キャスト演算子もオーバーロードしています。
で,上記コードはコンパイル出来ません。
原因は HANDLE 型が実はただの void * 型だから。windows.h をインクルードする前に STRICT を定義しておけば多くの型について厳密に解釈される事はよく知られていますが,ファイル等はどうにもこうにも。どうして特別に "HFILE" のような型を用意してくれていないのかと小一時間。UNIX に倣い ::ReadFile() 等の関数では socket() が生成したソケット記述子も扱えるようです。::ReadFile() の第 1 引数が HANDLE 型 = void * 型なのはそのためかと。
だからと言って,そんなのは ::CreateFile() の知ったことじゃないっすよね。。。
JPEG をデコードするためのストリームクラスに必要な機能は,一般の汎用的なストリームに必要な機能とは多少異なります。
// 一般的なストリームクラスの設計例
interface CommonInputStream {
readonly attribute unsigned long long length;
readonly attribute unsigned long long available;
attribute unsigned long long position;
void close();
octet show8Bits();
octet read8Bits();
unsigned short show16Bits();
unsigned short read16Bits();
unsigned long show32Bits();
unsigned long read32Bits();
unsigned long long show64Bits();
unsigned long long read64Bits();
unsigned long long read( octet[] buffer, unsigned long long offset,
unsigned long long length );
};
最近覚えたての IDL 用語ですが,octet は符号なしの 8 ビット整数です。length はストリームの全体の長さ。available は現在ストリームからブロッキングなしでデータを取得できるサイズ。position はストリームを読んでいる現在位置を表します。showNBits() や readNBits() はファイルから N ビット読み込む関数。show〜 はストリームの位置を進めず,read〜 はストリームの位置を進めます。
続いて JPEG を扱うのに必要なインタフェースはこれ。
// JPEG を扱うためのストリームクラス
interface BitwiseInputStream {
attribute unsigned long long position;
void close();
octet show8Bits();
octet read8Bits();
unsigned short read16Bits();
unsigned long readBits(int bits);
boolean isAligned();
};
position 以外のストリームの位置情報を表すアトリビュートが無くなりました。JPEG ( に限らず,大抵のイメージファイル形式 ) は,デコードが終了するまで一切「あとどれくらいファイルがあるか」を気にする必要がありません。また showNBits() や readNBits() も,JPEG のデコードにおいて不要なものを取り去るとこうなりました。
増えたメソッドは 2 つ。readBits() は,任意のビット数を読み込みます。ハフマン符号化のようなエントロピー圧縮はビット単位でデータを操作するので,当然ですね。
isAligned() は,現在 readBits(8) と read8Bits() が等価かどうかを示すフラグです。StartOfScan 直後のスキャンを読み込んでいる最中,0xFF が登場したら次は必ず 0x00 であり,その 0x00 は無視しなければなりません ( ・・ここまで,リセットマーカを無視して議論しました )。その判断を行う時,isAligned() メソッドが重要になります。
一般的なストリームクラスに比べて「不要なメソッドがある」ならば何も難しい事は考えませんが,「必要なメソッドが出てきた」ならば,ほんの少しだけ気をつけます。JPEG のローダクラスは,CommonInputStream ではなく BitwiseInputStream インタフェースを知っている必要があります。
明日からこれらを実装していきます。ていうか,無駄に汎用的なモノを作ろうとして深みにハマりそうな予感。警戒せねば。
これからどうしても私は C++ にて設計を行う必要があります。C での似非 OO には限界があります ── クラスの継承のやりにくさときたら! いや「やりにくい」じゃなくて「不可能」ですアレは。
さてここで「画像ファイル」というものを「ロードする」ための抽象クラスを設計してみます。
class AbstractImageLoader {
public:
virtual ~AbstractImageLoader(){}
virtual int getWidth();
virtual int getHeight();
virtual int getDepth();
virtual int getLine(unsigend char *);
};
getDepth() は画素深度,getLine() は 1 ラインのデータを取得します。すなわち全てのビットマップデータを得るには getLine() を getHeight() 回繰り返せばよい,という事になりそうです。実は既に BMP ファイルも TIFF ファイルもこのインタフェースでローダを作った事があり,単純ながらなかなか成功だったと思います。
なお BMP ファイルの場合,よく「画像は逆さに入っている」と表現される事があります。なぜこんな事をしているのか皆目見当もつきませんが,Windows の DDB,DIB ともに画像の上下とメモリ配置の上下の感覚が逆なんですよね。このようなファイルの場合,getHeight() でマイナスを値を返すようにしてみます。そうする事でこのクラスのユーザは注意深く getLine() を扱う事が出来るはずです。
目標はこんな感じのコードを書くこと:
void func() {
ImageLoader *il = createImageLoader(); // 適切な ImageLoader を生成する
int width = il.getWidth();
int height = il.getHeight();
int depth = il.getDepth();
// 画像は逆さで入っているものか?
bool reversed = (height < 0);
if (height < 0) height = -height;
// Bytes Per Line
int bpl = calcBPL(width, depth);
// 画像を保持するメモリを割り当てる
unsigned char *pixels = allocPixels( width, height, depth );
unsigned char *p = pixels;
// 画像が逆さの場合,メモリも後ろからつめていく
if (reversed)
p = pixels + bpl * (height - 1);
// 1 ラインずつ取得
while (height--) {
il->getLine( p );
if (reversed) p -= bpl;
else p += bpl;
}
}
おし,それなりに美しい ( ? ) モノが出来そうです。今日はこれまで。
ついに我がブラクラチェッカーもその毒牙にかかったようです:悪名高き NAVER の巡回ロボット,その名も「nabot」!!
アクセスログが異様に膨らんでいるのでログを調べてみようと思ったわけですよ。しかし腰を据える間もなく私の目に飛び込んで来たのはユーザエージェント「nabot」の嵐。どこから来ているのかは分かりませんが,とにかく手当たり次第にチェックを試みています ── もっとも,多重アクセス防止のトリックに引っかかっているとは思いますが。
こいつの「行儀悪すぎ」という評価の所以はこれ:なんとアクセス間隔が 0.3 秒に 1 回。これを絨毯爆撃と言います。ブラクラチェッカー @ coara は Socket を用いて外部と通信し,リソースを取得して検査を行います。つまり一般的な「掲示板」のような CGI に比べて極端に負荷がかかるって事。今でこそイタズラが出来ないトリックをいくつか用意していますが,2 年前ならば大惨事間違いなし。NewCOARA さんから賠償請求されてもおかしくない事態になったはずです。
2003 年 1 月 3 日のブラクラチェッカー @ coara への総アクセス数は 24,015 回。うち nabot のアクセス数は 10,297 回でした。“異常”という言葉はまさにこの状況を表現するために生まれた言葉です今私が決めました文句あるかっていうかナメてんのかマジで。
より広範な知識を上記サイトで手に入れる事が出来ます。しかし“かの国”絡みのネタはどうしてこんなにネガティブな事ばかりなんだろう・・・。
さて,あれからどうすればいいんだろう? 課題はいくつか残っています。画質の問題とか,インタフェースの問題とか,プログレッシブ JPEG の問題とか。そういえばまだインタバルにも対応していません。文献を探すのはかなり大変なんですが,まず「これからどうすればよいのか」を決めたいと思います。