editmamei 0.17.4 → 0.18.0

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 (69) hide show
  1. package/NOTICES.md +43 -0
  2. package/README.md +91 -8
  3. package/dist/api/extendscript/_helpers.js +8 -0
  4. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  5. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  6. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  7. package/dist/cli/clients/json-config.js +1 -1
  8. package/dist/core/server.js +92 -24
  9. package/dist/core/tool-groups.js +178 -0
  10. package/dist/core/tool-tiers.js +34 -48
  11. package/dist/delivery/bundle.js +8 -0
  12. package/dist/delivery/config.js +23 -0
  13. package/dist/delivery/provision.js +30 -2
  14. package/dist/delivery/store.js +40 -1
  15. package/dist/detection/detect-active-doc.js +84 -0
  16. package/dist/detection/detection-client.js +62 -0
  17. package/dist/detection/face-contour-geometry.js +137 -0
  18. package/dist/detection/face-detector.js +34 -0
  19. package/dist/detection/face-feature-geometry.js +180 -0
  20. package/dist/detection/geometry.js +35 -0
  21. package/dist/detection/landmark-detection-client.js +38 -0
  22. package/dist/detection/landmark-detector.js +63 -0
  23. package/dist/detection/landmark-spec.js +29 -0
  24. package/dist/detection/mesh-face.js +36 -0
  25. package/dist/detection/object-detector.js +135 -0
  26. package/dist/detection/runtime.js +46 -0
  27. package/dist/license/env-activation.js +46 -7
  28. package/dist/models/dfine-s-coco-quant.onnx +0 -0
  29. package/dist/models/ultraface-rfb-320.onnx +0 -0
  30. package/dist/modules/ce/index.js +23 -5
  31. package/dist/perception/facets.js +148 -0
  32. package/dist/perception/region-precompute.js +122 -0
  33. package/dist/perception/region-scorer.js +151 -0
  34. package/dist/perception/scene-model.js +156 -0
  35. package/dist/perception/select-recipes.js +493 -0
  36. package/dist/perception/sky-ground-flood.js +234 -0
  37. package/dist/perception/sky-mask-transfer.js +77 -0
  38. package/dist/platform/macos-executor.js +0 -1
  39. package/dist/platform/windows-executor.js +6 -1
  40. package/dist/preview/coordinate-grid.js +250 -0
  41. package/dist/skills/editmamei-skill.zip +0 -0
  42. package/dist/telemetry/events.js +2 -1
  43. package/dist/telemetry/sanitize.js +15 -1
  44. package/dist/tools/adjustment-tools.js +46 -51
  45. package/dist/tools/detection-tools.js +209 -0
  46. package/dist/tools/document-tools.js +41 -30
  47. package/dist/tools/filter-tools.js +616 -101
  48. package/dist/tools/guide-tools.js +186 -0
  49. package/dist/tools/history-tools.js +1 -39
  50. package/dist/tools/image-tools.js +88 -0
  51. package/dist/tools/inspect-tools.js +86 -0
  52. package/dist/tools/layer-properties-tools.js +195 -106
  53. package/dist/tools/layer-tools.js +55 -14
  54. package/dist/tools/layer-transform-tools.js +236 -73
  55. package/dist/tools/metadata-tools.js +2 -32
  56. package/dist/tools/object-tools.js +193 -0
  57. package/dist/tools/overview-tools.js +69 -43
  58. package/dist/tools/path-tools.js +274 -0
  59. package/dist/tools/portrait-tools.js +146 -0
  60. package/dist/tools/preview-tools.js +78 -3
  61. package/dist/tools/retouch-tools.js +45 -33
  62. package/dist/tools/scene-tools.js +401 -0
  63. package/dist/tools/selection-tools.js +766 -228
  64. package/dist/tools/text-on-object-tools.js +225 -0
  65. package/dist/tools/text-tools.js +47 -56
  66. package/dist/tools/transform-canvas-tools.js +138 -0
  67. package/dist/tools/vector-mask-tools.js +116 -0
  68. package/dist/version.js +1 -1
  69. package/package.json +4 -2
