2021-12-24 05:12:08 +00:00
|
|
|
import { buf } from "crc-32";
|
|
|
|
import { Buffer } from "buffer";
|
2022-01-05 01:14:23 +00:00
|
|
|
import type { ImageProcessor } from "./main";
|
2021-12-24 05:12:08 +00:00
|
|
|
|
2022-01-05 01:14:23 +00:00
|
|
|
type PNGChunk = [string, Buffer, number, number];
|
2021-12-24 05:12:08 +00:00
|
|
|
|
2022-01-05 01:14:23 +00:00
|
|
|
class PNGDecoder {
|
2021-12-24 05:12:08 +00:00
|
|
|
repr: Buffer;
|
2022-01-01 18:52:50 +00:00
|
|
|
|
2021-12-24 05:12:08 +00:00
|
|
|
req = 8;
|
2022-01-01 18:52:50 +00:00
|
|
|
|
2021-12-24 05:12:08 +00:00
|
|
|
ptr = 8;
|
|
|
|
|
|
|
|
constructor(private reader: ReadableStreamDefaultReader<Uint8Array>) {
|
|
|
|
this.repr = Buffer.from([]);
|
|
|
|
}
|
|
|
|
|
|
|
|
async catchup() {
|
|
|
|
while (this.repr.byteLength < this.req) {
|
2022-01-01 18:52:50 +00:00
|
|
|
const chunk = await this.reader.read();
|
2022-01-04 15:36:43 +00:00
|
|
|
if (chunk.done) {
|
|
|
|
throw new Error(`Unexpected EOF, got ${this.repr.byteLength}, required ${this.req}, ${chunk.value}`);
|
|
|
|
}
|
|
|
|
this.repr = Buffer.concat([this.repr, chunk.value]);
|
2021-12-24 05:12:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async *chunks() {
|
|
|
|
while (true) {
|
|
|
|
this.req += 8; // req length and name
|
|
|
|
await this.catchup();
|
2022-01-01 18:52:50 +00:00
|
|
|
const length = this.repr.readUInt32BE(this.ptr);
|
|
|
|
const name = this.repr.slice(this.ptr + 4, this.ptr + 8).toString();
|
2021-12-24 05:12:08 +00:00
|
|
|
this.ptr += 4;
|
|
|
|
this.req += length + 4; // crc
|
2022-01-04 16:37:17 +00:00
|
|
|
await this.catchup();
|
2022-01-04 15:36:43 +00:00
|
|
|
const pos = this.ptr;
|
2022-01-04 16:37:17 +00:00
|
|
|
yield [name, this.repr.slice(pos, pos + length + 4), this.repr.readUInt32BE(this.ptr + length + 4), this.ptr] as PNGChunk;
|
2021-12-24 05:12:08 +00:00
|
|
|
this.ptr += length + 8;
|
|
|
|
if (name == 'IEND')
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async dtor() {
|
2022-01-01 18:52:50 +00:00
|
|
|
//ugh
|
2021-12-24 05:12:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-05 01:14:23 +00:00
|
|
|
class PNGEncoder {
|
2021-12-24 05:12:08 +00:00
|
|
|
writer: WritableStreamDefaultWriter<Buffer>;
|
|
|
|
|
|
|
|
constructor(bytes: WritableStream<Buffer>) {
|
|
|
|
this.writer = bytes.getWriter();
|
|
|
|
this.writer.write(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]));
|
|
|
|
}
|
|
|
|
|
|
|
|
async insertchunk(chunk: PNGChunk) {
|
2022-01-01 18:52:50 +00:00
|
|
|
const b = Buffer.alloc(4);
|
2021-12-24 05:12:08 +00:00
|
|
|
b.writeInt32BE(chunk[1].length - 4, 0);
|
|
|
|
await this.writer.write(b); // write length
|
2022-01-04 16:37:17 +00:00
|
|
|
const buff = chunk[1];
|
2022-01-04 15:36:43 +00:00
|
|
|
await this.writer.write(buff); // chunk includes chunkname
|
|
|
|
b.writeInt32BE(buf(buff), 0);
|
2021-12-24 05:12:08 +00:00
|
|
|
await this.writer.write(b);
|
|
|
|
}
|
|
|
|
|
|
|
|
async dtor() {
|
|
|
|
this.writer.releaseLock();
|
|
|
|
await this.writer.close();
|
|
|
|
}
|
2022-01-01 18:52:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const CUM0 = Buffer.from("CUM\0" + "0");
|
|
|
|
|
2022-01-05 01:14:23 +00:00
|
|
|
const BufferReadStream = (b: Buffer) => {
|
2022-01-04 15:36:43 +00:00
|
|
|
const ret = new ReadableStream<Buffer>({
|
|
|
|
pull(cont) {
|
|
|
|
cont.enqueue(b);
|
|
|
|
cont.close();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return ret;
|
|
|
|
};
|
2022-01-01 18:52:50 +00:00
|
|
|
|
2022-01-05 01:14:23 +00:00
|
|
|
const extract = async (png: Buffer) => {
|
2022-01-04 15:36:43 +00:00
|
|
|
let magic = false;
|
|
|
|
const reader = BufferReadStream(png).getReader();
|
2022-01-01 18:52:50 +00:00
|
|
|
const sneed = new PNGDecoder(reader);
|
|
|
|
try {
|
|
|
|
let lastIDAT: Buffer | null = null;
|
|
|
|
for await (const [name, chunk, crc, offset] of sneed.chunks()) {
|
2022-01-04 15:36:43 +00:00
|
|
|
let buff: Buffer;
|
2022-01-01 18:52:50 +00:00
|
|
|
switch (name) {
|
|
|
|
// should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk
|
|
|
|
case 'tEXt':
|
2022-01-04 16:37:17 +00:00
|
|
|
buff = chunk;
|
2022-01-04 15:36:43 +00:00
|
|
|
if (buff.slice(4, 4 + CUM0.length).equals(CUM0))
|
2022-01-01 18:52:50 +00:00
|
|
|
magic = true;
|
|
|
|
break;
|
|
|
|
case 'IDAT':
|
|
|
|
if (magic) {
|
2022-01-04 16:37:17 +00:00
|
|
|
lastIDAT = chunk;
|
2022-01-01 18:52:50 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line no-fallthrough
|
|
|
|
case 'IEND':
|
|
|
|
if (!magic)
|
|
|
|
return; // Didn't find tExt Chunk;
|
|
|
|
// eslint-disable-next-line no-fallthrough
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (lastIDAT) {
|
|
|
|
let data = (lastIDAT as Buffer).slice(4);
|
|
|
|
const fnsize = data.readUInt32LE(0);
|
|
|
|
const fn = data.slice(4, 4 + fnsize).toString();
|
|
|
|
// Todo: xor the buffer to prevent scanning for file signatures (4chan embedded file detection)?
|
|
|
|
data = data.slice(4 + fnsize);
|
|
|
|
return { filename: fn, data };
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
} finally {
|
|
|
|
reader.releaseLock();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const buildChunk = (tag: string, data: Buffer) => {
|
|
|
|
const ret = Buffer.alloc(data.byteLength + 4);
|
|
|
|
ret.write(tag.substr(0, 4), 0);
|
|
|
|
data.copy(ret, 4);
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
2022-01-03 22:29:28 +00:00
|
|
|
export const BufferWriteStream = () => {
|
2022-01-01 18:52:50 +00:00
|
|
|
let b = Buffer.from([]);
|
|
|
|
const ret = new WritableStream<Buffer>({
|
|
|
|
write(chunk) {
|
2022-01-04 15:36:43 +00:00
|
|
|
b = Buffer.concat([b, chunk]);
|
2022-01-01 18:52:50 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
return [ret, () => b] as [WritableStream<Buffer>, () => Buffer];
|
|
|
|
};
|
|
|
|
|
2022-01-05 01:14:23 +00:00
|
|
|
const inject = async (container: File, inj: File) => {
|
2022-01-01 18:52:50 +00:00
|
|
|
const [writestream, extract] = BufferWriteStream();
|
|
|
|
const encoder = new PNGEncoder(writestream);
|
|
|
|
const decoder = new PNGDecoder(container.stream().getReader());
|
|
|
|
|
|
|
|
let magic = false;
|
|
|
|
for await (const [name, chunk, crc, offset] of decoder.chunks()) {
|
|
|
|
if (magic && name != "IDAT")
|
|
|
|
break;
|
|
|
|
if (!magic && name == "IDAT") {
|
2022-01-04 16:37:17 +00:00
|
|
|
await encoder.insertchunk(["tEXt", buildChunk("tEXt", CUM0), 0, 0]);
|
2022-01-01 18:52:50 +00:00
|
|
|
magic = true;
|
|
|
|
}
|
|
|
|
await encoder.insertchunk([name, chunk, crc, offset]);
|
|
|
|
}
|
|
|
|
const injb = Buffer.alloc(4 + inj.name.length + inj.size);
|
|
|
|
injb.writeInt32LE(inj.name.length, 0);
|
|
|
|
injb.write(inj.name, 4);
|
|
|
|
Buffer.from(await inj.arrayBuffer()).copy(injb, 4 + inj.name.length);
|
2022-01-04 16:37:17 +00:00
|
|
|
await encoder.insertchunk(["IDAT", buildChunk("IDAT", injb), 0, 0]);
|
|
|
|
await encoder.insertchunk(["IEND", buildChunk("IEND", Buffer.from([])), 0, 0]);
|
2022-01-01 18:52:50 +00:00
|
|
|
return extract();
|
2022-01-04 15:36:43 +00:00
|
|
|
};
|
|
|
|
|
2022-01-05 01:14:23 +00:00
|
|
|
const has_embed = async (png: Buffer) => {
|
2022-01-04 15:36:43 +00:00
|
|
|
const reader = BufferReadStream(png).getReader();
|
|
|
|
const sneed = new PNGDecoder(reader);
|
|
|
|
try {
|
|
|
|
for await (const [name, chunk, crc, offset] of sneed.chunks()) {
|
|
|
|
let buff: Buffer;
|
|
|
|
switch (name) {
|
|
|
|
// should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk
|
|
|
|
case 'tEXt':
|
2022-01-04 16:37:17 +00:00
|
|
|
buff = chunk;
|
2022-01-04 15:36:43 +00:00
|
|
|
if (buff.slice(4, 4 + CUM0.length).equals(CUM0)) {
|
|
|
|
return true;
|
|
|
|
} break;
|
|
|
|
case 'IDAT':
|
|
|
|
// eslint-disable-next-line no-fallthrough
|
|
|
|
case 'IEND':
|
|
|
|
return false; // Didn't find tExt Chunk; Definite no
|
|
|
|
// eslint-disable-next-line no-fallthrough
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// stream ended on chunk boundary, so no unexpected EOF was fired, need more data anyway
|
|
|
|
} catch (e) {
|
|
|
|
return; // possibly unexpected EOF, need more data to decide
|
|
|
|
} finally {
|
|
|
|
reader.releaseLock();
|
|
|
|
}
|
2022-01-05 01:14:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export default {
|
|
|
|
extract,
|
|
|
|
has_embed,
|
|
|
|
inject,
|
|
|
|
match: fn => !!fn.match(/\.png$/)
|
|
|
|
} as ImageProcessor;
|