295 lines
9.8 KiB
JavaScript
295 lines
9.8 KiB
JavaScript
function dropfile(ev, target, fn) {
|
|
ev.preventDefault();
|
|
let reader = new FileReader();
|
|
reader.readAsArrayBuffer(event.dataTransfer.files[0]);
|
|
reader.onload = function() {
|
|
tgt = document.getElementById(target);
|
|
tgt.value = [...new Uint8Array(reader.result)]
|
|
.map(x => x.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
tgt.dispatchEvent(new KeyboardEvent('keyup', {'key':'a'}));
|
|
};
|
|
}
|
|
|
|
|
|
// General
|
|
|
|
function determine_file_ext(str) {
|
|
let s8 = str.slice(0,8);
|
|
if (s8 == '\x89PNG\x0d\x0a\x1a\x0a') return 'png';
|
|
if (s8 == '\x00\x00\x00\x0CjP ' || s8 == '\xFF\x4F\xFF\x51\x00\x2F\x00\x00') return 'jp2';
|
|
if (str.slice(0,6) == 'bplist') return 'plist';
|
|
let s4 = str.slice(0,4);
|
|
if (s4 == 'ARGB') return 'argb';
|
|
if (s4 == 'icns') return 'icns';
|
|
if (str.slice(0,3) == '\xFF\xD8\xFF') return 'jpg';
|
|
return null;
|
|
}
|
|
|
|
function icns_type(head) {
|
|
if (['is32', 'il32', 'ih32', 'it32', 'icp4', 'icp5'].indexOf(head) > -1) return 'rgb';
|
|
if (['s8mk', 'l8mk', 'h8mk', 't8mk'].indexOf(head) > -1) return 'mask';
|
|
if (['ICN#', 'icm#', 'ics#', 'ich#'].indexOf(head) > -1) return 'iconmask';
|
|
if (['icm8', 'ics8', 'icl8', 'ich8'].indexOf(head) > -1) return 'icon8b';
|
|
if (['icm4', 'ics4', 'icl4', 'ich4'].indexOf(head) > -1) return 'icon4b';
|
|
if (['sbtp', 'slct', '\xFD\xD9\x2F\xA8'].indexOf(head) > -1) return 'icns';
|
|
if (head == 'ICON') return 'icon1b';
|
|
if (head == 'TOC ') return 'toc';
|
|
if (head == 'info') return 'plist';
|
|
return 'bin';
|
|
}
|
|
|
|
function is_it32(ext, itype, first4b) {
|
|
if (ext == 'rgb') return itype == 'it32';
|
|
if (ext == null) return first4b == [0,0,0,0] || first4b == '00000000';
|
|
return false;
|
|
}
|
|
|
|
function get_length(hex, i) {
|
|
return Number('0x'+hex.substring(i, i + 8));
|
|
}
|
|
|
|
function as_str(hex, i_start, i_end) {
|
|
var str = '';
|
|
for (var u = i_start; u < i_end; u += 2)
|
|
str += String.fromCharCode(parseInt(hex.substr(u, 2), 16));
|
|
return str;
|
|
}
|
|
|
|
function* parse_file(hex_str) {
|
|
function get_media_ext(itype, idx, len) {
|
|
let ext = determine_file_ext(as_str(hex_str, idx, idx + 16));
|
|
if (ext || !itype)
|
|
return [ext, idx, idx + len];
|
|
return [icns_type(itype), idx, idx + len];
|
|
}
|
|
var txt = '';
|
|
var i = 0;
|
|
let ext = get_media_ext(null, 0, hex_str.length);
|
|
if (ext[0] == 'icns') {
|
|
var num = get_length(hex_str, i + 8);
|
|
yield ['icns', i, num, null];
|
|
i += 8 * 2;
|
|
while (i < hex_str.length) {
|
|
let head = as_str(hex_str, i, i + 8);
|
|
num = get_length(hex_str, i + 8);
|
|
yield [head, i, num, get_media_ext(head, i + 16, num * 2 - 16)];
|
|
i += num * 2;
|
|
}
|
|
} else {
|
|
yield [null, 0, hex_str.length, ext];
|
|
}
|
|
}
|
|
|
|
|
|
// Image viewer
|
|
|
|
function num_arr_from_hex(hex) {
|
|
var ret = [];
|
|
for (var i = 0; i < hex.length; i += 2)
|
|
ret.push(parseInt(hex.substr(i, 2), 16));
|
|
return ret;
|
|
}
|
|
|
|
function msb_stream(source) {
|
|
var data = [];
|
|
for (var ii = 0; ii < source.length; ii++) {
|
|
let chr = source[ii];
|
|
for (var uu = 7; uu >= 0; uu--) {
|
|
data.push((chr & (1 << uu)) ? 255 : 0);
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function expand_rgb(num_arr) {
|
|
var i = 0;
|
|
var ret = [];
|
|
while (i < num_arr.length) {
|
|
x = num_arr[i];
|
|
i++;
|
|
if (x < 128) {
|
|
for (var u = i; u < i + x + 1; u++) { ret.push(num_arr[u]); }
|
|
i += x + 1;
|
|
} else {
|
|
for (var u = x - 128 + 3; u > 0; u--) { ret.push(num_arr[i]); }
|
|
i++;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
function make_image(data, channels) {
|
|
let scale = 2;
|
|
let per_channel = data.length / channels;
|
|
let orig_width = Math.sqrt(per_channel);
|
|
let width = orig_width * scale;
|
|
let height = width;
|
|
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
var ctx = canvas.getContext('2d');
|
|
let map;
|
|
switch (channels) {
|
|
case 1: map = [0, 0, 0, -1]; break;
|
|
case 2: map = [0, 0, 0, 1]; break;
|
|
case 3: map = [0, 1, 2, -1]; break;
|
|
case 4: map = [1, 2, 3, 0]; break;
|
|
}
|
|
|
|
for (var i = 0; i < per_channel; i++) {
|
|
let r = data[map[0] * per_channel + i];
|
|
let g = data[map[1] * per_channel + i];
|
|
let b = data[map[2] * per_channel + i];
|
|
let a = (map[3] == -1) ? 255 : data[map[3] * per_channel + i];
|
|
var imagedata = ctx.createImageData(scale, scale);
|
|
for (var idx = scale * scale * 4 - 4; idx >= 0; idx -= 4) {
|
|
imagedata.data[idx] = r;
|
|
imagedata.data[idx + 1] = g;
|
|
imagedata.data[idx + 2] = b;
|
|
imagedata.data[idx + 3] = a;
|
|
}
|
|
let y = Math.floor(i / orig_width);
|
|
let x = i - y * orig_width;
|
|
ctx.putImageData(imagedata, x * scale, y * scale);
|
|
}
|
|
return [canvas, orig_width];
|
|
}
|
|
|
|
|
|
// Entry point
|
|
|
|
function inspect_into(sender, dest) {
|
|
function fn(cls, hex, idx, len, tooltip) {
|
|
var tmp = '';
|
|
for (var u = idx; u < idx + len * 2; u += 2)
|
|
tmp += ' ' + hex[u] + hex[u+1];
|
|
|
|
let ttp = tooltip ? ' title="' + tooltip + '"' : '';
|
|
return `<span class="${cls}"${ttp}>${tmp.substring(1)}</span> `;
|
|
}
|
|
|
|
let output = document.getElementById(dest);
|
|
output.innerHTML = 'loading ...';
|
|
let src = sender.value.replace(/\s/g, '');
|
|
var txt = '';
|
|
for (let [head, i, len, ext] of parse_file(src)) {
|
|
txt += '<div>';
|
|
if (head) {
|
|
txt += '<h3 id="' + head + '">' + head + '</h3>';
|
|
txt += fn('head', src, i, 4, head);
|
|
txt += fn('len', src, i + 8, 4, 'len: ' + len);
|
|
} else {
|
|
txt += '<h3>raw data</h3>';
|
|
}
|
|
if (!ext) { txt += '</div>'; continue; } // top icns-header
|
|
|
|
let abbreviate;
|
|
if (['argb', 'rgb', null].indexOf(ext[0]) > -1) abbreviate = null;
|
|
else if (ext[0] == 'png') abbreviate = 'PNG data';
|
|
else if (ext[0] == 'plist') abbreviate = 'info.plist';
|
|
else if (ext[0] == 'icns') abbreviate = 'icns file';
|
|
else abbreviate = 'raw data';
|
|
if (abbreviate) {
|
|
txt += `<span class="data">... ${abbreviate}, ${len - 8} bytes ...</span>`;
|
|
txt += '</div>';
|
|
continue;
|
|
}
|
|
|
|
// parse unpacking
|
|
let is_argb = ext[0] == 'argb';
|
|
var u = ext[1];
|
|
if (is_argb || is_it32(ext[0], head, src.substring(u,u+8))) {
|
|
let title = (ext[0] == 'argb') ? 'ARGB' : 'it32-header';
|
|
txt += fn('head', src, u, 4, title);
|
|
u += 8;
|
|
}
|
|
var total = 0;
|
|
while (u < ext[2]) {
|
|
x = Number('0x'+src[u]+src[u+1]);
|
|
if (x < 128) {
|
|
txt += fn('ctrl', src, u, 1, 'Copy ' + (x + 1) + ' bytes');
|
|
txt += fn('data', src, u + 2, x + 1);
|
|
total += x + 1;
|
|
u += x * 2 + 4;
|
|
} else {
|
|
txt += fn('ctrl', src, u, 1, 'Repeat ' + (x - 128 + 3) + ' times');
|
|
txt += fn('data', src, u + 2, 1);
|
|
total += x - 128 + 3;
|
|
u += 4;
|
|
}
|
|
}
|
|
let w = Math.sqrt(total / (is_argb ? 4 : 3));
|
|
txt += '<p>Image size: ' + w + 'x' + w + '</p>';
|
|
txt += '</div>';
|
|
}
|
|
output.innerHTML = txt;
|
|
}
|
|
|
|
function put_images_into(sender, dest) {
|
|
function get_image_size(data, ext, i) {
|
|
if (ext == 'png') {
|
|
let w = get_length(data, i + 16 * 2);
|
|
let h = get_length(data, i + 20 * 2);
|
|
return [w, h];
|
|
}
|
|
if (ext == 'jp2') {
|
|
if (data.substring(i, i + 8).toUpperCase() == 'FF4FFF51') {
|
|
let w = get_length(data, i + 8 * 2);
|
|
let h = get_length(data, i + 12 * 2);
|
|
return [w, h];
|
|
}
|
|
let len_ftype = get_length(data, i + 12 * 2);
|
|
// file header + type box + header box (super box) + image header box
|
|
let offset = 12 + len_ftype + 8 + 8;
|
|
let h = get_length(data, i + offset * 2);
|
|
let w = get_length(data, i + offset * 2 + 8);
|
|
return [w, h];
|
|
}
|
|
}
|
|
function append_img_div(head, typ, w, h) {
|
|
let div = document.createElement('div');
|
|
var desc = typ || '?';
|
|
if (w) { desc += ': ' + w + 'x' + h; }
|
|
div.innerHTML = `<h3>${head || ''}</h3><p>${desc}</p>`;
|
|
output.appendChild(div);
|
|
return div;
|
|
}
|
|
let src = sender.value.replace(/\s/g, '');
|
|
let output = document.getElementById(dest);
|
|
output.innerHTML = '';
|
|
for (let [head, , , ext] of parse_file(src)) {
|
|
if (!ext) continue;
|
|
if (ext[0] == 'png' || ext[0] == 'jp2') {
|
|
let [w, h] = get_image_size(src, ext[0], ext[1]);
|
|
let img = append_img_div(head, ext[0], w, h);
|
|
img.innerHTML += `<img src="data:image/${ext[0]};base64,${btoa(as_str(src, ext[1], ext[2]))}" />`;
|
|
continue;
|
|
}
|
|
if (['argb', 'rgb', 'mask', 'iconmask', 'icon1b', null].indexOf(ext[0]) == -1) {
|
|
append_img_div(head, ext[0]).innerHTML += `<p>${(ext[2]-ext[1])/2} Bytes</p>`;
|
|
continue;
|
|
}
|
|
|
|
let num_arr = num_arr_from_hex(src.substring(ext[1], ext[2]));
|
|
let ch;
|
|
let data;
|
|
if (ext[0] == 'argb') {
|
|
ch = 4; data = expand_rgb(num_arr.slice(4));
|
|
} else if (ext[0] == 'mask') {
|
|
ch = 1; data = num_arr;
|
|
} else if (ext[0] == 'icon1b') {
|
|
ch = 1; data = msb_stream(num_arr);
|
|
} else if (ext[0] == 'iconmask') {
|
|
ch = 2; data = msb_stream(num_arr);
|
|
} else {
|
|
let it32 = is_it32(ext[0], head, num_arr.slice(0,4));
|
|
ch = 3; data = expand_rgb(it32 ? num_arr.slice(4) : num_arr);
|
|
}
|
|
let [img, w] = make_image(data, ch);
|
|
append_img_div(head, ext[0] || 'rgb', w, w).appendChild(img);
|
|
}
|
|
}
|