Browse Source

Experimental native JPG support

pull/46/head
coomdev 2 years ago
parent
commit
cde3a75566
  1. 2
      README.md
  2. 2304
      chrome/dist/main.js
  3. 2
      chrome/manifest.json
  4. 2617
      dist/main.js
  5. 23
      firefox/dist/background.js
  6. 2672
      firefox/dist/main.js
  7. 2
      firefox/manifest.json
  8. 2
      firefox_update.json
  9. 10
      main.d.ts
  10. 2
      main.meta.js
  11. 2619
      main.user.js
  12. 28
      package-lock.json
  13. 1
      package.json
  14. 8
      src/Components/App.svelte
  15. 57
      src/jpg.ts
  16. 16
      src/main.ts
  17. 1
      src/stores.ts

2
README.md

@ -1,7 +1,7 @@
PNGExtraEmbedder (PEE) PNGExtraEmbedder (PEE)
======================== ========================
Can embed any file in a PNG/WebM/GIF and upload it to a third-party host through 4chan. Can embed any file in a PNG/WebM/GIF/JPEG and upload it to a third-party host through 4chan.
Requires a userscript manager, such as ViolentMonkey. Requires a userscript manager, such as ViolentMonkey.
It should work with 4chan's native extension but 4ChanX is highly recommended as it is much more tested. It should work with 4chan's native extension but 4ChanX is highly recommended as it is much more tested.
Also supports desuarchive. Also supports desuarchive.

2304
chrome/dist/main.js

File diff suppressed because it is too large

2
chrome/manifest.json

@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "PngExtraEmbedder", "name": "PngExtraEmbedder",
"description": "Discover embedded files on 4chan and archives!", "description": "Discover embedded files on 4chan and archives!",
"version": "0.260", "version": "0.261",
"icons": { "icons": {
"64": "1449696017588.png" "64": "1449696017588.png"
}, },

2617
dist/main.js

File diff suppressed because it is too large

23
firefox/dist/background.js

