react-x11 2.15.1 → 2.15.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.15.1",
3
+ "version": "2.15.3",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -33,6 +33,7 @@
33
33
  "examples:attention": "tsx examples/attention.jsx",
34
34
  "examples:badge": "tsx examples/badge.jsx",
35
35
  "examples:tray": "tsx examples/tray.jsx",
36
+ "examples:desktop": "tsx examples/desktop.jsx",
36
37
  "examples:tooltips": "tsx examples/tooltips.jsx",
37
38
  "examples:chat": "tsx examples/chat.jsx",
38
39
  "examples:clipboard": "tsx examples/clipboard.jsx",
@@ -346,6 +346,18 @@ function environmentContext() {
346
346
  * that was mounted after the launch it is asking about.
347
347
  */
348
348
  let current = null;
349
+ /**
350
+ * The role of this process's registration, which is **not** the same question
351
+ * as `current`.
352
+ *
353
+ * `current` is the *primary* registration and is deliberately null for a
354
+ * second copy of an app, because a secondary must not own the badge or the
355
+ * launcher entry — the primary does. But "no registration at all" and "a
356
+ * registration that lost the race" are different facts, and a feature probe
357
+ * that cannot tell them apart tells an app author to call a function they
358
+ * already called. See `capabilities.js`.
359
+ */
360
+ let currentRole = null;
349
361
 
350
362
  /**
351
363
  * The schemes the registration declared, kept beside it for the transports
@@ -675,6 +687,7 @@ export async function registerApplication(options = {}) {
675
687
  await registration.remove().catch(() => {});
676
688
  await forward(ref, dbus, { appId, objectPath, uris });
677
689
  await ref.release();
690
+ currentRole = 'secondary';
678
691
  const secondary = {
679
692
  role: 'secondary',
680
693
  appId,
@@ -711,7 +724,10 @@ export async function registerApplication(options = {}) {
711
724
  async release() {
712
725
  if (released) return;
713
726
  released = true;
714
- if (current === primaryRegistration) current = null;
727
+ if (current === primaryRegistration) {
728
+ current = null;
729
+ currentRole = null;
730
+ }
715
731
  await ref.bus.releaseName(appId).catch(() => {});
716
732
  await registration?.remove().catch(() => {});
717
733
  registration = null;
@@ -721,6 +737,7 @@ export async function registerApplication(options = {}) {
721
737
  primaryRegistration[Symbol.asyncDispose] = () =>
722
738
  primaryRegistration.release();
723
739
  current = primaryRegistration;
740
+ currentRole = 'primary';
724
741
  return primaryRegistration;
725
742
  }
726
743
 
@@ -774,6 +791,12 @@ async function forward(ref, dbus, { appId, objectPath, uris }) {
774
791
  }
775
792
  }
776
793
 
794
+ /** `'primary' | 'secondary' | null`. Not public — `capabilities.js` uses it to
795
+ * tell "never registered" from "registered, but another copy is the app". */
796
+ export function currentRegistrationRole() {
797
+ return currentRole;
798
+ }
799
+
777
800
  /** This process's registration, or `null`. Not public; the docs use `role`. */
778
801
  export function currentRegistration() {
779
802
  return current;
@@ -782,6 +805,7 @@ export function currentRegistration() {
782
805
  /** Test seam, not public: forget every handler, buffer and registration. */
783
806
  export function _resetApplicationState() {
784
807
  current = null;
808
+ currentRole = null;
785
809
  currentSchemes = null;
786
810
  openHandlers.clear();
787
811
  activateHandlers.clear();
@@ -0,0 +1,324 @@
1
+ // What this desktop can actually do — feature discovery for the things an app
2
+ // does *outside* its own windows.
3
+ //
4
+ // ## Why this is not `useSupports()`
5
+ //
6
+ // `useSupports()` answers questions about the **display**: is there a
7
+ // compositor, is there a 32-bit visual, did this connection get the direct GL
8
+ // backend. All of them are local, synchronous, and knowable before the first
9
+ // frame.
10
+ //
11
+ // Everything on this page is the opposite on all three counts. Whether there
12
+ // is a notification daemon, a tray host or a launcher listening is a fact
13
+ // about **another process on a bus**, it takes a round trip to learn, and it
14
+ // changes while the app runs — a panel restarts, an extension is toggled, a
15
+ // user logs into a different session type. So the shape has to be render
16
+ // state that settles and then follows, not a boolean that is right on the
17
+ // first frame.
18
+ //
19
+ // ## Why a boolean is not enough
20
+ //
21
+ // "Does this desktop have notifications" is the wrong question, because the
22
+ // answer is yes on machines that mean four different things by it:
23
+ //
24
+ // - a freedesktop daemon with `actions` — a banner with buttons that call
25
+ // back into the app, updated in place, reporting what the user did;
26
+ // - a freedesktop daemon **without** `actions` — GNOME's own for years,
27
+ // and several minimal ones — where the same call shows a banner and the
28
+ // buttons silently never appear;
29
+ // - macOS's notification centre — actions and callbacks, but only for a
30
+ // signed bundle, and a different vocabulary underneath;
31
+ // - `notify-send` or `osascript` — one-way text with an urgency and an
32
+ // icon, no update, no close, no events, ever.
33
+ //
34
+ // An app that wants "reply from the notification" has to know which of those
35
+ // it has, and the honest unit is therefore a **feature set**, not a flag. The
36
+ // freedesktop daemons already publish exactly this through `GetCapabilities`;
37
+ // this module's job is to translate every backend's answer into one portable
38
+ // vocabulary so an app branches on the feature and never on the platform.
39
+ //
40
+ // ## The rule for adding one
41
+ //
42
+ // A capability name is a *thing an app wants to do*, and its features are the
43
+ // parts of it that a backend can honestly lack. If a feature is missing
44
+ // everywhere but one backend it is still a feature — `badgeText` is macOS's
45
+ // alone and is listed — but if a "feature" is really a different way of doing
46
+ // the same thing, it belongs in `backend` instead. Backends are named after
47
+ // the mechanism (`statusnotifier`, `cocoa`, `notify-send`), never after the
48
+ // platform, so that a second Linux mechanism does not need a second name for
49
+ // Linux.
50
+
51
+ import { sessionBus } from './bus.js';
52
+ import { currentRegistration, currentRegistrationRole } from './application.js';
53
+ import {
54
+ notificationBackend,
55
+ NOTIFICATIONS_NAME,
56
+ NOTIFICATIONS_PATH,
57
+ } from './notifications.js';
58
+ import { WATCHER_NAME } from './statusnotifier.js';
59
+ import { liveApps } from './trace-registry.js';
60
+
61
+ /** Every capability this module can answer for. */
62
+ export const CAPABILITIES = ['notifications', 'tray', 'launcher'];
63
+
64
+ /** The shape a probe resolves to when there is no mechanism at all. */
65
+ const NONE = Object.freeze({ available: false, backend: null, features: {} });
66
+
67
+ const frozen = (backend, features) =>
68
+ Object.freeze({
69
+ available: true,
70
+ backend,
71
+ features: Object.freeze(features),
72
+ });
73
+
74
+ /** The app to ask when the caller did not say. Mirrors `launcher.js`. */
75
+ function soleApp() {
76
+ const apps = liveApps();
77
+ if (apps.length <= 1) return apps[0] ?? null;
78
+ const showing = apps.filter((app) => (app._rootChildren ?? []).length > 0);
79
+ return showing.length === 1 ? showing[0] : null;
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // notifications
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * The freedesktop daemon's own `GetCapabilities`, translated.
88
+ *
89
+ * The daemon publishes a flat list of strings; the names below are the ones
90
+ * that change what an app may do. Anything not in the list is absent, which
91
+ * is why every field is read with `includes` rather than defaulted true —
92
+ * a daemon that lists nothing supports nothing but a banner.
93
+ */
94
+ function fromDaemonCaps(caps) {
95
+ const has = (name) => caps.includes(name);
96
+ return {
97
+ // The two that decide whether a notification is a conversation or a sign.
98
+ actions: has('actions'),
99
+ events: has('actions'),
100
+ // Every fd.o daemon can replace and close by id; neither is advertised
101
+ // as a capability because the protocol requires both.
102
+ update: true,
103
+ close: true,
104
+ body: has('body'),
105
+ bodyMarkup: has('body-markup'),
106
+ bodyImage: has('body-images'),
107
+ icon: has('icon-static') || has('icon-multi'),
108
+ sound: has('sound'),
109
+ // The banner survives in a tray/centre rather than expiring unseen.
110
+ persistence: has('persistence'),
111
+ urgency: true,
112
+ };
113
+ }
114
+
115
+ async function probeNotifications({ app } = {}) {
116
+ const backend = await notificationBackend({ app });
117
+ if (!backend) return NONE;
118
+
119
+ if (backend === 'dbus') {
120
+ const ref = await sessionBus();
121
+ if (!ref) return NONE;
122
+ try {
123
+ const iface = await ref.bus.getInterface(
124
+ NOTIFICATIONS_NAME,
125
+ NOTIFICATIONS_PATH,
126
+ NOTIFICATIONS_NAME,
127
+ );
128
+ const caps = await new Promise((resolve) => {
129
+ iface.GetCapabilities((err, list) => resolve(err ? [] : (list ?? [])));
130
+ });
131
+ return frozen('dbus', fromDaemonCaps(caps));
132
+ } catch {
133
+ // The name is there but the object will not answer. A banner will
134
+ // probably still post; nothing richer should be promised.
135
+ return frozen('dbus', fromDaemonCaps([]));
136
+ } finally {
137
+ await ref.release();
138
+ }
139
+ }
140
+
141
+ if (backend === 'cocoa') {
142
+ return frozen('cocoa', {
143
+ actions: true,
144
+ events: true,
145
+ update: true,
146
+ close: true,
147
+ body: true,
148
+ bodyMarkup: false, // the centre renders plain text
149
+ bodyImage: true,
150
+ icon: true,
151
+ sound: true,
152
+ persistence: true,
153
+ urgency: true, // mapped onto interruption levels
154
+ });
155
+ }
156
+
157
+ // `osascript` and `notify-send`: one way, and that is the whole of it.
158
+ // `notify-send -p` prints an id on newer libnotify, which is why `update`
159
+ // is not flatly false there — see notifications.js.
160
+ return frozen(backend, {
161
+ actions: false,
162
+ events: false,
163
+ update: backend === 'notify-send',
164
+ close: false,
165
+ body: true,
166
+ bodyMarkup: false,
167
+ bodyImage: false,
168
+ icon: true,
169
+ sound: false,
170
+ persistence: false,
171
+ urgency: backend === 'notify-send',
172
+ });
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // tray
177
+ // ---------------------------------------------------------------------------
178
+
179
+ async function probeTray({ app } = {}) {
180
+ const target = app ?? soleApp();
181
+ if (typeof target?.createStatusItem === 'function') {
182
+ return frozen('cocoa', {
183
+ menu: true,
184
+ iconName: true, // SF Symbols
185
+ iconBytes: true,
186
+ attention: false, // no NeedsAttention equivalent on a status item
187
+ overlay: false,
188
+ tooltip: true,
189
+ title: true,
190
+ click: true,
191
+ clickPosition: true,
192
+ clickRect: true,
193
+ clickModifiers: true,
194
+ scroll: false,
195
+ });
196
+ }
197
+
198
+ const ref = await sessionBus();
199
+ if (!ref) return NONE;
200
+ try {
201
+ // A **live owner**, not an activatable name — see `statusnotifier.js`.
202
+ if (!(await ref.bus.nameHasOwner(WATCHER_NAME))) return NONE;
203
+ } catch {
204
+ return NONE;
205
+ } finally {
206
+ await ref.release();
207
+ }
208
+ return frozen('statusnotifier', {
209
+ menu: true,
210
+ iconName: true, // themed icon names, which is the good path here
211
+ iconBytes: true, // ARGB pixmaps
212
+ attention: true,
213
+ overlay: true,
214
+ tooltip: true,
215
+ title: true,
216
+ click: true,
217
+ clickPosition: true,
218
+ // The protocol carries none of these — see `statusnotifier.js`.
219
+ clickRect: false,
220
+ clickModifiers: false,
221
+ scroll: true,
222
+ });
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // launcher
227
+ // ---------------------------------------------------------------------------
228
+
229
+ async function probeLauncher({ app } = {}) {
230
+ const target = app ?? soleApp();
231
+ if (typeof target?.setDockBadge === 'function') {
232
+ return frozen('cocoa', {
233
+ badge: true,
234
+ badgeText: true, // the tile takes any label
235
+ progress: false, // NSDockTile has no progress bar
236
+ urgent: true, // requestUserAttention, via window states
237
+ menu: typeof target.setDockMenu === 'function',
238
+ // The Dock always shows the app; nothing has to be installed for it.
239
+ needsDesktopFile: false,
240
+ });
241
+ }
242
+
243
+ // The Linux rung needs two things that are not the bus: an app id to
244
+ // attribute the entry to, and a `.desktop` file of that name for the
245
+ // launcher to hang it on. The first is knowable here; the second is not
246
+ // (it is a file on a path the launcher chooses), which is why it is
247
+ // reported as a *requirement* rather than as availability.
248
+ const ref = await sessionBus();
249
+ if (!ref) return NONE;
250
+ await ref.release();
251
+ if (!currentRegistration()?.appId) {
252
+ return Object.freeze({
253
+ available: false,
254
+ backend: null,
255
+ features: {},
256
+ // Two different "no"s, and only one is a mistake. A **secondary**
257
+ // instance called `registerApplication()` and lost the race for the
258
+ // name — the first copy owns the badge and the quicklist, which is the
259
+ // whole point of single-instance — so telling its author to call a
260
+ // function they already called sends them after a bug that is not
261
+ // there. The launcher is genuinely unavailable *to this process*
262
+ // either way; only the advice differs.
263
+ reason:
264
+ currentRegistrationRole() === 'secondary' ? 'not-primary' : 'no-app-id',
265
+ });
266
+ }
267
+ return frozen('launcherentry', {
268
+ badge: true,
269
+ badgeText: false, // the protocol carries a count and nothing else
270
+ progress: true,
271
+ urgent: true,
272
+ menu: true, // the quicklist
273
+ needsDesktopFile: true,
274
+ });
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+
279
+ const PROBES = {
280
+ notifications: probeNotifications,
281
+ tray: probeTray,
282
+ launcher: probeLauncher,
283
+ };
284
+
285
+ /**
286
+ * What this desktop can do for one feature, as a promise.
287
+ *
288
+ * ```js
289
+ * const n = await desktopCapability('notifications');
290
+ * if (n.features.actions) postWithReplyButton();
291
+ * else postPlainBanner();
292
+ * ```
293
+ *
294
+ * Resolves to `{ available, backend, features }`. `available` false means
295
+ * there is no mechanism at all and `features` is empty; otherwise `backend`
296
+ * names the mechanism — `'dbus'`, `'cocoa'`, `'statusnotifier'`,
297
+ * `'launcherentry'`, `'notify-send'`, `'osascript'` — and `features` is the
298
+ * portable vocabulary for this capability.
299
+ *
300
+ * **Never cached.** A panel restarting, an extension being enabled or a
301
+ * daemon being installed all change the answer, and a cached "no" would
302
+ * outlive every one of them. {@link useDesktopCapability} re-probes on the same
303
+ * events that would change it.
304
+ */
305
+ export async function desktopCapability(name, options = {}) {
306
+ const probe = PROBES[name];
307
+ if (!probe) {
308
+ throw new TypeError(
309
+ `react-x11: desktopCapability(${JSON.stringify(name)}) — no such ` +
310
+ `capability. Expected ${CAPABILITIES.join(', ')}.`,
311
+ );
312
+ }
313
+ try {
314
+ return await probe(options);
315
+ } catch {
316
+ // A probe that throws is a desktop that could not be asked, which is the
317
+ // same outcome for a caller as one that answered no.
318
+ return NONE;
319
+ }
320
+ }
321
+
322
+ /** The "nothing here" answer, exported so a caller can compare against it and
323
+ * so the hook has something stable to return on the first frame. */
324
+ export const NO_CAPABILITY = NONE;
package/src/cocoa/app.js CHANGED
@@ -38,6 +38,7 @@ import { CocoaPaneWindow } from './panewindow.js';
38
38
  import { CocoaColorSampler } from './screencolor.js';
39
39
  import { CocoaFilePanels } from './filepanels.js';
40
40
  import { CocoaFontManager } from './fonts.js';
41
+ import { releaseImageUpload } from './context2d.js';
41
42
  import { CocoaSurface } from './surface.js';
42
43
  import { CocoaWindow } from './window.js';
43
44
  import { decodeKey, modifierMask } from './keymap.js';
@@ -449,6 +450,18 @@ export class CocoaApp {
449
450
  return new CocoaSurface(this, options);
450
451
  }
451
452
 
453
+ /**
454
+ * The release seam for an `Image`'s upload (`freeImage`, src/imagesource.js).
455
+ * `ctx.drawImage(image)` here composites from a CG bitmap made for the
456
+ * Image on its first draw, which ntk's `Image.destroy()` — written for X,
457
+ * where the upload is a pixmap it tracks itself — cannot see; an owner
458
+ * letting go of an Image calls this too, and the bitmap is freed on the
459
+ * call. An Image nobody releases takes its bitmap with it when collected.
460
+ */
461
+ releaseImage(image) {
462
+ releaseImageUpload(image);
463
+ }
464
+
452
465
  /**
453
466
  * The `useGlobalMenu` transport seam: same owner shape as the D-Bus
454
467
  * GlobalMenuExport (start/stop/update), pointed at the macOS menu bar.
@@ -33,6 +33,100 @@ function parseColor(value) {
33
33
  return parsed;
34
34
  }
35
35
 
36
+ /** Bytes as a Buffer over the same memory — for a view, its own window of
37
+ * the ArrayBuffer, not the whole buffer from offset 0. */
38
+ function toBuffer(data) {
39
+ if (Buffer.isBuffer(data)) return data;
40
+ if (ArrayBuffer.isView(data)) {
41
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
42
+ }
43
+ return Buffer.from(data);
44
+ }
45
+
46
+ // --- ntk Images as drawImage sources -----------------------------------------
47
+ //
48
+ // An ntk `Image` is straight RGBA in JS memory. On X, ntk uploads it to a
49
+ // pixmap per connection and caches that on the Image; here the upload is a
50
+ // CG bitmap made on the first draw and kept in this map, then composited
51
+ // through `ctxDrawSurface` like any surface, scaling and cropping included.
52
+ // The bridge's `ctxPutImageData` does the conversion — it premultiplies the
53
+ // straight bytes into the bitmap's BGRA (ByteOrder32Host + AlphaFirst) —
54
+ // so the bytes go over as the Image holds them.
55
+ //
56
+ // Images are immutable content (ntk's contract, and `<image>`'s), so an
57
+ // entry is never refreshed. The map is keyed weakly: an Image that is
58
+ // dropped takes its entry along and the handle's finalizer frees the
59
+ // bitmap; an owner that is done with one frees it on the call instead,
60
+ // through `releaseImageUpload` (the app's `releaseImage` seam).
61
+
62
+ /** Image -> { native, handle } */
63
+ const imageUploads = new WeakMap();
64
+
65
+ /** An ntk `Image`: the duck type `isDirectImageSource` and ntk's own
66
+ * `drawImage` accept, plus the pixels this backend reads in place of the
67
+ * picture. A bare `{ width, height, data }` is not one — `ImageData` is
68
+ * written between draws, and caching it by identity would show stale
69
+ * pixels. */
70
+ function isImagePixels(image) {
71
+ const { width, height, data } = image;
72
+ return (
73
+ typeof image.picture === 'function' &&
74
+ Number.isInteger(width) &&
75
+ Number.isInteger(height) &&
76
+ width > 0 &&
77
+ height > 0 &&
78
+ data?.length === width * height * 4
79
+ );
80
+ }
81
+
82
+ function uploadImage(native, image) {
83
+ const held = imageUploads.get(image);
84
+ if (held?.native === native) return held.handle;
85
+ const { width, height } = image;
86
+ const handle = native.createSurface(width, height, 1);
87
+ native.ctxPutImageData(handle, toBuffer(image.data), width, height, 0, 0);
88
+ imageUploads.set(image, { native, handle });
89
+ return handle;
90
+ }
91
+
92
+ /** Free an Image's bitmap now, if it has one; drawing it again uploads it
93
+ * again, as ntk's `destroy()` promises for its own copies. */
94
+ export function releaseImageUpload(image) {
95
+ const held = image != null && imageUploads.get(image);
96
+ if (!held) return;
97
+ imageUploads.delete(image);
98
+ if (typeof held.native.releaseSurface === 'function') {
99
+ held.native.releaseSurface(held.handle);
100
+ }
101
+ }
102
+
103
+ const warnedSources = new Set();
104
+
105
+ /** A source this backend has no pixels for, said once per kind in
106
+ * development — the alternative is an empty box and no reason. */
107
+ function warnUndrawable(image) {
108
+ if (process.env.NODE_ENV === 'production') return;
109
+ const kind =
110
+ typeof image === 'object'
111
+ ? (image.constructor?.name ?? 'object')
112
+ : typeof image;
113
+ if (warnedSources.has(kind)) return;
114
+ warnedSources.add(kind);
115
+ const serverSide = typeof image === 'object' && 'id' in image;
116
+ const article = /^[aeiou]/i.test(kind) ? 'an' : 'a';
117
+ console.warn(
118
+ `react-x11: drawImage on the cocoa backend has no pixels for ${article} ${kind}, ` +
119
+ 'and draws nothing. ' +
120
+ (serverSide
121
+ ? 'It names an X server-side Picture or Drawable — <image picture>, ' +
122
+ '<image drawable>, an ntk Picture — and this backend has no X ' +
123
+ 'server to composite from, so those are X11-only. Hand <image src> ' +
124
+ 'the pixels instead: encoded PNG/JPEG bytes, raw RGBA, or an ntk Image.'
125
+ : 'This backend draws a Surface (react-x11/ntk) or an ntk Image; wrap ' +
126
+ 'raw RGBA as new Image({ width, height, data }).'),
127
+ );
128
+ }
129
+
36
130
  class LinearGradient {
37
131
  constructor(x0, y0, x1, y1) {
38
132
  this._coords = [x0, y0, x1, y1];
@@ -953,8 +1047,8 @@ export class CocoaContext2D {
953
1047
  }
954
1048
 
955
1049
  drawImage(image, ...args) {
956
- const src = image?._surfaceHandle ?? image?._surface?._surfaceHandle;
957
- if (!src) return; // ntk Images/Pictures are not on this backend yet
1050
+ const src = this._sourceHandle(image);
1051
+ if (!src) return;
958
1052
  const size = this._native.surfaceSize(src);
959
1053
  let sx = 0;
960
1054
  let sy = 0;
@@ -990,6 +1084,25 @@ export class CocoaContext2D {
990
1084
  this._dirty();
991
1085
  }
992
1086
 
1087
+ /**
1088
+ * The bitmap a `drawImage` source composites from: a surface's own, an
1089
+ * ntk Image's upload (made on its first draw, see `uploadImage`), or
1090
+ * none. A destroyed surface is none, silently — it had pixels once; any
1091
+ * other source is one this backend cannot draw at all, and development
1092
+ * says so once per kind.
1093
+ */
1094
+ _sourceHandle(image) {
1095
+ if (image == null) return null;
1096
+ if (typeof image === 'object') {
1097
+ if ('_surfaceHandle' in image || image._surface) {
1098
+ return image._surfaceHandle ?? image._surface?._surfaceHandle ?? null;
1099
+ }
1100
+ if (isImagePixels(image)) return uploadImage(this._native, image);
1101
+ }
1102
+ warnUndrawable(image);
1103
+ return null;
1104
+ }
1105
+
993
1106
  /**
994
1107
  * `drawImage` as a row memcpy, for the one shape where a copy is all it
995
1108
  * ever was: a surface composited into another at a translate, whole
@@ -1081,12 +1194,9 @@ export class CocoaContext2D {
1081
1194
 
1082
1195
  putImageData(data, x, y) {
1083
1196
  if (!data?.data) return;
1084
- const buf = Buffer.isBuffer(data.data)
1085
- ? data.data
1086
- : Buffer.from(data.data.buffer ?? data.data);
1087
1197
  this._native.ctxPutImageData(
1088
1198
  this._s(),
1089
- buf,
1199
+ toBuffer(data.data),
1090
1200
  data.width,
1091
1201
  data.height,
1092
1202
  Math.round(x),