Browse Source

streaming performance

pull/46/head
coomdev 2 years ago
parent
commit
348d3aae31
  1. 0
      .eslintrc.cjs
  2. 4
      README.md
  3. 2
      main.d.ts
  4. 2
      main.meta.js
  5. 735
      main.user.js
  6. 12
      src/App.svelte
  7. 25
      src/SettingsButton.svelte
  8. 67
      src/gif.ts
  9. 160
      src/main.ts
  10. 94
      src/png.ts
  11. 69
      src/requests.ts
  12. 39
      src/webm.ts

0
.eslintrc.js → .eslintrc.cjs

4
README.md

@ -1,7 +1,7 @@
PNG Extra Embedder (PEE)
========================
Can embed any file in a PNG/WebM and upload it through 4chanX.
Can embed any file in a PNG/WebM/GIF and upload it through 4chanX.
Requires 4chanX and violentmonkey.
How to Build
@ -53,4 +53,4 @@ Bugs
====
- fails to find files in new posts after a thread update
- more to come
- more to come

2
main.d.ts

@ -1,3 +1 @@
/* eslint-disable */
declare const GM_fetch = fetch;

2
main.meta.js

@ -1,7 +1,7 @@
// ==UserScript==
// @name PNGExtraEmbed
// @namespace https://coom.tech/
// @version 0.54
// @version 0.55
// @description uhh
// @author You
// @match https://boards.4channel.org/*

735
main.user.js