@ -1874,6 +1874,8 @@
// src/platform.ts // src/platform.ts
var lqueue = {}; var lqueue = {};
var localLoad = (key, def) => "__pee__" + key in localStorage ? JSON.parse(localStorage.getItem("__pee__" + key)) : def;
var localSet = (key, value) => localStorage.setItem("__pee__" + key, JSON.stringify(value));
var { port1, port2 } = new MessageChannel(); var { port1, port2 } = new MessageChannel();
console.log("ff_api", true); console.log("ff_api", true);
if (false) { if (false) {
@ -1917,6 +1919,15 @@
for (const k of keys) for (const k of keys)
ctor[k] = bridge(k, ctor[k]); ctor[k] = bridge(k, ctor[k]);
}; };
if (false) {
popupport = chrome.runtime.connect({ name: "popup" });
popupport.onMessage.addListener((msg) => {
if (msg.id in pendingcmds) {
pendingcmds[msg.id](msg);
delete pendingcmds[msg.id];
}
});
}
var Platform = class { var Platform = class {
static async openInTab(src, opts) { static async openInTab(src, opts) {
if (false) { if (false) {
@ -1928,7 +1939,14 @@
i = (await obj2.tabs.getCurrent()).index + 1; i = (await obj2.tabs.getCurrent()).index + 1;
return obj2.tabs.create({ active: opts.active, url: src, index: i }); return obj2.tabs.create({ active: opts.active, url: src, index: i });
} }
static getValue(name, def) {
return localLoad(name, def);
}
static setValue(name, val) {
localSet(name, val);
}
}; };
Platform.cmdid = 0;
Platform = __decorateClass([ Platform = __decorateClass([
Bridged Bridged
], Platform); ], Platform);
@ -1953,8 +1971,9 @@
var bgCorsFetch = async (c, id, input, init) => { var bgCorsFetch = async (c, id, input, init) => {
if (input.startsWith("//")) if (input.startsWith("//"))
input = "https:" + input; input = "https:" + input;
if (init?.body && false) if (init?.body && false) {
init.body = await deserialize(init.body); init.body = await bravedeserialize(init.body);
}
try { try {
const k = await fetch(input, init); const k = await fetch(input, init);
let headersStr = ""; let headersStr = "";

2672
firefox/dist/main.js

File diff suppressed because it is too large

2
firefox/manifest.json

@ -7,7 +7,7 @@
}, },
"name": "PngExtraEmbedder", "name": "PngExtraEmbedder",
"description": "Discover embedded files on 4chan and archives!", "description": "Discover embedded files on 4chan and archives!",
"version": "0.254", "version": "0.261",
"icons": { "icons": {
"64": "1449696017588.png" "64": "1449696017588.png"
}, },

2
firefox_update.json

@ -1 +1 @@
{"addons":{"{34ac4994-07f2-44d2-8599-682516a6c6a6}":{"updates":[{"version":"0.254","update_link":"https://git.coom.tech/fuckjannies/lolipiss/raw/branch/%E4%B8%AD%E5%87%BA%E3%81%97/pngextraembedder-0.254.xpi"}]}}} {"addons":{"{34ac4994-07f2-44d2-8599-682516a6c6a6}":{"updates":[{"version":"0.261","update_link":"https://git.coom.tech/fuckjannies/lolipiss/raw/branch/%E4%B8%AD%E5%87%BA%E3%81%97/pngextraembedder-0.261.xpi"}]}}}

10
main.d.ts

@ -6,6 +6,16 @@ declare module '*.png' {
export default new Uint8Array; export default new Uint8Array;
} }
declare module 'f5stegojs' {
export default class f5 {
constructor(private seed: ArrayLike<number>);
embed(jpegdata: Uint8Array, data: Uint8Array): Uint8Array;
extract(jpegdata: Uint8Array): Uint8Array | null;
}
}
declare module 'mp4box' { declare module 'mp4box' {
type BaseTrackInfo = { type BaseTrackInfo = {
id: number, id: number,

2
main.meta.js

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

2619
main.user.js

File diff suppressed because it is too large

28
package-lock.json

@ -13,6 +13,7 @@
"buffer": "^6.0.3", "buffer": "^6.0.3",
"crc-32": "^1.2.0", "crc-32": "^1.2.0",
"events": "^3.3.0", "events": "^3.3.0",
"f5stegojs": "^0.1.2",
"file-type": "^17.0.2", "file-type": "^17.0.2",
"hls.js": "^1.1.5", "hls.js": "^1.1.5",
"image-hash": "^5.0.1", "image-hash": "^5.0.1",
@ -4144,6 +4145,19 @@
"node >=0.6.0" "node >=0.6.0"
] ]
}, },
"node_modules/f5stegojs": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/f5stegojs/-/f5stegojs-0.1.2.tgz",
"integrity": "sha512-aOLoD2U9yENaq7ripE0PTrAcyYml5IYr1Kuf5k5rW33xhVKAFbQiFx3kRID7Te05vZoWYNbnjPuP8YrG01Fhug==",
"dependencies": {
"minimist": "^1.2.0"
},
"bin": {
"f5stego": "bin/f5stego",
"stegodctdump": "bin/stegodctdump",
"stegodcthist": "bin/stegodcthist"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -6239,8 +6253,7 @@
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
"dev": true
}, },
"node_modules/minimist-options": { "node_modules/minimist-options": {
"version": "4.1.0", "version": "4.1.0",
@ -12262,6 +12275,14 @@
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
}, },
"f5stegojs": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/f5stegojs/-/f5stegojs-0.1.2.tgz",
"integrity": "sha512-aOLoD2U9yENaq7ripE0PTrAcyYml5IYr1Kuf5k5rW33xhVKAFbQiFx3kRID7Te05vZoWYNbnjPuP8YrG01Fhug==",
"requires": {
"minimist": "^1.2.0"
}
},
"fast-deep-equal": { "fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -13879,8 +13900,7 @@
"minimist": { "minimist": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
"dev": true
}, },
"minimist-options": { "minimist-options": {
"version": "4.1.0", "version": "4.1.0",

1
package.json

@ -19,6 +19,7 @@
"buffer": "^6.0.3", "buffer": "^6.0.3",
"crc-32": "^1.2.0", "crc-32": "^1.2.0",
"events": "^3.3.0", "events": "^3.3.0",
"f5stegojs": "^0.1.2",
"file-type": "^17.0.2", "file-type": "^17.0.2",
"hls.js": "^1.1.5", "hls.js": "^1.1.5",
"image-hash": "^5.0.1", "image-hash": "^5.0.1",

8
src/Components/App.svelte

@ -123,6 +123,14 @@
{/if} {/if}
</TabList> </TabList>
<TabPanel> <TabPanel>
<label>
<input type="checkbox" bind:checked={$settings.jpeg} />
Enable JPGs support (JPG embed and extract)
<a
title="JPG embed detection is relatively slow, heavy, so you might want to also enable server cache loading"
>?</a
>
</label>
<label> <label>
<input type="checkbox" bind:checked={$cached} /> <input type="checkbox" bind:checked={$cached} />
Try to load embeds from server cache Try to load embeds from server cache

57
src/jpg.ts

@ -1,7 +1,15 @@
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import type { ImageProcessor } from "./main"; import type { ImageProcessor } from "./main";
import pngv3 from "./pngv3"; import pngv3 from "./pngv3";
import { fireNotification } from "./utils"; import f5 from 'f5stegojs';
import { settings } from "./stores";
import { decodeCoom3Payload } from "./utils";
export let csettings: Parameters<typeof settings['set']>[0];
settings.subscribe(b => {
csettings = b;
});
export const convertToPng = async (f: File): Promise<Blob | undefined> => { export const convertToPng = async (f: File): Promise<Blob | undefined> => {
const can = document.createElement("canvas"); const can = document.createElement("canvas");
@ -32,7 +40,7 @@ export const convertToPng = async (f: File): Promise<Blob | undefined> => {
can.width = dims[0]; can.width = dims[0];
can.height = dims[1]; can.height = dims[1];
const ctx = can.getContext("2d"); const ctx = can.getContext("2d");
if (!ctx) if (!ctx)
return; return;
ctx.drawImage(source, 0, 0, dims[0], dims[1]); ctx.drawImage(source, 0, 0, dims[0], dims[1]);
@ -46,7 +54,21 @@ export const convertToPng = async (f: File): Promise<Blob | undefined> => {
} }
}; };
const key = Buffer.from("CUNNYCUNNYCUNNY");
const f5inst = new f5(key);
const injectTrue = async (b: File, links: string[]) => {
// TODO: maybe do a lossless crop/embed/concat?
if (b.size / 20 < links.join(' ').length)
throw "Image too small to embed.";
const arr = new Uint8Array(await b.arrayBuffer());
const buff = f5inst.embed(arr, Buffer.from(links.join(' ')));
return Buffer.from(buff);
};
const inject = async (b: File, links: string[]) => { const inject = async (b: File, links: string[]) => {
if (csettings.jpeg)
return injectTrue(b, links);
const pngfile = await convertToPng(b); const pngfile = await convertToPng(b);
if (!pngfile || pngfile.size > 3000 * 1024) { if (!pngfile || pngfile.size > 3000 * 1024) {
throw new Error("Couldn't convert file to PNG: resulting filesize too big."); throw new Error("Couldn't convert file to PNG: resulting filesize too big.");
@ -54,10 +76,35 @@ const inject = async (b: File, links: string[]) => {
return pngv3.inject!(new File([pngfile], b.name), links); return pngv3.inject!(new File([pngfile], b.name), links);
}; };
// unfortunately, because of the way f5 work, we can't determine
// if there's an embedded message until we have the complete file
// but the way PEE was designed forces us to just try to extract something until it works
const has_embed = (b: Buffer) => {
if (!csettings.jpeg)
return false;
try {
const res = f5inst.extract(b);
if (!res)
return; // unsure
if (res.length > 1024) // probably garbage, allows for ~20 links from take-me-to.space, should be enough
return; // unsure
const str = Buffer.from(res).toString();
if (!str.match(/^[a-zA-Z0-9:/.\-_ ]+$/))
return; // unsure
return str; // sure
} catch {
return; // unsure
}
};
const extract = (b: Buffer, ex: string) => {
// if we reached here then ex is heckin cute and valid
return decodeCoom3Payload(Buffer.from(ex));
};
export default { export default {
skip: true,
match: fn => !!fn.match(/\.jpe?g$/), match: fn => !!fn.match(/\.jpe?g$/),
has_embed: () => false, has_embed,
extract: () => [], extract,
inject inject
} as ImageProcessor; } as ImageProcessor;

16
src/main.ts

@ -32,7 +32,7 @@ if (!supportedMainDomain(location.host) && !supportedAltDomain(location.host))
export interface ImageProcessor { export interface ImageProcessor {
skip?: true; skip?: true;
match(fn: string): boolean; match(fn: string): boolean;
has_embed(b: Buffer, fn?: string, prevurl?: string): boolean | Promise<boolean>; has_embed(b: Buffer, fn?: string, prevurl?: string): boolean | string | undefined | Promise<boolean | string | undefined>;
extract(b: Buffer, fn?: string): EmbeddedFile[] | Promise<EmbeddedFile[]>; extract(b: Buffer, fn?: string): EmbeddedFile[] | Promise<EmbeddedFile[]>;
inject?(b: File, c: string[]): Buffer | Promise<Buffer>; inject?(b: File, c: string[]): Buffer | Promise<Buffer>;
} }
@ -121,12 +121,16 @@ const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string
} else { } else {
chunk = { done: false, value } as ReadableStreamDefaultReadValueResult<Buffer>; chunk = { done: false, value } as ReadableStreamDefaultReadValueResult<Buffer>;
cumul = Buffer.concat([cumul, value!]); cumul = Buffer.concat([cumul, value!]);
found = await proc.has_embed(cumul); const v = await proc.has_embed(cumul);
if (typeof v == "string") {
return [await proc.extract(cumul, v), false] as [EmbeddedFile[], boolean];
}
found = v;
} }
} while (found !== false && !chunk.done /* Because we only embed links now, it's safe to assume we get everything we need in the first chunk */); } while (found !== false && !chunk.done /* Because we only embed links now, it's safe to assume we get everything we need in the first chunk */);
succ = true; succ = true;
await iter.next(true); await iter.next(true);
if (found === false) { if (found !== true) {
//console.log(`Gave up on ${src} after downloading ${cumul.byteLength} bytes...`); //console.log(`Gave up on ${src} after downloading ${cumul.byteLength} bytes...`);
return; return;
} }
@ -312,7 +316,11 @@ const scrapeBoard = async (self: HTMLButtonElement) => {
} else { } else {
chunk = { done: false, value } as ReadableStreamDefaultReadValueResult<Buffer>; chunk = { done: false, value } as ReadableStreamDefaultReadValueResult<Buffer>;
cumul = Buffer.concat([cumul, value!]); cumul = Buffer.concat([cumul, value!]);
found = await proc.has_embed(cumul); const v = await proc.has_embed(cumul);
if (typeof v == "string") {
return true;
}
found = v;
} }
} while (found !== false && !chunk.done); } while (found !== false && !chunk.done);
await iter.next(true); await iter.next(true);

1
src/stores.ts

@ -33,6 +33,7 @@ export const initial_settings = localLoad('settingsv2', {
mdist: -1, mdist: -1,
phash: false, phash: false,
hotlink: false, hotlink: false,
jpeg: false,
vercheck: false, vercheck: false,
cache: undefined as (boolean | undefined), // meaning defaults to false, except on b4k cache: undefined as (boolean | undefined), // meaning defaults to false, except on b4k
fhost: 0, fhost: 0,

Loading…
Cancel
Save