Link to the Demo. It shows the first chapter of "My Wife is Forcing Herself to Smile", a sweet romantic comedy.
This does need to be hosted behind a web server to function, I use
Python's builtin one for testing locally.
python -m http.server 8080 works well enough for me.
The Readable Version
I took an hour and a half to see what I can do to rewrite the original program. I'd say it turned out very well.
<!DOCTYPE html>
<html>
<head></head>
<body>
<div id='image_list'></div>
</body>
<script>
function add_image(data) {
const blob = new Blob([data], {type: "image/png"});
image_element = document.createElement('img');
image_element.src = URL.createObjectURL(blob);
image_list.append(image_element);
}
// Reader.read tends to only fill part of the buffer at a time, so I have
// to repeatedly call it until I have enough of the data I wanted. Oh and
// the buffer passed to it needs to be carefully setup so it reads data in
// correctly.
// (or so I think, the standard is difficult to parse, but it works)
function read_data(src, how_many_bytes) {
let how_many_so_far = 0;
return src.read(new Uint8Array(how_many_bytes))
// a recursive function is easier to loop
// when the next data is ready with .then()
.then(function loop({done, value}) {
how_many_so_far += value.byteLength;
const how_many_left = how_many_bytes - how_many_so_far;
if (done || how_many_so_far == how_many_bytes) {
// this is the whole buffer, not the
// restricted range of "value".
return value.buffer;
} else {
// this creates a partial view of the buffer, so
// reader.read doesn't clobber the old data
const next = new Uint8Array(value.buffer,
how_many_so_far,
how_many_left);
return src.read(next)
.then(loop);
}
});
}
function read_numbers(source, how_many) {
return read_data(source, how_many * 4).then(
data => {
return new Uint32Array(data);
}
);
}
async function read_number(source) {
return (await read_numbers(source, 1))[0];
}
async function load(source) {
const number_of_images = await read_number(source);
const sizes_of_images = await read_numbers(source, number_of_images);
sizes_of_images.forEach(
size => {
read_data(source, size).then(add_image);
}
);
}
const file_path = (new URLSearchParams(window.location.search)).get('f');
fetch(file_path).then(({ok, body}) => {
if (ok) {
load(body.getReader({mode: "byob"}));
}
})
</script>
</html>
The Original, Outdated, but Newly Commented Code
It's not pretty, but it works.
<body>
<div id='image_list'></div>
</body>
<script>
function addImage(data) {
let blob = new Blob([data], {type: "image/png"});
image_element = document.createElement('img');
image_element.src = URL.createObjectURL(blob);
image_list.append(image_element);
}
// note, this originally used the tar archive format, but it proved a poor
// fit for this usecase.
// so instead I made a slim archive format, one that includes three things:
//
// first is 4 bytes which counts the number of images in the file.
// second is a list of (4 * number of images) bytes representing the size
// of each file in bytes.
// last is every file concatenated together.
//
// this proved very useful for slower connections, each image could be
// loaded and displayed one at a time.
async function parse(stream) {
// number of images in the file...
let value = await stream.readBytes(4);
// read in the list of sizes...
let indices = new Uint32Array(await stream.readBytes(
new Uint32Array(value)[0] * 4));
// then load each image one at a time...
async function eachImage(index) {
let data = new Uint8Array(await stream.readBytes(indices[index]));
// once the data is loaded, show it on the screen...
addImage(data);
if (index < indices.length - 1) { return eachImage(index + 1); }
} eachImage(0);
}
// I tried to clean this up, I failed
async function extractTar(archive) {
// callback hell
return fetch(archive)
.then(response => {
if (response.ok) {
let reader = response.body.getReader({mode: "byob"});
// readBytes is used to fetch a number of bytes and no less.
// for some reason I couldn't find anything similar builtin
// to the language itself, so I build it here as a stream
// "object" to keep the rest of the code clean.
// the closest I could find can give fewer bytes than
// requested.
return { readBytes(size) {
let offset = 0;
return reader.read(new Uint8Array(size))
.then(function recurse({done, value}) {
offset += value.byteLength;
if (done || size == offset) { return value.buffer; }
return reader.read(
new Uint8Array(value.buffer,
offset, size - offset))
.then(recurse);
})}
}
}
throw new Error('failed to load file');
})
.then(parse);
}
let uri = (new URLSearchParams(window.location.search)).get('f');
extractTar(uri);
</script>