backdraw 0.0.0-stage → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +635 -2
  3. package/README.tr.md +653 -0
  4. package/bin/backdraw.js +60 -0
  5. package/cli.js +105 -0
  6. package/doctor.js +174 -0
  7. package/extension/_locales/en/messages.json +3 -0
  8. package/extension/_locales/tr/messages.json +3 -0
  9. package/extension/aspect.js +25 -0
  10. package/extension/background.js +1015 -0
  11. package/extension/collection.html +45 -0
  12. package/extension/collection.js +62 -0
  13. package/extension/collections.js +409 -0
  14. package/extension/i18n.js +359 -0
  15. package/extension/icons/icon128.png +0 -0
  16. package/extension/icons/icon16.png +0 -0
  17. package/extension/icons/icon32.png +0 -0
  18. package/extension/icons/icon48.png +0 -0
  19. package/extension/icons/mascot-192.png +0 -0
  20. package/extension/icons/mascot-96.png +0 -0
  21. package/extension/inpage.js +624 -0
  22. package/extension/manifest.json +20 -0
  23. package/extension/menus.js +50 -0
  24. package/extension/popup.css +90 -0
  25. package/extension/popup.html +63 -0
  26. package/extension/popup.js +376 -0
  27. package/extension/settings.js +35 -0
  28. package/extension/setup.js +57 -0
  29. package/extension/sidepanel.css +113 -0
  30. package/extension/sidepanel.html +31 -0
  31. package/extension/sidepanel.js +433 -0
  32. package/extension/theme.css +22 -0
  33. package/host.js +664 -0
  34. package/identity.js +21 -0
  35. package/install.js +427 -0
  36. package/lib/args.js +30 -0
  37. package/lib/aspect.js +85 -0
  38. package/lib/browsers.js +74 -0
  39. package/lib/collection-store.js +288 -0
  40. package/lib/collections.js +192 -0
  41. package/lib/doctor.js +143 -0
  42. package/lib/extension-calls.js +59 -0
  43. package/lib/gallery.js +63 -0
  44. package/lib/limits.js +18 -0
  45. package/lib/mcp-clients.js +178 -0
  46. package/lib/naming.js +25 -0
  47. package/lib/png.js +24 -0
  48. package/lib/projects.js +56 -0
  49. package/lib/prompt.js +25 -0
  50. package/lib/protocol.js +50 -0
  51. package/lib/results.js +170 -0
  52. package/lib/term.js +22 -0
  53. package/lib/toml.js +248 -0
  54. package/mcp-server.js +437 -0
  55. package/package.json +52 -4
  56. package/platform/index.js +14 -0
  57. package/platform/win32.js +268 -0
  58. package/setup.js +213 -0
  59. package/shared.js +118 -0
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // The `backdraw` command (npm package bin): setup, doctor, uninstall, and every cli.js command.
4
+ // npx backdraw@latest setup
5
+ const readline = require('readline');
6
+ const { parseArgs } = require('../lib/args');
7
+ const cli = require('../cli');
8
+
9
+ const VERSION = require('../package.json').version;
10
+
11
+ const HELP = `Backdraw ${VERSION}: let Claude generate images with your own ChatGPT account in a background Chrome tab.
12
+
13
+ Usage: backdraw <command>
14
+ setup [--yes] [--clients=a,b] [--browser=chrome|edge|brave|chromium] [--no-wait]
15
+ Install or update, register the browsers and MCP clients, load the extension
16
+ clients: claude-code, claude-desktop, codex, cursor, vscode, gemini
17
+ doctor [--fix] Check the install; --fix repairs registrations
18
+ uninstall [--dry-run] Remove the registrations (your images and settings stay)
19
+ mcp Run the MCP server on stdio (what MCP clients start; needs setup once)
20
+ --version Print the version
21
+ ${cli.USAGE.split('\n').slice(1).join('\n')}`;
22
+
23
+ // A question on the terminal, or no asking at all without one (setup then takes the found clients).
24
+ function asker() {
25
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return undefined;
26
+ return question => new Promise(resolve => {
27
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
28
+ rl.on('SIGINT', () => { rl.close(); process.exit(130); });
29
+ rl.question(question, answer => { rl.close(); resolve(answer); });
30
+ });
31
+ }
32
+
33
+ async function main(argv) {
34
+ const [cmd, ...rest] = argv;
35
+ const { flags } = parseArgs(rest, { booleans: ['yes', 'fix', 'dry-run', 'no-wait'] });
36
+ switch (cmd) {
37
+ // stdout belongs to the JSON-RPC stream from here on; the server reads stdin until it closes.
38
+ case 'mcp':
39
+ return require('../mcp-server');
40
+ case '-v': case '--version': case 'version':
41
+ return console.log(VERSION);
42
+ case 'setup':
43
+ return require('../setup').setup({ yes: !!flags.yes, clients: flags.clients, browser: flags.browser, wait: !flags['no-wait'] }, { ask: flags.yes ? undefined : asker() });
44
+ case 'doctor': {
45
+ const checks = await require('../doctor').doctor({ fix: !!flags.fix });
46
+ if (checks.some(c => c.status === 'fail')) process.exitCode = 1;
47
+ return undefined;
48
+ }
49
+ case 'uninstall':
50
+ return require('../install').uninstall({ dryRun: !!flags['dry-run'] });
51
+ case undefined: case '-h': case '--help': case 'help':
52
+ return console.log(HELP);
53
+ default:
54
+ if (cli.COMMANDS.includes(cmd)) return cli.run(argv);
55
+ console.log(HELP);
56
+ throw new Error(`Unknown command "${cmd}".`);
57
+ }
58
+ }
59
+
60
+ main(process.argv.slice(2)).catch(e => { console.error(`Error: ${e.message}`); process.exitCode = 1; });
package/cli.js ADDED
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+ // Command line access to the bridge, for setup and troubleshooting.
3
+ const path = require('path');
4
+ const bridge = require('./shared');
5
+ const { parseArgs } = require('./lib/args');
6
+ const { FINAL } = require('./lib/results');
7
+ const { MAX_REFS } = require('./lib/limits');
8
+ const { findCollection } = require('./lib/collections');
9
+
10
+ const USAGE = `Usage: node cli.js <command>
11
+ status Bridge status (Chrome connected, selected project, queue)
12
+ projects List ChatGPT projects
13
+ select <name | link> Select the project where images are generated
14
+ generate "<prompt>" [--ref IMAGE]... [--url CHAT_LINK] [--aspect W:H] [--transparent] [--reattach] [--out FOLDER] [--raw] [--wait 300]
15
+ [--collection NAME [--file FILE]...]
16
+ Generate an image; --ref attaches a reference/image to edit (up to ${MAX_REFS} in all);
17
+ --collection adds a reference collection's newest images (--file picks them);
18
+ --aspect 16:9 sets the shape (1:4 to 4:1), --transparent asks for a see-through background
19
+ collect <chat link> [--aspect W:H] [--out FOLDER] Download the latest images of an existing chat (sends nothing), cropped to W:H
20
+ job <id> [--wait 60] Show a job's status
21
+ cancel <id> Cancel a job
22
+ collections List the reference collections (Chrome: right-click an image, "Add to Backdraw")
23
+ collection <name> Show one collection and its images
24
+ outdir <folder> Change the central images folder
25
+ debug on|off Verbose logging (logs/host.log)
26
+ diagnose [link] [--selector CSS] [--max N] [--worker] Page diagnostics
27
+ reload Reload the Chrome extension`;
28
+
29
+ const FLAGS = { booleans: ['raw', 'reattach', 'worker', 'transparent'], lists: ['ref', 'file'] };
30
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
31
+
32
+ const print = v => console.log(JSON.stringify(v, null, 2));
33
+
34
+ async function follow(jobId, waitSec) {
35
+ const deadline = Date.now() + waitSec * 1000;
36
+ let job = await bridge.call('job', { jobId });
37
+ let last = '';
38
+ for (;;) {
39
+ const note = `${job.status} | ${job.step} | ${job.message || ''}`;
40
+ if (note !== last) { console.error(`[${new Date().toLocaleTimeString()}] ${note}`); last = note; }
41
+ if (FINAL.has(job.status) || Date.now() > deadline) return bridge.copyResults(job);
42
+ await sleep(1500);
43
+ job = await bridge.call('job', { jobId });
44
+ }
45
+ }
46
+
47
+ // argv: the words after "node cli.js" (or "backdraw").
48
+ async function run(argv) {
49
+ const [cmd, ...rest] = argv;
50
+ const { pos, flags } = parseArgs(rest, FLAGS);
51
+ const copyTo = typeof flags.out === 'string' ? path.resolve(flags.out) : undefined;
52
+ if (flags.aspect === true) throw new Error('--aspect needs a ratio, e.g. --aspect 16:9.');
53
+ switch (cmd) {
54
+ case 'status': return print(await bridge.call('status'));
55
+ case 'projects': return print(await bridge.call('list_projects', {}, 130000));
56
+ case 'select': {
57
+ const v = pos.join(' ');
58
+ if (!v) throw new Error('A project name or link is required.');
59
+ return print(await bridge.call('select_project', /^https?:\/\//i.test(v) ? { url: v } : { name: v }, 130000));
60
+ }
61
+ case 'generate': {
62
+ const prompt = pos.join(' ');
63
+ if (!prompt) throw new Error('A prompt is required.');
64
+ const job = await bridge.call('generate', {
65
+ prompt, copyTo, references: bridge.readReferences(flags.ref ? [].concat(flags.ref) : []),
66
+ startUrl: typeof flags.url === 'string' ? flags.url : undefined, sendAsIs: !!flags.raw, autoReattach: !!flags.reattach,
67
+ aspect: flags.aspect, transparent: !!flags.transparent,
68
+ referenceCollection: typeof flags.collection === 'string' ? flags.collection : undefined, referenceFiles: flags.file ? [].concat(flags.file) : undefined,
69
+ }, 60000);
70
+ console.error(`Job: ${job.id}`);
71
+ return print(await follow(job.id, Number(flags.wait) || 600));
72
+ }
73
+ case 'collect': {
74
+ if (!pos[0]) throw new Error('A chat link is required.');
75
+ const job = await bridge.call('generate', { mode: 'collect', startUrl: pos[0], copyTo, aspect: flags.aspect });
76
+ console.error(`Job: ${job.id}`);
77
+ return print(await follow(job.id, Number(flags.wait) || 300));
78
+ }
79
+ case 'job': return print(await follow(pos[0], Number(flags.wait) || 0));
80
+ case 'cancel': return print(await bridge.call('cancel', { jobId: pos[0] }));
81
+ case 'collections': return print(await bridge.call('list_collections', { summary: true }));
82
+ case 'collection': {
83
+ const name = pos.join(' ');
84
+ if (!name) throw new Error('A collection name is required.');
85
+ const list = await bridge.call('list_collections');
86
+ const { collection, error, ties } = findCollection(list, name);
87
+ if (error === 'AMBIGUOUS') throw new Error(`"${name}" matches several collections: ${ties.map(c => c.name).join(', ')}`);
88
+ if (error) throw new Error(`No collection matches "${name}". Available: ${list.map(c => c.name).join(', ') || '(none)'}`);
89
+ return print(collection);
90
+ }
91
+ case 'outdir': return print(await bridge.call('set_config', { outputDir: path.resolve(pos[0]) }));
92
+ case 'debug': return print(await bridge.call('set_config', { debug: pos[0] === 'on' }));
93
+ case 'diagnose':
94
+ return print(await bridge.call('diagnose', {
95
+ url: pos[0], selector: flags.selector, max: flags.max ? Number(flags.max) : undefined, target: flags.worker ? 'worker' : undefined,
96
+ }, 190000));
97
+ case 'reload': return print(await bridge.call('reload_extension'));
98
+ default:
99
+ console.log(USAGE);
100
+ }
101
+ }
102
+
103
+ if (require.main === module) run(process.argv.slice(2)).catch(e => { console.error(`Error: ${e.message}`); process.exitCode = 1; });
104
+
105
+ module.exports = { USAGE, COMMANDS: ['status', 'projects', 'select', 'generate', 'collect', 'job', 'cancel', 'collections', 'collection', 'outdir', 'debug', 'diagnose', 'reload'], run };
package/doctor.js ADDED
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+ // `backdraw doctor [--fix]`: gathers what can be checked about an install (files, host manifest,
3
+ // browser registry keys and policies, the bridge, the extension, ChatGPT, the MCP clients), prints
4
+ // it with lib/doctor.js, and with --fix repairs what is safe to repair: the host manifest, the
5
+ // registry keys, and client entries that point to the wrong place.
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
9
+ const identity = require('./identity');
10
+ const installer = require('./install');
11
+ const browsers = require('./lib/browsers');
12
+ const mcpClients = require('./lib/mcp-clients');
13
+ const report = require('./lib/doctor');
14
+ const { useColor, painter, line } = require('./lib/term');
15
+
16
+ const SELF = require('./package.json').version;
17
+ // What an install needs to run; anything else in the folder is optional.
18
+ const REQUIRED = ['package.json', 'identity.js', 'shared.js', 'host.js', 'mcp-server.js', 'cli.js', 'install.js',
19
+ path.join('extension', 'manifest.json'), path.join('platform', 'index.js'), 'lib'];
20
+
21
+ function readJson(file) {
22
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
23
+ }
24
+
25
+ function commandExists(p, cmd) {
26
+ if (typeof cmd !== 'string' || !cmd) return false;
27
+ return path.isAbsolute(cmd) ? fs.existsSync(cmd) : !!p.which(cmd);
28
+ }
29
+
30
+ // What is wrong with the host manifest, or null.
31
+ function manifestProblem(json, dest) {
32
+ if (!json) return 'unreadable';
33
+ if (json.name !== identity.HOST_NAME) return `names ${json.name}, not ${identity.HOST_NAME}`;
34
+ if (!json.path || !fs.existsSync(json.path)) return `its launcher ${json.path || '(none)'} is missing`;
35
+ const node = (/"([^"]+node(?:\.exe)?)"/i.exec(fs.readFileSync(json.path, 'utf8')) || [])[1];
36
+ if (node && !fs.existsSync(node)) return `its launcher starts ${node}, which is gone (Node.js moved?)`;
37
+ const ext = readJson(path.join(dest, 'extension', 'manifest.json'));
38
+ const origin = ext && ext.key ? `chrome-extension://${installer.extensionId(ext)}/` : null;
39
+ if (origin && !(json.allowed_origins || []).includes(origin)) return 'it does not allow the Backdraw extension';
40
+ return null;
41
+ }
42
+
43
+ // c: install.js context. call: the bridge's call(method, params, timeout).
44
+ async function gather(c, call) {
45
+ const p = c.platform;
46
+ const dest = p.installDir(c.home);
47
+ const pkg = readJson(path.join(dest, 'package.json'));
48
+ const ext = readJson(path.join(dest, 'extension', 'manifest.json'));
49
+ const exists = fs.existsSync(dest);
50
+ const facts = {
51
+ node: process.version, self: { version: SELF }, hostName: identity.HOST_NAME,
52
+ install: {
53
+ dir: dest, exists, missing: exists ? REQUIRED.filter(f => !fs.existsSync(path.join(dest, f))) : [],
54
+ version: pkg && pkg.version, extensionVersion: ext && ext.version,
55
+ },
56
+ };
57
+
58
+ const manifestFile = path.join(dest, `${identity.HOST_NAME}.json`);
59
+ const manifestThere = fs.existsSync(manifestFile);
60
+ facts.hostManifest = { file: manifestFile, exists: manifestThere, problem: manifestThere ? manifestProblem(readJson(manifestFile), dest) : null };
61
+
62
+ let reg = null;
63
+ try { reg = p.parseBrowserRegistry(p.readBrowserRegistry(identity.HOST_NAME)); } catch (e) { facts.registryError = e.message.split('\n')[0]; }
64
+ const targets = installer.targetBrowsers(p); // Chrome, and every other browser that is installed
65
+ facts.browsers = browsers.BROWSERS.map(b => ({
66
+ id: b.id, name: b.name, target: targets.includes(b.id),
67
+ registration: reg ? browsers.effectiveHost(b, reg.hosts) : null, policies: reg ? reg.policies[b.id] : null,
68
+ }));
69
+
70
+ try {
71
+ facts.bridge = { ok: true, status: await call('status', {}, 5000) };
72
+ } catch (e) {
73
+ facts.bridge = { ok: false, code: e.code, error: e.message };
74
+ }
75
+ if (facts.bridge.ok && facts.bridge.status.extension) {
76
+ try { facts.chatgpt = await call('chatgpt_state', {}, 15000); } catch (e) {
77
+ facts.chatgpt = { error: e.code === 'BAD_REQUEST' ? 'the running host is older than this version' : e.message };
78
+ }
79
+ }
80
+
81
+ const server = path.join(dest, 'mcp-server.js');
82
+ const problem = entry => (entry ? mcpClients.entryProblem(entry, server, cmd => commandExists(p, cmd)) : null);
83
+ const claudeFile = p.claudeConfigFile(c.home);
84
+ const claudeEntry = ((readJson(claudeFile) || {}).mcpServers || {})[identity.MCP_NAME] || null;
85
+ facts.clients = [{
86
+ id: 'claude-code', label: 'Claude Code', detected: !!c.claudeCli || fs.existsSync(claudeFile), entry: claudeEntry, problem: problem(claudeEntry),
87
+ }];
88
+ for (const client of mcpClients.FILE_CLIENTS) {
89
+ const fact = { id: client.id, label: client.label, detected: client.detect(c.clientEnv) };
90
+ try {
91
+ fact.entry = mcpClients.readEntry(client, c.clientEnv).entry;
92
+ fact.problem = problem(fact.entry);
93
+ } catch (e) { fact.error = e.message; }
94
+ facts.clients.push(fact);
95
+ }
96
+
97
+ const desktopFiles = p.desktopConfigFiles();
98
+ facts.desktop = {
99
+ files: desktopFiles.map(file => {
100
+ const json = readJson(file);
101
+ if (!json) return { file, error: `${file} is not valid JSON` };
102
+ const entry = (json.mcpServers || {})[identity.MCP_NAME] || null;
103
+ return { file, entry, problem: problem(entry) };
104
+ }),
105
+ running: desktopFiles.length ? p.desktopAppRunning() : false,
106
+ };
107
+ return facts;
108
+ }
109
+
110
+ // Repairs what `--fix` may: returns { fixed: [...], failed: [...] } lines.
111
+ function fix(c, facts) {
112
+ const fixed = [];
113
+ const failed = [];
114
+ const dest = facts.install.dir;
115
+ if (!facts.install.exists || facts.install.missing.length) {
116
+ return { fixed, failed: [`The install folder is incomplete; run: ${report.SETUP}`] };
117
+ }
118
+ const server = path.join(dest, 'mcp-server.js');
119
+ // The manifest can be rewritten even when the registry couldn't be read; registering again
120
+ // doesn't need that read either.
121
+ const manifestBad = !facts.hostManifest.exists || !!facts.hostManifest.problem;
122
+ const registryBad = !facts.registryError && facts.browsers.some(b => b.target && (!b.registration
123
+ || path.resolve(b.registration.path).toLowerCase() !== path.resolve(facts.hostManifest.file).toLowerCase()));
124
+ if (manifestBad || registryBad) {
125
+ try {
126
+ const r = installer.registerHost(c, dest);
127
+ fixed.push(`Rewrote the host manifest and registered it for ${r.browsers.map(b => browsers.byId(b).name).join(', ')}.`);
128
+ } catch (e) { failed.push(`Couldn't register the native host: ${e.message}`); }
129
+ }
130
+ for (const client of facts.clients.filter(x => x.entry && x.problem)) {
131
+ try {
132
+ if (client.id === 'claude-code') installer.registerMcp(c, server);
133
+ else mcpClients.addEntry(mcpClients.byId(client.id), c.clientEnv, installer.NODE, server);
134
+ fixed.push(`${client.label}: the Backdraw entry now starts ${server}.`);
135
+ } catch (e) { failed.push(`${client.label}: ${e.message}`); }
136
+ }
137
+ for (const file of facts.desktop.files.filter(x => x.entry && x.problem)) {
138
+ if (facts.desktop.running !== false) { failed.push('Claude desktop app: it is running; quit it completely (also from the system tray) and run this again.'); continue; }
139
+ try {
140
+ installer.updateDesktopConfig(file.file, installer.desktopEntry(server));
141
+ fixed.push(`Claude desktop app: updated ${file.file} (a backup was made).`);
142
+ } catch (e) { failed.push(`Claude desktop app: ${e.message}`); }
143
+ }
144
+ return { fixed, failed };
145
+ }
146
+
147
+ // The tag in the pipe's name (the Windows user name), masked like the home folder.
148
+ function userTag(p) {
149
+ try { return (/backdraw-(.+)$/.exec(p.pipePath()) || [])[1] || null; } catch { return null; }
150
+ }
151
+
152
+ async function doctor(opts = {}, deps = {}) {
153
+ const log = deps.log || (text => console.log(text));
154
+ const color = deps.color !== undefined ? deps.color : useColor();
155
+ const c = installer.context({ ...(deps.ctx || {}), report: false });
156
+ const call = deps.call || require('./shared').call;
157
+ const hide = text => report.mask(text, c.home, userTag(c.platform));
158
+ let facts = await gather(c, call);
159
+ if (opts.fix) {
160
+ const { fixed, failed } = fix(c, facts);
161
+ const paint = painter(color);
162
+ if (!fixed.length && !failed.length) log('Nothing to fix.');
163
+ for (const l of fixed) log(hide(line(paint, 'ok', 'Fixed:', l)));
164
+ for (const l of failed) log(hide(line(paint, 'fail', 'Not fixed:', l)));
165
+ log('');
166
+ facts = await gather(c, call);
167
+ }
168
+ const checks = report.evaluate(facts);
169
+ const header = `Backdraw doctor ${SELF} · Node ${process.version} · ${os.type()} ${os.release()}`;
170
+ log(hide(report.format(checks, { header, color })));
171
+ return checks;
172
+ }
173
+
174
+ module.exports = { REQUIRED, gather, fix, doctor };
@@ -0,0 +1,3 @@
1
+ {
2
+ "extDescription": { "message": "Lets Claude generate images with your own signed-in ChatGPT account in a background tab. Runs entirely on your computer.", "description": "Extension description (manifest). Every other text is in i18n.js." }
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "extDescription": { "message": "Claude'un, oturum açtığın kendi ChatGPT hesabınla arka planda bir sekmede görsel üretmesini sağlar. Tamamen bilgisayarında çalışır.", "description": "Extension description (manifest). Every other text is in i18n.js." }
3
+ }
@@ -0,0 +1,25 @@
1
+ // Centre-crop maths, shared by the service worker (importScripts: self.BackdrawAspect) and the
2
+ // Node tests (require). Pure and dependency-free, so loading it first in background.js can't fail.
3
+ (() => {
4
+ const TOLERANCE = 0.01; // lib/aspect.js TOLERANCE
5
+
6
+ // The largest rectangle of w:h centred in a width x height image ({x, y, width, height}, whole
7
+ // pixels): the limiting side is kept whole and the other is rounded to the nearest pixel. null
8
+ // when the image is already within tolerance of w:h, or too small to come any closer to it.
9
+ function cropRect(width, height, w, h, tolerance = TOLERANCE) {
10
+ if (!(width > 0 && height > 0 && w > 0 && h > 0)) return null;
11
+ // |(width/height) / (w/h) - 1| <= tolerance, cross-multiplied so exactly 1% off still counts.
12
+ if (Math.abs(width * h - height * w) <= tolerance * height * w) return null;
13
+ // Not whole multiples of the reduced ratio: with terms up to 100 those can cut dozens of pixels
14
+ // more than needed, from the side that should stay whole too, and beyond the band ChatGPT was told.
15
+ const wide = width * h > height * w;
16
+ const cw = wide ? Math.round((height * w) / h) : width;
17
+ const ch = wide ? height : Math.round((width * h) / w);
18
+ if (cw < 1 || ch < 1 || (cw === width && ch === height)) return null;
19
+ return { x: Math.floor((width - cw) / 2), y: Math.floor((height - ch) / 2), width: cw, height: ch };
20
+ }
21
+
22
+ const api = { TOLERANCE, cropRect };
23
+ if (typeof module === 'object' && module.exports) module.exports = api;
24
+ else self.BackdrawAspect = api;
25
+ })();