1use crate::noise::NoiseEngine;
24
25#[must_use]
47#[allow(clippy::too_many_lines)]
48pub fn canvas_noise_script(engine: &NoiseEngine) -> String {
49 let noise_fn = engine.js_noise_fn();
51 format!(
52 r"(function() {{
53 'use strict';
54
55 // ── Noise helpers (injected from T37 NoiseEngine) ──────────────────────
56 {noise_fn}
57
58 // ── Utility: apply pixel noise to an ImageData in-place ────────────────
59 function _applyCanvasNoise(imageData, offsetX, offsetY, operation) {{
60 const data = imageData.data;
61 const w = imageData.width;
62 for (let i = 0; i < data.length; i += 4) {{
63 const pixelIdx = (i / 4) | 0;
64 const px = (offsetX + (pixelIdx % w)) >>> 0;
65 const py = (offsetY + ((pixelIdx / w) | 0)) >>> 0;
66 // Only noise non-transparent, non-zero pixels to avoid noising blank canvases
67 if (data[i] === 0 && data[i | 1] === 0 && data[i | 2] === 0 && data[i | 3] === 0) {{
68 continue;
69 }}
70 const [dr, dg, db, da] = __stygian_noise(operation, px, py);
71 data[i] = Math.max(0, Math.min(255, data[i] + dr));
72 data[i | 1] = Math.max(0, Math.min(255, data[i | 1] + dg));
73 data[i | 2] = Math.max(0, Math.min(255, data[i | 2] + db));
74 data[i | 3] = Math.max(0, Math.min(255, data[i | 3] + da));
75 }}
76 return imageData;
77 }}
78
79 // ── Helper: copy canvas content to a temp canvas, noise it, return it ──
80 function _noisedCanvas(src) {{
81 const tmp = (typeof OffscreenCanvas !== 'undefined' && src instanceof OffscreenCanvas)
82 ? new OffscreenCanvas(src.width, src.height)
83 : document.createElement('canvas');
84 tmp.width = src.width;
85 tmp.height = src.height;
86 const ctx = tmp.getContext('2d');
87 ctx.drawImage(src, 0, 0);
88 const id = ctx.getImageData(0, 0, tmp.width, tmp.height);
89 _applyCanvasNoise(id, 0, 0, 'canvas.toDataURL');
90 ctx.putImageData(id, 0, 0);
91 return tmp;
92 }}
93
94 // ── Spoof toString so overrides look like native functions ───────────────
95 function _nativeToString(name) {{
96 return function toString() {{ return 'function ' + name + '() {{ [native code] }}'; }};
97 }}
98
99 function _defineNative(obj, prop, fn) {{
100 fn.toString = _nativeToString(prop);
101 Object.defineProperty(obj, prop, {{
102 value: fn,
103 writable: false,
104 configurable: false,
105 enumerable: false,
106 }});
107 }}
108
109 // ── CanvasRenderingContext2D.getImageData ────────────────────────────────
110 (function() {{
111 const ctx2d = CanvasRenderingContext2D.prototype;
112 const _origGetImageData = ctx2d.getImageData;
113 _defineNative(ctx2d, 'getImageData', function getImageData(sx, sy, sw, sh, settings) {{
114 const id = settings !== undefined
115 ? _origGetImageData.call(this, sx, sy, sw, sh, settings)
116 : _origGetImageData.call(this, sx, sy, sw, sh);
117 return _applyCanvasNoise(id, sx >>> 0, sy >>> 0, 'canvas.getImageData');
118 }});
119 }})();
120
121 // ── HTMLCanvasElement.toDataURL ──────────────────────────────────────────
122 (function() {{
123 if (typeof HTMLCanvasElement === 'undefined') return;
124 const _origToDataURL = HTMLCanvasElement.prototype.toDataURL;
125 _defineNative(HTMLCanvasElement.prototype, 'toDataURL', function toDataURL(type, quality) {{
126 const tmp = _noisedCanvas(this);
127 return quality !== undefined
128 ? _origToDataURL.call(tmp, type, quality)
129 : type !== undefined
130 ? _origToDataURL.call(tmp, type)
131 : _origToDataURL.call(tmp);
132 }});
133 }})();
134
135 // ── HTMLCanvasElement.toBlob ─────────────────────────────────────────────
136 (function() {{
137 if (typeof HTMLCanvasElement === 'undefined') return;
138 const _origToBlob = HTMLCanvasElement.prototype.toBlob;
139 _defineNative(HTMLCanvasElement.prototype, 'toBlob', function toBlob(callback, type, quality) {{
140 const tmp = _noisedCanvas(this);
141 if (quality !== undefined) {{
142 _origToBlob.call(tmp, callback, type, quality);
143 }} else if (type !== undefined) {{
144 _origToBlob.call(tmp, callback, type);
145 }} else {{
146 _origToBlob.call(tmp, callback);
147 }}
148 }});
149 }})();
150
151 // ── OffscreenCanvasRenderingContext2D.getImageData ───────────────────────
152 (function() {{
153 if (typeof OffscreenCanvasRenderingContext2D === 'undefined') return;
154 const octx2d = OffscreenCanvasRenderingContext2D.prototype;
155 const _origOGID = octx2d.getImageData;
156 _defineNative(octx2d, 'getImageData', function getImageData(sx, sy, sw, sh, settings) {{
157 const id = settings !== undefined
158 ? _origOGID.call(this, sx, sy, sw, sh, settings)
159 : _origOGID.call(this, sx, sy, sw, sh);
160 return _applyCanvasNoise(id, sx >>> 0, sy >>> 0, 'offscreencanvas.getImageData');
161 }});
162 }})();
163
164 // ── OffscreenCanvas.convertToBlob ────────────────────────────────────────
165 (function() {{
166 if (typeof OffscreenCanvas === 'undefined') return;
167 const _origCTB = OffscreenCanvas.prototype.convertToBlob;
168 _defineNative(OffscreenCanvas.prototype, 'convertToBlob', async function convertToBlob(options) {{
169 const tmp = new OffscreenCanvas(this.width, this.height);
170 const ctx = tmp.getContext('2d');
171 ctx.drawImage(this, 0, 0);
172 const id = ctx.getImageData(0, 0, tmp.width, tmp.height);
173 _applyCanvasNoise(id, 0, 0, 'offscreencanvas.convertToBlob');
174 ctx.putImageData(id, 0, 0);
175 return options !== undefined
176 ? _origCTB.call(tmp, options)
177 : _origCTB.call(tmp);
178 }});
179 }})();
180
181}})();
182"
183 )
184}
185
186#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::noise::{NoiseEngine, NoiseSeed};
194
195 fn engine(seed: u64) -> NoiseEngine {
196 NoiseEngine::new(NoiseSeed::from(seed))
197 }
198
199 #[test]
200 fn script_contains_seed() {
201 let js = canvas_noise_script(&engine(12345));
202 assert!(js.contains("12345"), "seed not embedded");
203 }
204
205 #[test]
206 fn script_contains_all_five_overrides() {
207 let js = canvas_noise_script(&engine(1));
208 assert!(js.contains("getImageData"), "missing getImageData");
209 assert!(js.contains("toDataURL"), "missing toDataURL");
210 assert!(js.contains("toBlob"), "missing toBlob");
211 assert!(
213 js.contains("OffscreenCanvasRenderingContext2D"),
214 "missing OffscreenCanvas getImageData"
215 );
216 assert!(js.contains("convertToBlob"), "missing convertToBlob");
217 }
218
219 #[test]
220 fn script_contains_native_tostring_spoofing() {
221 let js = canvas_noise_script(&engine(1));
222 assert!(
223 js.contains("[native code]"),
224 "missing native code toString spoof"
225 );
226 }
227
228 #[test]
229 fn script_contains_noise_helper() {
230 let js = canvas_noise_script(&engine(1));
231 assert!(js.contains("__stygian_noise"), "missing __stygian_noise");
232 }
233
234 #[test]
235 fn different_seeds_produce_different_seeds_in_script() {
236 let js1 = canvas_noise_script(&engine(111));
237 let js2 = canvas_noise_script(&engine(222));
238 assert_ne!(js1, js2, "scripts should differ for different seeds");
240 }
241}