-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
179 lines (179 loc) · 6.48 KB
/
index.html
File metadata and controls
179 lines (179 loc) · 6.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Gif Tool</title>
<style>
#error:not(:empty) { font: bold larger; color: red; border: 3px solid red }
img { max-width:100%; max-height:500px }
input[type=range], input[type=number] { margin: 0 auto }
input[type=number] { width: 7ch }
</style>
<input type=file accept=".gif,image/gif">
<progress hidden></progress>
<div id="error"></div>
<canvas hidden></canvas>
<output><img></output>
<div id="index-selector" hidden>
<input type=range min=0 value=0>
<input type=number min=0 value=0>
</div>
<script>
function parseGIF(buffer) {
const images = [];
const res = new Uint8ClampedArray(buffer);
let context = '', i;
document.querySelector('#error').textContent = '';
const error = (s='error: unrecognized format.') => {
document.querySelector('#error').textContent = s;
if (i) document.querySelector('#error').textContent += ` position: ${i}`;
if (context) document.querySelector('#error').textContent += ` context: ${context}`;
};
if (! ['GIF87a','GIF89a'].includes(String.fromCodePoint(...res.subarray(0,6)))) return error();
const width = res[6] | res[7] << 8;
const height = res[8] | res[9] << 8;
const has_global_color_table = res[10] & 0b10000000;
const global_color_table_size = 1 << ((res[10] & 0b00000111) + 1);
const background_color_index = res[11];
const GCT_byte_size = has_global_color_table && global_color_table_size * 3;
const GCT = res.subarray(13, 13 + GCT_byte_size);
i = 13 + GCT_byte_size;
let transparency_flag, transparent_color_index;
while (true) {
if (res[i] === 0x2C) {
const image_left_position = res[i+1] | res[i+2] << 8;
const image_top_position = res[i+3] | res[i+4] << 8;
const image_width = res[i+5] | res[i+6] << 8;
const image_height = res[i+7] | res[i+8] << 8;
const has_local_color_table = res[i+9] & 0b10000000;
const interlace_flag = res[i+9] & 0b01000000;
const local_color_table_size = 1 << ((res[i+9] & 0b00000111) + 1);
i += 10;
const LCT_byte_size = has_local_color_table && local_color_table_size * 3;
const color_table = has_local_color_table ? res.subarray(i, i + LCT_byte_size) : GCT;
i += LCT_byte_size;
const LZW_minimum_code_size = res[i];
i += 1;
let size = 0;
for (let j = i; res[j]; j += 1 + res[j])
size += res[j];
const data = new Uint8ClampedArray(size);
for (let n = 0; res[i]; n += res[i], i += 1 + res[i])
data.set(res.subarray(i+1, i+1+res[i]), n);
let LZW_code_size = LZW_minimum_code_size + 1;
const clear_code = 1 << LZW_minimum_code_size;
const clip = new Uint8ClampedArray(image_width * image_height);
let clip_index = 0;
let bit_index = 0;
const table = new Array(clear_code + 2);
for (let j = 0; j < clear_code; ++j)
table[j] = [j];
let last_value;
while (true) {
const byte_index = bit_index >> 3;
if (byte_index > data.length) return error();
const bytes = data[byte_index] | data[byte_index + 1] << 8 | data[byte_index + 2] << 16;
const value = (bytes >> (bit_index & 7)) & ((1 << LZW_code_size) - 1);
bit_index += LZW_code_size;
if (value === clear_code) {
table.length = clear_code + 2;
LZW_code_size = LZW_minimum_code_size + 1;
} else if (value === clear_code + 1)
break;
else if (value <= table.length) {
if (last_value !== clear_code) {
const val = table[value === table.length ? last_value : value][0];
const temp = table[last_value].slice();
temp.push(val);
table.push(temp);
}
clip.set(table[value], clip_index);
clip_index += table[value].length;
} else return error();
if ((table.length === (1 << LZW_code_size)) && LZW_code_size < 12)
++LZW_code_size;
last_value = value;
}
const image = new Uint8ClampedArray(width * height * 4);
if (images[images.length - 1])
image.set(images[images.length - 1].data);
let row = image_top_position;
let col = image_left_position;
for (let i = 0; i < clip.length; ++i) {
if (! transparency_flag || clip[i] !== transparent_color_index) {
const pos = (row*width+col)*4;
image[pos] = color_table[clip[i]*3];
image[pos+1] = color_table[clip[i]*3+1];
image[pos+2] = color_table[clip[i]*3+2];
image[pos+3] = 0xFF;
}
++col;
if (col - image_left_position >= image_width) {
col = image_left_position;
++row;
}
}
images.push(new ImageData(image, width, height));
} else if (res[i] === 0x21) {
context = 'Extension Block';
if (res[i+1] === 0xF9) {
context = 'Graphic Control Extension';
i += 2;
if (res[i] !== 4) return error();
transparency_flag = res[i+1] & 0b00000001;
transparent_color_index = res[i+4];
i += 1 + res[i];
} else {
i += 2;
while (res[i]) i += 1 + res[i];
}
context = '';
} else if (res[i] === 0x3B)
break;
else if (res[i] == null)
return error('error: unrecognized format. File may be incomplete.');
else return error();
i += 1;
}
return images;
}
let images = [];
const canvas = document.querySelector('canvas');
function changeIndex(value) {
document.querySelector('input[type=range]').value = value;
document.querySelector('input[type=number]').value = value;
createImageBitmap(images[value]).then(bitmap => {
canvas.getContext('bitmaprenderer').transferFromImageBitmap(bitmap);
document.querySelector('output img').src = canvas.toDataURL();
});
}
document.addEventListener('dragover', (e) => { e.preventDefault(); });
document.addEventListener('drop', (e) => {
e.preventDefault();
changeFile(e.dataTransfer.files[0]);
});
document.querySelector('input[type=file]').addEventListener('change', (e) => {
changeFile(e.target.files[0]);
});
async function changeFile(file) {
const t0 = performance.now();
document.querySelector('progress').hidden = false;
const buffer = await file.arrayBuffer();
const t1 = performance.now();
images = parseGIF(buffer);
const t2 = performance.now();
document.querySelector('#index-selector').hidden = false;
canvas.width = images[0].width;
canvas.height = images[0].height;
changeIndex(0);
const t3 = performance.now();
for (const input of document.querySelectorAll('#index-selector input')) {
input.max = images.length - 1;
input.addEventListener('input', (e) => changeIndex(e.target.value));
}
const benchmark = document.createElement('div');
benchmark.textContent = `read: ${t1 - t0}ms parse: ${t2 - t1}ms show: ${t3 - t2}ms\n`;
benchmark.className = 'benchmark';
document.body.append(benchmark);
document.querySelector('progress').hidden = true;
}
</script>