@ -1,7 +1,7 @@
// ==UserScript==
// @name PNGExtraEmbed
// @namespace https://coom.tech/
// @version 0.54
// @version 0.55
// @description uhh
// @author You
// @match https://boards.4channel.org/*
@ -2296,8 +2296,8 @@
}
return obj;
}
function _classCallCheck(instance2, Constructor) {
if (!(instance2 instanceof Constructor)) {
function _classCallCheck(instance3, Constructor) {
if (!(instance3 instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
@ -12300,7 +12300,10 @@
var supportedExtensions = new Set(extensions);
var supportedMimeTypes = new Set(mimeTypes);
// src/App.svelte
// src/stores.ts
init_esbuild_inject();
// node_modules/svelte/store/index.mjs
init_esbuild_inject();
// node_modules/svelte/internal/index.mjs
@ -12401,6 +12404,14 @@
function set_current_component(component) {
current_component = component;
}
function get_current_component() {
if (!current_component)
throw new Error("Function called outside component initialization");
return current_component;
}
function onDestroy(fn) {
get_current_component().$$.on_destroy.push(fn);
}
var dirty_components = [];
var binding_callbacks = [];
var render_callbacks = [];
@ -12499,7 +12510,7 @@
}
component.$$.dirty[i / 31 | 0] |= 1 << i % 31;
}
function init(component, options, instance2, create_fragment2, not_equal, props, append_styles2, dirty = [-1]) {
function init(component, options, instance3, create_fragment3, not_equal, props, append_styles2, dirty = [-1]) {
const parent_component = current_component;
set_current_component(component);
const $$ = component.$$ = {
@ -12522,7 +12533,7 @@
};
append_styles2 && append_styles2($$.root);
let ready = false;
$$.ctx = instance2 ? instance2(component, options.props || {}, (i, ret, ...rest) => {
$$.ctx = instance3 ? instance3(component, options.props || {}, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if (!$$.skip_bound && $$.bound[i])
@ -12535,7 +12546,7 @@
$$.update();
ready = true;
run_all($$.before_update);
$$.fragment = create_fragment2 ? create_fragment2($$.ctx) : false;
$$.fragment = create_fragment3 ? create_fragment3($$.ctx) : false;
if (options.target) {
if (options.hydrate) {
start_hydrating();
@ -12618,11 +12629,7 @@
}
};
// src/stores.ts
init_esbuild_inject();
// node_modules/svelte/store/index.mjs
init_esbuild_inject();
var subscriber_queue = [];
function writable(value, start = noop) {
let stop;
@ -12681,210 +12688,10 @@
localSet("settings", newVal);
});
// src/App.svelte
function add_css(target) {
append_styles(target, "svelte-6ot9e6", ".enabled.svelte-6ot9e6{display:block}.disabled.svelte-6ot9e6{display:none}.glow.svelte-6ot9e6{text-shadow:0 0 4px red}.clickable.svelte-6ot9e6{cursor:pointer}.content.svelte-6ot9e6{display:flex;flex-direction:column}hr.svelte-6ot9e6{width:100%}h1.svelte-6ot9e6{text-align:center}.backpanel.svelte-6ot9e6{position:absolute;right:32px;padding:10px;width:10%;top:32px;border:1px solid;border-radius:5px;background-color:rgba(0, 0, 0, 0.2)}.clickable.svelte-6ot9e6:hover{text-shadow:0 0 2px palevioletred}");
}
function create_fragment(ctx) {
let span;
let t1;
let div1;
let div0;
let h1;
let t3;
let hr;
let t4;
let label0;
let input0;
let t5;
let t6;
let label1;
let input1;
let t7;
let t8;
let label2;
let input2;
let t9;
let t10;
let label3;
let input3;
let t11;
let mounted;
let dispose;
return {
c() {
span = element("span");
span.textContent = "[PEE Settings]";
t1 = space();
div1 = element("div");
div0 = element("div");
h1 = element("h1");
h1.textContent = "PEE Settings";
t3 = space();
hr = element("hr");
t4 = space();
label0 = element("label");
input0 = element("input");
t5 = text("\n Autoplay Videos");
t6 = space();
label1 = element("label");
input1 = element("input");
t7 = text("\n Autoplay Audio");
t8 = space();
label2 = element("label");
input2 = element("input");
t9 = text("\n Autoexpand Images on opening.");
t10 = space();
label3 = element("label");
input3 = element("input");
t11 = text("\n Autoexpand Videos on opening.");
attr(span, "class", "clickable svelte-6ot9e6");
toggle_class(span, "glow", ctx[0]);
attr(h1, "class", "svelte-6ot9e6");
attr(hr, "class", "svelte-6ot9e6");
attr(input0, "type", "checkbox");
attr(input1, "type", "checkbox");
attr(input2, "type", "checkbox");
attr(input3, "type", "checkbox");
attr(div0, "class", "content svelte-6ot9e6");
attr(div1, "class", "backpanel svelte-6ot9e6");
toggle_class(div1, "enabled", ctx[0]);
toggle_class(div1, "disabled", !ctx[0]);
},
m(target, anchor) {
insert(target, span, anchor);
insert(target, t1, anchor);
insert(target, div1, anchor);
append(div1, div0);
append(div0, h1);
append(div0, t3);
append(div0, hr);
append(div0, t4);
append(div0, label0);
append(label0, input0);
input0.checked = ctx[1].apv;
append(label0, t5);
append(div0, t6);
append(div0, label1);
append(label1, input1);
input1.checked = ctx[1].apa;
append(label1, t7);
append(div0, t8);
append(div0, label2);
append(label2, input2);
input2.checked = ctx[1].xpi;
append(label2, t9);
append(div0, t10);
append(div0, label3);
append(label3, input3);
input3.checked = ctx[1].xpv;
append(label3, t11);
if (!mounted) {
dispose = [
listen(span, "click", ctx[3]),
listen(input0, "change", ctx[4]),
listen(input1, "change", ctx[5]),
listen(input2, "change", ctx[6]),
listen(input3, "change", ctx[7])
];
mounted = true;
}
},
p(ctx2, [dirty]) {
if (dirty & 1) {
toggle_class(span, "glow", ctx2[0]);
}
if (dirty & 2) {
input0.checked = ctx2[1].apv;
}
if (dirty & 2) {
input1.checked = ctx2[1].apa;
}
if (dirty & 2) {
input2.checked = ctx2[1].xpi;
}
if (dirty & 2) {
input3.checked = ctx2[1].xpv;
}
if (dirty & 1) {
toggle_class(div1, "enabled", ctx2[0]);
}
if (dirty & 1) {
toggle_class(div1, "disabled", !ctx2[0]);
}
},
i: noop,
o: noop,
d(detaching) {
if (detaching)
detach(span);
if (detaching)
detach(t1);
if (detaching)
detach(div1);
mounted = false;
run_all(dispose);
}
};
}
function instance($$self, $$props, $$invalidate) {
let $settings;
component_subscribe($$self, settings, ($$value) => $$invalidate(1, $settings = $$value));
let visible = false;
function opensettings() {
$$invalidate(0, visible = !visible);
}
console.log($settings);
const click_handler = () => opensettings();
function input0_change_handler() {
$settings.apv = this.checked;
settings.set($settings);
}
function input1_change_handler() {
$settings.apa = this.checked;
settings.set($settings);
}
function input2_change_handler() {
$settings.xpi = this.checked;
settings.set($settings);
}
function input3_change_handler() {
$settings.xpv = this.checked;
settings.set($settings);
}
return [
visible,
$settings,
opensettings,
click_handler,
input0_change_handler,
input1_change_handler,
input2_change_handler,
input3_change_handler
];
}
var App = class extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {}, add_css);
}
};
var App_default = App;
// src/png.ts
init_esbuild_inject();
var import_crc_32 = __toESM(require_crc32(), 1);
var import_buffer = __toESM(require_buffer(), 1);
var concatAB = (...bufs) => {
const sz = bufs.map((e) => e.byteLength).reduce((a, b) => a + b);
const ret = import_buffer.Buffer.alloc(sz);
let ptr = 0;
for (const b of bufs) {
b.copy(ret, ptr);
ptr += b.byteLength;
}
return ret;
};
var PNGDecoder = class {
constructor(reader) {
this.reader = reader;
@ -12895,9 +12702,10 @@
async catchup() {
while (this.repr.byteLength < this.req) {
const chunk = await this.reader.read();
if (chunk.done)
throw new Error("Unexpected EOF");
this.repr = concatAB(this.repr, import_buffer.Buffer.from(chunk.value));
if (chunk.done) {
throw new Error(`Unexpected EOF, got ${this.repr.byteLength}, required ${this.req}, ${chunk.value}`);
}
this.repr = import_buffer.Buffer.concat([this.repr, chunk.value]);
}
}
async *chunks() {
@ -12908,8 +12716,14 @@
const name = this.repr.slice(this.ptr + 4, this.ptr + 8).toString();
this.ptr += 4;
this.req += length + 4;
await this.catchup();
yield [name, this.repr.slice(this.ptr, this.ptr + length + 4), this.repr.readUInt32BE(this.ptr + length + 4), this.ptr];
const pos = this.ptr;
yield [name, async () => {
await this.catchup();
return this.repr.slice(pos, pos + length + 4);
}, async () => {
await this.catchup();
return this.repr.readUInt32BE(this.ptr + length + 4);
}, this.ptr];
this.ptr += length + 8;
if (name == "IEND")
break;
@ -12927,8 +12741,9 @@
const b = import_buffer.Buffer.alloc(4);
b.writeInt32BE(chunk[1].length - 4, 0);
await this.writer.write(b);
await this.writer.write(chunk[1]);
b.writeInt32BE((0, import_crc_32.buf)(chunk[1]), 0);
const buff = await chunk[1]();
await this.writer.write(buff);
b.writeInt32BE((0, import_crc_32.buf)(buff), 0);
await this.writer.write(b);
}
async dtor() {
@ -12937,20 +12752,32 @@
}
};
var CUM0 = import_buffer.Buffer.from("CUM\x000");
var extract = async (reader) => {
var BufferReadStream = (b) => {
const ret = new ReadableStream({
pull(cont) {
cont.enqueue(b);
cont.close();
}
});
return ret;
};
var extract = async (png) => {
let magic2 = false;
const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader);
try {
let lastIDAT = null;
for await (const [name, chunk, crc, offset] of sneed.chunks()) {
let buff;
switch (name) {
case "tEXt":
if (chunk.slice(4, 4 + CUM0.length).equals(CUM0))
buff = await chunk();
if (buff.slice(4, 4 + CUM0.length).equals(CUM0))
magic2 = true;
break;
case "IDAT":
if (magic2) {
lastIDAT = chunk;
lastIDAT = await chunk();
break;
}
case "IEND":
@ -12983,7 +12810,7 @@
let b = import_buffer.Buffer.from([]);
const ret = new WritableStream({
write(chunk) {
b = concatAB(b, chunk);
b = import_buffer.Buffer.concat([b, chunk]);
}
});
return [ret, () => b];
@ -12997,7 +12824,7 @@
if (magic2 && name != "IDAT")
break;
if (!magic2 && name == "IDAT") {
await encoder.insertchunk(["tEXt", buildChunk("tEXt", CUM0), 0, 0]);
await encoder.insertchunk(["tEXt", () => buildChunk("tEXt", CUM0), () => 0, 0]);
magic2 = true;
}
await encoder.insertchunk([name, chunk, crc, offset]);
@ -13006,10 +12833,36 @@
injb.writeInt32LE(inj.name.length, 0);
injb.write(inj.name, 4);
import_buffer.Buffer.from(await inj.arrayBuffer()).copy(injb, 4 + inj.name.length);
await encoder.insertchunk(["IDAT", buildChunk("IDAT", injb), 0, 0]);
await encoder.insertchunk(["IEND", buildChunk("IEND", import_buffer.Buffer.from([])), 0, 0]);
await encoder.insertchunk(["IDAT", () => buildChunk("IDAT", injb), () => 0, 0]);
await encoder.insertchunk(["IEND", () => buildChunk("IEND", import_buffer.Buffer.from([])), () => 0, 0]);
return extract4();
};
var has_embed = async (png) => {
const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader);
try {
for await (const [name, chunk, crc, offset] of sneed.chunks()) {
let buff;
switch (name) {
case "tEXt":
buff = await chunk();
if (buff.slice(4, 4 + CUM0.length).equals(CUM0)) {
return true;
}
break;
case "IDAT":
case "IEND":
return false;
default:
break;
}
}
} catch (e) {
return;
} finally {
reader.releaseLock();
}
};
// src/webm.ts
init_esbuild_inject();
@ -13085,7 +12938,7 @@
]);
return import_buffer2.Buffer.from(enc.encode(chunks.filter((e) => e.name != "unknown")));
};
var extractBuff = (webm) => {
var extract2 = (webm) => {
const dec = new ebml.Decoder();
const chunks = dec.decode(webm);
const embed2 = chunks.findIndex((e) => e.name == "TagName" && e.type == "8" && e.value == "COOM");
@ -13096,31 +12949,29 @@
return;
const chk = chunks[embed2 + 1];
if (chk.type == "b" && chk.name == "TagBinary")
return chk.data;
return { filename: "string", data: chk.data };
};
var extract2 = async (reader) => {
let total = import_buffer2.Buffer.from("");
let chunk;
do {
chunk = await reader.read();
if (chunk.value)
total = concatAB(total, import_buffer2.Buffer.from(chunk.value));
} while (!chunk.done);
const data = extractBuff(total);
if (!data)
var inject2 = async (container, inj) => embed(import_buffer2.Buffer.from(await container.arrayBuffer()), import_buffer2.Buffer.from(await inj.arrayBuffer()));
var has_embed2 = (webm) => {
const dec = new ebml.Decoder();
const chunks = dec.decode(webm);
const embed2 = chunks.findIndex((e) => e.name == "TagName" && e.type == "8" && e.value == "COOM");
const cl = chunks.find((e) => e.name == "Cluster");
if (cl && embed2 == -1)
return false;
if (embed2 == -1)
return;
return { filename: "embedded", data };
return true;
};
var inject2 = async (container, inj) => embed(import_buffer2.Buffer.from(await container.arrayBuffer()), import_buffer2.Buffer.from(await inj.arrayBuffer()));
// src/gif.ts
init_esbuild_inject();
var import_buffer3 = __toESM(require_buffer(), 1);
var netscape = import_buffer3.Buffer.from("!\xFF\vNETSCAPE2.0\0\0\0");
var netscape = import_buffer3.Buffer.from("!\xFF\vNETSCAPE2.0", "ascii");
var magic = import_buffer3.Buffer.from("!\xFF\vCOOMTECH0.1", "ascii");
var extractBuff2 = (gif) => {
let field = gif.readUInt8(10);
let gcte = !!(field & 1 << 7);
var extractBuff = (gif) => {
const field = gif.readUInt8(10);
const gcte = !!(field & 1 << 7);
let end = 13;
if (gcte) {
end += 3 * (1 << (field & 7) + 1);
@ -13128,8 +12979,8 @@
while (gif.readUInt8(end) == "!".charCodeAt(0)) {
if (magic.compare(gif, end, end + magic.byteLength) != 0) {
end += 3 + gif.readUInt8(end + 2);
while (1) {
let v = gif.readUInt8(end++);
while (true) {
const v = gif.readUInt8(end++);
if (!v)
break;
end += v;
@ -13142,7 +12993,7 @@
t += v;
count += v + 1;
}
let buff = import_buffer3.Buffer.alloc(t);
const buff = import_buffer3.Buffer.alloc(t);
count = end + magic.byteLength;
t = 0;
while ((v = gif.readUInt8(count)) != 0) {
@ -13150,23 +13001,11 @@
t += v;
count += v + 1;
}
return buff;
return { filename: "embedded", data: buff };
}
}
};
var extract3 = async (reader) => {
let total = import_buffer3.Buffer.from("");
let chunk;
do {
chunk = await reader.read();
if (chunk.value)
total = concatAB(total, import_buffer3.Buffer.from(chunk.value));
} while (!chunk.done);
const data = extractBuff2(total);
if (!data)
return;
return { filename: "embedded", data };
};
var extract3 = extractBuff;
var write_embedding = async (writer, inj) => {
await writer.write(magic);
const byte = import_buffer3.Buffer.from([0]);
@ -13187,9 +13026,9 @@
var inject3 = async (container, inj) => {
const [writestream, extract4] = BufferWriteStream();
const writer = writestream.getWriter();
let contbuff = import_buffer3.Buffer.from(await container.arrayBuffer());
let field = contbuff.readUInt8(10);
let gcte = !!(field & 1 << 7);
const contbuff = import_buffer3.Buffer.from(await container.arrayBuffer());
const field = contbuff.readUInt8(10);
const gcte = !!(field & 1 << 7);
let endo = 13;
if (gcte)
endo += 3 * (1 << (field & 7) + 1);
@ -13200,16 +13039,39 @@
await writer.write(contbuff.slice(endo));
return extract4();
};
// src/main.ts
var csettings;
settings.subscribe((b) => csettings = b);
var xmlhttprequest = typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : typeof GM != "undefined" ? GM.xmlHttpRequest : GM_xmlhttpRequest;
var headerStringToObject = (s) => Object.fromEntries(s.split("\n").map((e) => {
const [name, ...rest] = e.split(":");
return [name.toLowerCase(), rest.join(":").trim()];
}));
function GM_head(...[url, opt]) {
var has_embed3 = (gif) => {
const field = gif.readUInt8(10);
const gcte = !!(field & 1 << 7);
let end = 13;
if (gcte) {
end += 3 * (1 << (field & 7) + 1);
}
while (end < gif.byteLength && gif.readUInt8(end) == "!".charCodeAt(0)) {
if (magic.compare(gif, end, end + magic.byteLength) != 0) {
end += 3 + gif.readUInt8(end + 2);
while (true) {
const v = gif.readUInt8(end++);
if (!v)
break;
end += v;
}
} else {
return true;
}
}
if (end >= gif.byteLength)
return;
return false;
};
// src/requests.ts
init_esbuild_inject();
var xmlhttprequest = typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : typeof GM != "undefined" ? GM.xmlHttpRequest : GM_xmlhttpRequest;
var headerStringToObject = (s) => Object.fromEntries(s.split("\n").map((e) => {
const [name, ...rest] = e.split(":");
return [name.toLowerCase(), rest.join(":").trim()];
}));
function GM_head(...[url, opt]) {
return new Promise((resolve, reject) => {
const gmopt = {
url: url.toString(),
@ -13272,7 +13134,268 @@
xmlhttprequest(gmopt);
});
}
async function* streamRemote(url, chunkSize = 128 * 1024, fetchRestOnNonCanceled = true) {
// src/App.svelte
init_esbuild_inject();
// node_modules/svelte/index.mjs
init_esbuild_inject();
// src/App.svelte
function add_css(target) {
append_styles(target, "svelte-6ot9e6", ".enabled.svelte-6ot9e6{display:block}.disabled.svelte-6ot9e6{display:none}.glow.svelte-6ot9e6{text-shadow:0 0 4px red}.clickable.svelte-6ot9e6{cursor:pointer}.content.svelte-6ot9e6{display:flex;flex-direction:column}hr.svelte-6ot9e6{width:100%}h1.svelte-6ot9e6{text-align:center}.backpanel.svelte-6ot9e6{position:absolute;right:32px;padding:10px;width:10%;top:32px;border:1px solid;border-radius:5px;background-color:rgba(0, 0, 0, 0.2)}.clickable.svelte-6ot9e6:hover{text-shadow:0 0 2px palevioletred}");
}
function create_fragment(ctx) {
let span;
let t1;
let div1;
let div0;
let h1;
let t3;
let hr;
let t4;
let label0;
let input0;
let t5;
let t6;
let label1;
let input1;
let t7;
let t8;
let label2;
let input2;
let t9;
let t10;
let label3;
let input3;
let t11;
let mounted;
let dispose;
return {
c() {
span = element("span");
span.textContent = "[PEE Settings]";
t1 = space();
div1 = element("div");
div0 = element("div");
h1 = element("h1");
h1.textContent = "PEE Settings";
t3 = space();
hr = element("hr");
t4 = space();
label0 = element("label");
input0 = element("input");
t5 = text("\n Autoplay Videos");
t6 = space();
label1 = element("label");
input1 = element("input");
t7 = text("\n Autoplay Audio");
t8 = space();
label2 = element("label");
input2 = element("input");
t9 = text("\n Autoexpand Images on opening.");
t10 = space();
label3 = element("label");
input3 = element("input");
t11 = text("\n Autoexpand Videos on opening.");
attr(span, "class", "clickable svelte-6ot9e6");
toggle_class(span, "glow", ctx[0]);
attr(h1, "class", "svelte-6ot9e6");
attr(hr, "class", "svelte-6ot9e6");
attr(input0, "type", "checkbox");
attr(input1, "type", "checkbox");
attr(input2, "type", "checkbox");
attr(input3, "type", "checkbox");
attr(div0, "class", "content svelte-6ot9e6");
attr(div1, "class", "backpanel svelte-6ot9e6");
toggle_class(div1, "enabled", ctx[0]);
toggle_class(div1, "disabled", !ctx[0]);
},
m(target, anchor) {
insert(target, span, anchor);
insert(target, t1, anchor);
insert(target, div1, anchor);
append(div1, div0);
append(div0, h1);
append(div0, t3);
append(div0, hr);
append(div0, t4);
append(div0, label0);
append(label0, input0);
input0.checked = ctx[1].apv;
append(label0, t5);
append(div0, t6);
append(div0, label1);
append(label1, input1);
input1.checked = ctx[1].apa;
append(label1, t7);
append(div0, t8);
append(div0, label2);
append(label2, input2);
input2.checked = ctx[1].xpi;
append(label2, t9);
append(div0, t10);
append(div0, label3);
append(label3, input3);
input3.checked = ctx[1].xpv;
append(label3, t11);
if (!mounted) {
dispose = [
listen(span, "click", ctx[3]),
listen(input0, "change", ctx[4]),
listen(input1, "change", ctx[5]),
listen(input2, "change", ctx[6]),
listen(input3, "change", ctx[7])
];
mounted = true;
}
},
p(ctx2, [dirty]) {
if (dirty & 1) {
toggle_class(span, "glow", ctx2[0]);
}
if (dirty & 2) {
input0.checked = ctx2[1].apv;
}
if (dirty & 2) {
input1.checked = ctx2[1].apa;
}
if (dirty & 2) {
input2.checked = ctx2[1].xpi;
}
if (dirty & 2) {
input3.checked = ctx2[1].xpv;
}
if (dirty & 1) {
toggle_class(div1, "enabled", ctx2[0]);
}
if (dirty & 1) {
toggle_class(div1, "disabled", !ctx2[0]);
}
},
i: noop,
o: noop,
d(detaching) {
if (detaching)
detach(span);
if (detaching)
detach(t1);
if (detaching)
detach(div1);
mounted = false;
run_all(dispose);
}
};
}
function instance($$self, $$props, $$invalidate) {
let $settings;
component_subscribe($$self, settings, ($$value) => $$invalidate(1, $settings = $$value));
let visible = false;
function opensettings() {
$$invalidate(0, visible = !visible);
}
let penisEvent = () => {
$$invalidate(0, visible = !visible);
};
document.addEventListener("penis", penisEvent);
onDestroy(() => {
document.removeEventListener("penis", penisEvent);
});
const click_handler = () => opensettings();
function input0_change_handler() {
$settings.apv = this.checked;
settings.set($settings);
}
function input1_change_handler() {
$settings.apa = this.checked;
settings.set($settings);
}
function input2_change_handler() {
$settings.xpi = this.checked;
settings.set($settings);
}
function input3_change_handler() {
$settings.xpv = this.checked;
settings.set($settings);
}
return [
visible,
$settings,
opensettings,
click_handler,
input0_change_handler,
input1_change_handler,
input2_change_handler,
input3_change_handler
];
}
var App = class extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {}, add_css);
}
};
var App_default = App;
// src/SettingsButton.svelte
init_esbuild_inject();
function add_css2(target) {
append_styles(target, "svelte-101vs6b", ".glow.svelte-101vs6b{text-shadow:0 0 4px red}.clickable.svelte-101vs6b{cursor:pointer}.clickable.svelte-101vs6b:hover{text-shadow:0 0 2px palevioletred}");
}
function create_fragment2(ctx) {
let span;
let mounted;
let dispose;
return {
c() {
span = element("span");
span.textContent = "[PEE Settings]";
attr(span, "class", "clickable svelte-101vs6b");
toggle_class(span, "glow", ctx[0]);
},
m(target, anchor) {
insert(target, span, anchor);
if (!mounted) {
dispose = listen(span, "click", ctx[2]);
mounted = true;
}
},
p(ctx2, [dirty]) {
if (dirty & 1) {
toggle_class(span, "glow", ctx2[0]);
}
},
i: noop,
o: noop,
d(detaching) {
if (detaching)
detach(span);
mounted = false;
dispose();
}
};
}
function instance2($$self, $$props, $$invalidate) {
"use strict";
let visible = false;
function opensettings() {
$$invalidate(0, visible = !visible);
}
const click_handler = () => opensettings();
return [visible, opensettings, click_handler];
}
var SettingsButton = class extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance2, create_fragment2, safe_not_equal, {}, add_css2);
}
};
var SettingsButton_default = SettingsButton;
// src/main.ts
var csettings;
settings.subscribe((b) => csettings = b);
async function* streamRemote(url, chunkSize = 16 * 1024, fetchRestOnNonCanceled = true) {
const headers = await GM_head(url);
const h = headerStringToObject(headers);
const size = +h["content-length"];
@ -13281,41 +13404,52 @@
while (ptr != size) {
const res = await GM_fetch(url, { headers: { range: `bytes=${ptr}-${ptr + fetchSize - 1}` } });
const obj = headerStringToObject(res.responseHeaders);
if (!("content-length" in obj))
return;
if (!("content-length" in obj)) {
console.warn("no content lenght???", url);
break;
}
const len = +obj["content-length"];
ptr += len;
if (fetchRestOnNonCanceled)
fetchSize = size;
yield import_buffer4.Buffer.from(await res.arrayBuffer());
}
}
function iteratorToStream(iterator) {
return new ReadableStream({
async pull(controller) {
const { value, done } = await iterator.next();
if (done) {
controller.close();
} else {
controller.enqueue(value);
}
const val = import_buffer4.Buffer.from(await res.arrayBuffer());
const e = yield val;
if (e) {
break;
}
});
}
}
var processors = [
[/\.png$/, extract, inject],
[/\.webm$/, extract2, inject2],
[/\.gif$/, extract3, inject3]
[/\.png$/, has_embed, extract, inject],
[/\.webm$/, has_embed2, extract2, inject2],
[/\.gif$/, has_embed3, extract3, inject3]
];
var processImage = async (src) => {
const proc = processors.find((e) => src.match(e[0]));
if (!proc)
return;
const iter = streamRemote(src);
const reader = iteratorToStream(iter);
if (!reader)
if (!iter)
return;
return await proc[1](reader.getReader());
let cumul = import_buffer4.Buffer.alloc(0);
let found;
let chunk = { done: true };
do {
const { value, done } = await iter.next(found === false);
if (done) {
chunk = { done: true };
} else {
chunk = { done: false, value };
}
if (!done)
cumul = import_buffer4.Buffer.concat([cumul, value]);
found = await proc[1](cumul);
} while (found !== false && !chunk.done);
await iter.next(false);
if (found === false) {
return;
}
return await proc[2](cumul);
};
var textToElement = (s) => document.createRange().createContextualFragment(s).children[0];
var processPost = async (post) => {
@ -13448,9 +13582,9 @@
var startup = async () => {
await Promise.all([...document.querySelectorAll(".postContainer")].filter((e) => e.textContent?.includes("191 KB")).map((e) => processPost(e)));
document.addEventListener("ThreadUpdate", async (e) => {
let newPosts = e.detail.newPosts;
const newPosts = e.detail.newPosts;
for (const post of newPosts) {
let postContainer = document.getElementById("pc" + post.substring(post.indexOf(".") + 1));
const postContainer = document.getElementById("pc" + post.substring(post.indexOf(".") + 1));
processPost(postContainer);
}
});
@ -13473,10 +13607,13 @@
const posts = [...document.querySelectorAll(".postContainer")];
const scts = document.getElementById("shortcuts");
const button = textToElement(`<span></span>`);
const app = new App_default({
const settingsButton = new SettingsButton_default({
target: button
});
scts?.appendChild(button);
const appHost = textToElement(`<div class="pee-settings"></div>`);
const appInstance = new App_default({ target: appHost });
document.body.append(appHost);
await Promise.all(posts.map((e) => processPost(e)));
};
var getSelectedFile = () => {
@ -13508,7 +13645,7 @@
const proc = processors.find((e3) => file.name.match(e3[0]));
if (!proc)
throw new Error("Container filetype not supported");
const buff = await proc[2](file, input.files[0]);
const buff = await proc[3](file, input.files[0]);
document.dispatchEvent(new CustomEvent("QRSetFile", {
detail: { file: new Blob([buff], { type }), name: file.name }
}));

12
src/App.svelte

@ -1,10 +1,20 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { settings } from './stores'
let visible = false
function opensettings() {
visible = !visible
}
console.log($settings)
let penisEvent = () => {
visible = !visible;
}
document.addEventListener('penis', penisEvent);
onDestroy(() => {
document.removeEventListener('penis', penisEvent);
});
</script>
<span class="clickable" class:glow={visible} on:click={() => opensettings()}>

25
src/SettingsButton.svelte

@ -0,0 +1,25 @@
<script lang="ts">
let visible = false;
function opensettings() {
visible = !visible
}
</script>
<span class="clickable" class:glow={visible} on:click={() => opensettings()}>
[PEE Settings]
</span>
<style scoped>
.glow {
text-shadow: 0 0 4px red;
}
.clickable {
cursor: pointer;
}
.clickable:hover {
text-shadow: 0 0 2px palevioletred;
}
</style>

67
src/gif.ts

@ -1,12 +1,12 @@
import { Buffer } from "buffer";
import { BufferWriteStream, concatAB } from "./png";
import { BufferWriteStream } from "./png";
const netscape = Buffer.from("!\xFF\x0BNETSCAPE2.0\x03\x01\x00\x00\x00");
const netscape = Buffer.from("!\xFF\x0BNETSCAPE2.0", 'ascii');
const magic = Buffer.from("!\xFF\x0B" + "COOMTECH0.1", 'ascii');
const extractBuff = (gif: Buffer) => {
let field = gif.readUInt8(10);
let gcte = !!(field & (1 << 7));
const field = gif.readUInt8(10);
const gcte = !!(field & (1 << 7));
let end = 13;
if (gcte) {
end += 3 * (1 << ((field & 7) + 1));
@ -15,8 +15,9 @@ const extractBuff = (gif: Buffer) => {
while (gif.readUInt8(end) == '!'.charCodeAt(0)) {
if (magic.compare(gif, end, end + magic.byteLength) != 0) {
end += 3 + gif.readUInt8(end + 2);
while (1) { // skip sub blocks
let v = gif.readUInt8(end++);
// eslint-disable-next-line no-constant-condition
while (true) { // skip sub blocks
const v = gif.readUInt8(end++);
if (!v)
break;
end += v;
@ -29,34 +30,21 @@ const extractBuff = (gif: Buffer) => {
t += v;
count += v + 1;
}
let buff = Buffer.alloc(t);
const buff = Buffer.alloc(t);
count = end + magic.byteLength;
t = 0;
while ((v = gif.readUInt8(count)) != 0) {
gif.copy(buff, t, count + 1, count + 1 + v)
gif.copy(buff, t, count + 1, count + 1 + v);
t += v;
count += v + 1;
}
return buff;
return {filename: 'embedded', data: buff};
}
}
// metadata ended, nothing...
};
export const extract = async (reader: ReadableStreamDefaultReader<Uint8Array>): Promise<{ filename: string; data: Buffer } | undefined> => {
let total = Buffer.from('');
let chunk: ReadableStreamDefaultReadResult<Uint8Array>;
// todo: early reject
do {
chunk = await reader.read();
if (chunk.value)
total = concatAB(total, Buffer.from(chunk.value));
} while (!chunk.done);
const data = extractBuff(total);
if (!data)
return;
return { filename: 'embedded', data };
};
export const extract = extractBuff;
const write_embedding = async (writer: WritableStreamDefaultWriter<Buffer>, inj: Buffer) => {
await writer.write(magic);
@ -80,10 +68,10 @@ export const inject = async (container: File, inj: File) => {
const [writestream, extract] = BufferWriteStream();
const writer = writestream.getWriter();
let contbuff = Buffer.from(await container.arrayBuffer());
const contbuff = Buffer.from(await container.arrayBuffer());
let field = contbuff.readUInt8(10);
let gcte = !!(field & (1 << 0x7))
const field = contbuff.readUInt8(10);
const gcte = !!(field & (1 << 0x7));
let endo = 13;
if (gcte)
endo += 3 * (1 << ((field & 7) + 1));
@ -94,4 +82,31 @@ export const inject = async (container: File, inj: File) => {
await write_embedding(writer, Buffer.from(await inj.arrayBuffer()));
await writer.write(contbuff.slice(endo));
return extract();
};
export const has_embed = (gif: Buffer) => {
const field = gif.readUInt8(10);
const gcte = !!(field & (1 << 7));
let end = 13;
if (gcte) {
end += 3 * (1 << ((field & 7) + 1));
}
// skip beeg blocks
while (end < gif.byteLength && gif.readUInt8(end) == '!'.charCodeAt(0)) {
if (magic.compare(gif, end, end + magic.byteLength) != 0) {
end += 3 + gif.readUInt8(end + 2);
// eslint-disable-next-line no-constant-condition
while (true) { // skip sub blocks
const v = gif.readUInt8(end++);
if (!v)
break;
end += v;
}
} else {
return true;
}
}
if (end >= gif.byteLength)
return; // Don't know yet, need more to decide.
return false; // no more extension blocks, so definite no
};

160
src/main.ts

@ -1,89 +1,21 @@
import { Buffer } from "buffer";
import { fileTypeFromBuffer } from 'file-type';
import App from "./App.svelte";
import { settings } from "./stores";
import * as png from "./png";
import * as webm from "./webm";
import * as gif from "./gif";
let csettings: any;
settings.subscribe(b => csettings = b);
type Awaited<T> = T extends PromiseLike<infer U> ? U : T
const xmlhttprequest = typeof GM_xmlhttpRequest != 'undefined' ? GM_xmlhttpRequest : (typeof GM != "undefined" ? GM.xmlHttpRequest : GM_xmlhttpRequest);
const headerStringToObject = (s: string) =>
Object.fromEntries(s.split('\n').map(e => {
const [name, ...rest] = e.split(':');
return [name.toLowerCase(), rest.join(':').trim()];
}));
import { GM_fetch, GM_head, headerStringToObject } from "./requests";
function GM_head(...[url, opt]: Parameters<typeof fetch>) {
return new Promise<string>((resolve, reject) => {
// https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
const gmopt: Tampermonkey.Request<any> = {
url: url.toString(),
data: opt?.body?.toString(),
method: "HEAD",
onload: (resp) => {
resolve(resp.responseHeaders);
},
ontimeout: () => reject("fetch timeout"),
onerror: () => reject("fetch error"),
onabort: () => reject("fetch abort")
};
xmlhttprequest(gmopt);
});
}
import App from "./App.svelte";
import SettingsButton from './SettingsButton.svelte';
function GM_fetch(...[url, opt]: Parameters<typeof fetch>) {
function blobTo(to: string, blob: Blob) {
if (to == "arrayBuffer" && blob.arrayBuffer)
return blob.arrayBuffer();
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = function (event) {
if (!event) return;
if (to == "base64")
resolve(event.target!.result);
else
resolve(event.target!.result);
};
if (to == "arrayBuffer") fileReader.readAsArrayBuffer(blob);
else if (to == "base64") fileReader.readAsDataURL(blob); // "data:*/*;base64,......"
else if (to == "text") fileReader.readAsText(blob, "utf-8");
else reject("unknown to");
});
}
return new Promise<ReturnType<typeof fetch>>((resolve, reject) => {
// https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
const gmopt: Tampermonkey.Request<any> = {
url: url.toString(),
data: opt?.body?.toString(),
responseType: "blob",
headers: opt?.headers as any,
method: "GET",
onload: (resp) => {
const blob = resp.response as Blob;
const ref = resp as any as Awaited<ReturnType<typeof fetch>>;
ref.blob = () => Promise.resolve(blob);
ref.arrayBuffer = () => blobTo("arrayBuffer", blob) as Promise<ArrayBuffer>;
ref.text = () => blobTo("text", blob) as Promise<string>;
ref.json = async () => JSON.parse(await (blobTo("text", blob) as Promise<any>));
resolve(resp as any);
},
ontimeout: () => reject("fetch timeout"),
onerror: () => reject("fetch error"),
onabort: () => reject("fetch abort")
};
xmlhttprequest(gmopt);
});
}
let csettings: any;
settings.subscribe(b => csettings = b);
async function* streamRemote(url: string, chunkSize = 128 * 1024, fetchRestOnNonCanceled = true) {
// most pngs are encoded with 65k idat chunks
async function* streamRemote(url: string, chunkSize = 16 * 1024, fetchRestOnNonCanceled = true) {
const headers = await GM_head(url);
const h = headerStringToObject(headers);
const size = +h['content-length'];
@ -92,49 +24,59 @@ async function* streamRemote(url: string, chunkSize = 128 * 1024, fetchRestOnNon
while (ptr != size) {
const res = await GM_fetch(url, { headers: { range: `bytes=${ptr}-${ptr + fetchSize - 1}` } }) as any as Tampermonkey.Response<any>;
const obj = headerStringToObject(res.responseHeaders);
if (!('content-length' in obj))
return;
const len = +obj['content-length'];
if (!('content-length' in obj)) {
console.warn("no content lenght???", url);
break;
} const len = +obj['content-length'];
ptr += len;
if (fetchRestOnNonCanceled)
fetchSize = size;
yield Buffer.from(await (res as any).arrayBuffer());
const val = Buffer.from(await (res as any).arrayBuffer());
const e = (yield val) as boolean;
if (e) {
break;
}
}
//console.log("streaming ended, ", ptr, size);
}
function iteratorToStream<T>(iterator: AsyncGenerator<T>) {
return new ReadableStream<T>({
async pull(controller) {
const { value, done } = await iterator.next();
if (done) {
controller.close();
} else {
controller.enqueue(value);
}
},
});
}
type EmbeddedFile = { filename: string; data: Buffer };
const processors: [RegExp,
(reader: ReadableStreamDefaultReader<Uint8Array>) => Promise<{ filename: string; data: Buffer } | undefined>,
(b: Buffer) => (boolean | undefined) | Promise<boolean | undefined>,
(b: Buffer) => EmbeddedFile | undefined | Promise<EmbeddedFile | undefined>,
(container: File, inj: File) => Promise<Buffer>][] = [
[/\.png$/, png.extract, png.inject],
[/\.webm$/, webm.extract, webm.inject],
[/\.gif$/, gif.extract, gif.inject],
[/\.png$/, png.has_embed, png.extract, png.inject],
[/\.webm$/, webm.has_embed, webm.extract, webm.inject],
[/\.gif$/, gif.has_embed, gif.extract, gif.inject],
];
const processImage = async (src: string) => {
const proc = processors.find(e => src.match(e[0]));
if (!proc)
return;
// const resp = await GM_fetch(src);
// const reader = resp.body;
const iter = streamRemote(src);
const reader = iteratorToStream(iter);
if (!reader)
if (!iter)
return;
let cumul = Buffer.alloc(0);
let found: boolean | undefined;
let chunk: ReadableStreamDefaultReadResult<Buffer> = { done: true };
do {
const { value, done } = await iter.next(found === false);
if (done) {
chunk = { done: true } as ReadableStreamDefaultReadDoneResult;
} else {
chunk = { done: false, value } as ReadableStreamDefaultReadValueResult<Buffer>;
}
if (!done)
cumul = Buffer.concat([cumul, value!]);
found = await proc[1](cumul);
} while (found !== false && !chunk.done);
await iter.next(false);
if (found === false) {
//console.log(`Gave up on ${src} after downloading ${cumul.byteLength} bytes...`);
return;
return await proc[1](reader.getReader());
}
return await proc[2](cumul);
};
const textToElement = <T = HTMLElement>(s: string) =>
@ -289,13 +231,14 @@ const startup = async () => {
// Basically this is a misnommer: fires even when inlining existings posts, also posts are inlined through some kind of dom projection
document.addEventListener('ThreadUpdate', <any>(async (e: CustomEvent<any>) => {
let newPosts = e.detail.newPosts;
const newPosts = e.detail.newPosts;
for (const post of newPosts) {
let postContainer = document.getElementById("pc" + post.substring(post.indexOf(".") + 1)) as HTMLDivElement;
const postContainer = document.getElementById("pc" + post.substring(post.indexOf(".") + 1)) as HTMLDivElement;
processPost(postContainer);
}
}));
// keep this to handle posts getting inlined
const mo = new MutationObserver(reco => {
for (const rec of reco)
if (rec.type == "childList")
@ -318,11 +261,15 @@ const startup = async () => {
const scts = document.getElementById('shortcuts');
const button = textToElement(`<span></span>`);
const app = new App({
const settingsButton = new SettingsButton({
target: button
});
scts?.appendChild(button);
const appHost = textToElement(`<div class="pee-settings"></div>`);
const appInstance = new App({ target: appHost });
document.body.append(appHost);
await Promise.all(posts.map(e => processPost(e as any)));
};
@ -332,6 +279,7 @@ const getSelectedFile = () => {
document.dispatchEvent(new CustomEvent('QRGetFile'));
});
};
document.addEventListener('4chanXInitFinished', startup);
document.addEventListener('QRDialogCreation', <any>((e: CustomEvent<string>) => {
@ -356,7 +304,7 @@ document.addEventListener('QRDialogCreation', <any>((e: CustomEvent<string>) =>
const proc = processors.find(e => file.name.match(e[0]));
if (!proc)
throw new Error("Container filetype not supported");
const buff = await proc[2](file, input.files[0]);
const buff = await proc[3](file, input.files[0]);
document.dispatchEvent(new CustomEvent('QRSetFile', {
//detail: { file: new Blob([buff]), name: file.name, type: file.type }
detail: { file: new Blob([buff], { type }), name: file.name }

94
src/png.ts

@ -1,18 +1,7 @@
import { buf } from "crc-32";
import { Buffer } from "buffer";
export const concatAB = (...bufs: Buffer[]) => {
const sz = bufs.map(e => e.byteLength).reduce((a, b) => a + b);
const ret = Buffer.alloc(sz);
let ptr = 0;
for (const b of bufs) {
b.copy(ret, ptr);
ptr += b.byteLength;
}
return ret;
};
export type PNGChunk = [string, Buffer, number, number];
export type PNGChunk = [string, () => Buffer | Promise<Buffer>, () => number | Promise<number>, number];
export class PNGDecoder {
repr: Buffer;
@ -28,9 +17,10 @@ export class PNGDecoder {
async catchup() {
while (this.repr.byteLength < this.req) {
const chunk = await this.reader.read();
if (chunk.done)
throw new Error("Unexpected EOF");
this.repr = concatAB(this.repr, Buffer.from(chunk.value));
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]);
}
}
@ -42,8 +32,15 @@ export class PNGDecoder {
const name = this.repr.slice(this.ptr + 4, this.ptr + 8).toString();
this.ptr += 4;
this.req += length + 4; // crc
await this.catchup();
yield [name, this.repr.slice(this.ptr, this.ptr + length + 4 /* chunkname included in buffer for easier crc fixup */), this.repr.readUInt32BE(this.ptr + length + 4), this.ptr] as PNGChunk;
//await this.catchup();
const pos = this.ptr;
yield [name, async () => {
await this.catchup();
return this.repr.slice(pos, pos + length + 4);
}, async () => {
await this.catchup();
return this.repr.readUInt32BE(this.ptr + length + 4);
}, this.ptr] as PNGChunk;
this.ptr += length + 8;
if (name == 'IEND')
break;
@ -67,8 +64,9 @@ export class PNGEncoder {
const b = Buffer.alloc(4);
b.writeInt32BE(chunk[1].length - 4, 0);
await this.writer.write(b); // write length
await this.writer.write(chunk[1]); // chunk includes chunkname
b.writeInt32BE(buf(chunk[1]), 0);
const buff = await chunk[1]();
await this.writer.write(buff); // chunk includes chunkname
b.writeInt32BE(buf(buff), 0);
await this.writer.write(b);
}
@ -80,22 +78,34 @@ export class PNGEncoder {
const CUM0 = Buffer.from("CUM\0" + "0");
export const extract = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
let magic = false;
export const BufferReadStream = (b: Buffer) => {
const ret = new ReadableStream<Buffer>({
pull(cont) {
cont.enqueue(b);
cont.close();
}
});
return ret;
};
export const extract = async (png: Buffer) => {
let magic = false;
const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader);
try {
let lastIDAT: Buffer | null = null;
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':
if (chunk.slice(4, 4 + CUM0.length).equals(CUM0))
buff = await chunk();
if (buff.slice(4, 4 + CUM0.length).equals(CUM0))
magic = true;
break;
case 'IDAT':
if (magic) {
lastIDAT = chunk;
lastIDAT = await chunk();
break;
}
// eslint-disable-next-line no-fallthrough
@ -133,7 +143,7 @@ export const BufferWriteStream = () => {
let b = Buffer.from([]);
const ret = new WritableStream<Buffer>({
write(chunk) {
b = concatAB(b, chunk);
b = Buffer.concat([b, chunk]);
}
});
return [ret, () => b] as [WritableStream<Buffer>, () => Buffer];
@ -149,7 +159,7 @@ export const inject = async (container: File, inj: File) => {
if (magic && name != "IDAT")
break;
if (!magic && name == "IDAT") {
await encoder.insertchunk(["tEXt", buildChunk("tEXt", CUM0), 0, 0]);
await encoder.insertchunk(["tEXt", () => buildChunk("tEXt", CUM0), () => 0, 0]);
magic = true;
}
await encoder.insertchunk([name, chunk, crc, offset]);
@ -158,7 +168,37 @@ export const inject = async (container: File, inj: File) => {
injb.writeInt32LE(inj.name.length, 0);
injb.write(inj.name, 4);
Buffer.from(await inj.arrayBuffer()).copy(injb, 4 + inj.name.length);
await encoder.insertchunk(["IDAT", buildChunk("IDAT", injb), 0, 0]);
await encoder.insertchunk(["IEND", buildChunk("IEND", Buffer.from([])), 0, 0]);
await encoder.insertchunk(["IDAT", () => buildChunk("IDAT", injb), () => 0, 0]);
await encoder.insertchunk(["IEND", () => buildChunk("IEND", Buffer.from([])), () => 0, 0]);
return extract();
};
export const has_embed = async (png: Buffer) => {
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':
buff = await chunk();
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();
}
};

69
src/requests.ts

@ -0,0 +1,69 @@
const xmlhttprequest = typeof GM_xmlhttpRequest != 'undefined' ? GM_xmlhttpRequest : (typeof GM != "undefined" ? GM.xmlHttpRequest : GM_xmlhttpRequest);
export const headerStringToObject = (s: string) =>
Object.fromEntries(s.split('\n').map(e => {
const [name, ...rest] = e.split(':');
return [name.toLowerCase(), rest.join(':').trim()];
}));
export function GM_head(...[url, opt]: Parameters<typeof fetch>) {
return new Promise<string>((resolve, reject) => {
// https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
const gmopt: Tampermonkey.Request<any> = {
url: url.toString(),
data: opt?.body?.toString(),
method: "HEAD",
onload: (resp) => {
resolve(resp.responseHeaders);
},
ontimeout: () => reject("fetch timeout"),
onerror: () => reject("fetch error"),
onabort: () => reject("fetch abort")
};
xmlhttprequest(gmopt);
});
}
export function GM_fetch(...[url, opt]: Parameters<typeof fetch>) {
function blobTo(to: string, blob: Blob) {
if (to == "arrayBuffer" && blob.arrayBuffer)
return blob.arrayBuffer();
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = function (event) {
if (!event) return;
if (to == "base64")
resolve(event.target!.result);
else
resolve(event.target!.result);
};
if (to == "arrayBuffer") fileReader.readAsArrayBuffer(blob);
else if (to == "base64") fileReader.readAsDataURL(blob); // "data:*/*;base64,......"
else if (to == "text") fileReader.readAsText(blob, "utf-8");
else reject("unknown to");
});
}
return new Promise<ReturnType<typeof fetch>>((resolve, reject) => {
// https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
const gmopt: Tampermonkey.Request<any> = {
url: url.toString(),
data: opt?.body?.toString(),
responseType: "blob",
headers: opt?.headers as any,
method: "GET",
onload: (resp) => {
const blob = resp.response as Blob;
const ref = resp as any as Awaited<ReturnType<typeof fetch>>;
ref.blob = () => Promise.resolve(blob);
ref.arrayBuffer = () => blobTo("arrayBuffer", blob) as Promise<ArrayBuffer>;
ref.text = () => blobTo("text", blob) as Promise<string>;
ref.json = async () => JSON.parse(await (blobTo("text", blob) as Promise<any>));
resolve(resp as any);
},
ontimeout: () => reject("fetch timeout"),
onerror: () => reject("fetch error"),
onabort: () => reject("fetch abort")
};
xmlhttprequest(gmopt);
});
}

39
src/webm.ts

@ -1,6 +1,5 @@
import { Buffer } from "buffer";
import * as ebml from "ts-ebml";
import { concatAB } from "./png";
// unused, but will in case 4chan does file sig checks
//const password = Buffer.from("NOA");
@ -71,9 +70,9 @@ const embed = (webm: Buffer, data: Buffer) => {
chunks.splice(embed + 1, 0, ...stack as any);
tags = findEnclosingTag(chunks, n);
}
embed = tags![1];
embed = tags![1];
};
findOrInsert('Tags');
findOrInsert('Tag');
findOrInsert('Targets');
@ -108,7 +107,7 @@ const embed = (webm: Buffer, data: Buffer) => {
return Buffer.from(enc.encode(chunks.filter(e => e.name != "unknown")));
};
const extractBuff = (webm: Buffer) => {
export const extract = (webm: Buffer) => {
const dec = new ebml.Decoder();
const chunks = dec.decode(webm);
@ -120,23 +119,21 @@ const extractBuff = (webm: Buffer) => {
return;
const chk = chunks[embed + 1];
if (chk.type == "b" && chk.name == "TagBinary")
return chk.data;
};
export const extract = async (reader: ReadableStreamDefaultReader<Uint8Array>): Promise<{ filename: string; data: Buffer } | undefined> => {
let total = Buffer.from('');
let chunk: ReadableStreamDefaultReadResult<Uint8Array>;
// todo: early reject
do {
chunk = await reader.read();
if (chunk.value)
total = concatAB(total, Buffer.from(chunk.value));
} while (!chunk.done);
const data = extractBuff(total);
if (!data)
return;
return { filename: 'embedded', data };
return { filename: 'string', data: chk.data };
};
export const inject = async (container: File, inj: File): Promise<Buffer> =>
embed(Buffer.from(await container.arrayBuffer()), Buffer.from(await inj.arrayBuffer()));
embed(Buffer.from(await container.arrayBuffer()), Buffer.from(await inj.arrayBuffer()));
export const has_embed = (webm: Buffer) => {
const dec = new ebml.Decoder();
const chunks = dec.decode(webm);
const embed = chunks.findIndex(e => e.name == "TagName" && e.type == '8' && e.value == "COOM");
const cl = chunks.find(e => e.name == "Cluster");
if (cl && embed == -1)
return false; // Tags appear before Cluster, so if we have a Cluster and no coomtag, then it's a definite no
if (embed == -1) // Found no coomtag, but no cluster, so it might be further
return;
return true;
};
Loading…
Cancel
Save