Skip to content

Instantly share code, notes, and snippets.

@kuanyui
Last active November 24, 2024 14:10
Show Gist options
  • Save kuanyui/9214ebaa02369c6f3dbe938f83f2c89f to your computer and use it in GitHub Desktop.
Save kuanyui/9214ebaa02369c6f3dbe938f83f2c89f to your computer and use it in GitHub Desktop.
MAG (画像フォーマット)ローダー( http://emk.name/2015/03/magjs.html )のフォーク(1-クリックしたら「元の MAG ファイルの名前」で PNG として保存できてくなります。例:`REI.MAG -> REI.png`)
<!DOCTYPE html><meta charset=utf-8>
<title>HTML5 まぐろーだー</title>
<script>
function load_mag(ab, canvas) {
var check = new Uint8Array(ab);
if ('MAKI02 ' !== String.fromCharCode.apply(null, check.subarray(0, 8))) {
throw new Error('MAG画像ではありません');
}
// ヘッダの先頭まで読み捨て
for (var headerOffset = 30; check[headerOffset]; ++headerOffset)
;
var header = new DataView(ab, headerOffset, 32);
var top = header.getUint8(0); // ヘッダの先頭
var machine = header.getUint8(1); // 機種コード
var flags = header.getUint8(2); // 機種依存フラグ
var mode = header.getUint8(3); // スクリーンモード
var sx = header.getUint16(4, true);
var sy = header.getUint16(6, true);
var ex = header.getUint16(8, true);
var ey = header.getUint16(10, true);
var flagAOffset = header.getUint32(12, true);
var flagBOffset = header.getUint32(16, true);
var flagASize = flagBOffset - flagAOffset;
var flagBSize = header.getUint32(20, true);
var pixelOffset = header.getUint32(24, true);
var pixelSize = header.getUint32(28, true);
var colors = mode & 0x80 ? 256 : 16;
var pixelUnitLog = mode & 0x80 ? 1 : 2;
var palette = new Uint8Array(ab, headerOffset + 32, colors * 3);
var flagABuf = new Uint8Array(ab, headerOffset + flagAOffset, flagASize);
var flagBBuf = new Uint8Array(ab, headerOffset + flagBOffset, flagBSize);
var width = (ex & 0xFFF8 | 7) - (sx & 0xFFF8) + 1;
var flagSize = width >>> (pixelUnitLog + 1);
var height = ey - sy + 1;
var flagBuf = new Uint8Array(flagSize);
var pixel = new Uint8Array(ab, headerOffset + pixelOffset, pixelSize);
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
var imageData = ctx.createImageData(width, height);
var data = imageData.data;
var flagAPos = 0;
var flagBPos = 0;
var src = 0;
var dest = 0;
// コピー位置の計算
var copyx = [0, 1, 2, 4, 0, 1, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0];
var copyy = [0, 0, 0, 0, 1, 1, 2, 2, 2, 4, 4, 4, 8, 8, 8, 16];
var copypos = new Array(16);
for (var i = 0; i < 16; ++i) {
copypos[i] = -(copyy[i] * width + (copyx[i] << pixelUnitLog)) * 4;
}
var copysize = 4 << pixelUnitLog;
var mask = 0x80;
for (var y = 0; y < height; ++y) {
// フラグを1ライン分展開
for (var x = 0; x < flagSize; ++x) {
// フラグAを1ビット調べる
if (flagABuf[flagAPos] & mask) {
// 1ならフラグBから1バイト読んでXORを取る
flagBuf[x] ^= flagBBuf[flagBPos++];
}
if ((mask >>>= 1) === 0) {
mask = 0x80;
++flagAPos;
}
}
for (var x = 0; x < flagSize; ++x) {
// フラグを1つ調べる
var vv = flagBuf[x]
var v = vv >>> 4;
if (!v) {
// 0ならピクセルデータから1ピクセル(2バイト)読む
if (colors == 16) {
var c = (pixel[src] >>> 4) * 3;
data[dest] = palette[c + 1];
data[dest + 1] = palette[c];
data[dest + 2] = palette[c + 2];
data[dest + 3] = 0xFF;
c = (pixel[src++] & 0xF) * 3;
data[dest + 4] = palette[c + 1];
data[dest + 5] = palette[c];
data[dest + 6] = palette[c + 2];
data[dest + 7] = 0xFF;
c = (pixel[src] >>> 4) * 3;
data[dest + 8] = palette[c + 1];
data[dest + 9] = palette[c];
data[dest + 10] = palette[c + 2];
data[dest + 11] = 0xFF;
c = (pixel[src++] & 0xF) * 3;
data[dest + 12] = palette[c + 1];
data[dest + 13] = palette[c];
data[dest + 14] = palette[c + 2];
data[dest + 15] = 0xFF;
dest += 16;
} else {
var c = pixel[src++] * 3;
data[dest] = palette[c + 1];
data[dest + 1] = palette[c];
data[dest + 2] = palette[c + 2];
data[dest + 3] = 0xFF;
c = pixel[src++] * 3;
data[dest + 4] = palette[c + 1];
data[dest + 5] = palette[c];
data[dest + 6] = palette[c + 2];
data[dest + 7] = 0xFF;
dest += 8;
}
} else {
// 0以外なら指定位置から1ピクセル(16色なら4ドット/256色なら2ドット)コピー
var copySrc = dest + copypos[v];
data.set(data.subarray(copySrc, copySrc + copysize), dest);
dest += copysize;
}
v = vv & 0xF;
if (!v) {
// 0ならピクセルデータから1ピクセル(2バイト)読む
if (colors == 16) {
var c = (pixel[src] >>> 4) * 3;
data[dest] = palette[c + 1];
data[dest + 1] = palette[c];
data[dest + 2] = palette[c + 2];
data[dest + 3] = 0xFF;
c = (pixel[src++] & 0xF) * 3;
data[dest + 4] = palette[c + 1];
data[dest + 5] = palette[c];
data[dest + 6] = palette[c + 2];
data[dest + 7] = 0xFF;
c = (pixel[src] >>> 4) * 3;
data[dest + 8] = palette[c + 1];
data[dest + 9] = palette[c];
data[dest + 10] = palette[c + 2];
data[dest + 11] = 0xFF;
c = (pixel[src++] & 0xF) * 3;
data[dest + 12] = palette[c + 1];
data[dest + 13] = palette[c];
data[dest + 14] = palette[c + 2];
data[dest + 15] = 0xFF;
dest += 16;
} else {
var c = pixel[src++] * 3;
data[dest] = palette[c + 1];
data[dest + 1] = palette[c];
data[dest + 2] = palette[c + 2];
data[dest + 3] = 0xFF;
c = pixel[src++] * 3;
data[dest + 4] = palette[c + 1];
data[dest + 5] = palette[c];
data[dest + 6] = palette[c + 2];
data[dest + 7] = 0xFF;
dest += 8;
}
} else {
// 0以外なら指定位置から1ピクセル(16色なら4ドット/256色なら2ドット)コピー
var copySrc = dest + copypos[v];
data.set(data.subarray(copySrc, copySrc + copysize), dest);
dest += copysize;
}
}
}
ctx.putImageData(imageData, 0, 0);
}
(function() {
function nodrop(ev) {
ev.preventDefault();
ev.stopPropagation();
}
['dragenter', 'dragleave', 'dragover'].forEach(function(evname) {
document.documentElement.addEventListener(evname, nodrop, true);
});
document.documentElement.addEventListener('drop', function ondrop(e) {
[].forEach.call(e.dataTransfer.files, function(file) {
var fr = new FileReader();
fr.onload = function(ev) {
document.body.insertBefore(document.createElement('hr'), document.body.firstChild);
try {
var canvas = document.createElement('canvas');
var downloadFilename = file.name.replace(/\.mag$/i, '.png');
var a = document.createElement('a');
load_mag(ev.target.result, canvas);
var dataURL = canvas.toDataURL();
a.href = dataURL;
a.download = downloadFilename;
a.appendChild(canvas);
document.body.insertBefore(a, document.body.firstChild);
} catch (ex) {
document.body.insertBefore(document.createTextNode(ex), document.body.firstChild);
}
};
fr.readAsArrayBuffer(file);
});
e.preventDefault();
}, true);
})();
</script>
<body>
<script>
document.write('このページにMAG画像をドロップしてください。');
</script>
<noscript>JavaScriptが無効です。</noscript>
<hr>
<h1>HTML5 まぐろーだー</h1>
<h2>更新履歴</h2>
<ul>
<li>2015-03-16 文書を作成
<li>2015-03-15 初版公開
</ul>
<h2>これは何?</h2>
<p>いわゆるHTML5対応のブラウザの機能だけを利用して、<a href="https://ja.wikipedia.org/wiki/MAG%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88">MAGフォーマット</a>の画像を表示するスクリプトです。</p>
<h2>何がうれしいの?</h2>
<p><a href="../../dhtml/unlzh.html">unlzh.js</a>と組み合わせて、パソコン通信時代の、改変せずに転載することのみが認められている書庫に含まれた画像をWeb上で展示したいなどの非常に微妙な用途くらいしか思いつきません。</p>
<p>(2015-03-17追記) Intel Macでは<a href="http://www.remus.dti.ne.jp/~yoshiki/PixelCat/">PixelCat</a>でもMAGを開けないらしいので、最近のMacでMAG画像を見たいけどツールがないという向きには有用かもしれません。Windowsでは<a href="http://www2h.biglobe.ne.jp/~tobita/gv/gv.htm">GV</a>が普通に使えるのであまりありがたみがないと思いますが。</p>
<h2>技術情報</h2>
<h3>システム要件</h3>
<p>以下のようなWeb標準仕様をサポートしたブラウザであれば動くはずです。</p>
<ul>
<li><a href="https://www.w3.org/TR/2dcontext/">HTML Canvas 2D Context</a>
<li><a href="https://www.khronos.org/registry/typedarray/specs/latest/">型付き配列</a> (DataViewのサポートが必要です)
</ul>
<p>またこのページのデモを動かすには以下のサポートも必要です。</p>
<ul>
<li><a href="https://www.w3.org/TR/FileAPI/">File API</a>と、HTML5 ドラッグアンドドロップによるFileオブジェクトのサポート
<li><a href="https://www.w3.org/TR/DOM-Level-3-Events/">DOM Level 3 Events</a>
</ul>
<p>具体的にはInternet Explorer 11や、FirefoxおよびChromeの最新版、Windows 10 version 1511以降のMicrosoft Edgeでは動作しているようです。(2015-03-17追記) OS X 10.10のSafari 8.0.5で動作したという<a href="https://srad.jp/comments.pl?sid=653949&cid=2778801">報告</a>をいただきました。(2015-06-23追記、2015-11-13更新) Windows 10 RTM (version 1507)のMicrosoft Edgeはデスクトップからのドラッグアンドドロップをサポートしていないので、動作しません。</p>
<h3>実装メモ</h3>
<ul>
<li>MAGの仕様書はマルチペイントのマニュアルに掲載されていたはずだが発掘できなかったので、<a href="http://metanest.jp/mag/mag.xhtml">MAGBIBLE</a>をもとに実装する。</li>
<li>MAGBIBLEはところどころ16色画像しか想定していないような記述があって、適宜読み替えないと256色画像を正しく表示できない。たとえば「ピクセル」は仮想VRAM上の1ワード=16ビットを指す単位なので、16色画像では4ドット、256色画像では2ドットになるのだが、ピクセルと書くべきところで4ドットと書かれていたりする。</li>
<li>MAGBIBLEにはフラグデータをすべて展開すると1024×1024ドットの画像で128Kものメモリを使ってしまい、現実的ではないなどと書かれていて、時代を感じさせる(結局ラインバッファで実装しているが)。</li>
<li>(2015-03-17追記) フラグAやフラグからビット単位で読み取るときに、MSBから読むのかLSBから読むのか明記されていない(正解はMSBから)。</li>
<li>今のところスクリーンモードは256色かどうかしか見ていないので、200ライン画像は正しいアスペクト比で表示できないと思われる(対応は簡単だが画像の現物が手もとに見当たらなかった)。</li>
<li>(2015-03-17修正) MAGにはパレットの異なる部分セーブ画像を追加することで画像全体の色を変える表現手法が存在したが、canvasのバッキングストアに画像を展開した時点でもとのカラーインデックスの情報は失われてしまうので、未対応(それ以前に部分セーブ画像の追加自体に未対応だが)。</li>
<li>(2015-03-17追記) マルチペイントのマニュアルを発掘できたので、記述を見比べてみる。
<ul><li>やはりフラグが0以外のとき「4ドット」コピーすると書かれていた場所は、「1ピクセル」に訂正されていた。</li>
<li>また「<a href="http://metanest.jp/mag/mag.xhtml">鮪フォーマット解説</a>」のサイトで指摘されていた部分セーブ時の横幅切り上げについても、256色画像のときは4ドットと追記されていた。12bppのパレットデータを24bppに拡張するときの方法も、「まぐろのすべて」と同じく17倍になっていた。</li>
<li>一方、「正誤表01.DOC」に記載されているMSX SC8のスクリーンモードの訂正はなぜか反映されていない。</li>
<li>ビット単位の読み出し方向は相変わらず明記されていないが、サンプルコードがあるのでそれを見ればMSBから読めばいいことは一応わかる。</li>
</ul>
</li>
</ul>
<hr>
<address style="position: fixed; bottom: 0; right: 0; background-color: white; color: black">Copyright&copy; 2015 <a href="mailto:VYV03354&#64;nifty.ne.jp">Masatoshi Kimura (emk)</a></address>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment