+324
| import { util, consts, CreateBuilder } from '../steg.mjs'; | ||
| import fs from 'fs'; | ||
| import { StegFile, StegPartialFile, StegText } from '../stubs.mjs'; | ||
| let debug = false, channel = util.Channels.NORMAL; | ||
| util.debug(debug); | ||
| util.setChannel(channel); | ||
| function parseCmdLine(args) { | ||
| let state = {}, tester = /^-/; | ||
| let test = (i, t, v) => { | ||
| if ((t) && (i >= args.length)) { return false; } | ||
| else if (tester.test(args[i])) { | ||
| if (t) { return false; } | ||
| throw new Error(`Expected argument, got flag at word ${i} (${args[i-1]} ${args[i]} ${args[i+1]})`); | ||
| } | ||
| if (t) { return (v && v == args[i] ? true : v ? false : true); } | ||
| else { return true; } | ||
| }; | ||
| switch (args[0]) { | ||
| case '-pack': state.pack = true; break; | ||
| case '-unpack': state.unpack = true; break; | ||
| default: throw new Error('First argument must be -pack or -unpack'); | ||
| } | ||
| if (state.pack) { | ||
| for (let i = 1, l = args.length; i < l; i++) { | ||
| switch (args[i]) { | ||
| case '-version': case '-ver': test(i+1); state.version = args[++i]; break; | ||
| case '-headmode': case '-hm': test(i+1); state.hm = args[++i]; break; | ||
| case '-headmodemask': case '-hmm': test(i+1); state.hmm = args[++i]; break; | ||
| case '-mode': case '-m': test(i+1); state.m = args[++i]; break; | ||
| case '-modemask': case '-mm': test(i+1); state.mm = args[++i]; break; | ||
| case '-salt': test(i+1); state.salt = args[++i]; if (test(i+1, true, 'raw')) { state.raw = true; i++; } break; | ||
| case '-alpha': test(i+1); state.alpha = parseInt(args[++i]); break; | ||
| case '-rand': if (test(i+1, true)) { state.rand = args[++i]; } else { state.rand = true; } break; | ||
| case '-dryrun': state.dryrun = true; if (test(i+1, true, 'comp')) { state.dryrunc = true; i++; } break; | ||
| case '-savemap': test(i+1); test(i+2); if (!state.savemap) { state.savemap = []; } state.savemap.push({ n: args[++i], p: args[++i] }); break; | ||
| case '-map': test(i+1); state.map = args[++i]; break; | ||
| case '-in': test(i+1); state.in = args[++i]; break; | ||
| case '-out': test(i+1); state.out = args[++i]; break; | ||
| case '-cursor': test(i+1); test(i+2); state.cursor = { x: parseInt(args[++i]), y: parseInt(args[++i]) }; break; | ||
| case '-getloadopts': case '-glo': test(i+1); state.glo = args[++i]; if (test(i+1, true, 'enc')) { s.gloe = true; i++; } break; | ||
| case '-newsec': case '-ns': | ||
| { | ||
| let s = {}; | ||
| if (!state.secs) { state.secs = []; } | ||
| test(++i); | ||
| s.stype = args[i]; | ||
| switch (args[i]) { | ||
| case 'file': | ||
| test(i+1); test(i+2); | ||
| s.path = args[++i]; | ||
| if (test(i+1, true)) { s.name = args[++i]; } | ||
| if (test(i+1, true, 'comp')) { s.comp = true; i++; } | ||
| break; | ||
| case 'dir': | ||
| test(i+1); | ||
| s.path = args[++i]; | ||
| if (test(i+1, true, 'full')) { s.full = true; i++; } | ||
| if (test(i+1, true, 'recurse')) { s.recurse = true; i++; } | ||
| if (test(i+1, true, 'comp')) { s.compressed = true; i++; } | ||
| break; | ||
| case 'rand': | ||
| if (test(i+1, true)) { s.rand = args[++i]; i++; } | ||
| else { s.rand = true; } | ||
| break; | ||
| case 'imagetable': | ||
| s.table = { in: [], out: [] }; | ||
| for (let x = i+1, xl = l; x < xl; x+=2) { | ||
| let p = []; | ||
| if (!test(x, true)) { break; } | ||
| test(x); test(x+1); | ||
| s.table.in.push(args[++i]); | ||
| s.table.out.push(args[++i]); | ||
| } | ||
| break; | ||
| case 'rect': | ||
| test(i+1); test(i+2); test(i+3); test(i+4); | ||
| s.x = parseInt(args[++i]); s.y = parseInt(args[++i]); | ||
| s.w = parseInt(args[++i]); s.h = parseInt(args[++i]); | ||
| break; | ||
| case 'cursor': | ||
| test(i+1); | ||
| s.cmd = args[++i]; | ||
| switch (s.cmd) { | ||
| case 'push': case 'pop': break; | ||
| case 'move': | ||
| test(i+1); test(i+2); | ||
| s.x = parseInt(args[++i]); s.y = parseInt(args[++i]); | ||
| if (test(i+1, true)) { s.index = parseInt(args[++i]); } | ||
| break; | ||
| case 'image': | ||
| test(i+1); | ||
| s.index = parseInt(args[++i]); | ||
| break; | ||
| default: break; | ||
| } | ||
| break; | ||
| case 'compress': | ||
| test(i+1); test(i+2); | ||
| s.type = args[++i]; | ||
| s.level = parseInt(args[++i]); | ||
| if (test(i+1, true, 'text')) { s.text = true; i++; } | ||
| break; | ||
| case 'encrypt': | ||
| test(i+1); | ||
| s.type = args[++i]; | ||
| break; | ||
| case 'partialfile': | ||
| test(i+1); test(i+2); | ||
| s.path = args[++i]; | ||
| s.index = parseInt(args[++i]); | ||
| if (test(i+1, true)) { s.name = args[++i]; } | ||
| if (test(i+1, true, 'comp')) { s.compressed = true; i++; } | ||
| break; | ||
| case 'partialfilepiece': | ||
| test(i+1); test(i+2); | ||
| s.index = parseInt(args[++i]); | ||
| s.size = parseInt(args[++i]); | ||
| if (test(i+1, true, 'final')) { s.final = true; i++; } | ||
| break; | ||
| case 'mode': | ||
| test(i+1); | ||
| s.mode = args[++i]; | ||
| break; | ||
| case 'modemask': | ||
| test(i+1); | ||
| s.mask = args[++i]; | ||
| break; | ||
| case 'alpha': | ||
| test(i+1); | ||
| s.alpha = parseInt(args[++i]); | ||
| break; | ||
| case 'text': | ||
| test(i+1); | ||
| s.text = args[++i]; | ||
| if (test(i+1, true)) { s.honor = args[++i]; } | ||
| break; | ||
| default: throw new Error(`Unknown sect ${args[i]}`); break; | ||
| } | ||
| state.secs.push(s); | ||
| } | ||
| break; | ||
| case '-clearsec': case '-cs': | ||
| { | ||
| let s = {}; | ||
| s.clear = true; | ||
| test(i+1); | ||
| s.type = args[++i]; | ||
| switch (s.type) { | ||
| case 'rand': | ||
| case 'imagetable': | ||
| case 'rect': | ||
| case 'compress': | ||
| case 'encrypt': | ||
| case 'mode': | ||
| case 'modemask': | ||
| case 'alpha': | ||
| break; | ||
| default: throw new Error(`Unknown or unclearable sect ${s.type}`); break; | ||
| } | ||
| state.secs.push(s); | ||
| } | ||
| break; | ||
| case '-save': state.save = true; break; | ||
| default: throw new Error(`Unknown flag ${args[i]}`); break; | ||
| } | ||
| } | ||
| } else if (state.unpack) { | ||
| for (let i = 1, l = args.length; i < l; i++) { | ||
| switch (args[i]) { | ||
| case '-headmode': case '-hm': test(i+1); state.hm = args[++i]; break; | ||
| case '-headmodemask': case '-hmm': test(i+1); state.hmm = args[++i]; break; | ||
| case '-image': test(i+1); state.image = args[++i]; break; | ||
| case '-rand': test(i+1); state.rand = args[++i]; break; | ||
| case '-cursor': test(i+1); test(i+2); state.cursor = { x: parseInt(args[++i]), y: parseInt(args[++i]) }; break; | ||
| case '-loadmap': test(i+1); test(i+2); if (!state.loadmap) { state.loadmap = []; } state.loadmap.push({ n: args[++i], p: args[++i] }); break; | ||
| case '-salt': test(i+1); state.salt = args[++i]; if (test(i+1, true, 'raw')) { state.raw = true; i++; } break; | ||
| case '-setloadopts': case '-slo': test(i+1); state.slo = args[++i]; if (test(i+1, true, 'enc')) { s.sloe = true; i++; } break; | ||
| case '-extract': test(i+1); state.extract = args[++i]; break; | ||
| default: throw new Error(`Unknown flag ${args[i]}`); break; | ||
| } | ||
| } | ||
| } | ||
| return state; | ||
| } | ||
| function parseMode(m) { | ||
| let v = m.split('/'); | ||
| for (let i = 0; i <= 1; i++) { | ||
| switch (v[i]) { | ||
| case '3': v[i] = consts.MODE_3BPP; break; | ||
| case '6': v[i] = consts.MODE_6BPP; break; | ||
| case '9': v[i] = consts.MODE_9BPP; break; | ||
| case '12': v[i] = consts.MODE_12BPP; break; | ||
| case '15': v[i] = consts.MODE_15BPP; break; | ||
| case '24': v[i] = consts.MODE_24BPP; break; | ||
| case '32': v[i] = consts.MODE_32BPP; break; | ||
| default: v[i] = consts.MODE_NONE; break; | ||
| } | ||
| } | ||
| return v[0] | (v[1] << 3); | ||
| } | ||
| function parseModeMask(m) { | ||
| let out = 0; | ||
| for (let i = 0, l = Math.min(3, m.length); i < l; i++) { | ||
| switch (m[i]) { | ||
| case 'r': out |= consts.MODEMASK_R; break; | ||
| case 'g': out |= consts.MODEMASK_G; break; | ||
| case 'b': out |= consts.MODEMASK_B; break; | ||
| default: break; | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
| function parseHonor(h) { | ||
| let s = h.split('/'), o = 0; | ||
| if ((s[0] == 'encrypt') || (s[1] == 'encrypt')) { o |= consts.TEXT_HONOR_ENCRYPTION; } | ||
| if ((s[0] == 'compress') || (s[1] == 'compress')) { o |= consts.TEXT_HONOR_COMPRESSION; } | ||
| return o; | ||
| } | ||
| async function main() { | ||
| let state = parseCmdLine(process.argv.slice(2)); | ||
| let major = consts.LATEST_MAJOR, minor = consts.LATEST_MINOR, bldr; | ||
| if (state.pack) { | ||
| if (state.version) { let v = state.version.split('.'); bldr = CreateBuilder(parseInt(v[0]), parseInt(v[1])); } | ||
| else { bldr = CreateBuilder(); } | ||
| bldr.cliPasswordHandler(); | ||
| if (state.hm) { bldr.setHeaderMode(parseMode(state.hm)); } | ||
| if (state.hmm) { bldr.setHeaderModeMask(parseModeMask(state.hmm)); } | ||
| if (state.m) { bldr.setGlobalMode(parseMode(state.m)); } | ||
| if (state.mm) { bldr.setGlobalModeMask(parseModeMask(state.mm)); } | ||
| if (state.salt) { bldr.setSalt(state.salt, state.raw); } | ||
| if (state.alpha) { bldr.setGlobalAlphaBounds(Math.max(0, Math.min(7, state.alpha))); } | ||
| if (state.rand) { bldr.setGlobalSeed(state.rand); } | ||
| if (state.cursor) { bldr.setInitialCursor(state.cursor.x, state.cursor.y); } | ||
| if (state.dryrun) { bldr.dryrun(!!state.dryrunc); } | ||
| if (state.loadmap) { for (let i = 0, maps = state.loadmap, l = maps.length; i < l; i++) { bldr.loadMap(maps[i].n, maps[i].p); } } | ||
| if (state.in) { bldr.inputImage(state.in); } | ||
| if (state.out) { bldr.outputImage(state.out); } | ||
| if (state.secs) { | ||
| for (let i = 0, { secs } = state, l = secs.length; i < l; i++) { | ||
| let sec = secs[i]; | ||
| switch (sec.stype) { | ||
| case 'file': bldr.addFile(sec.path, sec.name, !!sec.compressed); break; | ||
| case 'dir': bldr.addDirectory(sec.path, sec.full, sec.recurse, sec.compressed); break; | ||
| case 'rand': | ||
| if (sec.clear) { bldr.clearSeed(); } | ||
| else if (bldr.rand === true) { bldr.setSeed(decToHash(randomBytes(4).readUInt32LE())); } | ||
| else { bldr.setSeed(bldr.rand); } | ||
| break; | ||
| case 'imagetable': if (sec.clear) { bldr.clearImageTable(); } else { bldr.setImageTable(sec.table.in, sec.table.out); } break; | ||
| case 'rect': if (sec.clear) { bldr.clearRect(); } else { bldr.setRect(sec.x, sec.y, sec.w, sec.h); } break; | ||
| case 'cursor': | ||
| switch (sec.cmd) { | ||
| case 'push': bldr.pushCursor(); break; | ||
| case 'pop': bldr.popCursor(); break; | ||
| case 'move': bldr.moveCursor(sec.x, sec.y, sec.index); break; | ||
| case 'image': bldr.moveImage(sec.index); break; | ||
| default: throw new Error(`Unknown cursor command ${sec.cmd}`); break; | ||
| } | ||
| break; | ||
| case 'compress': | ||
| if (sec.clear) { bldr.clearCompression(); } | ||
| else { | ||
| switch (sec.type) { | ||
| case 'gzip': bldr.setCompression(consts.COMP_GZIP, sec.level); break; | ||
| case 'brotli': bldr.setCompression(consts.COMP_BROTLI, sec.level, sec.text); break; | ||
| default: throw new Error(`Unknown compression type ${sec.type}`); break; | ||
| } | ||
| } | ||
| break; | ||
| case 'encrypt': | ||
| if (sec.clear) { bldr.clearEncryption(); } | ||
| else { | ||
| switch (sec.type) { | ||
| case 'aes256': bldr.setEncryption(consts.CRYPT_AES256); break; | ||
| case 'camellia256': bldr.setEncryption(consts.CRYPT_CAMELLIA256); break; | ||
| case 'aria256': bldr.setEncryption(consts.CRYPT_ARIA256); break; | ||
| default: throw new Error(`Unknown encryption type ${sec.type}`); break; | ||
| } | ||
| } | ||
| break; | ||
| case 'partialfile': bldr.addPartialFile(sec.path, sec.name, sec.index, sec.compressed); break; | ||
| case 'partialfilepiece': bldr.addPartialFilePiece(sec.index, sec.size, sec.final); break; | ||
| case 'mode': if (sec.clear) { bldr.clearMode(); } else { bldr.setMode(parseMode(state.mode)); } break; | ||
| case 'modemask': if (sec.clear) { bldr.clearModeMask(); } else { bldr.setModeMask(parseModeMask(state.mask)); } break; | ||
| case 'alpha': if (sec.clear) { bldr.clearAlphaBounds(); } else { bldr.setAlphaBounds(Math.max(0, Math.min(7, state.alpha))); } break; | ||
| case 'text': bldr.addText(sec.text, parseHonor(sec.honor)); break; | ||
| } | ||
| } | ||
| } | ||
| if (state.glo) { fs.writeFileSync(state.glo, await bldr.getLoadOpts(true, state.gloe), 'binary'); } | ||
| if (state.save) { | ||
| if (state.savemap) { bldr.keep(); } | ||
| await bldr.save(); | ||
| for (let i = 0, maps = state.savemap, l = maps.length; i < l; i++) { bldr.saveMap(maps[i].n, maps[i].p); } | ||
| } | ||
| } else if (state.unpack) { | ||
| bldr = CreateBuilder(); | ||
| bldr.cliPasswordHandler(); | ||
| if (state.hm) { bldr.setHeaderMode(parseMode(state.hm)); } | ||
| if (state.hmm) { bldr.setHeaderModeMask(parseModeMask(state.hmm)); } | ||
| if (state.rand) { bldr.setGlobalSeed(state.rand); } | ||
| if (state.cursor) { bldr.setInitialCursor(state.cursor.x, state.cursor.y); } | ||
| if (state.loadmap) { for (let i = 0, maps = state.loadmap, l = maps.length; i < l; i++) { bldr.loadMap(maps[i].n, maps[i].p); } } | ||
| if (state.salt) { bldr.setSalt(state.salt, state.raw); } | ||
| if (state.image) { bldr.inputImage(state.image); } | ||
| if (state.slo) { await bldr.setLoadOpts(fs.readFileSync(state.slo, 'binary'), true, state.sloe); } | ||
| let secs = await bldr.load(); | ||
| if (state.extract) { | ||
| secs = await bldr.extractAll(secs, state.extract); | ||
| if (secs.length) { | ||
| console.log('Text sections:'); | ||
| for (let i = 0, l = secs.length; i < l; i++) { console.log(secs[i]); } | ||
| } | ||
| } else { | ||
| for (let i = 0, l = secs.length; i < l; i++) { | ||
| if (secs[i] instanceof StegFile) { console.log(`File\n Name: ${secs[i].name}\n Size: ${secs[i].size}${secs[i].state.com?'\n Compressed':''}${secs[i].state.enc?'\n Encrypted':''}`); } | ||
| else if (secs[i] instanceof StegPartialFile) { console.log(`Partial File\n Name: ${secs[i].name}\n Size: ${secs[i].size}${secs[i].state.com?'\n Compressed':''}${secs[i].state.enc?'\n Encrypted':''}\n Piece count: ${secs[i].count}`); } | ||
| else if (secs[i] instanceof StegText) { console.log(`Text\n Size: ${secs[i].size}${secs[i].state.com?'\n Compressed':''}${secs[i].state.enc?'\n Encrypted':''}`); if (secs[i].size < 100) { console.log(' Text:', await secs[i].extract()); } else { console.log(` Text length too long to comfortably preview (${secs[i].size})`); } } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| main().then(()=>{}); |
+437
| # node-steg | ||
| ### Usage | ||
| First import the CreateBuilder helper and constants. <br /> | ||
| `import { consts, CreateBuilder } from 'node-steg';` <br /> | ||
| You can also import `util` if you want to control how verbose it is for debugging purposes. <br /> | ||
| `import { util } from 'node-steg';` | ||
| Now, lets create a builder. <br /> | ||
| `let steg = CreateBuilder();` <br /> | ||
| The arguments are `CreateBuilder([major version, [minor version]])`, if you want to use a specific version. <br /> | ||
| At the time of writing, the default is v1.2. | ||
| For just packing a file in and calling it a day, | ||
| ```javascript | ||
| steg.inputImage('path/to/input/image.ext') | ||
| .outputImage('path/to/output/image2.ext') | ||
| .addFile('path/to/file') | ||
| .save(); | ||
| ``` | ||
| And to extract it. If reusing the same `steg` object, `clear()` must be called. | ||
| ```javascript | ||
| steg.clear() | ||
| .inputImage('path/to/output/image2.ext') | ||
| .load() | ||
| .then((secs) => { | ||
| // At this point, you're given a list of extractable items. If you don't care what's in it, or already know, there's a helper function for making it easier to extract everything | ||
| return steg.extractAll(secs); | ||
| }) | ||
| .then(() => console.log('Done')); | ||
| ``` | ||
| Supported image formats are PNG and WEBP (both static and animated). <br /> | ||
| When it saves PNGs, it saves at compression 9 (highest). <br /> | ||
| When it saves WEBPs, it saves as lossless+exact (save transparent pixels). <br /> | ||
| For what I hope are obvious reasons, lossy formats can't work. | ||
| For animated WEBP images, the syntax is mostly the same. However, when both supplying paths for saving and loading (except for one exception below) you must provide them in the format `frame|<frame number, starting at 0>|<path>`. | ||
| ```javascript | ||
| steg.clear() | ||
| .inputImage('frame|0|animated.webp') | ||
| .outputImage('out.webp') // This is the one exception where you don't need the special format | ||
| // do things as normal | ||
| .save(); | ||
| steg.clear() | ||
| .inputImage('frame|0|out.webp') | ||
| // etc | ||
| .load() | ||
| // etc | ||
| ``` | ||
| There are a number of storage modes available and can be applied separately for both alpha and non-alpha pixels. <br /> | ||
| 3bpp: Stores 3 bits in the pixel, using the LSB of each of the RGB color channels <br /> | ||
| 6bpp: Stores 6 bits using the lower 2 bits of each channel <br /> | ||
| 9bpp: Ditto, but lower 3 bits <br /> | ||
| 12bpp: Lower 4 <br /> | ||
| 15bpp: Lower 5 <br /> | ||
| 24bpp: Uses the full RGB values. This is handy if you don't care so much about the hiding but do want to use the storage <br /> | ||
| 32bpp: This is a semi-special mode that forces overwriting the full RGBA data of every pixel. This is more implemented for completeness than anything. | ||
| There are also modifiers for what's considered alpha vs non-alpha, and which color channels to use if you want to leave one or more untouched. | ||
| Below is a full list of what the current steg builder can do. <br /> | ||
| The term "out-of-band" is used to describe information that's needed but *not* stored in the image(s) and must be found by other means. | ||
| ### Helper or utility | ||
| #### `clear()` | ||
| Resets the object for re-use. | ||
| #### `dryrun(comp = false)` | ||
| Switches to doing a dry run of the saving process. Everything is supported, but the `save()` call doesn't do the final saving. This does *not* create or modify any files. Any compression it would do is skipped and the full size is used instead. <br /> | ||
| Set `comp` to `true` to enable compression. This *does* create temporary files and runs files through compression (where applicable) as it would during the normal saving process. | ||
| #### `realrun()` | ||
| Switches to doing a real run. This way, if a dry run succeeds, you can call this and do a proper run. | ||
| #### `keep(s = true)` | ||
| Toggle on/off keeping the image maps. This is only really useful when paired with `saveMap()`. | ||
| #### `setPasswords(pws)` | ||
| This is an out-of-band setting. <br /> | ||
| More of a helper function. Pass it an array of passwords to pull from (in order) whenever it needs a password rather than prompting the user. | ||
| #### `cliPasswordHandler()` | ||
| Asks the user for missing passwords via the command line and a silent 'Enter password:' prompt. | ||
| ### Input/output | ||
| #### `inputImage(path)` | ||
| The input image for both saving and loading. | ||
| #### `outputImage(path)` | ||
| The output image for saving. | ||
| #### `async save()` | ||
| Saves the image(s). | ||
| #### `async load()` | ||
| Parses the image(s) and returns a list of data sections (see _Classes_ section below). | ||
| #### `loadMap(name, path)` | ||
| Load `path` as the image map to use when saving/loading `name` rather than a default empty one. See `saveMap()` for more information. | ||
| #### `async saveMap(name, path)` | ||
| Save `name`'s input map to `path`. `name` is the filename+frame prefix if applicable but without any path sections, example: the file `tests/orig.png`'s map would require `name` to be `orig.png`. The map is the list of pixels that have been used internally. This can allow you to save multiple entire `Steg` instances on the same image without risking overwriting any by simply re-saving/loading the map after every time. | ||
| #### `async extractAll(secs = this.#secs, path = './extracted')` | ||
| Extract all the sections in `secs` or, if null/undefined, extract all sections found, to the directory `path`. | ||
| #### `async getLoadOpts(packed = false, enc = false)` | ||
| If `packed` is `false`, this returns the object representing the options required to load the current Steg instance (such as header mode, global seed, salt, etc).<br /> | ||
| If `packed` is `true`, it returns the object as a JSON string with byte 0 being a flag if it's encrypted.<br /> | ||
| If `enc` is also `true`, you'll be prompted for a password to use to encrypt via AES256 and a salt unique to this pair of functions.<br /> | ||
| Does not currently honor `setSalt()`. Does not save passwords set by `setPasswords()`. | ||
| #### `async setLoadOpts(blob, packed = false, enc = false)` | ||
| Loads the appropriate settings defined in `blob` into this `Steg` instance so that all that must be supplied are any passwords required and the input path before `load()` can be used. `packed` and `enc` function the way they do with `getLoadOpts()`. | ||
| ### Out-of-band | ||
| #### `setHeaderMode(mode)` | ||
| This sets the mode used to store the first half of the header. It defaults to `MODE_A3BPP | MODE_3BPP`. | ||
| #### `setHeaderModeMask(mask)` | ||
| This changes which channels are used to store the first half of the header. It defaults to `MODEMASK_RGB`. | ||
| #### `setGlobalSeed(seed)` | ||
| This uses `seed` to randomly distribute the header and data around the image. It defaults to disabled.<br /> | ||
| `seed` is an arbitrary-length string consisting of a-z, A-Z, 0-9, and spaces. | ||
| #### `setInitialCursor(x, y)` | ||
| This sets the cursor to x, y rather than the default 0, 0. Has no effect if a global seed is enabled. | ||
| #### `setSalt(salt, raw = false)` | ||
| This overrides the internally-defined default salt when using encryption.<br /> | ||
| If `raw` is `false`, then `salt` is a string that is hashed using SHA256.<br /> | ||
| If `raw` is `true`, then `salt` is a hex-encoded 32-byte value that is directly used as the salt.<br /> | ||
| If `salt` is undefined, then a crypto-safe 32-byte PRNG value is generated and used. The downside to this last option is the only way to obtain the salt is via `getLoadOpts()`. | ||
| ### Header | ||
| #### `setGlobalMode(mode)` | ||
| This sets the mode used to store the second half of the header, as well as the rest of the data in general. It defaults to `MODE_A3BPP | MODE_3BPP`. | ||
| #### `setGlobalModeMask(mask)` | ||
| This changes which channels are used to store the second half of the header, as well as the rest of the data in genereal. It defaults to `MODEMASK_RGB`. | ||
| #### `setGlobalAlphaBounds(bounds)` | ||
| This changes what alpha value is considered alpha vs non-alpha. It supports 8 steps, each roughly 36 apart. Defaults to `ALPHA_255`. | ||
| ### Sections | ||
| #### `setAlphaBounds(bounds)` | ||
| This changes what alpha value is considered alpha vs non-alpha from what is set globally until another `setAlphaBounds()` is called or is cleared. | ||
| #### `clearAlphaBounds()` | ||
| Removes the active `setAlphaBounds()` and returns the alpha value to the global one. | ||
| #### `setRect(x, y, width, height)` | ||
| Bounds all operations within the defined rectangle until another `setRect()` called or is cleared. | ||
| #### `clearRect()` | ||
| Removes the active `setRect()`. | ||
| #### `setMode(mode)` | ||
| Override the global mode until another `setMode()` is called or is cleared. | ||
| #### `clearMode()` | ||
| Reset the mode back to the global mode. | ||
| #### `setModeMask(mask)` | ||
| Override the global mode mask until another `setModeMask()` is called or is cleared. | ||
| #### `clearModeMask()` | ||
| Reset the mode mask back to the global mode mask. | ||
| #### `setSeed(seed)` | ||
| Override the global seed until another `setSeed()` is called or is cleared. | ||
| #### `clearSeed()` | ||
| Reset the seed back to the global seed. If there was no global seed, this disables the randomness. | ||
| #### `pushCursor()`/`popCursor()` | ||
| Save/load the image index and x, y position of the cursor. | ||
| #### `moveCursor(x, y, index = 0)` | ||
| Move the cursor to x, y in the current image or the one specified by `index`. | ||
| #### `moveImage(index = 0)` | ||
| Move the cursor to the one specified by `index`. Doesn't touch the cursor position of the target image. Does nothing if `index` is already the current image. | ||
| #### `setImageTable(inputFiles, outputFiles)` | ||
| This sets up a table of images you can jump around between with `moveCursor()`.<br /> | ||
| Both arguments are arrays and *must* be the same length.<br /> | ||
| Naming rules from inputImage apply to both (so WEBP anim `frame|<n>|<path>` rules apply to `outputFiles()`).<br /> | ||
| It is, however, currently unsupported to mix anim and non-anim WEBP, or mix frames.<br /> | ||
| Example:<br /> | ||
| Each assuming `.inputImage('frame|0|in.webp').outputImage('frame|0|out.webp')` | ||
| * `.setImageTable([ 'frame|1|in.webp' ], [ 'frame|1|out.webp' ])`<br /> | ||
| This is valid<br /> | ||
| * `.setImageTable([ 'frame|4|in.webp' ], [ 'frame|1|out.webp' ])`<br /> | ||
| This is unsupported | ||
| * `.setImageTable([ 'random.png' ], [ 'frame|1|out.webp' ])`<br /> | ||
| This is also unsupported, as is using `random.webp` | ||
| * `.setImageTable([ 'frame|1|in.webp' ], [ 'random.webp' ])`<br /> | ||
| This is also unsupported | ||
| * `.setImageTable([ 'frame|1|in.webp' ], [ 'frame|1|different.webp' ])`<br /> | ||
| This is also unsupported<br /> | ||
| The short version is that it only supports modifying frames in the same animation, not replacing or extracting them. See `node-webpmux` or the official `webpmux` tool if you need that (I'd recommend `node-webpmux` as I've got a more-complete toolset than `webpmux` does in it). | ||
| #### `clearImageTable()` | ||
| Disables the active table and moves the cursor back to the main image. Any images from any previously-active tables will still be written to properly. | ||
| #### `setCompression(type, level = 0, text = false)` | ||
| Set the active compression algorithm to run files/text through. Currently, only `COMP_GZIP` and `COMP_BROTLI` are supported.<br /> | ||
| For GZIP:<br /> | ||
| * `level` must range 0 - 9 | ||
| * `text` is unused | ||
| For BROTLI:<br /> | ||
| * `level` must range 0 - 11 | ||
| * `text` enables BROTLI's special text-mode compression | ||
| #### `clearCompression()` | ||
| Clear an active `setCompression()`. | ||
| #### `setEncryption(type, pw)` | ||
| Set the active encryption algorithm to run files/text through.<br /> | ||
| Currently supported algorithms:<br /> | ||
| * `CRYPT_AES256` (AES-256-CBC). | ||
| * `CRYPT_CAMELLIA256` (CAMELLIA-256-CBC). | ||
| * `CRYPT_ARIA256` (ARIA-256-CBC). | ||
| #### `clearEncryption()` | ||
| Clear an active `setEncryption()`. | ||
| ### Files/text | ||
| #### `addFile(path, name, compressed = false)` | ||
| Add the file at `path` to the image under the name `name`.<br /> | ||
| Set `compressed` to `true` if the file is already compressed via the active compression mode. | ||
| #### `addDirectory(path, full = false, recursive = false, compressed = false)` | ||
| Add the contents of the directory at `path` to the image. File names are preserved as-is and the basename of path is used as the base path. Example, `addDirectory('a/b/c')` will add the contents of that directory under `c/`.<br /> | ||
| Set `full` to `true` to add the path names as-is rather than the basename. Example, `addDirectory('a/b/c')` will then add the contents of that directory under `a/b/c/`.<br /> | ||
| Set `recursive` to `true` to recursively add any other directories under the path.<br /> | ||
| Set `compressed` to `true` if ALL files under `path` are already compressed via the active compression mode. | ||
| #### `addPartialFile(path, name, index, compressed = false)` | ||
| Add the file at `path` you intend to store in pieces under the name `name` and index `index`.<br /> | ||
| Set `compressed` to `true` if the file is already compressed via the active compression mode.<br /> | ||
| `index` can be any integer 0 <= n <= 255 and is used solely for your own reference in `addPartialFilePiece()`. | ||
| #### `addPartialFilePiece(index, size = 0, last = false)` | ||
| Add a piece of file `index`.<br /> | ||
| If `size` is 0 or greater than the remaining size of the file, the rest of the file is written and `last` is assumed `true`.<br /> | ||
| Set `last` to `true` to flag that this is the last piece you intend to write. You can use this if you don't intend to write the entire file. | ||
| #### `addText(text, honor = TEXT_HONOR_NONE)` | ||
| Adds a simple block of text to the image. More simple than a text file.<br /> | ||
| `honor` is a mask of `TEXT_HONOR_ENCRYPTION` and `TEXT_HONOR_COMPRESSION` to control which, if any, are desired to apply to this text block. | ||
| ### Classes | ||
| `StegFile` | ||
| * `name`: Name of the file. | ||
| * `size`: Size of the file as it was stored (after compression/encryption). | ||
| * `realSize`: Uncompressed/decrypted size of the file (only computed *after* extracting). | ||
| * `async extract(path = './extracted')`: Extract the file to `path`. | ||
| `StegPartialFile` | ||
| * `name`: Name of the file. | ||
| * `size`: Size of the file as it was stored (after compression/encryption). | ||
| * `realSize`: Uncompressed/decrypted size of the file (only computed *after* extracting). | ||
| * `count`: The number of pieces this file is in. | ||
| * `async extract(path = './extracted')`: Extract the file to `path`. | ||
| `StegText` | ||
| * `size`: Size of the text as it was stored (after compression/encryption). | ||
| * `realSize`: Uncompressed/decrypted size of the text (only computed *after* extracting). | ||
| * `async extract()`: Extracts and returns the text. | ||
| ### Util | ||
| `util` has many things, but for controlling verbosity, only `util.Channels`, `util.debug`, and `util.setChannel()` are important. | ||
| #### `debug(v)` | ||
| Set `v` to `true` to enable debug mode, `false` to disable it, or pass nothing to get the current debug state.<br /> | ||
| This mostly only disables the file extraction progress messages ("Saved x of size").<br /> | ||
| Does NOT set channel to `DEBUG`. | ||
| #### `Channels` | ||
| * SILENT: Outputs nothing at all. | ||
| * NORMAL: Default; Outputs basic information during saving/extracting, such as the number of pixels changed per image and extraction progress during exracting. | ||
| * VERBOSE: Ouputs more detailed information about what it's doing to the image. Mostly useless. | ||
| * VVERBOSE: Outputs even *more* information about what it's doing, but mostly during loading. | ||
| * DEBUG: Outputs each and every modified pixel of every image it touches. | ||
| #### `setChannel(channel)` | ||
| Sets the output channel to one of the above. | ||
| For a full (more technical) description of the format things are stored in the image(s), see the file `steg/specs/v<major>.<minor>.spec` (like `steg/specs/v1.2.spec`).<br /> | ||
| Also see `test.mjs` for more examples in a very ugly file. | ||
| ### Command-line tool | ||
| In `bin/` is `steg.mjs`. This is a somewhat simple CLI tool for packing/unpacking. | ||
| For packing: | ||
| * `-pack` (must be first argument)<br /> | ||
| Set to packing mode | ||
| * `-version/-ver <version>`<br /> | ||
| Set the version wanted in the format `<major>.<minor>` like 1.2. | ||
| * `-headmode/-hm <mode>`<br /> | ||
| Set the header mode in the format `[non-alpha]/[alpha]` like `9/24`. Values are the bits-per-pixel (3, 6, 9, 12, 15, 24, 32). | ||
| * `-headmodemask/-hmm <mask>`<br /> | ||
| Set the header mode mask in the format `[r][g][b]` like `rb`. | ||
| * `-mode/-m <mode>`<br /> | ||
| Set the global mode. Same format as `-headmode`. | ||
| * `-modemask/-mm <mask>`<br /> | ||
| Set the global mode mask. Same format as `-headmodemask`. | ||
| * `-salt <salt> [raw]`<br /> | ||
| Override the salt with the SHA256 hash of `<salt>`. If `[raw]` is provided, `<salt>` is considered raw. | ||
| * `-alpha <threshhold>`<br /> | ||
| Set the global alpha threshhold where `<threshhold>` is a value between 0 and 7 inclusive.<br /> | ||
| The meanings are as follows:<br /> | ||
| 0: alpha 255, 1: 220, 2: 184, 3: 148, 4: 112, 5: 76, 6: 40, 7: 0<br /> | ||
| This is in line with the `ALPHA_*` constants. | ||
| * `-rand [seed]`<br /> | ||
| Set the global seed to the value of `[seed]` if provided, otherwise generate one to use. | ||
| * `-dryrun [comp]`<br /> | ||
| Set to dryrun mode. If `[comp]` is set, compress files/text blocks during the dry run. | ||
| * `-savemap <name> <path>`<br /> | ||
| Save `<name>`'s map to `<path>`. | ||
| * `-loadmap <name> <path>`<br /> | ||
| Load `<name>`'s map from `<path>`. | ||
| * `-in <path>`<br /> | ||
| Use `<path>` as the input image. | ||
| * `-out <path>`<br /> | ||
| Use `<path>` as the output image. | ||
| * `-cursor <x> <y>`<br /> | ||
| Set the initial cursor to `<x>`, `<y>`. | ||
| * `-getloadopts/-glo <path> [enc]`<br /> | ||
| Save the load opts to `<path>`. If `[enc]` is provided, encrypt the opts. | ||
| * `-newsec/-ns <sec> <opts...>`<br /> | ||
| Define a new section.<br /> | ||
| <sec> and their options are defined below:<br /> | ||
| * * `file <path> [name] [comp]`<br /> | ||
| Argument order is important.<br /> | ||
| Save `<path>` under the name `[name]` if provided, or the base filename if not.<br /> | ||
| If `[comp]` is provided, consider this file already compressed. | ||
| * * `dir <path> [full] [recurse] [comp]`<br /> | ||
| If `[full]` is provided, use the full pathname (minus `<path>`) as the file name. Otherwise use only the base file name.<br /> | ||
| If `[recurse]` is provided, add this directory recursively rather than only the files.<br /> | ||
| If `[comp]` is provided, consider all files to already be compressed.<br /> | ||
| * * `rand [seed]`<br /> | ||
| Use the seed `[seed]` if provided, otherwise generate a new random seed to use. | ||
| * * `imagetable <in1> <out1> [<in2> <out2> [...]]`<br /> | ||
| Create an image table using the provided `<in>` `<out>` path pairs. | ||
| * * `rect <x> <y> <w> <h>`<br /> | ||
| Limit to the rect defined by x, y, w, h. | ||
| * * `cursor <cmd> <args...>`<br /> | ||
| `<cmd>` is one of..<br /> | ||
| `push`: Push the cursor onto the stack<br /> | ||
| `pop`: Pop the cursor off of the stack and use it<br /> | ||
| * * `move <x> <y> [index]`<br /> | ||
| Move the cursor to x, y of the image at `[index]` if provided, otherwise use the current image.<br /> | ||
| * * `image [index]`<br /> | ||
| Move to the image at `[index]` and use whatever the cursor last was on it. Does nothing if `[index]` is the current image. If `[index]` isn't provided, it returns to the primary image.<br /> | ||
| * * `compress <type> <args...>`<br /> | ||
| `<type>` can be one of..<br /> | ||
| `gzip <level>`: Use gzip. `<level>` is between 0 and 9.<br /> | ||
| `brotli <level> [text]`: Use Brotli. `<level>` is between 0 and 11. If `[text]` is provided, set Brotli into text compression mode.<br /> | ||
| * * `encrypt <type>`<br /> | ||
| <type> can be one of..<br /> | ||
| `aes256`: Use AES 256.<br /> | ||
| `camellia256`: Use CAMELLIA 256.<br /> | ||
| `aria256`: Use ARIA 256.<br /> | ||
| * * `partialfile <path> <index> [name] [comp]`<br /> | ||
| Define a file at `<path>` that is to be saved in discreet chunks. `<index>` is an arbitrary integer to use to refer to it in `partialfilepiece` blocks. If `[name]` is defined, use `[name]` as the filename rather than the base filename. If `[comp]` is provided, consider the file already compressed. | ||
| * * `partialfilepiece <index> <size> [final]`<br /> | ||
| Define a piece of the partial file `<index>` and of size `<size>` bytes. If `[final]` is provided, this is the final piece that is going to be defined and the file is considered complete. | ||
| * * `mode <mode>`<br /> | ||
| Set a new mode. `<mode>` is the same format as `-headmode`. | ||
| * * `modemask <mask>`<br /> | ||
| Set a new mode mask. `<mask>` is the same format as `-headmodemask`. | ||
| * * `alpha <threshhold>`<br /> | ||
| Set a new alpha threshhold. `<threshhold>` is the same format as `-alpha`. | ||
| * * `text <text> [honor]`<br /> | ||
| Save the text block `<text>`. If `[honor]` is defined, set whether compression/encryption should be honored. Format is `<encrypt/compress>[/<encrypt/compress>]` like `encrypt` or `compress/encrypt`. | ||
| * `-clearsec/-cs <sec>`<br /> | ||
| Clear a section's effects.<br /> | ||
| Valid `<sec>` are defined below: | ||
| * * `rand`<br /> | ||
| Disable the seed. If a global seed was previously in effect, return to using it. | ||
| * * `imagetable`<br /> | ||
| Disable the image table. Any changes are kept and if a new table is defined using any of the previous images, their existing data is preserved. | ||
| * * `rect`<br /> | ||
| Disable the rect limitation and return to using the whole image's bounds. | ||
| * * `compress`<br /> | ||
| Disable compression. | ||
| * * `encrypt`<br /> | ||
| Disable encryption. | ||
| * * `mode`<br /> | ||
| Return to using the global mode. | ||
| * * `modemask`<br /> | ||
| Return to using the global mode mask. | ||
| * * `alpha`<br /> | ||
| Return to using the global alpha threshhold. | ||
| * `-save`<br /> | ||
| Actually perform the actions defined. Omitting this is useful if you're only interested in using `-getloadopts`. | ||
| For unpacking: | ||
| * `-unpack` (must be the first argument)<br /> | ||
| Set to unpacking mode. | ||
| * `-headmode/-hm <mode>`<br /> | ||
| Same as `-headmode` of packing. | ||
| * `-headmodemask/-hmm <mask>`<br /> | ||
| Same as `-headmodemask` of packing. | ||
| * `-image <path>`<br /> | ||
| Use `<path>` as the input image. | ||
| * `-rand [seed]`<br /> | ||
| Same as `-rand` of packing. | ||
| * `-cursor <x> <y>`<br /> | ||
| Same as `-cursor` of packing. | ||
| * `-loadmap <name> <path>`<br /> | ||
| Load `<name>`'s map from `<path>`. | ||
| * `-salt <salt> [raw]`<br /> | ||
| Same as `-salt` of packing. | ||
| * `-setloadopts/-slo <path> [enc]`<br /> | ||
| Load the load opts from `<path>`. If `[enc]` is provided, then treat it as encrypted. | ||
| * `-extract <path>`<br /> | ||
| Extract the contents to the directory `<path>` and print any text blocks in full.<br /> | ||
| Omitting this gives a summary of the contents and any text blocks under 100 bytes in size. |
Sorry, the diff of this file is not supported yet
+5
-0
| export default { | ||
| LATEST_MAJOR: 1, | ||
| LATEST_MINOR: 2, | ||
| SEC_MODE: 1, | ||
@@ -36,2 +39,4 @@ SEC_RECT: 2, | ||
| CRYPT_AES256: 1, | ||
| CRYPT_CAMELLIA256: 2, | ||
| CRYPT_ARIA256: 3, | ||
@@ -38,0 +43,0 @@ TEXT_HONOR_NONE: 0b0000, |
| import v1 from './specs/v1.mjs'; | ||
| export const LATEST_MAJOR = 1, LATEST_MINOR = 1; | ||
| export default function CreateBuilder(verMajor = LATEST_MAJOR, verMinor = LATEST_MINOR) { | ||
| import consts from './consts.mjs'; | ||
| export const LATEST_MAJOR = consts.LATEST_MAJOR, LATEST_MINOR = consts.LATEST_MINOR; | ||
| export default function CreateBuilder(verMajor = consts.LATEST_MAJOR, verMinor = consts.LATEST_MINOR) { | ||
| switch (verMajor) { | ||
@@ -5,0 +6,0 @@ case 1: return new v1.Builder(verMajor, verMinor); |
+64
-71
| import _PNG from 'pngjs'; | ||
| import fs from 'fs'; | ||
| import { randr, print, Channels, binToDec, decToBin, pad } from './util.mjs'; | ||
| import { randr, print, Channels, binToDec, decToBin, pad, uintToVLQ } from './util.mjs'; | ||
| import consts from './consts.mjs'; | ||
@@ -8,24 +8,2 @@ import WebP from 'node-webpmux'; | ||
| const PNG = _PNG.PNG; | ||
| // since webp-converter has console.log calls you can't disable.. | ||
| import enwebp from 'webp-converter/src/cwebp.js'; | ||
| import dewebp from 'webp-converter/src/dwebp.js'; | ||
| import { execFile } from 'child_process'; | ||
| async function cwebp(pathIn, pathOut, opts = '') { | ||
| const args = `${opts} "${pathIn}" -o "${pathOut}"`; | ||
| return new Promise((res, rej) => { | ||
| execFile(`"${enwebp()}"`, args.split(/\s+/), { shell: true }, (err, stdout, stderr) => { | ||
| if (err) { rej(err); } | ||
| res(stdout ? stdout : stderr); | ||
| }); | ||
| }); | ||
| } | ||
| async function dwebp(pathIn, pathOut, opts = '-o') { | ||
| const args = `"${pathIn}" ${opts} "${pathOut}"`; | ||
| return new Promise((res, rej) => { | ||
| execFile(`"${dewebp()}"`, args.split(/\s+/), { shell: true }, (err, stdout, stderr) => { | ||
| if (err) { rej(err); } | ||
| res(stdout ? stdout : stderr); | ||
| }); | ||
| }); | ||
| } | ||
@@ -36,3 +14,3 @@ export class Image { | ||
| async load(_p) { | ||
| let p = _p, frame = -1, anim; | ||
| let p = _p, frame = -1, webp; | ||
| if (/\.png$/i.test(p)) { await this.#loadPNG(p); } | ||
@@ -45,8 +23,8 @@ else if (/\.webp$/i.test(p)) { | ||
| } | ||
| if (frame != -1) { | ||
| if (Image.map[p]) { anim = Image.map[p]; } | ||
| else { anim = Image.map[p] = new WebP.Image(); await anim.load(p); } | ||
| } | ||
| await this.#loadWEBP(p, anim, frame); | ||
| if (frame != -1) { this.frame = frame; } | ||
| if (Image.map[p]) { webp = Image.map[p]; } | ||
| else { webp = Image.map[p] = await this.#loadWEBP(p); } | ||
| await this.#loadWEBPData(webp, frame); | ||
| } else { throw new Error(`Unknown image ext for "${p}"`); } | ||
| this.webp = webp; | ||
| this.data = this.img.data; | ||
@@ -65,2 +43,10 @@ this.used = { count: 0, max: this.width*this.height }; | ||
| } | ||
| loadMap(p) { | ||
| let buf = Buffer.from(fs.readFileSync(p, 'binary'), 'binary'), { used } = this; | ||
| for (let i = 0, l = buf.length; i < l; i += 4) { | ||
| let x = buf.readUInt16LE(i), y = buf.readUInt16LE(i+2); | ||
| used.count++; | ||
| used[`${x},${y}`] = true; | ||
| } | ||
| } | ||
| async save(p) { | ||
@@ -73,2 +59,14 @@ switch (this.type) { | ||
| } | ||
| saveMap(p) { | ||
| let { used } = this, keys = Object.keys(used); | ||
| let buf = Buffer.alloc((keys.length-2)*4), c = 0; | ||
| for (let i = 0, l = keys.length; i < l; i++) { | ||
| if (!/,/.test(keys[i])) { continue; } | ||
| let [x, y] = keys[i].split(','); | ||
| buf.writeUInt16LE(parseInt(x), c); | ||
| buf.writeUInt16LE(parseInt(y), c+2); | ||
| c += 4; | ||
| } | ||
| fs.writeFileSync(p, buf); | ||
| } | ||
| get width() { return this.img.width; } | ||
@@ -206,2 +204,8 @@ get height() { return this.img.height; } | ||
| } | ||
| writeInt(n, s) { return this.writeBits(pad(decToBin(n), s, '0')); } | ||
| writeVLQ(n, s) { | ||
| let b = uintToVLQ(n, s), os = ''; | ||
| for (let i = 0, l = b.length; i < l; i++) { os += pad(decToBin(b[i]), s, '0'); } | ||
| return this.writeBits(os); | ||
| } | ||
| readPixel(silent = false) { | ||
@@ -254,2 +258,3 @@ let { x, y } = this.cursor; | ||
| k = buf.substr(0, count); this.buf = buf.substr(count); | ||
| print(Channels.DEBUG, `Read ${count} bits and got ${k}`); | ||
| return k; | ||
@@ -266,3 +271,14 @@ } | ||
| } | ||
| readInt(s) { return binToDec(this.readBits(s)); } | ||
| readVLQ(c) { | ||
| let s = '', last = false, b; | ||
| while (!last) { | ||
| b = this.readBits(c); | ||
| if (b[0] == '1') { last = true; } | ||
| s = b.substr(1) + s; | ||
| } | ||
| return binToDec(s); | ||
| } | ||
| static checkMode(m, c) { return (m&7)==c; } | ||
| static resetMap() { delete Image.map; Image.map = {}; } | ||
@@ -276,53 +292,30 @@ async #loadPNG(p) { | ||
| async #loadWEBP(p, anim = undefined, frame = -1) { | ||
| let webpImg = new WebP.Image(); | ||
| await webpImg.load(p); | ||
| try { fs.mkdirSync('./tmp'); } catch (e) {} | ||
| async #loadWEBP(p) { | ||
| let webp = new WebP.Image(); | ||
| await webp.load(p); | ||
| return webp; | ||
| } | ||
| async #loadWEBPData(webp, frame = -1) { | ||
| await webp.initLib(); | ||
| if (frame == -1) { | ||
| switch (webpImg.type) { | ||
| case WebP.TYPE_LOSSY: case WebP.TYPE_LOSSLESS: case WebP.TYPE_EXTENDED: this.type = consts.IMGTYPE_WEBP; break; | ||
| default: throw new Error('Unhandled WebP type'); | ||
| } | ||
| let pt = `./tmp/${basename(p)}.png`; | ||
| await dwebp(p, pt); | ||
| this.img = PNG.sync.read(fs.readFileSync(pt)); | ||
| fs.unlinkSync(pt); | ||
| } else { | ||
| this.type = consts.IMGTYPE_WEBP; | ||
| this.img = { width: webp.width, height: webp.height, data: await webp.getImageData() }; | ||
| } | ||
| else { | ||
| if ((frame < 0) || (frame >= webp.frames.length)) { throw new Error(`Frame ${frame} out of range (0-${webp.frameCount}) of animated webp "${p}"`); } | ||
| let f = webp.frames[frame]; | ||
| this.type = consts.IMGTYPE_WEBPANIM; | ||
| if ((frame < 0) || (frame >= webpImg.frameCount)) { throw new Error(`Frame ${frame} out of range (0-${webpImg.frameCount}) of animated webp "${p}"`); } | ||
| await anim.demuxAnim('./tmp', frame); | ||
| let pt = `./tmp/${basename(p, '.webp')}_${frame}.`; | ||
| await dwebp(`${pt}webp`, `${pt}png`); | ||
| this.img = PNG.sync.read(fs.readFileSync(`${pt}png`)); | ||
| fs.unlinkSync(`${pt}webp`); | ||
| fs.unlinkSync(`${pt}png`); | ||
| this.img = { width: f.width, height: f.height, data: await webp.getFrameData(frame) }; | ||
| } | ||
| this.webp = webpImg; | ||
| this.frame = frame; | ||
| this.main = anim; | ||
| } | ||
| async #saveWEBP(p) { | ||
| try { fs.mkdirSync('./tmp'); } catch (e) {} | ||
| let d = this.img.data; | ||
| switch (this.type) { | ||
| case consts.IMGTYPE_WEBP: | ||
| fs.writeFileSync('./tmp/tmp.png', PNG.sync.write(this.img, { deflateLevel: 9 })); | ||
| await cwebp('./tmp/tmp.png', p, '-lossless -exact -z 9 -mt'); | ||
| fs.unlinkSync('./tmp/tmp.png'); | ||
| await this.webp.setImageData(this.img.data, { width: this.img.width, height: this.img.height, lossless: 9, exact: true }); | ||
| await this.webp.save(p); | ||
| break; | ||
| case consts.IMGTYPE_WEBPANIM: | ||
| fs.writeFileSync('./tmp/tmp.png', PNG.sync.write(this.img, { deflateLevel: 9 })); | ||
| await cwebp('./tmp/tmp.png', './tmp/tmp.webp', '-lossless -exact -z 9 -mt'); | ||
| await this.main.replaceFrame('./tmp/tmp.webp', this.frame); | ||
| fs.unlinkSync('./tmp/tmp.png'); | ||
| fs.unlinkSync('./tmp/tmp.webp'); | ||
| if (p) { | ||
| await WebP.Image.muxAnim({ | ||
| path: p, | ||
| frames: this.main.anim.frames, | ||
| width: this.main.width, | ||
| height: this.main.height, | ||
| bgColor: this.main.anim.bgColor, | ||
| loops: this.main.anim.loopCount | ||
| }); | ||
| } | ||
| await this.webp.setFrameData(this.frame, this.img.data, { width: this.img.width, height: this.img.height, lossless: 9, exact: true }); | ||
| if (p) { await this.webp.save(p); } | ||
| break; | ||
@@ -329,0 +322,0 @@ } |
+3
-4
| { | ||
| "name": "node-steg", | ||
| "version": "1.1.0", | ||
| "version": "1.2.0", | ||
| "description": "Over-complicated module for PNG/lossless WEBP-based steganography", | ||
@@ -16,7 +16,6 @@ "main": "steg.mjs", | ||
| "dependencies": { | ||
| "node-webpmux": "^1.0.0", | ||
| "node-webpmux": "^2.0.0", | ||
| "pngjs": "^5.0.0", | ||
| "seedrandom": "^3.0.5", | ||
| "webp-converter": "^2.3.0" | ||
| "seedrandom": "^3.0.5" | ||
| } | ||
| } |
+336
-222
@@ -1,29 +0,4 @@ | ||
| /* | ||
| support webp images | ||
| how though | ||
| convert to png -> manipulate -> convert back? | ||
| support animations? | ||
| this would be tricky | ||
| do as implicit SEC_IMAGETABLE is active, with frame number being image index? | ||
| would also need special syntax in SEC_IMAGETABLE to support overriding this with your own mapping | ||
| perhaps an object of { src: 'anim.webp', frame: N } instead of a path | ||
| or 'frame|N|anim.webp' because SEC_IMAGETABLE needs to store it somehow | ||
| defaulting to frame 0 if it's an animated webp but no frame specified | ||
| might need to bump SEC_IMAGETABLE's count field and SEC_CURSOR's index field to 16bit | ||
| since animations could certainly be above 255 frames in length | ||
| will need to write custom parser in addition to using webp-converter | ||
| webpmux to dump unprocessed frames to webp | ||
| dwebp to convert the frames/regular webp to png | ||
| cwebp to convert back to webp (using -lossess -exact -z 9 -mt) | ||
| parser to extract data from webp, since apparently none of the shipped tools support doing it | ||
| webp.mjs to parse (await readWebP(path)) | ||
| switch (type) | ||
| lossy: lossless: dwebp | ||
| extended: | ||
| hasAnim: for 0..frameCount, webpmux to extract | ||
| else: dwebp | ||
| */ | ||
| import fs from 'fs'; | ||
| import { basename, dirname, normalize, join as pathJoin } from 'path'; | ||
| import { createHash, randomBytes } from 'crypto'; | ||
| import { Image } from '../image.mjs'; | ||
@@ -40,2 +15,5 @@ import { Builder as _Builder } from '../builder.mjs'; | ||
| cryptaes256, decryptaes256, | ||
| cryptcamellia256, decryptcamellia256, | ||
| cryptaria256, decryptaria256, | ||
| packString, unpackString, | ||
| copyf, | ||
@@ -45,5 +23,16 @@ print, Channels, debug | ||
| import { Steg, StegFile, StegPartialFile, StegText } from '../stubs.mjs'; | ||
| const VERSION_MAJOR = 1, VERSION_MINOR = 1; | ||
| const CRYPT_SALT = '546ac12e6786afb81045a6401a0e0342cb341b450cfc06f87e081b7ec4cae6a7'; | ||
| const VERSION_MAJOR = 1, VERSION_MINOR = 2; | ||
| const CRYPT_SALT = { | ||
| v11: '546ac12e6786afb81045a6401a0e0342cb341b450cfc06f87e081b7ec4cae6a7', | ||
| v12: '192f8633473d2a6e8a35e886d3f5c29bdf807bab22c73630efb54cc11d9aed23' | ||
| }; | ||
| function getSalt(a,b,h) { | ||
| switch (a) { case 1: break; default: throw new Error(`Unknown major verion ${a}`); } | ||
| if ((b >= 2) && (h)) { return h; } | ||
| switch (b) { | ||
| case 1: return CRYPT_SALT.v11; | ||
| case 2: return CRYPT_SALT.v12; | ||
| default: throw new Error(`Unknown minor version ${b}`); | ||
| } | ||
| } | ||
| function fixMode(m) { | ||
@@ -62,6 +51,6 @@ if (((m&consts.MODE_32BPP) == consts.MODE_32BPP) || | ||
| async save(input) { | ||
| let img = this.img = new Image(), headmode = input.headmode||consts.HEADMODE, headmodeMask = input.headmodeMask||consts.HEADMODEMASK, { mode, modeMask, secs, dryrun, dryrunComp } = input, verMajor = input.verMajor||this.#VERSION_MAJOR, verMinor = input.verMinor||this.#VERSION_MINOR; | ||
| let img = this.img = new Image(), headmode = input.headmode||consts.HEADMODE, headmodeMask = input.headmodeMask||consts.HEADMODEMASK, { mode, modeMask, secs, dryrun, dryrunComp, rand, x = 0, y = 0, salt, maps } = input, verMajor = input.verMajor||this.#VERSION_MAJOR, verMinor = input.verMinor||this.#VERSION_MINOR; | ||
| if (verMajor != this.#VERSION_MAJOR) { throw new Error(`Trying to build a version ${verMajor}.x with a ${this.#VERSION_MAJOR}.x constructor`); } | ||
| switch (verMinor) { | ||
| case 0: case 1: break; | ||
| case 0: case 1: case 2: break; | ||
| default: throw new Error(`Trying to build an unsupported version ${verMajor}.${verMinor}`); | ||
@@ -73,2 +62,3 @@ } | ||
| this.dryrunComp = dryrunComp; | ||
| this.salt = salt; | ||
| if (dryrun) { print(Channels.NORMAL, 'DOING A DRY RUN! No changes to any images will be saved.'); if (!dryrunComp) { print(Channels.NORMAL, 'No files will be created or modified.'); } } | ||
@@ -79,16 +69,25 @@ headmode = fixMode(headmode); | ||
| await img.load(input.in); | ||
| if (verMinor >= 2) { let bn = basename(input.in); if ((maps) && (maps[bn])) { img.loadMap(maps[bn]); } this.maps = maps; } | ||
| else { | ||
| if (x !== 0) { x = 0; } | ||
| if (y !== 0) { y = 0; } | ||
| } | ||
| img.master = this.master = img; | ||
| this.master.modeMask = this.modeMask = headmodeMask; | ||
| img.writing = true; | ||
| if (input.rand) { img.rand.seed = hashToDec(input.rand); img.resetCursor(); } | ||
| if (rand) { img.rand.seed = hashToDec(rand); img.resetCursor(); } | ||
| else { | ||
| img.setCursor(x, y); | ||
| if (img.used[`${img.cursor.x},${img.cursor.y}`]) { img.advanceCursor(); } | ||
| } | ||
| this.mode = mode; | ||
| if ((modeMask&0b111 == 0) && (headmode&consts.MODE_32BPP != consts.MODE_32BPP)) { throw new Error('Cannot use mode mask 000 unless mode 32BPP is active (header)'); } | ||
| if ((headmodeMask&0b111 == 0) && (headmode&consts.MODE_32BPP != consts.MODE_32BPP)) { throw new Error('Cannot use mode mask 000 unless mode 32BPP is active (header)'); } | ||
| img.setMode(headmode); | ||
| img.setModeMask(headmodeMask); | ||
| print(Channels.VERBOSE, 'Setting version...'); | ||
| img.writeBits(pad(decToBin(verMajor), 6, '0')); | ||
| img.writeBits(pad(decToBin(verMinor), 6, '0')); | ||
| img.writeInt(verMajor, 6); | ||
| img.writeInt(verMinor, 6); | ||
| print(Channels.VERBOSE, 'Setting mode...'); | ||
| img.writeBits(pad(decToBin(mode), 6, '0')); | ||
| if ((modeMask&0b111 == 0) && (mode&consts.MODE_32BPP != consts.MODE_32BPP)) { throw new Error('Cannot use mode mask 000 unless mode 32BPP is active (global)'); } | ||
| img.writeInt(mode, 6); | ||
| if ((headmodeMask&0b111 == 0) && (mode&consts.MODE_32BPP != consts.MODE_32BPP)) { throw new Error('Cannot use mode mask 000 unless mode 32BPP is active (global)'); } | ||
| img.setMode(mode); | ||
@@ -104,4 +103,5 @@ print(Channels.VERBOSE, 'Setting settings...'); | ||
| img.setModeMask(modeMask); | ||
| print(Channels.VERBOSE, 'Setting sec count...'); | ||
| img.writeBits(pad(decToBin(secs.length), 9, '0')); | ||
| print(Channels.VERBOSE, 'Setting sec count...'+secs.length); | ||
| if (verMinor >= 2) { img.writeVLQ(secs.length, 4); } | ||
| else { img.writeInt(secs.length, 9); } | ||
| print(Channels.VERBOSE, 'Saving secs...'); | ||
@@ -116,8 +116,12 @@ this.fullTable = {}; | ||
| print(Channels.NORMAL, `Number of pixels changed in ${input.out}: ${img.used.count} of ${img.width*img.height} (${Math.floor(img.used.count/(img.width*img.height)*10000)/100}%)`); | ||
| delete this.table; | ||
| delete this.fullTable; | ||
| if (!input.keep) { | ||
| delete this.table; | ||
| delete this.fullTable; | ||
| } | ||
| return true; | ||
| } | ||
| async load(input) { | ||
| let img = this.img = new Image(), headmode = input.headmode||consts.HEADMODE, headmodeMask = input.headmodeMask||consts.HEADMODEMASK, { in: image, rand, modeMask } = input, v, verMajor, verMinor, mode, secCount, ret; | ||
| let img = this.img = new Image(), headmode = input.headmode||consts.HEADMODE, headmodeMask = input.headmodeMask||consts.HEADMODEMASK, { in: image, rand, modeMask, x, y, salt, maps } = input, v, verMajor, verMinor, mode, secCount, ret; | ||
| let usingInitPos = x !== undefined || y !== undefined; | ||
| x = x !== undefined ? x : 0; y = y !== undefined ? y : 0; | ||
| this._files = []; | ||
@@ -128,2 +132,3 @@ this._partialFiles = []; | ||
| await img.load(image); | ||
| if (maps) { let bn = basename(image); if (maps[bn]) { img.loadMap(maps[bn]); } } | ||
| img.master = this.master = img; | ||
@@ -133,8 +138,12 @@ this.master.modeMask = this.modeMask = headmodeMask; | ||
| if (rand) { img.rand.seed = hashToDec(rand); img.resetCursor(); } | ||
| else { | ||
| img.setCursor(x, y); | ||
| if (img.used[`${img.cursor.x},${img.cursor.y}`]) { img.advanceCursor(); } | ||
| } | ||
| headmode = fixMode(headmode); | ||
| if ((modeMask&0b111 == 0) && (headmode&consts.MODE_32BPP != consts.MODE_32BPP)) { throw new Error('Cannot use mode mask 000 unless mode 32BPP is active (header)'); } | ||
| if ((headmodeMask&0b111 == 0) && (headmode&consts.MODE_32BPP != consts.MODE_32BPP)) { throw new Error('Cannot use mode mask 000 unless mode 32BPP is active (header)'); } | ||
| img.setMode(headmode); | ||
| img.setModeMask(headmodeMask); | ||
| print(Channels.VERBOSE, 'Unpacking...\nReading version...'); | ||
| v = img.readBits(6); verMajor = binToDec(v); | ||
| verMajor = img.readInt(6); | ||
| switch (verMajor) { | ||
@@ -144,14 +153,19 @@ case this.#VERSION_MAJOR: break; | ||
| } | ||
| v = img.readBits(6); verMinor = binToDec(v); | ||
| verMinor = img.readInt(6); | ||
| switch (verMinor) { | ||
| case 0: case 1: break; | ||
| case 0: case 1: case 2: break; | ||
| default: throw new Error(`Unsupported version ${verMajor}.${verMinor}`); | ||
| } | ||
| print(Channels.VVERBOSE, `Got version ${verMajor}.${verMinor}`); | ||
| if (verMinor == 1) { | ||
| if (map) { print(Channels.NORMAL, 'Warning: Version 1.1 found but `map` is in use, which is a 1.2 feature'); } | ||
| if (usingInitPos) { print(Channels.NORMAL, 'Warning: Version 1.1 found but initial cursor is in use'); } | ||
| } | ||
| this.verMajor = verMajor; | ||
| this.verMinor = verMinor; | ||
| if (verMinor >= 2) { this.salt = salt; } | ||
| print(Channels.VERBOSE, 'Reading mode...'); | ||
| v = img.readBits(6); mode = this.mode = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got mode ${v} (${mode})`); | ||
| if ((modeMask&0b111 == 0) && (mode&consts.MODE_32BPP != consts.MODE_32BPP)) { throw new Error('Cannot use mode mask 000 unless mode 32BPP is active (global)'); } | ||
| mode = this.mode = img.readInt(6); | ||
| print(Channels.VVERBOSE, `Got mode (${mode})`); | ||
| if ((headmodeMask&0b111 == 0) && (mode&consts.MODE_32BPP != consts.MODE_32BPP)) { throw new Error('Cannot use mode mask 000 unless mode 32BPP is active (global)'); } | ||
| img.setMode(mode); | ||
@@ -164,3 +178,3 @@ print(Channels.VERBOSE, 'Reading settings...'); | ||
| this.alphaThresh = img.alphaThresh = bitsToAlpha(v.substr(0, 3)); | ||
| print(Channels.VVERBOSE, `Got settings ${v} (threshhold ${this.alphaThresh})`); | ||
| print(Channels.VVERBOSE, `Got settings: threshhold ${this.alphaThresh}`); | ||
| break; | ||
@@ -171,3 +185,3 @@ case 1: default: | ||
| this.master.modeMask = this.modeMask = binToDec(v.substr(3, 3)); | ||
| print(Channels.VVERBOSE, `Got settings ${v} (threshhold ${this.alphaThresh}, mode mask ${this.modeMask})`); | ||
| print(Channels.VVERBOSE, `Got settings: threshhold ${this.alphaThresh}, mode mask ${this.modeMask}`); | ||
| break; | ||
@@ -178,4 +192,5 @@ } | ||
| print(Channels.VERBOSE, 'Reading sec count...'); | ||
| v = img.readBits(9); secCount = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${secCount})`); | ||
| if (verMinor >= 2) { secCount = img.readVLQ(4); } | ||
| else { secCount = img.readBits(9); } | ||
| print(Channels.VVERBOSE, `Got (${secCount})`); | ||
| for (let i = 0; i < secCount; i++) { | ||
@@ -187,3 +202,2 @@ ret = await this.#readSec(); | ||
| } | ||
| async #switchImage(index) { | ||
@@ -194,3 +208,6 @@ if (!this.table) { return false; } | ||
| let i = this.table[index]; | ||
| if (!i.img.loaded) { await i.img.load(i.input||i.name); } | ||
| if (!i.img.loaded) { | ||
| await i.img.load(i.input||i.name); | ||
| if ((this.minor >= 2) && (this.maps) && (this.maps[i.name])) { i.img.loadMap(this.maps[i.name]); } | ||
| } | ||
| print(Channels.VERBOSE, `Switching to ${i.name}...`); | ||
@@ -278,3 +295,3 @@ if (this.img == i.img) { return false; } | ||
| #loadState(state) { | ||
| let { img, master, size } = state; | ||
| let { img, master } = state; | ||
| print(Channels.VERBOSE, 'Loading state...'); | ||
@@ -294,5 +311,5 @@ img.master = master; | ||
| async #packSec(sec) { | ||
| let { img, master } = this; | ||
| let { img } = this; | ||
| print(Channels.VERBOSE, 'Saving sec id...'); | ||
| img.writeBits(pad(decToBin(sec.id|(sec.rem?1<<8:0)), 9, '0')); | ||
| img.writeInt(sec.id | (sec.rem ? 1 << 8 : 0), 9); | ||
| switch (sec.id) { | ||
@@ -324,4 +341,4 @@ case consts.SEC_FILE: await this.#packSecFile(sec); break; | ||
| print(Channels.VERBOSE, `Packing SEC_FILE...\nPacking length (${sec.len})...`); | ||
| s = pad(decToBin(sec.len), 24, '0'); | ||
| img.writeBits(s); | ||
| if (this.verMinor >= 2) { img.writeVLQ(sec.len, 8); } | ||
| else { img.writeInt(sec.len, 24); } | ||
| print(Channels.VERBOSE, 'Packing file name...'); | ||
@@ -371,4 +388,5 @@ img.writeString(sec.newName||basename(sec.path)); | ||
| switch (this.verMinor) { | ||
| case 0: img.writeBits(pad(decToBin(sec.out.length), 8, '0')); break; | ||
| case 1: default: img.writeBits(pad(decToBin(sec.out.length), 16, '0')); break; | ||
| case 0: img.writeInt(sec.out.length, 8); break; | ||
| case 1: img.writeInt(sec.out.length, 16); break; | ||
| case 2: default: img.writeVLQ(sec.out.length, 4); break; | ||
| } | ||
@@ -400,10 +418,16 @@ print(Channels.VERBOSE, 'Packing file names...'); | ||
| async #packSecRect(sec) { | ||
| let { img } = this, s = '', x, y; | ||
| let { img } = this; | ||
| if (sec.rem) { print(Channels.VERBOSE, 'Clearing SEC_RECT...'); delete img.state.rect; return; } | ||
| s = pad(decToBin(sec.x), 16, '0'); | ||
| s += pad(decToBin(sec.y), 16, '0'); | ||
| s += pad(decToBin(sec.w), 16, '0'); | ||
| s += pad(decToBin(sec.h), 16, '0'); | ||
| print(Channels.VERBOSE, 'Packing SEC_RECT...\nPacking x, y, w, h...'); | ||
| img.writeBits(s); | ||
| if (this.verMinor >= 2) { | ||
| img.writeVLQ(sec.x, 8); | ||
| img.writeVLQ(sec.y, 8); | ||
| img.writeVLQ(sec.w, 8); | ||
| img.writeVLQ(sec.h, 8); | ||
| } else { | ||
| img.writeInt(sec.x, 16); | ||
| img.writeInt(sec.y, 16); | ||
| img.writeInt(sec.w, 16); | ||
| img.writeInt(sec.h, 16); | ||
| } | ||
| img.flush(); | ||
@@ -414,7 +438,7 @@ img.state.rect = { x: sec.x, y: sec.y, w: sec.w, h: sec.h, max: sec.w*sec.h }; | ||
| async #packSecCursor(sec) { | ||
| let { img, master } = this, s = '', { cursorStack } = master.state; | ||
| let { img, master } = this, s, { cursorStack } = master.state; | ||
| print(Channels.VERBOSE, 'Packing SEC_CURSOR...\nPacking command...'); | ||
| if (!cursorStack) { master.state.cursorStack = cursorStack = []; } | ||
| if ((sec.command == consts.CURSOR_CMD_MOVE) && ((img.state.rand) || (master.rand.seed != -1))) { sec.command = consts.CURSOR_CMD_MOVEIMG; } | ||
| img.writeBits(pad(decToBin(sec.command), 3, '0')); | ||
| img.writeInt(sec.command, 3); | ||
| switch (sec.command) { | ||
@@ -435,9 +459,14 @@ case consts.CURSOR_CMD_PUSH: cursorStack.push([ this.imageIndex, img.cursor.x, img.cursor.y ]); break; | ||
| switch (this.verMinor) { | ||
| case 0: img.writeBits(pad(decToBin(sec.index), 8, '0')); break; | ||
| case 1: default: img.writeBits(pad(decToBin(sec.index), 16, '0')); break; | ||
| case 0: img.writeInt(sec.index, 8); break; | ||
| case 1: img.writeInt(sec.index, 16); break; | ||
| case 2: default: img.writeVLQ(sec.index, 4); break; | ||
| } | ||
| print(Channels.VERBOSE, 'Packing x, y...'); | ||
| s = pad(decToBin(sec.x), 16, '0'); | ||
| s += pad(decToBin(sec.y), 16, '0'); | ||
| img.writeBits(s); | ||
| if (this.verMinor >= 2) { | ||
| img.writeVLQ(sec.x, 8); | ||
| img.writeVLQ(sec.y, 8); | ||
| } else { | ||
| img.writeInt(sec.x, 16); | ||
| img.writeInt(sec.y, 16); | ||
| } | ||
| img.flush(); | ||
@@ -462,4 +491,5 @@ await this.#switchImage(sec.index); | ||
| switch (this.verMinor) { | ||
| case 0: img.writeBits(pad(decToBin(sec.index), 8, '0')); break; | ||
| case 1: default: img.writeBits(pad(decToBin(sec.index), 16, '0')); break; | ||
| case 0: img.writeInt(sec.index, 8); break; | ||
| case 1: img.writeInt(sec.index, 16); break; | ||
| case 2: default: img.writeVLQ(sec.index, 4); break; | ||
| } | ||
@@ -479,14 +509,14 @@ await this.#switchImage(sec.index); | ||
| case consts.COMP_GZIP: | ||
| img.writeBits(pad(decToBin(sec.type), 4, '0')); | ||
| img.writeInt(sec.type, 4); | ||
| print(Channels.VERBOSE, 'Packing level...'); | ||
| img.writeBits(pad(decToBin(com.level=sec.level?sec.level:0), 4, '0')); | ||
| img.writeInt(com.level = sec.level ? sec.level : 0, 4); | ||
| break; | ||
| case consts.COMP_BROTLI: | ||
| img.writeBits(pad(decToBin(sec.type), 4, '0')); | ||
| img.writeInt(sec.type, 4); | ||
| print(Channels.VERBOSE, 'Packing level...'); | ||
| img.writeBits(pad(decToBin(com.level=sec.level?sec.level:0), 4, '0')); | ||
| img.writeInt(com.level = sec.level ? sec.level : 0, 4); | ||
| print(Channels.VERBOSE, 'Packing text flag...'); | ||
| img.writeBits(decToBin(com.text=sec.text?1:0)); | ||
| img.writeInt(com.text = sec.text ? 1 : 0, 1); | ||
| break; | ||
| default: img.writeBits(pad(decToBin(consts.COMP_NONE), 4, '0')); return; | ||
| default: img.writeInt(consts.COMP_NONE, 4); return; | ||
| } | ||
@@ -496,3 +526,3 @@ master.state.compress = com; | ||
| async #packSecEncryption(sec) { | ||
| let { img, master } = this, s = '', enc = {}; | ||
| let { img, master } = this, enc = {}; | ||
| if (sec.rem) { print(Channels.VERBOSE, 'Clearing SEC_ENCRYPTION...'); delete master.state.encrypt; return; } | ||
@@ -502,19 +532,18 @@ print(Channels.VERBOSE, 'Packing SEC_ENCRYPTION...'); | ||
| switch (sec.type) { | ||
| case consts.CRYPT_AES256: | ||
| { | ||
| if (!sec.pw) { sec.pw = await this.#requestPassword(); } | ||
| switch (this.verMinor) { | ||
| case 0: enc.key = getMD5Key(sec.pw); break; | ||
| case 1: default: enc.key = await getCryptKey(sec.pw, CRYPT_SALT); break; | ||
| } | ||
| enc.iv = generateIV(); | ||
| print(Channels.VERBOSE, 'Packing type...'); | ||
| img.writeBits(pad(decToBin(sec.type), 4, '0')); | ||
| for (let i = 0; i < 16; i++) { s += pad(decToBin(enc.iv[i]), 8, '0'); } | ||
| print(Channels.VERBOSE, 'Packing IV...'); | ||
| img.writeBits(s); | ||
| } | ||
| case consts.CRYPT_CAMELLIA256: | ||
| case consts.CRYPT_ARIA256: | ||
| if (this.verMinor < 2) { img.writeInt(consts.CRYPT_NONE, 4); return; } | ||
| break; | ||
| default: img.writeBits(pad(decToBin(consts.CRYPT_NONE), 4, '0')); return; | ||
| case consts.CRYPT_AES256: break; | ||
| } | ||
| if (!sec.pw) { sec.pw = await this.#requestPassword(); } | ||
| switch (this.verMinor) { | ||
| case 0: enc.key = getMD5Key(sec.pw); break; | ||
| default: enc.key = await getCryptKey(sec.pw, getSalt(this.verMajor, this.verMinor, this.salt)); break; | ||
| } | ||
| enc.iv = generateIV(); | ||
| print(Channels.VERBOSE, 'Packing type...'); | ||
| img.writeInt(sec.type, 4); | ||
| print(Channels.VERBOSE, 'Packing IV...'); | ||
| for (let i = 0; i < 16; i++) { img.writeInt(enc.iv[i], 8); } | ||
| master.state.encrypt = enc; | ||
@@ -533,20 +562,25 @@ } | ||
| print(Channels.VERBOSE, `Packing SEC_PARTIALFILE...\nPacking size (${f.size})...`); | ||
| img.writeBits(pad(decToBin(f.size), 24, '0')); | ||
| if (this.verMinor >= 2) { img.writeVLQ(f.size, 8); } | ||
| else { img.writeInt(f.size, 24); } | ||
| print(Channels.VERBOSE, 'Packing file name...'); | ||
| img.writeString(sec.newName||basename(sec.path)); | ||
| print(Channels.VERBOSE, 'Packing file index...'); | ||
| img.writeBits(pad(decToBin(sec.index), 8, '0')); | ||
| if (this.verMinor >= 2) { img.writeVLQ(sec.index, 4); } | ||
| else { img.writeInt(sec.index, 8); } | ||
| } | ||
| async #packSecPartialFilePiece(sec) { | ||
| let { img, master } = this, f = master.state.partialTable[sec.index], w = 0, s = ''; | ||
| let { img, master } = this, f = master.state.partialTable[sec.index], w = 0, s; | ||
| if ((!sec.size) || (sec.size > f.size-f.written)) { sec.size = f.size - f.written; sec.last = true; } | ||
| if (f.done) { sec.size = 0; } | ||
| print(Channels.VERBOSE, 'Packing SEC_PARTIALFILEPIECE...\nPacking file index...'); | ||
| img.writeBits(pad(decToBin(sec.index), 8, '0')); | ||
| if (this.verMinor >= 2) { img.writeVLQ(sec.index, 4); } | ||
| else { img.writeInt(sec.index, 8); } | ||
| print(Channels.VERBOSE, 'Packing piece index...'); | ||
| img.writeBits(pad(decToBin(f.pieces++), 8, '0')); | ||
| if (this.verMinor >= 2) { img.writeVLQ(f.pieces++, 4); } | ||
| else { img.writeInt(f.pieces++, 8); } | ||
| print(Channels.VERBOSE, 'Packing last piece flag...'); | ||
| img.writeBits(decToBin(sec.last||f.done?1:0)); | ||
| img.writeInt(sec.last || f.done ? 1 : 0, 1); | ||
| print(Channels.VERBOSE, 'Packing piece size...'); | ||
| img.writeBits(pad(decToBin(sec.size), 24, '0')); | ||
| if (this.verMinor >= 2) { img.writeVLQ(sec.size, 8); } | ||
| else { img.writeInt(sec.size, 24); } | ||
| print(Channels.VERBOSE, 'Packing piece...'); | ||
@@ -588,3 +622,3 @@ if (sec.size > 0) { | ||
| print(Channels.VERBOSE, 'Packing SEC_MODE...\nPacking mode...'); | ||
| img.writeBits(pad(decToBin(m), 6, '0')); | ||
| img.writeInt(m, 6); | ||
| img.flush(); | ||
@@ -594,3 +628,3 @@ img.setMode(master.state.mode = m); | ||
| async #packSecAlpha(sec) { | ||
| let { img } = this; | ||
| let { img } = this, n; | ||
| if (sec.rem) { | ||
@@ -602,3 +636,3 @@ print(Channels.VERBOSE, 'Clearing SEC_ALPHA...'); | ||
| print(Channels.VERBOSE, 'Packing SEC_ALPHA...\nPacking threshhold...'); | ||
| let n = bitsToAlpha(alphaToBits(sec.alpha)); | ||
| n = bitsToAlpha(alphaToBits(sec.alpha)); | ||
| img.writeBits(pad(alphaToBits(n), 3, '0')); | ||
@@ -608,8 +642,8 @@ img.alphaThresh = n; | ||
| async #packSecText(sec) { | ||
| let { img } = this, { text, honor } = sec, s = '', fmods, buf; | ||
| let { img } = this, { text, honor } = sec, fmods, buf; | ||
| print(Channels.VERBOSE, 'Packing SEC_TEXT...\nPacking honor mask...'); | ||
| img.writeBits(pad(decToBin(honor), 4, '0')); | ||
| img.writeInt(honor, 4); | ||
| fmods = this.#prepFilePack(honor & consts.TEXT_HONOR_COMPRESSION, honor & consts.TEXT_HONOR_ENCRYPTION, true) | ||
| if (fmods.length) { | ||
| let b = fmods[0], st = b, bufs = [], obufs = [], k; | ||
| let b = fmods[0], st = b, bufs = []; | ||
| for (let i = 1, l = fmods.length; i < l; i++) { b.pipe(fmods[i]); b = fmods[i]; } | ||
@@ -623,6 +657,6 @@ if (!debug()) { print(Channels.VERBOSE, 'Processing...'); } | ||
| print(Channels.VERBOSE, `Packing text length (${buf.length})...`); | ||
| img.writeBits(pad(decToBin(buf.length), 16, '0')); | ||
| if (this.verMinor >= 2) { img.writeVLQ(buf.length, 8); } | ||
| else { img.writeInt(buf.length, 16); } | ||
| print(Channels.VERBOSE, 'Packing text...'); | ||
| for (let i = 0, l = buf.length; i < l; i++) { s += pad(decToBin(buf[i]), 8, '0'); } | ||
| img.writeBits(s); | ||
| for (let i = 0, l = buf.length; i < l; i++) { img.writeInt(buf[i], 8); } | ||
| } | ||
@@ -641,3 +675,3 @@ async #packSecModeMask(sec) { | ||
| print(Channels.VERBOSE, 'Packing SEC_MODEMASK...\nPacking mask...'); | ||
| img.writeBits(pad(decToBin(sec.mask), 3, '0')); | ||
| img.writeInt(sec.mask, 3); | ||
| img.flush(); | ||
@@ -647,7 +681,7 @@ img.setModeMask(master.state.modeMask = sec.mask); | ||
| async #readSec() { | ||
| let { img } = this, v, secId, rem; | ||
| let { img } = this, secId, rem; | ||
| function err(id) { return { v: false, secId: id }; } | ||
| print(Channels.VERBOSE, 'Reading sec id...'); | ||
| v = img.readBits(9); secId = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${secId})`); | ||
| secId = img.readInt(9); | ||
| print(Channels.VVERBOSE, `Got ${secId}`); | ||
| print(Channels.VERBOSE, 'Reading sec...'); | ||
@@ -675,8 +709,9 @@ rem = secId&(1<<8); | ||
| async #readSecFile(rem) { | ||
| let { img, master } = this, o = {}, s = 0, v; | ||
| let { img, master } = this, o = {}, s = 0; | ||
| print(Channels.VERBOSE, 'Reading SEC_FILE...\nReading size...'); | ||
| v = img.readBits(24); o.size = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${o.size})`, `Got ${o.size}`); | ||
| if (this.verMinor >= 2) { o.size = img.readVLQ(8); } | ||
| else { o.size = img.readInt(24); } | ||
| print(Channels.VVERBOSE, `Got ${o.size}`); | ||
| print(Channels.VERBOSE, 'Reading name...'); | ||
| v = img.readString(); o.name = v; | ||
| o.name = img.readString(); | ||
| print(Channels.VERBOSE, `Got ${o.name}`); | ||
@@ -690,3 +725,3 @@ print(Channels.VERBOSE, 'Saving current state...'); | ||
| async unpackFile(file, output = './extracted') { | ||
| let { state } = file, { img, size } = state, s = 0, r = Buffer.alloc(1), fd, v, path, p, fmods; | ||
| let { state } = file, { img, size } = state, s = 0, r = Buffer.alloc(1), fd, path, p, fmods; | ||
| this.#loadState(state); | ||
@@ -698,4 +733,3 @@ print(Channels.NORMAL, `Extracting ${file.state.name}...`); | ||
| while (s < size) { | ||
| v = img.readBits(8); | ||
| r[0] = binToDec(v); | ||
| r[0] = img.readInt(8); | ||
| fs.writeSync(fd, r); | ||
@@ -718,8 +752,8 @@ s++; | ||
| async #readSecRand(rem) { | ||
| let { img, master } = this, v, seed; | ||
| let { img, master } = this, seed; | ||
| if (rem) { print(Channels.VERBOSE, 'Clearing SEC_RAND...'); delete img.state.rand; return; } | ||
| print(Channels.VERBOSE, 'Reading SEC_RAND...\nReading seed...'); | ||
| v = img.readBits(32); seed = binToDec(v); | ||
| seed = img.readInt(32); | ||
| img.clear(); | ||
| print(Channels.VVERBOSE, `Got ${v} (${seed})`); | ||
| print(Channels.VVERBOSE, `Got ${seed}`); | ||
| if (!img.state.rand) { img.state.rand = new randr(); } | ||
@@ -739,7 +773,7 @@ img.state.rand.seed = seed; | ||
| switch (this.verMinor) { | ||
| case 0: v = img.readBits(8); break; | ||
| case 1: default: v = img.readBits(16); break; | ||
| case 0: n = img.readInt(8); break; | ||
| case 1: n = img.readInt(16); break; | ||
| case 2: default: n = img.readVLQ(4); break; | ||
| } | ||
| n = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${n})`); | ||
| print(Channels.VVERBOSE, `Got ${n}`); | ||
| print(Channels.VERBOSE, 'Reading file names...'); | ||
@@ -763,12 +797,18 @@ for (let i = 0; i < n; i++) { | ||
| async #readSecRect(rem) { | ||
| let { img } = this, rect = {}, v; | ||
| let { img } = this, rect = {}; | ||
| if (rem) { print(Channels.VERBOSE, 'Clearing SEC_RECT...'); delete img.state.rect; return; } | ||
| print(Channels.VERBOSE, 'Reading SEC_RECT...\nReading x, y, w, h...'); | ||
| v = img.readBits(64); | ||
| if (this.verMinor >= 2) { | ||
| rect.x = img.readVLQ(8); | ||
| rect.y = img.readVLQ(8); | ||
| rect.w = img.readVLQ(8); | ||
| rect.h = img.readVLQ(8); | ||
| } else { | ||
| rect.x = img.readInt(16); | ||
| rect.y = img.readInt(16); | ||
| rect.w = img.readInt(16); | ||
| rect.h = img.readInt(16); | ||
| } | ||
| img.clear(); | ||
| rect.x = binToDec(v.substr(0, 16)); | ||
| rect.y = binToDec(v.substr(16,16)); | ||
| rect.w = binToDec(v.substr(32,16)); | ||
| rect.h = binToDec(v.substr(48,16)); | ||
| print(Channels.VVERBOSE, `Got ${v} (${rect.x}, ${rect.y}, ${rect.w}, ${rect.h})`); | ||
| print(Channels.VVERBOSE, `Got ${rect.x}, ${rect.y}, ${rect.w}, ${rect.h}`); | ||
| img.state.rect = rect; | ||
@@ -781,4 +821,4 @@ img.resetCursor(); | ||
| print(Channels.VERBOSE, 'Reading SEC_CURSOR...\nReading command...'); | ||
| v = img.readBits(3); cmd = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${cmd})`); | ||
| cmd = img.readInt(3); | ||
| print(Channels.VVERBOSE, `Got ${cmd}`); | ||
| switch (cmd) { | ||
@@ -799,10 +839,11 @@ case consts.CURSOR_CMD_PUSH: cursorStack.push([ this.imageIndex, img.cursor.x, img.cursor.y ]); break; | ||
| switch (this.verMinor) { | ||
| case 0: v = img.readBits(8); break; | ||
| case 1: default: v = img.readBits(16); break; | ||
| case 0: ind = img.readInt(8); break; | ||
| case 1: ind = img.readInt(16); break; | ||
| case 2: default: ind = img.readVLQ(4); break; | ||
| } | ||
| ind = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${ind})`); | ||
| print(Channels.VVERBOSE, `Got ${ind}`); | ||
| print(Channels.VERBOSE, 'Reading x, y...'); | ||
| v = img.readBits(32); x = binToDec(v.substr(0, 16)); y = binToDec(v.substr(16)); | ||
| print(Channels.VVERBOSE, `Got ${v} (${x}, ${y})`); | ||
| if (this.verMinor >= 2) { x = img.readVLQ(8); y = img.readVLQ(8); } | ||
| else { x = img.readInt(16); y = img.readInt(16); } | ||
| print(Channels.VVERBOSE, `Got ${x}, ${y}`); | ||
| img.clear(); | ||
@@ -824,7 +865,7 @@ await this.#switchImage(ind); | ||
| switch (this.verMinor) { | ||
| case 0: v = img.readBits(8); break; | ||
| case 1: default: v = img.readBits(16); break; | ||
| case 0: ind = img.readInt(8); break; | ||
| case 1: ind = img.readInt(16); break; | ||
| case 2: default: ind = img.readVLQ(4); break; | ||
| } | ||
| ind = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${ind})`); | ||
| print(Channels.VVERBOSE, `Got ${ind}`); | ||
| await this.#switchImage(ind); | ||
@@ -837,20 +878,20 @@ this.img.resetCursor(); | ||
| async #readSecCompress(rem) { | ||
| let { img, master } = this, com = {}, v; | ||
| let { img, master } = this, com = {}; | ||
| if (rem) { print(Channels.VERBOSE, 'Clearing SEC_COMPRESSION...'); delete master.state.compress; return; } | ||
| print(Channels.VERBOSE, 'Reading SEC_COMPRESSION...\nReading type...'); | ||
| v = img.readBits(4); com.type = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${com.type})`); | ||
| com.type = img.readInt(4); | ||
| print(Channels.VVERBOSE, `Got ${com.type}`); | ||
| switch (com.type) { | ||
| case consts.COMP_GZIP: | ||
| print(Channels.VERBOSE, 'Reading level...'); | ||
| v = img.readBits(4); com.level = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${com.level})`); | ||
| com.level = img.readInt(4); | ||
| print(Channels.VVERBOSE, `Got ${com.level}`); | ||
| break; | ||
| case consts.COMP_BROTLI: | ||
| print(Channels.VERBOSE, 'Reading level...'); | ||
| v = img.readBits(4); com.level = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${com.level})`); | ||
| com.level = img.readInt(4); | ||
| print(Channels.VVERBOSE, `Got ${com.level}`); | ||
| print(Channels.VERBOSE, 'Reading text flag...'); | ||
| v = img.readBits(1); com.text = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${com.text})`); | ||
| com.text = img.readInt(1); | ||
| print(Channels.VVERBOSE, `Got ${com.text}`); | ||
| break; | ||
@@ -862,42 +903,43 @@ default: print(Channels.VERBOSE, 'Unknown compression type specified, doing nothing...'); return; | ||
| async #readSecEncrypt(rem) { | ||
| let { img, master } = this, enc = {}, v; | ||
| let { img, master } = this, enc = {}; | ||
| if (rem) { print(Channels.VERBOSE, 'Clearing SEC_ENCRYPTION...'); delete master.state.encrypt; return; } | ||
| print(Channels.VERBOSE, 'Reading SEC_ENCRYPTION...\nReading type...'); | ||
| v = img.readBits(4); enc.type = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${enc.type})`); | ||
| enc.type = img.readInt(4); | ||
| print(Channels.VVERBOSE, `Got ${enc.type}`); | ||
| switch (enc.type) { | ||
| case consts.CRYPT_AES256: | ||
| { | ||
| switch (this.verMinor) { | ||
| case 0: | ||
| if (master.state.pws.length) { enc.key = getMD5Key(master.state.pws.shift()); } | ||
| else { enc.key = getMD5Key(await this.#requestPassword()); } | ||
| break; | ||
| case 1: default: | ||
| if (master.state.pws.length) { enc.key = await getCryptKey(master.state.pws.shift(), CRYPT_SALT); } | ||
| else { enc.key = await getCryptKey(await this.#requestPassword(), CRYPT_SALT); } | ||
| } | ||
| enc.iv = new Buffer.alloc(16); | ||
| print(Channels.VERBOSE, 'Reading IV...'); | ||
| v = img.readBits(128); | ||
| for (let i = 0; i < 16; i++) { enc.iv[i] = binToDec(v.substr(i*8, 8)); } | ||
| print(Channels.VVERBOSE, `Got ${enc.iv.toString('hex')}`); | ||
| } | ||
| case consts.CRYPT_CAMELLIA256: | ||
| case consts.CRYPT_ARIA256: | ||
| if (this.verMinor < 2) { throw new Error('SEC_ENCRYPTION found using CAMELLIA256 or ARIA256 in a version < 1.2; This is not valid and may be a sign of a corrupt or invalid image. Aborting.'); } | ||
| break; | ||
| case consts.CRYPT_AES256: break; | ||
| default: print(Channels.VERBOSE, 'Unknown encryption type specified, doing nothing...'); return; | ||
| } | ||
| switch (this.verMinor) { | ||
| case 0: | ||
| if (master.state.pws.length) { enc.key = getMD5Key(master.state.pws.shift()); } | ||
| else { enc.key = getMD5Key(await this.#requestPassword()); } | ||
| break; | ||
| default: | ||
| if (master.state.pws.length) { enc.key = await getCryptKey(master.state.pws.shift(), getSalt(this.verMajor, this.verMinor, this.salt)); } | ||
| else { enc.key = await getCryptKey(await this.#requestPassword(), getSalt(this.verMajor, this.verMinor, this.salt)); } | ||
| } | ||
| enc.iv = new Buffer.alloc(16); | ||
| print(Channels.VERBOSE, 'Reading IV...'); | ||
| for (let i = 0; i < 16; i++) { enc.iv[i] = img.readInt(8); } | ||
| print(Channels.VVERBOSE, `Got ${enc.iv.toString('hex')}`); | ||
| master.state.encrypt = enc; | ||
| } | ||
| async #readSecPartialFile(rem) { | ||
| let { img, master } = this, f = { piece: 0 }, table = master.state.partialTable, v; | ||
| let { img, master } = this, f = { piece: 0 }, table = master.state.partialTable; | ||
| if (!table) { table = master.state.partialTable = {}; } | ||
| print(Channels.VERBOSE, 'Reading SEC_PARTIALFILE...\nReading file size...'); | ||
| v = img.readBits(24); f.size = binToDec(v); | ||
| if (debug()) { print(Channels.VVERBOSE, `Got ${v} (${f.size})`); } | ||
| else { print(Channels.VERBOSE, `Got ${f.size}`); } | ||
| if (this.verMinor >= 2) { f.size = img.readVLQ(8); } | ||
| else { f.size = img.readInt(24); } | ||
| print(Channels.VVERBOSE, `Got ${f.size}`); | ||
| print(Channels.VERBOSE, 'Reading file name...'); | ||
| f.name = img.readString(); | ||
| print(Channels.VERBOSE, `Got ${f.name}\nReading file index...`); | ||
| v = img.readBits(8); table[binToDec(v)] = f; | ||
| print(Channels.VVERBOSE, `Got ${v} (${binToDec(v)})`); | ||
| if (this.verMinor >= 2) { table[binToDec(v)] = img.readVLQ(4); } | ||
| else { table[binToDec(v)] = img.readInt(8); } | ||
| print(Channels.VVERBOSE, `Got ${table[binToDec(v)]}`); | ||
| f.com = master.state.compress; | ||
@@ -907,17 +949,20 @@ f.enc = master.state.encrypt; | ||
| async #readSecPartialFilePiece(rem) { | ||
| let { img, master } = this, table = master.state.partialTable, s = 0, o = {}, v, f; | ||
| let { img, master } = this, table = master.state.partialTable, s = 0, o = {}, f; | ||
| print(Channels.VERBOSE, 'Reading SEC_PARTIALFILEPIECE...\nReading file index...'); | ||
| v = img.readBits(8); f = table[binToDec(v)]; | ||
| if (this.verMinor >= 2) { f = img.readVLQ(4); } | ||
| else { f = img.readInt(8); } | ||
| print(Channels.VVERBOSE, `Got ${f}`); | ||
| f = table[f]; | ||
| if (!f.pieces) { f.pieces = []; } | ||
| print(Channels.VVERBOSE, `Got ${v} (${binToDec(v)})`); | ||
| print(Channels.VERBOSE, 'Reading piece index...'); | ||
| v = img.readBits(8); o.ind = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${o.ind})`); | ||
| if (this.verMinor >= 2) { o.ind = img.readVLQ(4); } | ||
| else { o.ind = img.readInt(8); } | ||
| print(Channels.VVERBOSE, `Got ${o.ind}`); | ||
| print(Channels.VERBOSE, 'Reading last piece flag...'); | ||
| v = img.readBits(1); o.last = !!binToDec(v); | ||
| o.last = !!img.readInt(1); | ||
| print(Channels.VVERBOSE, `Got ${v}`); | ||
| print(Channels.VERBOSE, 'Reading piece size...'); | ||
| v = img.readBits(24); o.size = binToDec(v); | ||
| if (debug()) { print(Channels.VVERBOSE, `Got ${v} (${o.size})`); } | ||
| else { print(Channels.VERBOSE, `Got ${o.size}`); } | ||
| if (this.verMinor >= 2) { o.size = img.readVLQ(8); } | ||
| else { o.size = img.readInt(24); } | ||
| print(Channels.VERBOSE, `Got ${o.size}`); | ||
| print(Channels.VERBOSE, 'Saving state...'); | ||
@@ -933,3 +978,3 @@ this.#saveState(o); | ||
| async unpackPartialFile(file, output = './extracted') { | ||
| let { state } = file, { img, pieces } = state, r = Buffer.alloc(1), path = `${output}/${state.name}`, p, fmods, size, fd, v, s; | ||
| let { state } = file, { img, pieces } = state, r = Buffer.alloc(1), path = `${output}/${state.name}`, p, fmods, size, fd, s; | ||
| if (!fs.existsSync('tmp/')) { fs.mkdirSync('tmp'); } | ||
@@ -947,4 +992,3 @@ path = normalize(path).replace(/^\.\.\/(\.\.\/)*/g, ''); | ||
| while (s < size) { | ||
| v = img.readBits(8); | ||
| r[0] = binToDec(v); | ||
| r[0] = img.readInt(8); | ||
| fs.writeSync(fd, r); | ||
@@ -968,3 +1012,3 @@ s++; | ||
| async #readSecMode(rem) { | ||
| let { img, master, modeMask } = this, v, mode; | ||
| let { img, master, modeMask } = this, mode; | ||
| if (rem) { | ||
@@ -978,8 +1022,7 @@ print(Channels.VERBOSE, 'Clearing SEC_MODE...'); | ||
| print(Channels.VERBOSE, 'Reading SEC_MODE...\nReading mode...'); | ||
| v = img.readBits(6); mode = binToDec(v); | ||
| mode = fixMode(mode); | ||
| mode = fixMode(img.readInt(6)); | ||
| if ((modeMask&0b111 == 0) && (mode&consts.MODE_32BPP != consts.MODE_32BPP)) { throw new Error('Cannot use mode mask 000 unless mode 32BPP is active (sec)'); } | ||
| img.setMode(master.state.mode = binToDec(v)); | ||
| img.setMode(master.state.mode = mode); | ||
| img.clear(); | ||
| print(Channels.VVERBOSE, `Got ${v} (${master.state.mode})`); | ||
| print(Channels.VVERBOSE, `Got ${master.state.mode}`); | ||
| } | ||
@@ -998,9 +1041,10 @@ async #readSecAlpha(rem) { | ||
| async #readSecText(rem) { | ||
| let { img, master } = this, o = {}, s = 0, v; | ||
| let { img, master } = this, o = {}, s = 0; | ||
| print(Channels.VERBOSE, 'Reading SEC_TEXT...\nReading honor mask...'); | ||
| v = img.readBits(4); o.mask = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${o.mask})`); | ||
| o.mask = img.readInt(4); | ||
| print(Channels.VVERBOSE, `Got ${o.mask}`); | ||
| print(Channels.VERBOSE, 'Reading length...'); | ||
| v = img.readBits(16); o.len = binToDec(v); | ||
| print(Channels.VVERBOSE, `Got ${v} (${o.len})`); | ||
| if (this.verMinor >= 2) { o.len = img.readVLQ(8); } | ||
| else { o.len = img.readInt(16); } | ||
| print(Channels.VVERBOSE, `Got ${o.len}`); | ||
| print(Channels.VERBOSE, 'Saving state...'); | ||
@@ -1013,7 +1057,7 @@ this.#saveState(o); | ||
| async unpackText(text) { | ||
| let { state } = text, { img, len } = state, fmods, v, s, buf, txt; | ||
| let { state } = text, { img, len } = state, fmods, s, buf, txt; | ||
| print(Channels.NORMAL, 'Extracting text...'); | ||
| this.#loadState(state); | ||
| buf = Buffer.alloc(len); | ||
| for (let i = 0; i < len; i++) { v = img.readBits(8); v = binToDec(v); buf[i] = v; } | ||
| for (let i = 0; i < len; i++) { buf[i] = img.readInt(8); } | ||
| fmods = this.#prepFileUnpack(state.mask & consts.TEXT_HONOR_COMPRESSION, state.mask & consts.TEXT_HONOR_ENCRYPTION, true); | ||
@@ -1033,3 +1077,3 @@ if (fmods.length) { | ||
| async #readSecModeMask(rem) { | ||
| let { img, master, mode, modeMask } = this, v, mask, m; | ||
| let { img, master, mode, modeMask } = this, mask, m; | ||
| if (rem) { | ||
@@ -1044,5 +1088,5 @@ print(Channels.VERBOSE, 'Clearing SEC_MODEMASK...'); | ||
| print(Channels.VERBOSE, 'Reading SEC_MODEMASK...\nReading mask...'); | ||
| v = img.readBits(3); mask = binToDec(v); | ||
| mask = img.readInt(3); | ||
| if ((mask&0b111 == 0) && (m&consts.MODE_32BPP != consts.MODE_32BPP)) { throw new Error('Cannot use mode mask 000 unless mode 32BPP is active (sec)'); } | ||
| img.setModeMask(master.state.modeMask = binToDec(v)); | ||
| img.setModeMask(master.state.modeMask = mask); | ||
| img.clear(); | ||
@@ -1078,2 +1122,3 @@ print(Channels.VVERBOSE, `Got ${v} (${master.state.modeMask})`); | ||
| get state() { return this.#state; } | ||
| get count() { return this.#state.pieces.length; } | ||
| async extract(path = './extracted') { await this.#steg.unpackPartialFile(this, path); } | ||
@@ -1097,2 +1142,3 @@ } | ||
| #secs = null; | ||
| #steg = null; | ||
| constructor(verMajor, verMinor) { | ||
@@ -1103,3 +1149,3 @@ super(); | ||
| switch (verMajor) { case 1: break; default: throw new Error(`Unknown version ${verMajor}.x`); } | ||
| switch (verMinor) { case 0: case 1: break; default: throw new Error(`Unknown version ${verMajor}.${verMinor}`); } | ||
| switch (verMinor) { case 0: case 1: case 2: break; default: throw new Error(`Unknown version ${verMajor}.${verMinor}`); } | ||
| this.clear(); | ||
@@ -1124,13 +1170,33 @@ } | ||
| dryrun: false, | ||
| dryrunComp: false | ||
| dryrunComp: false, | ||
| keep: false, | ||
| x: undefined, | ||
| y: undefined, | ||
| salt: undefined, | ||
| maps: undefined | ||
| }; | ||
| Image.resetMap(); | ||
| return this; | ||
| } | ||
| dryrun(comp = false) { this.#out.dryrun = true; this.#out.dryrunComp = !!comp; return this; } | ||
| realrun() { if (this.#out.dryrun) { delete this.#out.dryrun; delete this.#out.dryrunComp; } return this; } | ||
| keep(s = true) { this.#out.keep = !!s; return this; } | ||
| setHeaderMode(mode) { this.#out.headmode = mode&0b111111; return this; } | ||
| setGlobalMode(mode) { this.#out.mode = mode&0b111111; return this; } | ||
| setHeaderModeMask(mask) { this.#out.headmodeMask = mask&0b111; return this; } | ||
| setGlobalModeMask(mask) { this.#out.modeMask = mask&0b111; return this; } | ||
| setHeaderModeMask(mask) { if (mask <= 0) { throw new Error('Mode mask must be greater than 0'); } this.#out.headmodeMask = mask&0b111; return this; } | ||
| setGlobalModeMask(mask) { if (mask <= 0) { throw new Error('Mode mask must be greater than 0'); } this.#out.modeMask = mask&0b111; return this; } | ||
| setGlobalSeed(seed) { this.#out.rand = seed; return this; } | ||
| setInitialCursor(x, y) { this.#out.x = x; this.#out.y = y; return this; } | ||
| setPasswords(pws) { this.#out.pws = pws; return this; } | ||
| setSalt(salt, raw = false) { | ||
| let s = salt; | ||
| if ((raw) && (typeof s !== 'string') && (s.length != 64)) { throw new Error('Salt must be a hex string of length 64'); } | ||
| else if (typeof s === 'string') { | ||
| let hash = createHash('sha256'); | ||
| hash.update(s); | ||
| s = hash.digest('hex'); | ||
| } else { s = randomBytes(32).toString('hex'); } | ||
| this.#out.salt = s; | ||
| return this; | ||
| } | ||
| inputImage(path) { this.#out.in = path; return this; } | ||
@@ -1173,3 +1239,3 @@ outputImage(path) { this.#out.out = path; return this; } | ||
| clearMode() { this.#out.secs.push({ id: consts.SEC_MODE, rem: true }); return this; } | ||
| setModeMask(mask) { this.#out.secs.push({ id: consts.SEC_MODEMASK, mask: mask&0b111 }); return this; } | ||
| setModeMask(mask) { if (mask <= 0) { throw new Error('Mode mask must be greater than 0'); } this.#out.secs.push({ id: consts.SEC_MODEMASK, mask: mask&0b111 }); return this; } | ||
| clearModeMask() { this.#out.secs.push({ id: consts.SEC_MODEMASK, rem: true }); return this; } | ||
@@ -1201,3 +1267,6 @@ setSeed(seed) { this.#out.secs.push({ id: consts.SEC_RAND, seed }); return this; } | ||
| switch (type) { | ||
| case consts.CRYPT_AES256: break; | ||
| case consts.CRYPT_AES256: | ||
| case consts.CRYPT_CAMELLIA256: | ||
| case consts.CRYPT_ARIA256: | ||
| break; | ||
| default: throw new Error(`Unknown encryption type ${type}`); | ||
@@ -1232,16 +1301,61 @@ } | ||
| } | ||
| async getLoadOpts(packed, enc) { | ||
| if (!packed) { | ||
| return { | ||
| headmode: this.#out.headmode, | ||
| headmodeMask: this.#out.headmodeMask, | ||
| rand: this.#out.rand, | ||
| salt: this.#out.salt, | ||
| x: this.#out.x, | ||
| y: this.#out.y | ||
| }; | ||
| } else { | ||
| let key = undefined; | ||
| if (enc) { key = await (this.getPasswordHandler())(); } | ||
| return packString(JSON.stringify(await this.getLoadOpts(false)), key); | ||
| } | ||
| } | ||
| async setLoadOpts(blob, packed, enc) { | ||
| if (!packed) { | ||
| this.#out.headmode = blob.headmode; | ||
| this.#out.headmodeMask = blob.headmodeMask; | ||
| this.#out.rand = blob.rand; | ||
| this.#out.salt = blob.salt; | ||
| this.#out.x = blob.x; | ||
| this.#out.y = blob.y; | ||
| } else { | ||
| let key = undefined; | ||
| if (enc) { key = await (this.getPasswordHandler())(); } | ||
| this.setLoadOpts(JSON.parse(await unpackString(blob, key)), false); | ||
| } | ||
| } | ||
| async save() { | ||
| let steg = new v1(); | ||
| if (this.#out.keep) { this.#steg = steg; } | ||
| steg.pwcb = this.getPasswordHandler(); | ||
| return steg.save(this.#out); | ||
| } | ||
| loadMap(n, p) { | ||
| if (!this.#out.maps) { this.#out.maps = {}; } | ||
| this.#out.maps[n] = p; | ||
| return this; | ||
| } | ||
| saveMap(n, p) { | ||
| if (this.#steg) { | ||
| let { fullTable } = this.#steg; | ||
| if (this.#steg.master.src == n) { this.#steg.master.saveMap(p); } | ||
| else if ((fullTable[n]) && (fullTable[n].img)) { fullTable[n].img.saveMap(p); } | ||
| } | ||
| return this; | ||
| } | ||
| async load() { | ||
| let steg = new v1(); | ||
| if (this.#out.keep) { this.#steg = steg; } | ||
| steg.pwcb = this.getPasswordHandler(); | ||
| return steg.load(this.#out).then((secs) => { this.#secs = secs; return secs; }); | ||
| } | ||
| async extractAll(secs = this.#secs) { | ||
| async extractAll(secs = this.#secs, path = './extracted') { | ||
| let arr = [], s, bytes = 0, bytesStored = 0, time = (new Date()).getTime(); | ||
| for (let i = 0, l = secs.length; i < l; i++) { | ||
| s = await secs[i].extract(); | ||
| s = await secs[i].extract(path); | ||
| if ((s instanceof String) || (typeof s === 'string')) { arr.push(s); } | ||
@@ -1248,0 +1362,0 @@ bytes += secs[i].realSize; |
+70
-25
| import { util, consts, CreateBuilder } from '../steg.mjs'; | ||
| import { join as pathJoin } from 'path'; | ||
| import _PNG from 'pngjs'; | ||
| import fs from 'fs'; | ||
| const { PNG } = _PNG; | ||
| const verMajor = 1, verMinor = 1; | ||
| const verMajor = 1, verMinor = 2; | ||
@@ -61,24 +61,69 @@ let debug = false, channel = util.Channels.NORMAL; | ||
| let steg = CreateBuilder(verMajor, verMinor); | ||
| console.log('Testing saving...'); | ||
| steg.inputImage('frame|0|tests/orig.webp') | ||
| .outputImage('tests/out.webp') | ||
| .cliPasswordHandler() | ||
| .setGlobalAlphaBounds(consts.ALPHA_40) | ||
| .setGlobalMode(consts.MODE_A24BPP | consts.MODE_9BPP) | ||
| .setGlobalModeMask(consts.MODEMASK_RB) | ||
| .setCompression(consts.COMP_BROTLI, 11, true) | ||
| .setEncryption(consts.CRYPT_AES256) | ||
| .setImageTable(['frame|1|tests/orig.webp', 'frame|0|tests/orig.webp'], ['frame|1|tests/out.webp', 'frame|0|tests/out.webp']) | ||
| .moveImage(0) | ||
| .addDirectory('specs', true, true) | ||
| .moveImage(1) | ||
| .addText('This contains the specs for how this image is formatted', consts.TEXT_HONOR_COMPRESSION | consts.TEXT_HONOR_ENCRYPTION) | ||
| .save() | ||
| .catch(console.log) | ||
| .then(() => console.log('Testing loading...')) | ||
| .then(() => steg.clear().inputImage('frame|0|tests/out.webp') | ||
| .cliPasswordHandler() | ||
| .load() | ||
| .then((secs) => { console.log('Finished loading\nExtracting...'); return steg.extractAll(secs); }) | ||
| .then(console.log)); | ||
| function cleanTmpDir() { | ||
| let paths = fs.readdirSync('./tests/tmp', { withFileTypes: true }); | ||
| for (let i = 0, l = paths.length; i < l; i++) { | ||
| if (paths[i].isDirectory()) { fs.rmdirSync(`./tests/tmp/${paths[i].name}`, { recursive: true }); } | ||
| else { fs.unlinkSync(`./tests/tmp/${paths[i].name}`); } | ||
| } | ||
| } | ||
| async function main() { try { await go(); } catch (e) { console.log(e); } } | ||
| async function go() { | ||
| let secs; | ||
| steg.cliPasswordHandler(); | ||
| try { fs.mkdirSync('tests/tmp'); } catch (e) {} | ||
| console.log('Testing saving...'); | ||
| await steg.inputImage('frame|0|tests/orig.webp') | ||
| .outputImage('tests/tmp/out.webp') | ||
| .setGlobalAlphaBounds(consts.ALPHA_40) | ||
| .setGlobalMode(consts.MODE_A24BPP | consts.MODE_9BPP) | ||
| .setGlobalModeMask(consts.MODEMASK_RB) | ||
| .setCompression(consts.COMP_BROTLI, 11, true) | ||
| .setEncryption(consts.CRYPT_AES256) | ||
| .setImageTable(['frame|1|tests/orig.webp', 'frame|0|tests/orig.webp'], ['frame|1|tests/tmp/out.webp', 'frame|0|tests/tmp/out.webp']) | ||
| .moveImage(0) | ||
| .addDirectory('specs', true, true) | ||
| .moveImage(1) | ||
| .addText('This contains the specs for how this image is formatted', consts.TEXT_HONOR_COMPRESSION | consts.TEXT_HONOR_ENCRYPTION) | ||
| .save(); | ||
| console.log('Testing loading...'); | ||
| steg.clear() | ||
| .inputImage('frame|0|tests/tmp/out.webp') | ||
| .cliPasswordHandler(); | ||
| secs = await steg.load(); | ||
| console.log('Finished loading\nExtracting...'); | ||
| console.log(await steg.extractAll(secs, './tests/tmp')); | ||
| console.log('Testing multipack...'); | ||
| fs.writeFileSync('tests/tmp/img.opts', await steg.clear().setHeaderMode(consts.MODE_A24BPP | consts.MODE_9BPP).getLoadOpts(true, true), 'binary'); | ||
| console.log('Saving opts file...'); | ||
| await steg.clear() | ||
| .inputImage('tests/text.clean.png') | ||
| .outputImage('tests/tmp/out.png') | ||
| .setHeaderMode(consts.MODE_A24BPP | consts.MODE_9BPP) | ||
| .setGlobalMode(consts.MODE_A24BPP | consts.MODE_9BPP) | ||
| .addFile('tests/tmp/img.opts', 'out.opts') | ||
| .keep() | ||
| .save(); | ||
| console.log('Saving data...'); | ||
| await steg.saveMap('tests/text.clean.png', 'tests/tmp/img.map').clear() | ||
| .inputImage('tests/tmp/out.png') | ||
| .loadMap('out.png', 'tests/tmp/img.map') | ||
| .outputImage('tests/tmp/out.png') | ||
| .setHeaderMode(consts.MODE_A24BPP | consts.MODE_9BPP) | ||
| .setGlobalMode(consts.MODE_A24BPP | consts.MODE_9BPP) | ||
| .addText('Did this work?') | ||
| .save(); | ||
| steg.clear() | ||
| .inputImage('tests/tmp/out.png') | ||
| .setHeaderMode(consts.MODE_A24BPP | consts.MODE_9BPP); | ||
| console.log('Extracting opts file...'); | ||
| secs = await steg.load(); | ||
| await steg.extractAll(secs, './tests/tmp'); | ||
| console.log('Extracting data...'); | ||
| await steg.clear().setLoadOpts(fs.readFileSync('tests/tmp/out.opts', 'binary'), true, true); | ||
| secs = await steg.inputImage('tests/tmp/out.png') | ||
| .loadMap('out.png', 'tests/tmp/img.map') | ||
| .load(); | ||
| console.log(await steg.extractAll(secs, './tests/tmp')); | ||
| cleanTmpDir(); | ||
| } | ||
| main().then(()=>{}); |
+60
-0
@@ -7,2 +7,3 @@ import fs from 'fs'; | ||
| let _debug = false, _channel = Channels.NORMAL; | ||
| const utilSalt = '9cec15573a52086a0266af3b05deabf8503421ca49de1a4625b8e4a585ba7f4d'; | ||
| export function debug(v) { | ||
@@ -35,2 +36,3 @@ if (v === undefined) { return _debug; } | ||
| export function hashToDec(h) { return parseInt(baseConv(h, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ', '0123456789')); } | ||
| export function decToHash(d) { return baseConv(d.toString(), '0123456789', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 '); } | ||
| export function pad(s, l, c) { let o = s; for (let i = s.length; i < l; i++) { o = c+o; } return o; } | ||
@@ -100,2 +102,60 @@ export function setChannel(chan) { _channel = chan; } | ||
| export function decryptaes256(key, iv) { return createDecipheriv('aes-256-cbc', key, iv); } | ||
| export function cryptcamellia256(key, iv) { return createCipheriv('camellia-256-cbc', key, iv); } | ||
| export function decryptcamellia256(key, iv) { return createDecipheriv('camellia-256-cbc', key, iv); } | ||
| export function cryptaria256(key, iv) { return createCipheriv('aria-256-cbc', key, iv); } | ||
| export function decryptaria256(key, iv) { return createDecipheriv('aria-256-cbc', key, iv); } | ||
| export async function packString(s, pw) { | ||
| let fmods = [ ], bufs = [], b, st; | ||
| if (pw) { | ||
| let key = await getCryptKey(pw, utilSalt), iv = generateIV(); | ||
| b = Buffer.alloc(17); | ||
| b[0] = 1; | ||
| iv.copy(b, 1); | ||
| bufs.push(b); | ||
| fmods.push(cryptaes256(key, iv)); } | ||
| else { | ||
| b = Buffer.alloc(1); | ||
| b[0] = 0; | ||
| bufs.push(b); | ||
| } | ||
| if (fmods.length > 0) { | ||
| b = st = fmods[0]; | ||
| for (let i = 1, l = fmods.length; i < l; i++) { b.pipe(fmods[i]); b = fmods[i]; } | ||
| st.write(s, 'utf8'); | ||
| st.end(); | ||
| for await (const chunk of b) { bufs.push(chunk); } | ||
| } else { bufs.push(Buffer.from(s, 'binary')); } | ||
| return Buffer.concat(bufs); | ||
| } | ||
| export async function unpackString(s, pw) { | ||
| let fmods = [ ], buf = Buffer.from(s, 'binary'), bufs = []; | ||
| if ((buf[0]) && (!pw)) { throw new Error('Input blob is encrypted but no key provided'); } | ||
| else if ((buf[0]) && (pw)) { | ||
| let key = await getCryptKey(pw, utilSalt), iv = buf.slice(1, 17); | ||
| buf = buf.slice(16); | ||
| fmods.unshift(decryptaes256(key, iv)); | ||
| } | ||
| buf = buf.slice(1); | ||
| if (fmods.length > 0) { | ||
| let b = fmods[0], st = b, txt = ''; | ||
| for (let i = 1, l = fmods.length; i < l; i++) { b.pipe(fmods[i]); b = fmods[i]; } | ||
| st.write(buf); | ||
| st.end(); | ||
| for await (const chunk of b) { txt += chunk; } | ||
| return txt; | ||
| } else { return buf.toString(); } | ||
| } | ||
| export function uintToVLQ(uint, chkSize) { | ||
| let out = [], i = 0, k, n = uint, s = chkSize-1, mask = (1<<s)-1; | ||
| while (true) { | ||
| k = n & (mask<<(s*i)); | ||
| n -= k; | ||
| k = k >> s*i; | ||
| out.push(k); | ||
| if (n == 0) { break; } | ||
| i++; | ||
| } | ||
| out[out.length-1] |= 1<<s; | ||
| return out; | ||
| } | ||
| export class randr { | ||
@@ -102,0 +162,0 @@ constructor(s) { this._seed = 0; if (s) { this.seed = s; } } |
-277
| First import the CreateBuilder helper and constants from steg | ||
| import { consts, CreateBuilder } from 'steg'; | ||
| You can also import util if you want to control how verbose it is for debugging purposes. | ||
| import { util } from 'steg'; | ||
| Now, lets create a builder. | ||
| let steg = CreateBuilder(); | ||
| The arguments are CreateBuilder([major version, [minor version]]), if you want to use a specific version. | ||
| At the time of writing, the default is v1.1. | ||
| For just packing a file in and calling it a day, | ||
| steg.inputImage('path/to/input/image.ext') | ||
| .outputImage('path/to/output/image2.ext') | ||
| .addFile('path/to/file') | ||
| .save(); | ||
| And to extract it. If reusing the same steg object, clear() must be called. | ||
| steg.clear() | ||
| .inputImage('path/to/output/image2.ext') | ||
| .load() | ||
| .then((secs) => { | ||
| // At this point, you're given a list of extractable items. If you don't care what's in it, or already know, there's a helper function for making it easier to extract everything | ||
| return steg.extractAll(secs); | ||
| }) | ||
| .then(() => console.log('Done')); | ||
| Supported image formats are PNG and WEBP (both static and animated). | ||
| When it saves PNGs, it saves at compression 9 (highest). | ||
| When it saves WEBPs, it saves as lossless+exact (save transparent pixels). | ||
| For what I hope are obvious reasons, lossy formats can't work. | ||
| For animated WEBP images, the syntax is mostly the same. However, when both supplying paths for saving and loading (except for one exception below) you must provide them in the format frame|<frame number, starting at 0>|<path>. | ||
| steg.clear() | ||
| .inputImage('frame|0|animated.webp') | ||
| .outputImage('out.webp') // This is the one exception where you don't need the special format | ||
| // do things as normal | ||
| .save(); | ||
| steg.clear() | ||
| .inputImage('frame|0|out.webp') | ||
| // etc | ||
| .load() | ||
| // etc | ||
| There are a number of storage modes available and can be applied separately for both alpha and non-alpha pixels. | ||
| 3bpp: Stores 3 bits in the pixel, using the LSB of each of the RGB color channels | ||
| 6bpp: Stores 6 bits using the lower 2 bits of each channel | ||
| 9bpp: Ditto, but lower 3 bits | ||
| 12bpp: Lower 4 | ||
| 15bpp: Lower 5 | ||
| 24bpp: Uses the full RGB values. This is handy if you don't care so much about the hiding but do want to use the storage | ||
| 32bpp: This is a semi-special mode that forces overwriting the full RGBA data of every pixel. This is more implemented for completeness than anything. | ||
| There are also modifiers for what's considered alpha vs non-alpha, and which color channels to use if you want to leave one or more untouched. | ||
| Below is a full list of what the current steg builder can do. | ||
| The term "out-of-band" is used to describe information that's needed but *not* stored in the image(s) and must be found by other means. | ||
| == Helper or utility == | ||
| clear() | ||
| Resets the object for re-use. | ||
| dryrun(comp = false) | ||
| Switches to doing a dry run of the saving process. Everything is supported, but the save() call doesn't do the final saving. This does *not* create or modify any files. Any compression it would do is skipped and the full size is used instead. | ||
| Set comp to `true` to enable compression. This *does* create temporary files and runs files through compression (where applicable) as it would during the normal saving process. | ||
| setPasswords(pws) | ||
| This is an out-of-band setting. | ||
| More of a helper function. Pass it an array of passwords to pull from (in order) whenever it needs a password rather than prompting the user. | ||
| cliPasswordHandler() | ||
| Asks the user for missing passwords via the command line and a silent 'Enter password:' prompt. | ||
| == Input/output == | ||
| inputImage(path) | ||
| The input image for both saving and loading. | ||
| outputImage(path) | ||
| The output image for saving. | ||
| save() (async) | ||
| Saves the image(s). | ||
| load() (async) | ||
| Parses the image(s) and returns a list of data sections (see Classes section below). | ||
| extractAll(secs = this.#secs) (async) | ||
| Extract all the sections in `secs` or, if null/undefined, extract all sections found. | ||
| == Out-of-band == | ||
| setHeaderMode(mode) | ||
| This sets the mode used to store the first half of the header. It defaults to MODE_A3BPP | MODE_3BPP. | ||
| setHeaderModeMask(mask) | ||
| This changes which channels are used to store the first half of the header. It defaults to MODEMASK_RGB. | ||
| setGlobalSeed(seed) | ||
| This uses `seed` to randomly distribute the header and data around the image. It defaults to disabled. | ||
| `seed` is an arbitrary-length string consisting of a-z, A-Z, 0-9, and spaces. | ||
| == Header == | ||
| setGlobalMode(mode) | ||
| This sets the mode used to store the second half of the header, as well as the rest of the data in general. It defaults to MODE_A3BPP | MODE_3BPP. | ||
| setGlobalModeMask(mask) | ||
| This changes which channels are used to store the second half of the header, as well as the rest of the data in genereal. It defaults to MODEMASK_RGB. | ||
| setGlobalAlphaBounds(bounds) | ||
| This changes what alpha value is considered alpha vs non-alpha. It supports 8 steps, each roughly 36 apart. Defaults to ALPHA_255. | ||
| == Sections == | ||
| setAlphaBounds(bounds) | ||
| This changes what alpha value is considered alpha vs non-alpha from what is set globally until another setAlphaBounds is called or is cleared. | ||
| clearAlphaBounds() | ||
| Removes the active setAlphaBounds and returns the alpha value to the global one. | ||
| setRect(x, y, width, height) | ||
| Bounds all operations within the defined rectangle until another setRect called or is cleared. | ||
| clearRect() | ||
| Removes the active setRect. | ||
| setMode(mode) | ||
| Override the global mode until another setMode is called or is cleared. | ||
| clearMode() | ||
| Reset the mode back to the global mode. | ||
| setModeMask(mask) | ||
| Override the global mode mask until another setModeMask is called or is cleared. | ||
| clearModeMask() | ||
| Reset the mode mask back to the global mode mask. | ||
| setSeed(seed) | ||
| Override the global seed until another setSeed is called or is cleared. | ||
| clearSeed() | ||
| Reset the seed back to the global seed. If there was no global seed, this disables the randomness. | ||
| pushCursor()/popCursor() | ||
| Save/load the image index and x,y position of the cursor. | ||
| moveCursor(x, y, index = 0) | ||
| Move the cursor to x, y in the current image or the one specified by index. | ||
| setImageTable(inputFiles, outputFiles) | ||
| This sets up a table of images you can jump around between with moveCursor. | ||
| Both arguments are arrays and *must* be the same length. | ||
| Naming rules from inputImage apply to both (so WEBP anim 'frame|<n>|<path>' rules apply to outputFiles). | ||
| It is, however, currently unsupported to mix anim and non-anim WEBP, or mix frames. | ||
| Example: | ||
| Each assuming .inputImage('frame|0|in.webp').outputImage('frame|0|out.webp') | ||
| .setImageTable([ 'frame|1|in.webp' ], [ 'frame|1|out.webp' ]) | ||
| This is valid | ||
| .setImageTable([ 'frame|4|in.webp' ], [ 'frame|1|out.webp' ]) | ||
| This is unsupported | ||
| .setImageTable([ 'random.png' ], [ 'frame|1|out.webp' ]) | ||
| This is also unsupported, as is using 'random.webp' | ||
| .setImageTable([ 'frame|1|in.webp' ], [ 'random.webp' ]) | ||
| This is also unsupported | ||
| .setImageTable([ 'frame|1|in.webp' ], [ 'frame|1|different.webp' ]) | ||
| This is also unsupported | ||
| The short version is that it only supports modifying frames in the same animation, not replacing or extracting them. See steg/webp.mjs or the `webpmux` tool if you need that (I'd recommend webp.mjs as I've got a more-complete toolset than `webpmux` does in it). | ||
| clearImageTable() | ||
| Disables the active table and moves the cursor back to the main image. Any images from any previously-active tables will still be written to properly. | ||
| setCompression(type, level = 0, text = false) | ||
| Set the active compression algorithm to run files/text through. Currently, only COMP_GZIP and COMP_BROTLI are supported. | ||
| For GZIP: | ||
| `level` must range 0-9 | ||
| `text` is unused | ||
| For BROTLI: | ||
| `level` must range 0-11 | ||
| `text` enables BROTLI's special text-mode compression | ||
| clearCompression() | ||
| Clear an active setCompression. | ||
| setEncryption(type, pw) | ||
| Set the active encryption algorithm to run files/text through. Currently only CRYPT_AES256 (AES-256-CBC) is supported. | ||
| clearEncryption() | ||
| Clear an active setEncryption. | ||
| == Files/text == | ||
| addFile(path, name, compressed = false) | ||
| Add the file at `path` to the image under the name `name`. | ||
| Set `compressed` to `true` if the file is already compressed via the active compression mode. | ||
| addDirectory(path, full = false, recursive = false, compressed = false) | ||
| Add the contents of the directory at `path` to the image. File names are preserved as-is and the basename of path is used as the base path. Example, `addDirectory('a/b/c')` will add the contents of that directory under `c/`. | ||
| Set `full` to `true` to add the path names as-is rather than the basename. Example, `addDirectory('a/b/c')` will then add the contents of that directory under `a/b/c/`. | ||
| Set `recursive` to `true` to recursively add any other directories until the path. | ||
| Set `compressed` to `true` if ALL files under `path` are already compressed via the active compression mode. | ||
| addPartialFile(path, name, index, compressed = false) | ||
| Add the file at `path` you intend to store in pieces under the name `name` and index `index`. | ||
| Set `compressed` to `true` if the file is already compressed via the active compression mode. | ||
| `index` can be any integer 0 <= n <= 255 and is used solely for your own reference in addPartialFilePiece. | ||
| addPartialFilePiece(index, size = 0, last = false) | ||
| Add a piece of file `index`. | ||
| If `size` is 0 or greater than the remaining size of the file, the rest of the file is written and `last` is assumed `true`. | ||
| Set `last` to `true` to flag that this is the last piece you intend to write. You can use this if you don't intend to write the entire file. | ||
| addText(text, honor = TEXT_HONOR_NONE) | ||
| Adds a simple block of text to the image. More simple than a text file. | ||
| `honor` is a mask of TEXT_HONOR_ENCRYPTION and TEXT_HONOR_COMPRESSION to control which, if any, are desired to apply to this text block. | ||
| == Classes == | ||
| StegFile | ||
| name | ||
| Name of the file | ||
| size | ||
| Size of the file as it was stored (after compression/encryption) | ||
| realSize | ||
| Uncompressed/decrypted size of the file (only computed *after* extracting) | ||
| extract(path = './extracted') (async) | ||
| Extract the file to `path` | ||
| StegPartialFile | ||
| name | ||
| Name of the file | ||
| size | ||
| Size of the file as it was stored (after compression/encryption) | ||
| realSize | ||
| Uncompressed/decrypted size of the file (only computed *after* extracting) | ||
| extract(path = './extracted') (async) | ||
| Extract the file to `path` | ||
| StegText | ||
| size | ||
| Size of the text as it was stored (after compression/encryption) | ||
| realSize | ||
| Uncompressed/decrypted size of the text (only computed *after* extracting) | ||
| extract() (async) | ||
| Extracts and returns the text | ||
| == Util == | ||
| `util` has many things, but for controlling verbosity, only util.Channels, util.debug, and util.setChannel are important. | ||
| debug(v) | ||
| Set `v` to `true` to enable debug mode, `false` to disable it, or pass nothing to get the current debug state. | ||
| This mostly only disables the file extraction progress messages ("Saved x of size") | ||
| Does NOT set channel to DEBUG | ||
| Channels | ||
| SILENT: Outputs nothing at all | ||
| NORMAL: Default; Outputs basic information during saving/extracting, such as the number of pixels changed per image and extraction progress during exracting. | ||
| VERBOSE: Ouputs more detailed information about what it's doing to the image. Mostly useless. | ||
| VVERBOSE: Outputs even *more* information about what it's doing, but mostly during loading. | ||
| DEBUG: Outputs each and every modified pixel of every image it touches. | ||
| setChannel(channel) | ||
| Sets the output channel to one of the above. | ||
| For a full (more technical) description of the format things are stored in the image(s), see the file steg/specs/v<major>.<minor>.spec (like steg/specs/v1.1.spec). | ||
| Also see test.mjs for more examples in a very ugly file. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
603384
7.79%3
-25%18
12.5%2365
30.09%438
57.55%6
-57.14%0
-100%+ Added
- Removed
- Removed
- Removed
- Removed
Updated