@@ -0,0 +1,234 @@
1
+ import { decode } from 'jpeg-js';
2
+ const GREEN_SCALE = 35;
3
+ const DARK_LUM = 60;
4
+ const DARK_SCALE = 45;
5
+ const DARK2 = 145;
6
+ const DARK2_RANGE = 90;
7
+ const BLUE_PROTECT = 22;
8
+ const TEX_SCALE = 0.085;
9
+ const TEX_LUM_MAX = 178;
10
+ const GROUND_BIN = 0.45;
11
+ const OPEN_R = 1;
12
+ const CLOSE_R = 1;
13
+ const SKY_CLOSE_R = 8;
14
+ const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
15
+ export function computeSkyMask(rgba, srcW, srcH, boxes, opts = {}) {
16
+ const targetWidth = opts.targetWidth ?? 520;
17
+ const F = Math.max(1, Math.round(srcW / targetWidth));
18
+ const W = Math.max(1, Math.floor(srcW / F));
19
+ const H = Math.max(1, Math.floor(srcH / F));
20
+ const N = W * H;
21
+ const R = new Float64Array(N);
22
+ const G = new Float64Array(N);
23
+ const B = new Float64Array(N);
24
+ for (let y = 0; y < H; y++)
25
+ for (let x = 0; x < W; x++) {
26
+ let r = 0, g = 0, b = 0, n = 0;
27
+ for (let dy = 0; dy < F; dy++)
28
+ for (let dx = 0; dx < F; dx++) {
29
+ const i = ((y * F + dy) * srcW + (x * F + dx)) * 4;
30
+ r += rgba[i];
31
+ g += rgba[i + 1];
32
+ b += rgba[i + 2];
33
+ n++;
34
+ }
35
+ const o = y * W + x;
36
+ R[o] = r / n;
37
+ G[o] = g / n;
38
+ B[o] = b / n;
39
+ }
40
+ const lum = new Float64Array(N);
41
+ const greenness = new Float64Array(N);
42
+ for (let i = 0; i < N; i++) {
43
+ lum[i] = 0.299 * R[i] + 0.587 * G[i] + 0.114 * B[i];
44
+ greenness[i] = clamp((G[i] - Math.max(R[i], B[i])) / GREEN_SCALE, 0, 1);
45
+ }
46
+ const edge = new Float64Array(N);
47
+ for (let y = 1; y < H - 1; y++)
48
+ for (let x = 1; x < W - 1; x++) {
49
+ const gx = lum[(y - 1) * W + x + 1] +
50
+ 2 * lum[y * W + x + 1] +
51
+ lum[(y + 1) * W + x + 1] -
52
+ (lum[(y - 1) * W + x - 1] + 2 * lum[y * W + x - 1] + lum[(y + 1) * W + x - 1]);
53
+ const gy = lum[(y + 1) * W + x - 1] +
54
+ 2 * lum[(y + 1) * W + x] +
55
+ lum[(y + 1) * W + x + 1] -
56
+ (lum[(y - 1) * W + x - 1] + 2 * lum[(y - 1) * W + x] + lum[(y - 1) * W + x + 1]);
57
+ edge[y * W + x] = Math.min(1, Math.hypot(gx, gy) / 600);
58
+ }
59
+ const textureDensity = boxBlur(edge, W, H, 2);
60
+ const notSky = new Uint8Array(N);
61
+ for (let i = 0; i < N; i++) {
62
+ const darkSolid = lum[i] < DARK_LUM ? clamp((DARK_LUM - lum[i]) / DARK_SCALE, 0, 1) : 0;
63
+ const texturedNonSky = lum[i] < TEX_LUM_MAX ? clamp(textureDensity[i] / TEX_SCALE, 0, 1) : 0;
64
+ const blueDom = B[i] - Math.max(R[i], G[i]);
65
+ const darkNotBlue = clamp((DARK2 - lum[i]) / DARK2_RANGE, 0, 1) * (1 - clamp(blueDom / BLUE_PROTECT, 0, 1));
66
+ const ground = Math.max(greenness[i], darkSolid, 0.85 * texturedNonSky, darkNotBlue);
67
+ if (ground >= GROUND_BIN)
68
+ notSky[i] = 1;
69
+ }
70
+ const solidGroundRaw = open(notSky, W, H, OPEN_R);
71
+ const landmass = floodFromBottom(solidGroundRaw, W, H);
72
+ const skyFill = floodTop((i) => landmass[i] === 0, W, H);
73
+ const objMask = rasterizeBoxes(boxes, W, H, F);
74
+ const closed = close(skyFill, W, H, SKY_CLOSE_R);
75
+ const mask = new Uint8Array(N);
76
+ for (let i = 0; i < N; i++)
77
+ mask[i] = skyFill[i] || (closed[i] && !objMask[i]) ? 1 : 0;
78
+ fillEnclosedHoles(mask, W, H);
79
+ const finalMask = close(mask, W, H, CLOSE_R);
80
+ return { mask: finalMask, width: W, height: H };
81
+ }
82
+ export function computeSkyMaskFromJpeg(jpegBytes, boxes, opts = {}) {
83
+ const img = decode(jpegBytes, { useTArray: true, formatAsRGBA: true });
84
+ return computeSkyMask(img.data, img.width, img.height, boxes, opts);
85
+ }
86
+ function rasterizeBoxes(boxes, W, H, F) {
87
+ const m = new Uint8Array(W * H);
88
+ for (const b of boxes) {
89
+ const x1 = Math.max(0, Math.floor(b[0] / F));
90
+ const y1 = Math.max(0, Math.floor(b[1] / F));
91
+ const x2 = Math.min(W, Math.ceil(b[2] / F));
92
+ const y2 = Math.min(H, Math.ceil(b[3] / F));
93
+ for (let y = y1; y < y2; y++)
94
+ for (let x = x1; x < x2; x++)
95
+ m[y * W + x] = 1;
96
+ }
97
+ return m;
98
+ }
99
+ function floodTop(passable, W, H) {
100
+ const vis = new Uint8Array(W * H);
101
+ const st = [];
102
+ for (let x = 0; x < W; x++)
103
+ if (passable(x)) {
104
+ vis[x] = 1;
105
+ st.push(x);
106
+ }
107
+ pushNeighbors(st, vis, passable, W, H);
108
+ return vis;
109
+ }
110
+ function floodFromBottom(mask, W, H) {
111
+ const vis = new Uint8Array(W * H);
112
+ const st = [];
113
+ for (let x = 0; x < W; x++) {
114
+ const i = (H - 1) * W + x;
115
+ if (mask[i]) {
116
+ vis[i] = 1;
117
+ st.push(i);
118
+ }
119
+ }
120
+ pushNeighbors(st, vis, (i) => mask[i] === 1, W, H);
121
+ return vis;
122
+ }
123
+ function pushNeighbors(st, vis, passable, W, H) {
124
+ while (st.length) {
125
+ const p = st.pop();
126
+ const x = p % W;
127
+ const y = (p - x) / W;
128
+ if (x + 1 < W && !vis[p + 1] && passable(p + 1)) {
129
+ vis[p + 1] = 1;
130
+ st.push(p + 1);
131
+ }
132
+ if (x - 1 >= 0 && !vis[p - 1] && passable(p - 1)) {
133
+ vis[p - 1] = 1;
134
+ st.push(p - 1);
135
+ }
136
+ if (y + 1 < H && !vis[p + W] && passable(p + W)) {
137
+ vis[p + W] = 1;
138
+ st.push(p + W);
139
+ }
140
+ if (y - 1 >= 0 && !vis[p - W] && passable(p - W)) {
141
+ vis[p - W] = 1;
142
+ st.push(p - W);
143
+ }
144
+ }
145
+ }
146
+ export function fillEnclosedHoles(mask, W, H) {
147
+ const reach = new Uint8Array(W * H);
148
+ const st = [];
149
+ const seed = (i) => {
150
+ if (!mask[i] && !reach[i]) {
151
+ reach[i] = 1;
152
+ st.push(i);
153
+ }
154
+ };
155
+ for (let x = 0; x < W; x++) {
156
+ seed(x);
157
+ seed((H - 1) * W + x);
158
+ }
159
+ for (let y = 0; y < H; y++) {
160
+ seed(y * W);
161
+ seed(y * W + W - 1);
162
+ }
163
+ pushNeighbors(st, reach, (i) => mask[i] === 0, W, H);
164
+ for (let i = 0; i < W * H; i++)
165
+ if (!mask[i] && !reach[i])
166
+ mask[i] = 1;
167
+ }
168
+ function dilate(mask, W, H, r) {
169
+ const out = new Uint8Array(W * H);
170
+ for (let y = 0; y < H; y++)
171
+ for (let x = 0; x < W; x++) {
172
+ let v = 0;
173
+ for (let dy = -r; dy <= r && !v; dy++)
174
+ for (let dx = -r; dx <= r && !v; dx++) {
175
+ const xx = x + dx;
176
+ const yy = y + dy;
177
+ if (xx < 0 || yy < 0 || xx >= W || yy >= H)
178
+ continue;
179
+ if (mask[yy * W + xx])
180
+ v = 1;
181
+ }
182
+ out[y * W + x] = v;
183
+ }
184
+ return out;
185
+ }
186
+ function erode(mask, W, H, r) {
187
+ const out = new Uint8Array(W * H);
188
+ for (let y = 0; y < H; y++)
189
+ for (let x = 0; x < W; x++) {
190
+ let v = 1;
191
+ for (let dy = -r; dy <= r && v; dy++)
192
+ for (let dx = -r; dx <= r && v; dx++) {
193
+ const xx = x + dx;
194
+ const yy = y + dy;
195
+ if (xx < 0 || yy < 0 || xx >= W || yy >= H)
196
+ continue;
197
+ if (!mask[yy * W + xx])
198
+ v = 0;
199
+ }
200
+ out[y * W + x] = v;
201
+ }
202
+ return out;
203
+ }
204
+ function open(mask, W, H, r) {
205
+ return dilate(erode(mask, W, H, r), W, H, r);
206
+ }
207
+ function close(mask, W, H, r) {
208
+ return erode(dilate(mask, W, H, r), W, H, r);
209
+ }
210
+ function boxBlur(src, W, H, iters) {
211
+ let a = Float64Array.from(src);
212
+ let b = new Float64Array(W * H);
213
+ for (let it = 0; it < iters; it++) {
214
+ for (let y = 0; y < H; y++)
215
+ for (let x = 0; x < W; x++) {
216
+ let s = 0;
217
+ let n = 0;
218
+ for (let dy = -1; dy <= 1; dy++)
219
+ for (let dx = -1; dx <= 1; dx++) {
220
+ const xx = x + dx;
221
+ const yy = y + dy;
222
+ if (xx < 0 || yy < 0 || xx >= W || yy >= H)
223
+ continue;
224
+ s += a[yy * W + xx];
225
+ n++;
226
+ }
227
+ b[y * W + x] = s / n;
228
+ }
229
+ const t = a;
230
+ a = b;
231
+ b = t;
232
+ }
233
+ return a;
234
+ }
@@ -0,0 +1,77 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { encode } from 'jpeg-js';
3
+ import { runScript } from '../utils/run-script.js';
4
+ import { TempDir, userOwnedTempRoot } from '../utils/temp.js';
5
+ import { getContextInfo } from '../api/extendscript/_helpers.js';
6
+ import { jsLit, jsNum } from '../utils/jsx.js';
7
+ function encodeMaskJpeg(mask, w, h) {
8
+ const rgba = Buffer.alloc(w * h * 4);
9
+ for (let i = 0; i < w * h; i++) {
10
+ const v = mask[i] ? 255 : 0;
11
+ rgba[i * 4] = v;
12
+ rgba[i * 4 + 1] = v;
13
+ rgba[i * 4 + 2] = v;
14
+ rgba[i * 4 + 3] = 255;
15
+ }
16
+ return encode({ data: rgba, width: w, height: h }, 92).data;
17
+ }
18
+ function buildLoadScript(maskPath, docW, docH) {
19
+ return `
20
+ ${getContextInfo}
21
+ function __skyRestoreComposite(d) {
22
+ try {
23
+ app.activeDocument = d;
24
+ var ref = new ActionReference();
25
+ ref.putEnumerated(app.charIDToTypeID('Chnl'), app.charIDToTypeID('Chnl'), app.charIDToTypeID('RGB '));
26
+ var desc = new ActionDescriptor();
27
+ desc.putReference(app.charIDToTypeID('null'), ref);
28
+ app.executeAction(app.charIDToTypeID('slct'), desc, DialogModes.NO);
29
+ } catch (eRC) {}
30
+ }
31
+ if (app.documents.length === 0) { throw new Error('No active document'); }
32
+ var orig = app.activeDocument;
33
+
34
+ // Bring the upscaled mask onto the clipboard FIRST, so no document switch sits
35
+ // between the channel setup and the paste below.
36
+ var maskDoc = app.open(new File(${jsLit(maskPath)}));
37
+ maskDoc.resizeImage(
38
+ UnitValue(${jsNum(docW, 1)}, 'px'),
39
+ UnitValue(${jsNum(docH, 1)}, 'px'),
40
+ null,
41
+ ResampleMethod.BICUBIC
42
+ );
43
+ maskDoc.selection.selectAll();
44
+ maskDoc.selection.copy();
45
+ maskDoc.close(SaveOptions.DONOTSAVECHANGES);
46
+
47
+ // Paste the doc-sized mask INTO a full-canvas alpha channel on the original. With
48
+ // the alpha channel the explicit active channel, paste writes grayscale into it (no
49
+ // new layer). selectAll + paste-into keeps the doc-sized clipboard aligned to canvas.
50
+ app.activeDocument = orig;
51
+ var ch = orig.channels.add();
52
+ orig.activeChannels = [ch];
53
+ orig.selection.selectAll();
54
+ orig.paste(true);
55
+ orig.selection.deselect();
56
+
57
+ // Load the channel as the working selection (white = sky), then drop the temp channel.
58
+ orig.selection.load(ch, SelectionType.REPLACE);
59
+ try { ch.remove(); } catch (eRm) {}
60
+ __skyRestoreComposite(orig);
61
+ return { ok: true };
62
+ `;
63
+ }
64
+ export async function loadSkyMaskAsSelection(connection, mask, maskW, maskH, docW, docH) {
65
+ const jpg = encodeMaskJpeg(mask, maskW, maskH);
66
+ const dir = process.platform === 'darwin'
67
+ ? await TempDir.createWithRoot(userOwnedTempRoot(), 'editmamei-sky-')
68
+ : await TempDir.create('editmamei-sky-');
69
+ try {
70
+ const maskPath = dir.path('skymask.jpg');
71
+ await fs.writeFile(maskPath, jpg);
72
+ await runScript(connection, buildLoadScript(maskPath, docW, docH));
73
+ }
74
+ finally {
75
+ await dir.cleanup();
76
+ }
77
+ }
@@ -104,7 +104,6 @@ export class MacOSExecutor {
104
104
  }
105
105
  return `tell application "${this.appName}"
106
106
  \tactivate
107
- \tset jsxFile to POSIX file "${posixPath}"
108
107
  \tdo javascript "$.evalFile(decodeURI('${encodeURI(posixPath)}'))"
109
108
  end tell`;
110
109
  }
@@ -104,7 +104,12 @@ If Err.Number <> 0 Then
104
104
  WScript.Quit 1
105
105
  End If
106
106
 
107
- ' Execute the JSX script
107
+ ' Execute the JSX script. The path is the mkdtemp temp file (quote-free by the
108
+ ' guard above), but its single backslashes must be DOUBLED here: this string is
109
+ ' handed to DoJavaScript as ExtendScript SOURCE, and a backslash inside a quoted
110
+ ' path there is a JS string-literal escape (a Users/Temp segment would be parsed
111
+ ' as an escape sequence and corrupt the path). The VBS Replace doubles each
112
+ ' backslash so ExtendScript resolves it back. Load-bearing — NOT a no-op.
108
113
  Dim result
109
114
  result = photoshopApp.DoJavaScript("$.evalFile(""" & Replace("${jsxPath}", "\\", "\\\\") & """);")
110
115
 
@@ -0,0 +1,250 @@
1
+ import { decode, encode } from 'jpeg-js';
2
+ const FONT_5x7 = {
3
+ '0': [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],
4
+ '1': [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
5
+ '2': [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111],
6
+ '3': [0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110],
7
+ '4': [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010],
8
+ '5': [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110],
9
+ '6': [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110],
10
+ '7': [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000],
11
+ '8': [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110],
12
+ '9': [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100],
13
+ '-': [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000],
14
+ '.': [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b01100, 0b01100],
15
+ A: [0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
16
+ B: [0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110],
17
+ C: [0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110],
18
+ D: [0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110],
19
+ E: [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111],
20
+ F: [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000],
21
+ G: [0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01111],
22
+ H: [0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
23
+ I: [0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
24
+ J: [0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100],
25
+ K: [0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001],
26
+ L: [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111],
27
+ M: [0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001],
28
+ N: [0b10001, 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001],
29
+ O: [0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
30
+ P: [0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000],
31
+ Q: [0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101],
32
+ R: [0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001],
33
+ S: [0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110],
34
+ T: [0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],
35
+ U: [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
36
+ V: [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100],
37
+ W: [0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001],
38
+ X: [0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001],
39
+ Y: [0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100],
40
+ Z: [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111],
41
+ };
42
+ const GLYPH_W = 5;
43
+ const GLYPH_H = 7;
44
+ function hexToRgb(hex, fallback) {
45
+ if (!hex || hex[0] !== '#' || hex.length !== 7)
46
+ return fallback;
47
+ const r = parseInt(hex.slice(1, 3), 16);
48
+ const g = parseInt(hex.slice(3, 5), 16);
49
+ const b = parseInt(hex.slice(5, 7), 16);
50
+ if ([r, g, b].some((v) => Number.isNaN(v)))
51
+ return fallback;
52
+ return { r, g, b };
53
+ }
54
+ const WHITE = { r: 255, g: 255, b: 255 };
55
+ const BLACK = { r: 0, g: 0, b: 0 };
56
+ function setPx(data, w, h, x, y, c) {
57
+ if (x < 0 || y < 0 || x >= w || y >= h)
58
+ return;
59
+ const i = (y * w + x) * 4;
60
+ data[i] = c.r;
61
+ data[i + 1] = c.g;
62
+ data[i + 2] = c.b;
63
+ data[i + 3] = 255;
64
+ }
65
+ function drawVLine(data, w, h, x, core) {
66
+ for (let y = 0; y < h; y++) {
67
+ setPx(data, w, h, x, y, core);
68
+ setPx(data, w, h, x + 1, y, BLACK);
69
+ }
70
+ }
71
+ function drawHLine(data, w, h, y, core) {
72
+ for (let x = 0; x < w; x++) {
73
+ setPx(data, w, h, x, y, core);
74
+ setPx(data, w, h, x, y + 1, BLACK);
75
+ }
76
+ }
77
+ function drawGlyph(data, w, h, glyph, px, py, scale, text) {
78
+ for (let row = 0; row < GLYPH_H; row++) {
79
+ const bits = glyph[row];
80
+ for (let col = 0; col < GLYPH_W; col++) {
81
+ if (!(bits & (1 << (GLYPH_W - 1 - col))))
82
+ continue;
83
+ const bx = px + col * scale;
84
+ const by = py + row * scale;
85
+ for (let oy = -1; oy <= scale; oy++) {
86
+ for (let ox = -1; ox <= scale; ox++) {
87
+ setPx(data, w, h, bx + ox, by + oy, BLACK);
88
+ }
89
+ }
90
+ for (let sy = 0; sy < scale; sy++) {
91
+ for (let sx = 0; sx < scale; sx++) {
92
+ setPx(data, w, h, bx + sx, by + sy, text);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ function textWidth(str, scale) {
99
+ return str.length * (GLYPH_W + 1) * scale;
100
+ }
101
+ function drawText(data, w, h, str, px, py, scale, text) {
102
+ let cx = px;
103
+ for (const ch of str) {
104
+ const glyph = FONT_5x7[ch];
105
+ if (glyph)
106
+ drawGlyph(data, w, h, glyph, cx, py, scale, text);
107
+ cx += (GLYPH_W + 1) * scale;
108
+ }
109
+ }
110
+ export function niceStep(target) {
111
+ if (!(target > 0))
112
+ return 1;
113
+ const mag = Math.pow(10, Math.floor(Math.log10(target)));
114
+ const norm = target / mag;
115
+ const candidates = [1, 2, 5, 10];
116
+ let best = 1;
117
+ let bestDist = Infinity;
118
+ for (const c of candidates) {
119
+ const d = Math.abs(norm - c);
120
+ if (d <= bestDist) {
121
+ bestDist = d;
122
+ best = c;
123
+ }
124
+ }
125
+ return Math.max(1, Math.round(best * mag));
126
+ }
127
+ export function colLabel(i) {
128
+ let n = Math.floor(i) + 1;
129
+ let s = '';
130
+ while (n > 0) {
131
+ const r = (n - 1) % 26;
132
+ s = String.fromCharCode(65 + r) + s;
133
+ n = Math.floor((n - 1) / 26);
134
+ }
135
+ return s;
136
+ }
137
+ function computeLines(opts) {
138
+ const { docWidth, docHeight } = opts;
139
+ const vertical = [];
140
+ const horizontal = [];
141
+ if (typeof opts.columns === 'number' && opts.columns >= 2) {
142
+ for (let i = 1; i < opts.columns; i++)
143
+ vertical.push(Math.round((docWidth * i) / opts.columns));
144
+ }
145
+ if (typeof opts.rows === 'number' && opts.rows >= 2) {
146
+ for (let j = 1; j < opts.rows; j++)
147
+ horizontal.push(Math.round((docHeight * j) / opts.rows));
148
+ }
149
+ if (vertical.length > 0 || horizontal.length > 0) {
150
+ return { vertical, horizontal, step: null };
151
+ }
152
+ const longEdge = Math.max(docWidth, docHeight);
153
+ const requested = typeof opts.stepPx === 'number' && opts.stepPx > 0 ? opts.stepPx : niceStep(longEdge / 10);
154
+ const MAX_LINES_PER_AXIS = 80;
155
+ const step = Math.max(requested, Math.ceil(longEdge / MAX_LINES_PER_AXIS));
156
+ for (let x = step; x < docWidth; x += step)
157
+ vertical.push(x);
158
+ for (let y = step; y < docHeight; y += step)
159
+ horizontal.push(y);
160
+ return { vertical, horizontal, step };
161
+ }
162
+ export function overlayCoordinateGrid(jpegBuffer, opts) {
163
+ const img = decode(jpegBuffer, { useTArray: true, formatAsRGBA: true });
164
+ const w = img.width;
165
+ const h = img.height;
166
+ const data = img.data;
167
+ const scaleX = w / opts.docWidth;
168
+ const scaleY = h / opts.docHeight;
169
+ const core = hexToRgb(opts.color, WHITE);
170
+ const labelEvery = Math.max(1, Math.floor(opts.labelEvery ?? 1));
171
+ const glyphScale = Math.max(2, Math.round(Math.max(w, h) / 600));
172
+ const labelH = GLYPH_H * glyphScale;
173
+ if (opts.cellLabels) {
174
+ const numCols = Math.max(1, Math.floor(opts.columns ?? 8));
175
+ const numRows = Math.max(1, Math.floor(opts.rows ?? 10));
176
+ const cellW = opts.docWidth / numCols;
177
+ const cellH = opts.docHeight / numRows;
178
+ for (let i = 1; i < numCols; i++)
179
+ drawVLine(data, w, h, Math.round(i * cellW * scaleX), core);
180
+ for (let j = 1; j < numRows; j++)
181
+ drawHLine(data, w, h, Math.round(j * cellH * scaleY), core);
182
+ for (let i = 0; i < numCols; i++) {
183
+ for (let j = 0; j < numRows; j++) {
184
+ const px = Math.round(i * cellW * scaleX) + 3;
185
+ const py = Math.round(j * cellH * scaleY) + 3;
186
+ drawText(data, w, h, colLabel(i) + (j + 1), px, py, glyphScale, core);
187
+ }
188
+ }
189
+ const out = encode({ data, width: w, height: h }, 85);
190
+ const cw = Math.round(cellW);
191
+ const ch = Math.round(cellH);
192
+ const legend = `Cell grid: ${numCols} columns (A–${colLabel(numCols - 1)}) × ${numRows} rows (1–${numRows}) ` +
193
+ `over a ${opts.docWidth}×${opts.docHeight} px document; each cell ≈ ${cw}×${ch} doc px. ` +
194
+ `Pick the CELL a feature sits in. Cell <Col><Row> covers x=[(col−1)·${cw} … col·${cw}], ` +
195
+ `y=[(row−1)·${ch} … row·${ch}] (col A=1…${colLabel(numCols - 1)}=${numCols}); ` +
196
+ `the cell center is x=(col−0.5)·${cw}, y=(row−0.5)·${ch}.`;
197
+ return {
198
+ jpeg: out.data,
199
+ legend,
200
+ verticalLines: Array.from({ length: numCols - 1 }, (_, i) => Math.round((i + 1) * cellW)),
201
+ horizontalLines: Array.from({ length: numRows - 1 }, (_, j) => Math.round((j + 1) * cellH)),
202
+ previewWidth: w,
203
+ previewHeight: h,
204
+ };
205
+ }
206
+ const { vertical, horizontal, step } = computeLines(opts);
207
+ for (const docX of vertical)
208
+ drawVLine(data, w, h, Math.round(docX * scaleX), core);
209
+ for (const docY of horizontal)
210
+ drawHLine(data, w, h, Math.round(docY * scaleY), core);
211
+ for (let i = 0; i < vertical.length; i++) {
212
+ if (i % labelEvery !== 0)
213
+ continue;
214
+ const docX = vertical[i];
215
+ const px = Math.round(docX * scaleX);
216
+ drawText(data, w, h, String(docX), px + 3, 2, glyphScale, core);
217
+ if (opts.normalized) {
218
+ const frac = (docX / opts.docWidth).toFixed(2);
219
+ drawText(data, w, h, frac, px + 3, h - labelH - 2, glyphScale, core);
220
+ }
221
+ }
222
+ for (let j = 0; j < horizontal.length; j++) {
223
+ if (j % labelEvery !== 0)
224
+ continue;
225
+ const docY = horizontal[j];
226
+ const py = Math.round(docY * scaleY);
227
+ drawText(data, w, h, String(docY), 2, py + 3, glyphScale, core);
228
+ if (opts.normalized) {
229
+ const frac = (docY / opts.docHeight).toFixed(2);
230
+ const s = frac;
231
+ drawText(data, w, h, s, w - textWidth(s, glyphScale) - 2, py + 3, glyphScale, core);
232
+ }
233
+ }
234
+ const out = encode({ data, width: w, height: h }, 85);
235
+ const spacingNote = step !== null
236
+ ? `lines every ${step} px`
237
+ : `${vertical.length + 1}×${horizontal.length + 1} divisions`;
238
+ const normNote = opts.normalized ? '; normalized 0–1 labels on bottom/right edges' : '';
239
+ const legend = `Coordinate grid: ${spacingNote}, labeled in DOCUMENT pixels, top-left origin. ` +
240
+ `Document is ${opts.docWidth}×${opts.docHeight} px (preview ${w}×${h}). ` +
241
+ `Read the x/y value off a labeled line and pass it straight to a tool${normNote}.`;
242
+ return {
243
+ jpeg: out.data,
244
+ legend,
245
+ verticalLines: vertical,
246
+ horizontalLines: horizontal,
247
+ previewWidth: w,
248
+ previewHeight: h,
249
+ };
250
+ }
Binary file
@@ -77,7 +77,8 @@ export function looksLikeAbsolutePath(value) {
77
77
  return (/^[A-Za-z]:[\\/]/.test(value) ||
78
78
  /^[\\/]/.test(value) ||
79
79
  value.includes('\\') ||
80
- /^file:\/\//i.test(value));
80
+ /^file:\/\//i.test(value) ||
81
+ /(?:\/[^\s/]+){2,}/.test(value));
81
82
  }
82
83
  export function isContentSafe(event) {
83
84
  for (const value of Object.values(event)) {
@@ -22,14 +22,28 @@ function homedirPrefixes() {
22
22
  }
23
23
  return [...set];
24
24
  }
25
+ const SINGLE_SEG_POSIX = /(^|\s)\/[^\s"'/]+(?=$|\s)/;
25
26
  function basenamePaths(s) {
26
- return s.replace(/[A-Za-z]:[\\/][^\s"']*|\\\\[^\s"']+|file:\/\/[^\s"']*|(?:\/[^\s"'/]+){2,}\/?/gi, (m) => m.split(/[\\/]/).filter(Boolean).pop() ?? '');
27
+ return s.replace(/[A-Za-z]:[\\/][^\s"']*|\\\\[^\s"']+|file:\/\/[^\s"']*|(?:\/[^\s"'/]+){2,}\/?|(?:^|\s)\/[^\s"'/]+(?=$|\s)/gi, (m) => {
28
+ const single = SINGLE_SEG_POSIX.exec(m);
29
+ if (single)
30
+ return `${single[1]}/…`;
31
+ return m.split(/[\\/]/).filter(Boolean).pop() ?? '';
32
+ });
33
+ }
34
+ function redactGenericUserDirs(s) {
35
+ let out = s.replace(/\bUsers([\\/])[^\\/\s"']+/g, 'Users$1~');
36
+ if (process.platform !== 'win32') {
37
+ out = out.replace(/\bhome([\\/])[^\\/\s"']+/g, 'home$1~');
38
+ }
39
+ return out;
27
40
  }
28
41
  export function sanitizeMessage(input, maxLen = MAX_MESSAGE_LEN) {
29
42
  let out = input;
30
43
  for (const prefix of homedirPrefixes()) {
31
44
  out = redactHomedirIn(out, prefix);
32
45
  }
46
+ out = redactGenericUserDirs(out);
33
47
  out = basenamePaths(out);
34
48
  out = out.replace(/\\/g, '/');
35
49
  out = out.replace(/^[/]+/, '');